Repository navigation
fix(ui): build save/restore options per engine, not in a module global - #5876
Conversation
update_save_restore_list rebound a module global and assigned that shared list as the saverestore options, so several engine instances in one process could see each other's saved settings files. Build a local list per call and assign it to this engine's config entry only. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
springfall2008
left a comment
There was a problem hiding this comment.
Automated comment from the triage bot.
Reviewed head 4a179ace61f7aedbecbf06381c39f12968a00497. The change is sound: 0 findings inline, 1 out-of-diff note in the body.
What I checked and confirmed by reading both versions (main via git show origin/main:... and this head):
- The bug this fixes is real. On main
update_save_restore_list()rebinds the module global and only later assigns it to the engine'sconfig_indexentry, with theos.walkin between (userinterface.py:689-702 on this head mirrors main's shape). It runs per engine at the end offetch_config_options()every cycle (apps/predbat/fetch.py:4721) and from the saverestore select handler in an executor thread (apps/predbat/userinterface.py:415, 682-683), so two engines in one process can interleave across that window and the last rebind wins: engine A's dropdown gets engine B's saved file list, and sinceexpose_configappends to the assigned list (apps/predbat/userinterface.py:626-628) as does the web config page (apps/predbat/web.py:4370-4372), a cross-assigned list also collects the other engine's mutations. Building a local list per engine removes both. Single-engine installs are behaviourally unchanged. - The body's deep-copy claim holds:
apps/predbat/predbat.py:340deep-copiesCONFIG_ITEMSper engine andload_user_config()buildsconfig_indexfrom that copy (apps/predbat/userinterface.py:1172-1174), so the per-engine assignment cannot leak. - The test is registered in
TEST_REGISTRY(apps/predbat/unit_test.py:575,slow=Falseso it runs under--quick), matches the harness call shape, and fails on main for the right reason: main'suserinterface.pyhas no module-levelPREDBAT_SAVE_RESTOREdefinition, so after the two engines run the attribute exists and thegetattr(...) is global_beforeassertion can only pass once the function stops writing the global. The options-content assertions also discriminate (main returns distinct lists here because each engine rebinds a fresh list; the failing assertion is the one that pinpoints the removed shared state). - No docs impact: no setting, entity, service or user-visible default changes; nothing needs a
docs/update or aCONFIG_ITEMSentry.
apps/predbat/github.py:152 (untouched by this PR, same pattern, low) — download_predbat_releases() still does exactly what this PR removes from update_save_restore_list(): global PREDBAT_UPDATE_OPTIONS (apps/predbat/github.py:152), rebind (apps/predbat/github.py:185), appends (apps/predbat/github.py:197, 212) and item["options"] = PREDBAT_UPDATE_OPTIONS (apps/predbat/github.py:221), with the this_tag append at apps/predbat/github.py:227 mutating the list currently assigned to the update dropdown. Two engines in one process get the same last-rebind-wins race this fix closes for saverestore, on a path that also runs per engine per cycle. A follow-up applying the same local-list fix would close out the pattern; flagged here for visibility, not as a defect of this diff.
Summary
update_save_restore_listbuilt the save/restore options by rebinding a module-levelPREDBAT_SAVE_RESTOREwithglobal, then assigned that list to the engine'sconfig_index["saverestore"]["options"]. With more than one engine instance in a process, the last engine to run wins the module global, and options could be read from another engine's list.It now builds a local list and assigns it to this engine's entry only. The module global is no longer written, and the shared default list in
config.pyis not mutated. Config entries are already deep-copied per engine, so the per-engine assignment cannot leak.Tests
test_save_restore_options_are_per_engineintests/test_fetch_config_options.py: two engines with different config folders each see only their own saved file, the lists are distinct, and neither the module global nor the default list changes. It fails onmainand passes with this change.--quickstops attest_data_age_metrics_round_trip, which fails the same way onmainwithout this change.🤖 Generated with Claude Code