spec(SPEC-011): idempotent submission for the ingress endpoint #8
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "spec/SPEC-011-idempotent-submission"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
Wires the standard HTTP
Idempotency-Keyheader into a working dedup path onPOST /ingress/v1/messages. Pre-existing state: the header was read atcbcl-ingress-handler.lfe:39but never threaded to the dispatcher, so producer retries across transient network failures caused the same work to be dispatched twice. This PR closes that gap.What's in this PR
specs/SPEC-011-idempotent-submission.md. Carved out of the broader (and parked) SPEC-010; SPEC-010 is included as a record of the original design exploration and markedstatus: superseded.cbcl-storage,cbcl-dispatcher,cbcl-ingress-handler, andcbcl-agent-ws.plans/idempotent-submission-impl.splfor hence-driven coordination.docs/HTTP-API.mdgains a full Idempotent Submission section with a runnable Python recipe;docs/status.mdupdated.Semantics (settled across the review iterations)
Idempotency-Keyheader on a meta request is ignored and logged at debug level.idempotent_replay: trueand the receipt's actual current state. Cross-principal collision → 409 with a byte-exact constant body that discloses nothing.Cache-Controlhint planned in v0.1.0 has been explicitly dropped from V1 (NFR-002 v0.2.0); it returns once SPEC-041 lands durable storage. Producers MUST handle restart-window resubmission as fresh.claim-retry-exhausted: distinct from 409. Reserved for future retention-pressure paths; today unreachable under V1.dispatched_toon the receipt row: persisted at append time, survivescbcl-inflightclearance on terminal, so replays of completed receipts can report the handling agent.Response shape (REQ-004)
Plus
Location: /receipts/v1/<receipt_id>header on every 202.Adversarial review trajectory
Per PROTO-001 §"Adversarial Review" (Constitutional Principle 12), the work was reviewed by four fresh-context agents — no session validated its own output.
212353f725acedf28678bc8520d0b364b69)Notable defects caught and fixed in flight (each only by adversarial review, not by the implementing session):
test-ddialect);build-replay-replyreturning placeholder strings (status="pending",capability="replay",dispatched_to="unknown") instead of real state; disclosure-property test was a tautology.dispatched_toreturned null (regression —cbcl-inflightis cleared on terminal);respond-202hardcodedstatus: "pending"for every response.Location: /receipts/v1/<id>pointed at a route that didn't exist (404). HTTP test had encoded the wrong URL shape and passed silently against the buggy code.dispatched_to=#"router"placeholder vs. schema "null"); TEST-009 undispatched case unreachable under V1 architecture; 503 mapping in HTTP-API but absent from spec body.The fourth-round reviewer's verdict: "Further adversarial passes would produce diminishing returns. The artifact is converged."
Test coverage
35 new tests in two new modules, plus 21 regression-verified pre-existing dialect-distribution tests:
cbcl-idempotency-tests(31): TEST-001..010 with positive + negative-input + negative-output decomposition (per PROTO-001 RTTD). Property-based concurrency test (50 concurrent claims → exactly one winner). Boundary tests at 0, 1, 0x21, 0x7E, 255, 256 bytes. Principal type-confusion (map vs binary). REQ-005 retention (storage restart drops keys). claim-retry-exhausted distinct atom.cbcl-ingress-http-tests(4): real cowboy listener on ephemeral port via httpc. Asserts Location header format AND that the URL resolves to 200. Asserts Cache-Control absence under V1. 400 and 409 bodies match the exportedidempotency-conflict-body/invalid-idempotency-key-bodyconstants byte-for-byte.Known limitations / out of scope
enaclNIF compatibility issue with newer OTP versions. The 55 SPEC-011-relevant tests run cleanly undererlang@26withCFLAGS="-Wno-incompatible-function-pointer-types -Wno-int-conversion". Tracked separately as an environment issue./receipts/:receipt_id(unversioned) is kept as a back-compat alias alongside/receipts/v1/:receipt_id. New code should use/v1/.Commits
1373c7ac9e194a212353f725acedf28678bc8520d0b364b69Merge strategy recommendation:
--no-ffto preserve the adversarial-review history. The trajectory is the constitutional-principle evidence and is worth keeping visible. If a flatter history is preferred, rebase-squash before merging.🤖 Generated with Claude Code
P1 addressed in
fe2fa52.Fix: Replaced the two-step
try-claim(directets:insert_newfrom dispatcher) +append-ask(gen_server:cast) with a single synchronouscbcl-storage:claim-and-append-ask/7. Both ETS writes now happen inside the storage gen_server'shandle_call, so they share lifecycle: either both land (gen_server:call returned) or both die (storage process crash drops both tables together). The orphan-claim window is eliminated by construction.Side effect caught by the new test: the cleanest implementation passed the principal-map from cbcl-auth verbatim to storage; the subsequent same-principal-replay check then compared a map against a binary and 409'd legitimate retries. Normalising principal to its binary id at the storage boundary (
principal->idem-id) fixes this. The HTTP testhttp_202_replay_shows_true_and_current_status_testcaught it immediately — exactly the kind of cross-layer regression the integration tests are there for.Regression tests added:
claim-and-receipt-are-atomic-no-orphan-window: pins the atomicity property at the storage layer. Asserts that on successful claim, both the receipt row AND the index entry are queryable from the test process before the call returns. A regression to cast-based append would produceget-receiptreturning[]while the index says{ok, receipt-id}— exactly the defect window.claim-conflict-does-not-insert-receipt: pins REQ-003 case 2's "no row created on conflict" post-condition at the storage layer.Verification: 57/57 tests pass across
cbcl-idempotency-tests(33),cbcl-ingress-http-tests(4),cbcl-dialect-distribution-tests(20).View command line instructions
Checkout
From your project repository, check out a new branch and test the changes.Merge
Merge the changes and update on Forgejo.Warning: The "Autodetect manual merge" setting is not enabled for this repository, you will have to mark this pull request as manually merged afterwards.