Repository navigation
feat: multi-actor concurrency harness for race-prone mutations (#218) - #379
Merged
github-actions[bot] merged 5 commits intoOct 18, 2026
Merged
github-actions[bot] merged 5 commits into
github-actions[bot] merged 5 commits into
Conversation
…IS-STELLAR#218) runConcurrently / duplicateSubmissions / retryStorm give every race the same shape: one start barrier (all actors enter before any can complete), and a report that classifies every outcome so tests assert invariants (exactly one winner, everyone else 409) instead of microtask ordering. Rejections are captured as data - Promise.all would hide all but the first. Deterministic by construction: no sleeps, no jitter, no threads. Self-tests pin the barrier itself; if it ever degrades, races everywhere silently stop racing.
…IS-STELLAR#218) The entity documents 'the loser fails on the unique index and is mapped to a 409', and the service doc promises concurrent duplicates are rejected with 409 - but neither was true: execute() never caught the INSERT collision, so a racing duplicate surfaced SQLSTATE 23505 to the client, and two losers could both pass the lookup. The claim is now a bounded loop: on a unique violation (isUniqueViolation: driver code or message fallback) it re-reads the winner's record and replays it, rejects with 409, or - their attempt already failed - claims the key itself. Spec repairs uncovered while making the suite trustworthy: the fake repository's remove() ignored arrays (TypeORM remove accepts both, and purgeExpired passes one - the purge test never actually deleted), and the interceptor harness lacked getHandler/getClass, so all seven interceptor tests died on a TypeError. 18/18 now pass (was 10/18). Adds the harness-driven concurrency suite: duplicate submissions execute the handler exactly once, in-flight retry storms see 409s, stale views replay instead of re-running, and payload-differing key reuse loses with the documented message.
…S-STELLAR#218) cancel() and expireIntent() did read - check the loaded copy - then save unconditionally, so a withdrawal racing a live execution would rewrite the executing/completed row to cancelled/expired, and two racing cancellations could both 'succeed' and double-write evidence. Both now claim with UPDATE ... WHERE status = 'proposed' and answer 409 with the observed status when the claim is gone - the same discipline execute() already used. execute()'s claim-failure path also re-reads state so the 409 says whether the winner was another execution or a terminal row. Mutual exclusion between cancel and execute is now structural: exactly one side effect can land, whichever conditional update wins first.
…TELLAR#218) Extract the in-memory repository/signer/builders from the lifecycle spec into testing/approval-intent.testkit.ts so the concurrency suite observes the exact same claim semantics - one fake, two suites, no divergence. New harness coverage around the intent claim: five concurrent actors yield exactly one executing claim and one operation run; a retry storm against a completed intent gets nothing but 409s; a stale proposed-state view loses cleanly to the peer's completion; and cancel-vs-execute under a race leaves exactly one side effect with the row agreeing on which.
…STELLAR#218) Usage, why plain Promise.all lies, the scenario-to-invariant table, the three races this work surfaced (unique-index loser as 500, cancel/expire clobbering executions, and the documented sequential-only proposal dedupe limitation), operator failure modes, SQL recovery for stuck in_progress rows and stuck executing intents, and rollback paths.
Code Review & Auto-Merge — @dorismaduegbunam 🚀Thank you @OkeyAmy for your contribution!
Verified code changes and repository requirements. Merging pull request. |
github-actions
Bot
merged commit Oct 18, 2026
37e1010
into
TRELLIS-STELLAR:main
1 of 3 checks passed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #218
What this delivers
A multi-actor concurrency harness for race-prone mutations, plus harness-driven tests against the two critical writes with the most exposure: the idempotency claim that guards every idempotent write, and the approval-intent claim that guards maintainer actions. All four scenarios from the acceptance criteria — concurrent actors, duplicate submissions, stale versions, retry storms — are covered as invariant tests, with an operator guide covering edge cases, failure modes and recovery.
The harness —
src/testing/concurrency/Sits alongside the existing
chaos/andfault-injection/helpers:runConcurrentlyreleases every actor through one start barrier: each operation's synchronous prefix runs before any actor can complete, so the race actually races. StaggeredPromise.allstarts never overlap, and a broken claim path silently passes.duplicateSubmissionsandretryStormmodel the two production shapes: identical payloads arriving at once (double-clicks, proxy replays) and one client hammering while the original is still in flight.Promise.allwould hide all but the first.What the harness caught (all fixed here)
IdempotencyService.executenever handled the INSERT collision: the loser's SQLSTATE 23505 went straight to the client. The claim is now a bounded loop that catches the violation, re-reads the winner's record, and either replays it, rejects with 409, or — the winner's attempt having failed — claims the key itself.ApprovalIntentService.cancelandexpireIntentdid read → check → unconditional save: a withdrawal racing an in-flight execution rewroteexecutingrows tocancelled, and two racing cancellations could both "succeed" and double-write evidence. Both now claim withUPDATE ... WHERE status = 'proposed'and answer 409 with the observed status when the claim is gone — the disciplineexecutealready used. Cancel-vs-execute mutual exclusion is now structural.remove()ignored arrays (TypeORM accepts both, andpurgeExpiredpasses one — the purge test never deleted anything), and the interceptor harness lackedgetHandler/getClass(all seven tests died on a TypeError). That suite went from 10/18 to 18/18.Known limitation, documented rather than papered over
Intent proposal dedupe is sequential-strength only: genuinely concurrent identical proposals can both persist, because the table has no live-row uniqueness on
fingerprint. Duplicates stay single-use, audited and cancel-able;docs/CONCURRENCY_HARNESS.mdrecords the hardening path (partial unique index + conflict mapping) as follow-up rather than smuggling a migration into a test-focused PR.Scenario coverage
proposed → executingwinner, one operation run, losers 409Failure modes and recovery (in the guide)
Why 409 storms from retry loops are correct responses; the two stuck-row modes with SQL recovery recipes — idempotent
in_progressrows (TTL expiry +purgeExpired, early-release DELETE) and intents stuckexecuting(... AND receipt IS NULLguard so a completed execution can never be faked into failed). Rollback: no schema changes anywhere in this PR — both fixes are code-only and backward compatible with every existing status code and response shape.Verification
14 suites / 122 tests across the affected surface, all green (harness self-tests, idempotency 18+5 concurrency, intents 53+4 concurrency, plus migrations, quota and admin-role suites). The concurrency suites were run three times back to back with identical results.
npx tsc --noEmitreports nothing for any file in this PR. The intent-side specs build on the approval-intent foundation that landed in #377/#378.