Skip to content

fix(idempotency): in-flight reservation closes concurrent double-create race (bug-bash #21)#232

Merged
mastermanas805 merged 5 commits into
masterfrom
fix/idempotency-inflight-reservation-2026-06-04
Jun 3, 2026
Merged

fix(idempotency): in-flight reservation closes concurrent double-create race (bug-bash #21)#232
mastermanas805 merged 5 commits into
masterfrom
fix/idempotency-inflight-reservation-2026-06-04

Conversation

@mastermanas805

Copy link
Copy Markdown
Member

What

The idempotency middleware did GET(miss) → run handler → SET, with no atomic reservation between the GET and the SET. Two requests carrying the same Idempotency-Key (or the same body-fingerprint) racing in that window both observed redis.Nil and both ran the handler — double-creating real backend resources on the authenticated /db,/cache,/nosql provision paths, which have no other per-burst dedup gate.

(The deploy path is already race-safe via CreateDeploymentWithCap's SELECT … FOR UPDATE; this race is the db/cache/mongo resource-waste case.)

Fix

On a cache miss, write an in-flight reservation marker via SETNX (idemEntry.InFlight, 60s TTL) the instant the handler starts. A concurrent same-key request reads the marker in the top GET hit-block and returns 409 idempotency_key_in_progress (retryable — same envelope as the existing conflict 409) instead of re-running the handler.

  • Real response overwrites the marker on completion (plain SET).
  • Non-cacheable paths (5xx, handler error, mutable 4xx) DELETE the marker so an immediate legitimate retry isn't blocked.
  • Applied symmetrically to both the explicit-key and fingerprint branches.
  • SETNX is best-effort / fail-open — a Redis hiccup never blocks provisioning (documented dedup posture: best-effort, never a correctness gate).

Coverage

Symptom:        concurrent same-key requests both run handler → double provision
Enumeration:    GET→Next→SET in idempotencyExplicit + idempotencyFingerprint (2 sites)
Sites found:    2
Sites touched:  2
Coverage test:  TestIdempotency_{ExplicitKey,Fingerprint}_ConcurrentInFlight_Returns409
Live verified:  pass under -race; new funcs + all 6 Del lines 100% covered

Both tests hold request A inside the handler (blocked on a channel) so B is guaranteed to observe A's live reservation → 409, handler runs exactly once.

🤖 Generated with Claude Code

@mastermanas805 mastermanas805 enabled auto-merge (squash) June 3, 2026 19:10
…te race (bug-bash #21)

The middleware did GET(miss) → run handler → SET with no atomic reservation
between the GET and the SET. Two requests carrying the same Idempotency-Key
(or the same body-fingerprint) racing in that window both saw redis.Nil and
both ran the handler — double-creating real backend resources on the
authenticated /db|/cache|/nosql provision paths, which have no other
per-burst dedup gate (the per-fingerprint INCR cap lives only on the anon
branch; CreateDeployment already has its own FOR UPDATE cap).

Fix: on a cache miss, write an in-flight reservation marker via SETNX
(idemEntry.InFlight, 60s TTL) the instant the handler starts. A concurrent
same-key request reads the marker in the top GET hit-block and returns 409
idempotency_key_in_progress (retryable — same envelope as the existing
conflict 409) instead of re-running the handler. The real response overwrites
the marker on completion (plain SET); non-cacheable paths (5xx, handler
error, mutable 4xx) DELETE the marker so an immediate legitimate retry isn't
blocked. Applied symmetrically to both the explicit-key and fingerprint
branches. SETNX is best-effort/fail-open — a Redis hiccup never blocks
provisioning (documented dedup posture, never a correctness gate).

Tests: TestIdempotency_{ExplicitKey,Fingerprint}_ConcurrentInFlight_Returns409
hold request A in the handler (blocked on a channel) so B observes A's live
reservation → 409, handler runs exactly once. Pass under -race; new code 100%
covered.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@mastermanas805 mastermanas805 force-pushed the fix/idempotency-inflight-reservation-2026-06-04 branch from c5e7663 to 5c73b49 Compare June 3, 2026 19:12
@mastermanas805 mastermanas805 merged commit 3c53dc4 into master Jun 3, 2026
18 checks passed
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