You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
ProjectOps.Create/Update race their queued closure when the caller's context is cancelled #85
ProjectOps.Update and Create run their mutation through runQueued (Queue.Do). If the caller's context is cancelled after the command is admitted, Do returns ctx.Err() while the closure may still be running. The closure ignores that context, so it can still commit. Meanwhile Update reads updated, found and updateErr straight away. That is a data race, and the caller can be told a write failed when it happened.
Root cause
These ops use runQueued where runCommitted, which waits for the closure to finish, exists for exactly this case.
Fix
Switch both to runCommitted, or wait for the closure before reading its results. Test under -race with a context cancelled after admission.
Found in the review of #46. It predates that change.
Approved 2026-09-26 by the coordinator under the owner's delegation: a real data race that can report a failed write that actually committed, with an existing helper (runCommitted) as the fix. Queued right after #46, which touches the same code.
Scope. Four ProjectOps methods (cmd/relay/project_ops.go) run a cancellable ctx through runQueued and read the closure's outputs after it returns: Create, Update, RotateToken and NarrowForEnrolment. RotateToken is the worst of them: a cancelled caller is told the rotation failed while the new token is already stored, and its plaintext is gone. Each switches o.runQueued(ctx, …) to o.runCommitted(ctx, …). That is the existing helper (Queue.DoCommitted), which never abandons an admitted step on caller cancellation. Remove already uses context.Background() and is unchanged.
Nothing else changes: signatures, return tuples, gate, audit and wire shapes stay the same. The structural queue test already accepts runCommitted.
Test (red first). New file cmd/relay/project_ops_commit_race_test.go, run with -race. For each of the four methods:
Build a ProjectOps with a real config.CommandQueue (or whatever Queue field type production uses) and a store whose write blocks on a channel once the closure is admitted.
Cancel the caller's context.
Release the write.
Assert that the method returned the committed result (not ctx.Err()), that the stored state matches it, and that -race reports nothing.
Where blocking inside the store is impractical, a hook store in the style of odwHookStore is fine. Tests must be red, or race-flagged, on the current code.
Files. Dev: cmd/relay/project_ops.go. Test writer: cmd/relay/project_ops_commit_race_test.go (new).
Contract amendment (coordinator): the widening to four methods is approved. RotateToken's test must pin the invariant:
When the call reports success, the returned token is the stored one.
When the context is cancelled, the caller either receives the new token or storage is unchanged. There must never be a stored new token the caller did not receive.
Contract amendment 2 (coordinator): the invariant excludes the deliberate audit-failure path. When recordProjectTokenRotated fails, RotateToken commits the rotation and withholds the token (errProjectTokenUnrecorded), so issuance is never unrecorded. Outside that path: on success the returned token is the stored one, and on cancellation the caller receives the new token or storage is unchanged. A rotation cancelled while it is still queued, but not yet started, now runs (accepted; the cancel-until-started queue mode is filed separately at low priority).
Activity
barelyworkingcode commentedon Sep 26, 2026
Approved 2026-09-26 by the coordinator under the owner's delegation: a real data race that can report a failed write that actually committed, with an existing helper (runCommitted) as the fix. Queued right after #46, which touches the same code.
barelyworkingcode commentedon Sep 26, 2026
Contract
Approved by the coordinator.
Scope. Four
ProjectOpsmethods (cmd/relay/project_ops.go) run a cancellablectxthroughrunQueuedand read the closure's outputs after it returns:Create,Update,RotateTokenandNarrowForEnrolment.RotateTokenis the worst of them: a cancelled caller is told the rotation failed while the new token is already stored, and its plaintext is gone. Each switcheso.runQueued(ctx, …)too.runCommitted(ctx, …). That is the existing helper (Queue.DoCommitted), which never abandons an admitted step on caller cancellation.Removealready usescontext.Background()and is unchanged.Nothing else changes: signatures, return tuples, gate, audit and wire shapes stay the same. The structural queue test already accepts
runCommitted.Test (red first). New file
cmd/relay/project_ops_commit_race_test.go, run with-race. For each of the four methods:ProjectOpswith a realconfig.CommandQueue(or whateverQueuefield type production uses) and a store whose write blocks on a channel once the closure is admitted.ctx.Err()), that the stored state matches it, and that-racereports nothing.Where blocking inside the store is impractical, a hook store in the style of
odwHookStoreis fine. Tests must be red, or race-flagged, on the current code.Files. Dev:
cmd/relay/project_ops.go. Test writer:cmd/relay/project_ops_commit_race_test.go(new).barelyworkingcode commentedon Sep 26, 2026
Contract amendment (coordinator): the widening to four methods is approved.
RotateToken's test must pin the invariant:barelyworkingcode commentedon Sep 26, 2026
Contract amendment 2 (coordinator): the invariant excludes the deliberate audit-failure path. When
recordProjectTokenRotatedfails,RotateTokencommits the rotation and withholds the token (errProjectTokenUnrecorded), so issuance is never unrecorded. Outside that path: on success the returned token is the stored one, and on cancellation the caller receives the new token or storage is unchanged. A rotation cancelled while it is still queued, but not yet started, now runs (accepted; the cancel-until-started queue mode is filed separately at low priority).