mirror of
https://github.com/got-feedBack/feedBack.git
synced 2026-07-26 06:42:32 +00:00
The guard in startup_events() read:
if getenv_compat("FEEDBACK_DEMO_MODE") or getenv_compat("FEEDBACK_DEMO_MODE") == "1" \
and not _DEMO_JANITOR_STARTED:
`and` binds tighter than `or`, so that is `A or (B and C)`. The not-already-started half
never ran when the env var was truthy — the only case that reaches it at all. A second
startup started a SECOND janitor thread, overwrote the handle, and shutdown then joined
only the last: the first leaked and kept firing registered hooks hourly, forever.
The guard now lives INSIDE start_janitor(). A caller cannot get operator precedence wrong
if there is nothing left for it to get wrong.
━━━ THREE WAYS TO WRITE THIS GUARD WRONG. I HIT ALL THREE. ━━━
1. NO GUARD — the original bug. Double-start, orphaned thread.
2. GUARD ON THE FLAG (`if _DEMO_JANITOR_STARTED: return`). Codex [P2]. stop_janitor()
DELIBERATELY leaves that flag True when a hook outruns its join timeout, so that a later
startup cannot spawn a janitor beside a live one. But the hook usually finishes a moment
later: the thread exits and the flag is stale. A flag-keyed guard then refuses to start a
replacement for the rest of the process — demo cleanup silently dead. (The original bug
accidentally MASKED this by always starting.)
3. GUARD ON LIVENESS ALONE (`if thread.is_alive(): return`). Codex [P2], second pass. A
timed-out stop leaves the old thread ALIVE BUT DOOMED — its stop event is set and it
exits as soon as its current hook returns. Treating that as a running janitor skips the
replacement, and we are back at (2) a second later.
So: a janitor counts as running only if its thread is alive AND it has not been told to stop.
━━━ AND EACH JANITOR NOW OWNS ITS STOP EVENT ━━━
start_janitor() used to `_DEMO_JANITOR_STOP.clear()` a single SHARED Event. Start a
replacement while a doomed thread is still finishing a hook and that clear RESURRECTS it: it
loops back to wait(), sees the flag cleared, and carries on. Two janitors — the exact bug we
started from. A fresh Event per janitor makes it impossible; the old thread waits on its own
event, which stays set, so it can only exit.
Env semantics UNCHANGED, verified across every value ("", "1", "0", "true", "false", "off"):
the old expression and demo_mode_enabled() agree on all of them. The only behavioural change
is the idempotency fix.
FOUR tests, and each of the three wrong guards fails a different subset:
no guard -> 2 fail (double start; orphaned thread)
guard on the flag -> 2 fail (never restarts after a timed-out stop)
liveness alone -> 1 fail (no replacement for a doomed janitor)
liveness + not-stopping -> all pass
pytest 2416, pyflakes 0, Codex 0.
Closes #902
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|---|---|---|
| .. | ||
| routers | ||
| acoustid_match.py | ||
| appconfig.py | ||
| appstate.py | ||
| audio_effects_db.py | ||
| audio.py | ||
| builtin_content.py | ||
| demo_mode.py | ||
| diagnostics_bundle.py | ||
| diagnostics_hardware.py | ||
| diagnostics_redact.py | ||
| dlc_paths.py | ||
| drums.py | ||
| enrichment.py | ||
| env_compat.py | ||
| gp2midi.py | ||
| gp2notation.py | ||
| gp2rs_gpx.py | ||
| gp2rs.py | ||
| gp8_audio_sync.py | ||
| gp_autosync.py | ||
| jsonc.py | ||
| library_registry.py | ||
| logging_setup.py | ||
| loosefolder.py | ||
| lyrics_transcribe.py | ||
| mb_match.py | ||
| metadata_db.py | ||
| midi_import.py | ||
| notation_lift.py | ||
| notation.py | ||
| progression.py | ||
| reqfields.py | ||
| safepath.py | ||
| scan_worker.py | ||
| scan.py | ||
| sloppak.py | ||
| song_score.py | ||
| song.py | ||
| songmeta.py | ||
| tailwind_rebuild.py | ||
| tones.py | ||
| tunings.py | ||
| vocal_pitch.py | ||
| wem_decode.py | ||
| xp.py | ||