Skip to content

fix(scheduler-sidecar): host AsyncIOScheduler inside a real event loop#104

Merged
RubyWolff27 merged 1 commit into
mainfrom
fix/sidecar-event-loop
Jul 10, 2026
Merged

fix(scheduler-sidecar): host AsyncIOScheduler inside a real event loop#104
RubyWolff27 merged 1 commit into
mainfrom
fix/sidecar-event-loop

Conversation

@RubyWolff27

Copy link
Copy Markdown
Owner

#103's sidecar crash-loops on prod: RuntimeError: no running event loop. core/scheduler.py uses AsyncIOScheduler — inside the gunicorn workers it bound to uvicorn's loop; the standalone entrypoint was synchronous, so start_scheduler() had no loop to bind. The unit tests had mocked start_scheduler, hiding exactly this integration contract.

  • scheduler_main.py: DB-wait stays sync, then asyncio.run(_amain()) — signal handlers via loop.add_signal_handler (signal.signal fallback), start_scheduler() under the running loop, await shutdown event, stop_scheduler(), exit 0. core/scheduler.py untouched.
  • Regression test now asserts the loop contract: the start_scheduler stub calls asyncio.get_running_loop() itself — verified to FAIL against the previous sync implementation before the fix.
  • 383 unit tests pass (26 pre-existing no-local-Postgres failures); four linters clean; mypy 9-baseline.

Prod is currently scheduler-dark (workers gated off per #103, sidecar looping) — this restores it.

🤖 Generated with Claude Code

Prod crash-loop: RuntimeError: no running event loop from
apscheduler/schedulers/asyncio.py, raised inside start_scheduler() ->
AsyncIOScheduler.start(). It ran fine in-process before because it
bound to uvicorn's already-running loop; the sidecar's sync _run()
called it with no loop at all, so nothing scheduled in prod and the
container restarted forever.

DB wait stays synchronous (plain blocking I/O, no loop needed).
Everything from start_scheduler() onward now runs inside
asyncio.run(_amain()): signal handlers install via
loop.add_signal_handler (falling back to signal.signal +
call_soon_threadsafe where add_signal_handler isn't implemented),
start_scheduler() runs with a loop bound, an asyncio.Event gates
shutdown, then stop_scheduler(). core/scheduler.py is untouched.

New regression test stubs start_scheduler() to call
asyncio.get_running_loop() itself and asserts it doesn't raise —
verified against the previous synchronous implementation that this
reproduces the exact prod RuntimeError, and passes against the fix.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@RubyWolff27
RubyWolff27 merged commit eda6f48 into main Jul 10, 2026
8 checks passed
@RubyWolff27
RubyWolff27 deleted the fix/sidecar-event-loop branch July 10, 2026 23:16
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.

1 participant