fix(sync): treat recently-started running jobs as active to stop concurrent pile-up (RES-948) - #335
fix(sync): treat recently-started running jobs as active to stop concurrent pile-up (RES-948)#335jhkchan wants to merge 1 commit into
Conversation
…urrent pile-up (RES-948) `SyncRunner.start_sync`'s concurrency guard rejected a new sync only when `_is_task_active(channel_id)` — an IN-PROCESS check of `self._active_tasks`. A `status=="running"` sync_jobs row created moments ago by a concurrent trigger, or by a different worker process, is not in this process's registry, so the guard treated it as a crashed-process leftover, "recovered" it (marked failed), and started yet another sync. A burst of triggers (bulk import + repeated sync/consolidate calls) thus each mis-recovered the others' fresh rows and piled up dozens of concurrent "running" jobs on one channel — observed at 58 running, thrashing the GPU and starving each other. Add `_running_row_is_active`: a running row blocks a new sync if this process is running its task OR it was started within `_RECENT_RUNNING_GRACE_SECONDS` (120s). Only a row that is BOTH not task-active here AND older than the grace window is a genuine stale leftover safe to recover — preserving the existing process-restart recovery path. Naive `started_at` values (as Mongo may return) are coerced to UTC. Adds tests/services/test_sync_runner_concurrent_guard.py: recent-row-blocks, old-row-recoverable, task-active-always-blocks, missing-started_at, naive-utc. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PMMM6KQXmzAEA42UxpyMUm
|
Independent adversarial review — APPROVE as a burst mitigation (with a known-limitation follow-up). Verified correct for what it claims: Honest scope call — [P1, by-design limitation not a bug]: this is a recency-based mitigation, not an atomic one-job-per-channel guarantee. For a sync that outlives the 120s window in a multi-process/replica deployment, at T+130s another process sees |
Problem (RES-948)
A burst of sync triggers piled up dozens of concurrent
runningsync_jobs on one channel (observed 58), thrashing the GPU.start_sync's guard rejected a new sync only when_is_task_active(channel_id)— an in-process check ofself._active_tasks. Arunningrow created moments ago by a concurrent trigger (or a different worker process) isn't in this process's registry, so the guard treated it as a crashed leftover, "recovered" it, and started another sync. Each trigger mis-recovered the others' fresh rows → pile-up.Fix
Add
_running_row_is_active(existing, channel_id): arunningrow blocks a new sync if either this process is running its task or it was started within_RECENT_RUNNING_GRACE_SECONDS(120s) — a concurrent/other-process run this registry can't see yet. Only a row that is both not-task-active here and older than the grace window is a genuine stale leftover, preserving the existing process-restart recovery path. Naivestarted_at(as Mongo may return) is coerced to UTC.Tests
tests/services/test_sync_runner_concurrent_guard.py: recent-row-blocks-without-local-task, old-row-recoverable, task-active-always-blocks, missing-started_at, naive-utc-coercion. 5 passed locally.Note: this closes the common race at the guard. A fully atomic one-job-per-channel claim (Mongo findAndModify/CAS) is a larger follow-up; the grace-window guard removes the observed pile-up without changing the recovery contract.
Part of epic RES-943 (RLP full-corpus scale + no-cloud gaps).
🤖 Generated with Claude Code
https://claude.ai/code/session_01PMMM6KQXmzAEA42UxpyMUm