Skip to content

fix(statewrite): classify the branch-ref-CAS 409 as a conflict so concurrent writes retry - #535

Merged
joshua-temple merged 1 commit into
mainfrom
fix/conflict-classification-branch-race-409
Jul 9, 2026
Merged

joshua-temple merged 1 commit into
mainfrom
fix/conflict-classification-branch-race-409

Conversation

@joshua-temple

Copy link
Copy Markdown
Collaborator

Problem

cascade's optimistic state-write retry was defeated for the real concurrent-writer conflict shape. The Contents API returns two 409 shapes: a blob If-Match mismatch carrying does not match, and a branch-ref compare-and-swap failure reading like is at X but expected Y with no does not match. Both classifyPutError and IsConflict gated the whole conflict test behind a does not match check, so the is at branch was unreachable dead code and a branch-ref-CAS 409 was returned as a fatal error.

Two component finalizes racing one trunk branch produce exactly that shape, so promote, hotfix, and rollback hard-failed on the first concurrent-write 409 with zero re-apply retries. This affects adopters running concurrent lifecycle operations on a busy trunk.

Fix

Classify on the status marker: require a 409 or Conflict, and accept either the does not match or the is at body. Applied identically to classifyPutError and IsConflict so the typed path and the forwarded-error fallback agree. Conservative: a non-409/Conflict error is still never classified as a conflict.

Verification

New tests (red before, green after): a branch-ref-CAS 409 body classifies as a conflict via both functions; CommitWithRetry now re-fetches and re-applies on that shape instead of hard-failing; a non-conflict error is not classified. Existing does not match blob-shape cases untouched. go build, go test ./... -race, golangci-lint all clean.

…current writes retry

The Contents API returns two 409 shapes for a rejected optimistic state write: a blob If-Match mismatch carrying 'does not match', and a branch-ref-CAS failure reading like 'is at X but expected Y' with no 'does not match'. classifyPutError and IsConflict both gated the whole conflict test behind a 'does not match' check, so the 'is at' branch was unreachable and a branch-ref-CAS 409 was returned as a fatal error. Two component finalizes racing one trunk branch produce exactly that shape, so promote, hotfix, and rollback hard-failed on the first concurrent-write 409 with zero re-apply retries. Classify on the status marker instead: require a 409 or Conflict, and accept either the 'does not match' or the 'is at' body. Applied identically to both functions so the typed path and the forwarded-error fallback agree.

Signed-off-by: Joshua Temple <joshua.temple@stablekernel.com>
@joshua-temple
joshua-temple merged commit 3d43c70 into main Jul 9, 2026
20 checks passed
@joshua-temple
joshua-temple deleted the fix/conflict-classification-branch-race-409 branch July 9, 2026 05:31
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