Repository navigation
fix(ri-exchange): guard direct execute against an RI held by an in-flight exchange - #1838
Conversation
…he outcome is unknown The in-flight guard blocks on unknown rows, so every plain pre-accept error (payment over max, invalid exchange, quote throttle) would have blocked the RI. Record unknown only for errors.Is(err, common.ErrOutcomeUnknown), which classifyAcceptError (pkg/exchange/outcome.go:21-35, go pin 790f369f4822) applies to every accept error except a definite 4xx or throttle. Azure keeps its rule. Refs #1829 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…irect execute Sibling of the processing-row guard: an AWS direct row still submitted or unknown holds its source RIs against ReserveRIExchange and ReserveApprovedRIExchange, under the same advisory lock. It normalises its own input and the stored ids, fails closed on a lookup error, and keeps the blocking row id out of the error. Refs #1829 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The sibling guard's refusal is stored whole in ri_exchange_history.error, so the history sanitizer needs a closed pattern for it or it reads back as a generic message. Only the shared RI ids vary. Refs #1829 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Save the audit row in one transaction that takes the advisory lock, refuses a source RI held by a processing history row or an in-flight direct row, then inserts with created_at = clock_timestamp() so the lock wait cannot age the row. created_at = clock_timestamp() applies to both providers and is harmless for Azure, which takes no lock and has a short tx. Azure rows are not guarded. Existing shared-database subtests get distinct AWS source ids. Refs #1829 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ore the claim The guarded audit save already runs before the submit claim and the AWS call. Map its in-flight refusal to a 409 that names only the caller's RIs, so nothing is claimed or sent to AWS. Adds the issue repro and the reverse (lost accept response holds the RI against AUTO) against a real database. Refs #1829 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… rows Add the DBA settle statement for a direct row left submitted or unknown, with a test that runs the runbook's own SQL against fixtures, and record the cap, resolve-endpoint and id-case limitations. Refs #1829 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Warning Review limit reachedYou've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository. Next included review available in 16 minutes. View limit details
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. |
Lint Code (misspell) flagged serialises. Refs #1829 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
APPROVED f80af7a by claude-ui-fix-1 I did the cold review at 2933fdc. The head then moved to f80af7a with one comment-only commit: Findings (none block; I tried to refute each one first)
What I attacked
Tests (local, macOS, testcontainers, throttled)
Mutation probes (each reverted)
CI. At 2933fdc, Lint Code failed only on the misspelling this head fixes. The merge needs CI green at f80af7a. Gaps. I did no real-account AWS verification, and none is authorized. |
psql substitutes :x raw, so a uuid or string value needs :'x'. The runbook now uses the quoted form and shows how to set the variables; the runbook test maps the quoted placeholders. Refs #1829 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
APPROVED 57f0711 by claude-ui-fix-1 This is a delta over my approval at f80af7a: one commit, 2 files, +11/-10, docs and test only.
The merge needs CI green at this SHA. |
|
APPROVED 57f0711 by claude-qa-ui-4 Cold review at 2933fdc. The two commits to 57f0711 change no production code: the "serializes" comment fix at internal/config/store_postgres.go:3529 (it fixes the Lint Code misspell failure at 2933fdc), the runbook SQL switched to psql
Runs at 2933fdc (GOMAXPROCS=4, -p 2):
CI run 42331019594 at 57f0711 was still running when posted (28 pass, 9 pending including Lint Code). The merge needs it green. Non-blocking:
|
Summary
Closes #1829. The direct AWS RI exchange execute path bypassed #1826's source-in-flight guard, in both directions:
Either way, two exchanges of one source RI could both reach AWS.
The design follows #1829's owner decisions (
D1 = D2 = D3 = A):submittedorunknown, under the same advisory spend lock that approve and AUTO already take.Commits (each one builds; all at or under 400 lines)
unknownonly when the outcome is unknown (T0). An AWS direct execute is recordedunknownonly when the library marks the errorErrOutcomeUnknown, otherwisefailed.classifyAcceptError(cloud-commitments-gopkg/exchange/outcome.go) wraps every accept error except a definite 4xx or a throttle.unknown, and with the guard that would have blocked RIs on ordinary user errors.refuseRIExchangeSourceInFlightDirect, runs next to fix(ri-exchange): keep an exchange with a lost accept response in flight #1826's inReserveRIExchangeandReserveApprovedRIExchange, in the same transaction after the lock.btrim(x, E' \t\n\r\f\x0B').SaveRIExchangeDirectExecutionis one transaction: AWS takes the shared spend lock (lockRIExchangeSpend, now shared withreserveRIExchangeCeiling), then runs both guards, then inserts withcreated_at = clock_timestamp(), taken after the lock.runbooks/ri-exchange-direct-execution-settle.mdis the interim operator step: find the row, verify at AWS, then the guarded settle SQL.submittedrows younger than 5 minutes (the execution window is 4 minutes).errorrather than overwriting it.Behaviour changes
POSTdirect AWS execute can now return 409 when a source RI is held by an in-flight approve or AUTO exchange, or by an unresolved direct execution. openapi is updated. The envelope is the existing error envelope.failed, notunknown, unless the accept outcome is genuinely unknown. Migration 000131's comment wording is stale for AWS; the runbook says so.Known issues (in known-issues.md)
Verification
clock_timestamp(), the runbook age or provider clause, or the errnotes pattern; leaking the blocker ID; running the executor or the claim before the save. A trim set using\v, which PostgreSQL reads as the letter v, is also caught by a dedicated test.AwaitBlockedBy, and a race test (6 parallel saves,-count=20, exactly one wins).unknown; then AUTO and approve refused; then a disjoint RI reserves).Lock ordering: the only row lock taken before the advisory lock is
ReserveApprovedRIExchange's ownFOR UPDATE. Every other advisory-lock holder takes no other row lock, and the claim and Finalize run outside the locked transaction, so there's no cycle.Note for whichever of this and #1831 merges second:
directGuardIDListPatternduplicates #1831'sriIDListPattern, so reuse that one.🤖 Generated with Claude Code