Skip to content

Run APScheduler in a dedicated sidecar, not inside gunicorn workers#103

Merged
RubyWolff27 merged 1 commit into
mainfrom
feat/scheduler-sidecar
Jul 10, 2026
Merged

Run APScheduler in a dedicated sidecar, not inside gunicorn workers#103
RubyWolff27 merged 1 commit into
mainfrom
feat/scheduler-sidecar

Conversation

@RubyWolff27

Copy link
Copy Markdown
Owner

Fixes the scheduler disease diagnosed during the Wave-2.4 live acceptance (2026-07-10): APScheduler runs inside BOTH gunicorn UvicornWorkers, so the cockpit snapshot build (~128s, grown past --timeout 120) gets the worker SIGABRT'd (code 134) mid-build — every scheduled 06/09/18/21 snapshot store has been silently failing since the build crossed 120s, each kill blips live requests on that worker, and every job double-fires (one scheduler per worker).

Change

  • rivaflow/scheduler_main.py — standalone blocking entrypoint (python -m rivaflow.scheduler_main): shared logging config, bounded DB wait, start_scheduler(), SIGTERM/SIGINT → clean shutdown. Explicitly does NOT run migrations (the api's start.sh owns those).
  • scheduler compose service — same image + env as the api, no ports, restart: unless-stopped, 768m cap, healthcheck disabled (no HTTP surface). deploy.sh's compose up -d auto-creates it on deploy.
  • RIVAFLOW_RUN_SCHEDULER gate (settings.RUN_SCHEDULER): api service sets "0" — web workers no longer schedule. Unset ≡ "1", so dev/single-process behavior is byte-identical.
  • Shared logging setup extracted to core/logging_config.py (app + sidecar emit the same JSON format).

One topology move kills both failure modes: jobs no longer inherit the web worker's 120s timeout, and the per-worker double-fire is gone.

Verification

  • 382 unit tests pass locally (11 new: env gate both ways, run-loop start/stop ordering + signal path, DB-wait retry/exhaust, no-migrations assertion, compose sanity); 26 pre-existing no-local-Postgres failures identical on main.
  • Four linters clean under CI flags; mypy at the 9-error baseline.
  • Post-merge live acceptance planned: rivaflow-scheduler-1 up, jobs registered once, the post-deploy warmup build runs to completion and stores a fresh whoop-summary-v2 snapshot, api logs free of scheduler activity and code-134 across the next tick.

ISA: PAI/MEMORY/WORK/whoop-bitter-lesson-audit/ISA-scheduler-sidecar.md.

🤖 Generated with Claude Code

The cockpit-snapshot job (~128s) exceeds gunicorn's --timeout 120, so
gunicorn SIGABRTs the worker mid-build. That silently drops every
06/09/18/21 Melbourne snapshot, kills whatever live request shared the
worker, and — since one scheduler ran per worker — double-fires every
job.

docker-compose.prod.yml now runs a separate `scheduler` service
(rivaflow.scheduler_main) that owns all cron jobs exclusively; the api
service sets RIVAFLOW_RUN_SCHEDULER=0 so it no longer starts one.
Behavior is unchanged when the env var is unset (dev/sqlite).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@RubyWolff27
RubyWolff27 merged commit 749e1fa into main Jul 10, 2026
8 checks passed
@RubyWolff27
RubyWolff27 deleted the feat/scheduler-sidecar 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