Repository navigation
fix(exchange): reserve approved RI exchange ceiling atomically and keep in-flight holds across UTC rollover - #690
Conversation
…ep in-flight holds across UTC rollover Approved (manual) RI exchanges moved pending->processing and then read the daily spend in a separate unlocked step, so two approvals could each see the same headroom, and an approval counted its own initial quote against itself. They now call ReserveApprovedRIExchange, which takes the same global advisory lock as the AUTO reservation, leaves the row's own amount out of spend, reserves the full ceiling min(per-exchange cap, daily headroom) as the row's payment_due, and the fresh accepted amount settles on the same row. The daily spend query now counts every processing row regardless of day, so a reservation still executing across UTC midnight keeps holding headroom until it completes or fails. Completed rows still count on their acceptance day. Refs #656 Refs #657 Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
|
Warning Review limit reached
This review includes 13 billable files and costs up to $3.25.
Reviews can continue after your included limit without a manual trigger. An admin must approve usage-based billing. Or wait 9 minutes for your next included review. View limit detailsLimit details: You’ve used the included review currently available. Your 82 included PR review attempts over the past 7 days set your current allowance at 1 review per hour. Review configuration: ⚙️ Run configuration
📒 Files selected for processing (13)
Comment |
Independent review of PR 690 at 9e81e93Verdict: the money logic is correct; CHANGES REQUESTED on two small items before merge (R1 test strength, R2 Lint Code). Not approved until both are closed or explained. What I checked and found sound
Evidence reproduced (macOS, exact SHA in the author's worktree, non-race, under the platform test-suite lock, 9 GiB free)
Findings
|
…ervation internals The concurrent-approval test now holds the global advisory lock from a third transaction and requires both approvals to be parked on it, so it fails when the lock is removed (it previously passed without it). Cap refusals carry a sentinel error; any other reservation error is logged and the approver sees a fixed message instead of the raw database error. Refs #656 Refs #657 Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
|
Delta pushed: 9e81e93..754a888, addressing review 9e81e93.
Unit tests for config and api passed locally before the push; no further builds until disk recovers. |
Delta review of PR 690: 9e81e93 -> f68b75bVerdict: R1, R2 and R5 are closed. APPROVE at f68b75b, conditional on green CI on this SHA (all checks were still running when I wrote this). The 9e81e93 full-PR review otherwise stands.
|
Delta check: f68b75b -> e543fb8Verdict: APPROVE at e543fb8 (my f68b75b approval carries over), still conditional on green CI on this SHA.
|
…ge-cap-reservation
…ator resolve them (#1109) A run that crashes between the provider call and its ledger write leaves an ri_exchange_history row in processing. Since #690 a processing row counts against the daily cap on every later day, and nothing reconciled it, so a crashed run could block every later exchange indefinitely. - The scheduled ri_exchange_reshape run now logs an ERROR (the application error alarm channel) for every processing row unchanged for over an hour, naming the row, its held amount and how to resolve it. It runs before the enabled check and never settles or releases anything itself: whether the exchange happened at AWS is unknown, so the hold stays counted. - POST /api/ri-exchange/resolve/{id} (execute:ri-exchange, same account scope and region constraints as direct execution, signed-in user only) records an operator's decision after checking AWS: "completed" keeps the reserved ceiling counted on today's UTC day; "failed" releases the hold and needs acknowledge_unknown_outcome. - PostgresStore.ResolveStaleRIExchange locks the row and refuses unless it is still processing and unchanged for 6h on the database clock, the cloud check names the row's own account and region and postdates its last change, and (for a release) no provider exchange id is recorded. A row a late writer settled is never overwritten; identical retries replay. Closes #692 Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Summary
Approved (manual) RI exchanges now reserve their full execution ceiling atomically under the same global advisory lock as the AUTO reservation, and in-flight reservations keep counting across UTC midnight.
Closes #656
Closes #657
What changed
PostgresStore.ReserveApprovedRIExchange(id, dailyCap, perCap): underpg_advisory_xact_lock(riExchangeDailySpendLockKey), reads the spend with the row's own id excluded, computesceiling = min(perCap, dailyCap - spend), refuses when the initial quote does not fit, and stores the ceiling as the row'spayment_due. SharesreserveRIExchangeCeilingwithReserveRIExchange(AUTO), so both paths use one protocol.executeApprovedExchangeno longer reads daily spend separately; it reserves, runsExecutewith the reserved ceiling asMaxPaymentDueUSD, and settles the fresh accepted amount on the same row viaCompleteRIExchangeWithPayment. Region and cap-config checks still run first and do not reserve.riExchangeDailySpendQuery: everyprocessingrow counts regardless of day (an unsettled hold),completedrows count on theircompleted_atUTC day. Optional$2excludes one row.checkDailyCap,checkCapsAndComputeHeadroomandhandlerChooseEffectiveCap(replaced by the store logic) and their unit tests.Evidence (real PostgreSQL 16, local image, no purchases, no cloud calls)
go test -tags integration ./internal/config -run 'PostgresStoreDB_(Reserve|DailySpend)', 8 tests pass:Non-vacuity (mutations, then restored): reverting to the day-windowed query fails the rollover tests; removing the self-exclusion fails the $600 and concurrent-approval tests. Note: the table has a
BEFORE UPDATEtrigger that rewritesupdated_at, so the tests back-date it withsession_replication_role = replica(superuser, dedicatedissue41_test DB only).Unit:
go test ./internal/{config,api,server,analytics,mocks}pass (non-race).Notes for the reviewer
Executereturns no fresh quote amount the ledger now records the reserved ceiling (previously the initial quote). That over-counts, which fails safe.🤖 Generated with Claude Code