fix: preserve full sync requests during active runs - #908
Conversation
A user refresh accepted while another sync owned the single-flight slot returned immediately without scheduling follow-up work. If provider data changed after the active pass took its snapshot, the database stayed stale despite the full-sync response being accepted. Coalesce accepted full, priority, and scoped intents into one lifecycle-owned follow-up pass while preserving the existing no-overlap and cadence rules. Full work subsumes scoped work, and repository selection follows stable provider identity so a rename or route reuse cannot redirect queued work.
The syncer regression covered provider and SQLite behavior directly, but it did not protect the user-facing 202 response path. A route regression could therefore accept a full sync during an active fetch while leaving the database stale. Drive both overlapping requests through the generated HTTP client and real server, hold the first provider snapshot in flight, then require the queued pass to persist newer repository metadata. This keeps the cross-boundary concurrency contract executable at the boundary where users observe it.
A full sync accepted behind a scoped run could still be lost when another run claimed the single-flight slot between completion and asynchronous replay. An explicitly empty scoped intent was also collapsed to nil while queued, widening it into a full sync. Transfer slot ownership atomically until the accepted follow-up is registered, and preserve scope nilness while deduplicating repository intents. Later triggers still coalesce into one bounded pending intent, so request bursts do not turn into a click-count-sized queue.
roborev: Combined Review (
|
A scoped user refresh coalesced into a cadence-respecting full pass by ORing its bypass flag into the whole pending run. The follow-up could then ignore cadence and feature cooldowns for unrelated repositories, causing provider work the user did not request. Carry scoped bypasses by stable provider repository identity and evaluate them per repository while the full pass retains its own cadence policy. This preserves one bounded follow-up and immediate user refreshes without widening either bypass to unrelated repositories.
roborev: Combined Review (
|
A scoped only_repo request can merge into queued cadence-respecting background work while another scoped refresh owns the sync slot. Without one full-stack guard, HTTP filter parsing, coalescing, and SQLite persistence could drift independently and widen the scoped bypass to unrelated repositories.\n\nExercise that mixed path through the generated client and real SQLite while keeping the provider boundary synthetic. The regression verifies that the selected repository advances to the queued snapshot and an unrelated repository remains untouched inside its cadence window.
roborev: Combined Review (
|
The HTTP sync route could return 202 while the trigger goroutine had not yet acquired the single-flight lock. A request accepted in that window was not yet part of active or pending work, making the API contract scheduler-dependent and leaving the strict boundary regression racy. Admit or coalesce triggered work synchronously under the lifecycle and run locks, then start only newly claimed provider work asynchronously. Coalesced requests create no extra goroutine, stopped syncers are rejected, and cadence, provider dispatch, and SQLite behavior remain unchanged.
This comment has been minimized.
This comment has been minimized.
An accepted follow-up retained the single-flight slot, but each completed pass still published Running:false before its deferred handoff started the queued pass. Status pollers and SSE consumers could therefore treat the earlier provider snapshot as terminal and stop waiting before fresh data reached SQLite. Hold each pass's terminal status snapshot until the handoff confirms that no pending work remains. Per-pass completion hooks and provider execution remain unchanged, while the HTTP/SQLite regression now rejects any idle-to-running transition inside one accepted sync chain. Addresses roborev-ci finding: #908 (comment)
This comment has been minimized.
This comment has been minimized.
Keeping sync status running across queued passes moved terminal publication after the final run-slot release. A new trigger could claim that slot and publish Running:true before the completed pass published Running:false, leaving status pollers idle while provider work was active. Order terminal publication with slot release under the existing status mutex, with statusMu preceding runMu whenever both are needed. The concurrency regression holds the real publication boundary, then verifies a real trigger remains coalesced until the terminal snapshot is ordered.
roborev: Combined Review (
|
What changed
Failure scenario
running: falsebefore starting its queued follow-up, so status and SSE clients could stop waiting before fresh data reached SQLite.Admission now happens before the response, handoffs retain the slot continuously, and terminal status is published only when no pending pass remains. Terminal publication is ordered with slot release so a later run cannot be overwritten by a stale idle snapshot. A stopped syncer rejects new requests instead of returning 202.