Settlement saga
The state machine, the journal and idempotency.
After the hammer, GAVEL runs a settlement saga. It is a pure state machine (step(journal, event) has no I/O, no clock and no randomness) driven by a runner that saves the journal before each effect.
Settlement saga: every state and legal move
Scroll the figure sideways, or open it full size.
docs/GATES.md cover single calls and the nightly specs. States and moves are copied from the transition table in packages/payments/src/saga/machine.ts.Text version of this diagram
The main path is HELD, BIDDING (start_auction), WINNER_SELECTED (hammer), CAPTURING (begin), CAPTURED (capture_ok or adopted), VOIDING (begin_void), SETTLED (all_released).
- cancel moves HELD or BIDDING to VOIDING. no_candidate moves WINNER_SELECTED to VOIDING.
- definite_fail moves CAPTURING to FALLBACK_RUNNER_UP. next_candidate returns to CAPTURING. no_candidates_left moves it to VOIDING.
- FAILED_NEEDS_REVIEW is terminal. It is reached by invariant (from WINNER_SELECTED, CAPTURING, FALLBACK_RUNNER_UP), unknown_deadline (from CAPTURING, after ten minutes), mismatch (from CAPTURING or CAPTURED) and void_budget_exhausted (from VOIDING).
- Each transition is one compare-and-set save of the journal. The capture intent is saved before the PayPal call and cleared when the outcome is saved.
Three ideas
- The journal comes first. Every transition is one compare-and-set save of the journal before the next effect. The capture intent (capture, amount, request id) is saved before the PayPal call.
- An unknown answer is not a failure. If a capture times out or returns a 5xx, the settlement stays CAPTURING with an unknown clock. It never moves to the next bidder. After ten minutes it goes to FAILED_NEEDS_REVIEW.
- Only a definite refusal falls back. After a definite failure the saga reads the truth first; if there is no capture it records the failure and moves to the next candidate at that candidate's own price.
Idempotency
Every PayPal call carries a deterministic request id: gv1- followed by a hash of the auction id, the operation, the hold id, the amount in cents and a generation number. The generation only changes when parameters change, never on retry or resume, so a resumed capture repeats the same call and PayPal answers it once.
Limits you should know
| Setting | Value | Meaning |
|---|---|---|
voidConcurrency | 3 | At most three voids in flight |
maxVoidResumes | 5 | A void that fails five resumes goes to review |
unknownDeadlineMs | 600000 | Ten minutes for an unknown capture outcome |
Where this lives in the code
- packages/payments/src/saga/machine.tsthe transition table and step()
- packages/payments/src/saga/capture.tscapture phase
- packages/payments/src/saga/fallback.tsrunner-up fallback facts
- packages/payments/src/saga/unknown.tsunknown outcome and the deadline
- packages/payments/src/saga/void.tsvoid and release phase
- packages/payments/src/config.tsSAGA_DEFAULTS
Last updated on