feat(server): add background scheduling engine (COD-377) - #4
Conversation
Spawn a tokio task during server startup that polls list_pending_schedules() every 30 seconds and publishes due content via Iris. - scheduler.rs (new): poll loop with tokio::select for shutdown signal, per-schedule processing with atomic claim, Iris timeout, variant selection - storage.rs: add claim_schedule (atomic Pending→InProgress with status guard), update_schedule_status, get_schedule_status - lib.rs: spawn scheduler after listener bind, graceful shutdown via watch channel driven by SIGTERM + SIGINT, bounded sched.await with timeout - api.rs: add build_router_from_arc for Arc<AppState> sharing Co-authored-by: Archon <archon@purelymail.com>
🤖 Automated Review PanelTwo-model review was run before opening this PR. Both reviewers independently identified the same three issues, which were all fixed before the PR was opened. GPT-5.5 (openai/gpt-5.5)Verdict: Well-structured, mostly achieves the goal. Three issues found (all addressed): 🔴 SIGTERM not handled (acceptance criterion miss): 🟠 Non-atomic Pending→InProgress claim: The original 🟠 No Iris timeout: ✅ Correct: poll interval, shutdown break logic, variant selection, enum serialization, error handling per-schedule, test coverage. Gemini 3 Flash (google/gemini-3-flash-preview)Verdict: Largely achieves the goal, compiles, all CI gates green. Same three issues identified: 🔴 SIGTERM handling: Confirmed 🟠 Atomic claim needed: Identified the same TOCTOU race and recommended 🟡 Bound ✅ Additional positive notes: first-tick correctly deferred, variant selection matches spec, enum DB serialization consistent with conventions, per-schedule errors caught and logged. Summary: All blocking issues from both reviewers were addressed before PR creation. No outstanding concerns. |
Auto-Merge Gate — ApprovalConfidence: 0.92 (threshold: 0.80) RationaleThis PR cleanly implements all 5 acceptance criteria from COD-377:
Checks observed (all green)
Review panel outcomeBoth GPT-5.5 and Gemini 3 Flash independently flagged 3 issues (SIGTERM handling, non-atomic claim, missing Iris timeout) — all addressed before this PR was opened. Scope limits
Auto-merged by CodeFold Auto-Merge Gate (cron ceb0befd1f30) |
Summary
Implements COD-377: background scheduling engine for PostGhost. Schedules stored in SQLite are now automatically picked up and published via Iris when their
scheduled_fortime arrives.Closes #COD-377
Changes
New module:
scheduler.rslist_pending_schedules(), and processes any schedule whosescheduled_for <= nowclaim_schedule()transitions Pending → InProgress atomically (UPDATE ... WHERE status = 'pending'), preventing double-publishing under concurrent poll cycles or multiple server instancestokio::sync::watchchannel; the loop exits cleanly when SIGINT or SIGTERM is receivedtracingand marked Failed; the loop never panics on individual schedule failuresModified:
storage.rsclaim_schedule(id)— atomic Pending→InProgress claim with status precondition guardupdate_schedule_status(id, status)— general status transitionget_schedule_status(id)— read schedule status (used by tests)insert_schedule_raw_for_test()—#[cfg(test)]helper for FK-bypass test fixturesModified:
lib.rswait_for_shutdown_signal()handles both SIGINT and SIGTERM viatokio::signal::unixsched.awaitbounded with 10s timeout so a hung Iris call can't pin shutdownModified:
api.rsbuild_router_from_arc(state: Arc<AppState>)— accepts pre-Arc'd state for sharing between server and schedulerReview Panel
Both reviewers (GPT-5.5, Gemini 3 Flash) independently flagged three issues that were addressed before this PR was opened:
ctrl_c()which only catches SIGINT. Fixed withtokio::signal::unixhandling both SIGTERM and SIGINT.update_schedule_statushad no status precondition, risking double-publish. Fixed withclaim_schedule()usingWHERE status = 'pending'.tokio::time::timeout(30s).Verification
New tests:
test_update_schedule_status_round_trip— storage method round-triptest_poll_once_marks_failed_when_iris_unreachable— Iris failure → Failed statustest_poll_once_skips_not_yet_due— future schedules remain Pendingtest_poll_once_marks_failed_when_content_missing— missing content → Failed status