Skip to content

backend: recover studies stuck in "running" — startup reconciliation, CancelledError handling, strong task refs - #42

Open
ChrisW09 wants to merge 1 commit into
mainfrom
fix/backend-run-lifecycle
Open

backend: recover studies stuck in "running" — startup reconciliation, CancelledError handling, strong task refs#42
ChrisW09 wants to merge 1 commit into
mainfrom
fix/backend-run-lifecycle

Conversation

@ChrisW09

Copy link
Copy Markdown
Collaborator

Fixes #19.

A study whose in-process run task died stayed "running" in the DB forever: POST /run returned 409 "already running" indefinitely, and the only escape was archive + permanent delete of the (possibly expensive) study. Three gaps closed:

  1. Startup reconciliationPROGRESS is in-memory, so any DB-"running" study at boot is orphaned by definition, but lifespan only ran init_db() + seed_if_empty(). It now marks orphaned rows failed (re-runnable) and logs how many it repaired.
  2. CancelledError handling — graceful shutdown/reload cancels the task with a BaseException that the except Exception failure path never saw; docker-compose runs uvicorn --reload with the backend mounted, so any file edit cancelled in-flight runs into the stuck state. Now caught: progress marked failed, best-effort DB write (guarded — the startup reconciliation covers the case where it can't land during shutdown), and re-raised.
  3. Strong task referenceslaunch() discarded the asyncio.Task; the event loop holds only weak refs, so a long run could be garbage-collected mid-flight ("a task that isn't referenced elsewhere may get garbage collected at any time" — asyncio docs). launch() now keeps the reference in a module-level set until the task completes.

Verified against a real SQLite DB: an orphaned "running" study is marked failed at startup while other statuses are untouched; launch() holds the task reference during the run and releases it after; app.main imports and builds cleanly.

Note: the SSE stream's infinite-idle loop for such studies is #20 (separate fix), and the run-endpoint check-then-act race is #21.

🤖 Generated with Claude Code

Three gaps guaranteed a study whose in-process run task died stayed
'running' in the DB forever (POST /run -> 409 'already running'; the
only escape was archive + permanent delete):

- no startup reconciliation: PROGRESS is in-memory, so any DB-'running'
  study at boot is orphaned by definition, but lifespan never repaired
  it. Startup now marks those rows failed (re-runnable).
- asyncio.CancelledError is a BaseException, so the 'except Exception'
  failure path never fired on graceful shutdown/reload (docker-compose
  runs uvicorn --reload, so ANY backend edit cancelled in-flight runs).
  It is now caught, best-effort marks the study failed, and re-raises.
- launch() discarded the Task reference; the event loop holds only weak
  refs, so a long run could be garbage-collected mid-flight and vanish
  silently. launch() now keeps a strong reference until done.

Verified against a SQLite DB: orphaned 'running' study marked failed at
startup (other statuses untouched), task reference held during the run
and released after.

Fixes #19

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

backend: studies stuck in "running" forever after restart/crash (no startup reconciliation; CancelledError uncaught; task GC risk)

1 participant