Repository navigation
feat(commands): retry-safe commands with proven dedup over SQLite - #502
Merged
Merged
Conversation
handle runs one command per BEGIN IMMEDIATE transaction: it finds the record for the id, runs a pure decide on a new id, and commits state, record, and outbox effects together. Duplicates replay the recorded outcome; an id reused with another payload is a conflict. Laws over the pure step cover replay, conflict, recorded results, refusals, and a retry after the first commit. ledger.bend is a webhook-fed consumer. Closes #375
…mands-375 # Conflicts: # CHANGELOG.md # README.md
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.
Summary
A webhook, job, or provisioning handler can now take a delivery twice and apply it once:
commands.handlecommits the domain state, a record of the outcome, and the outbox effects in one SQLite transaction, and a retry of the same id replays the recorded outcome. Reusing an id with a different payload is a conflict, and both paths leave the state untouched.What is proved, and what is trusted
LAWS.bendholds for every domain (any state, payload, result, and effect types, anyeq, anydecide) over the purestepthathandleruns inside the transaction:duplicate_replaysconflict_rejectedConflictaccept_recordedrefuse_keeps_stateretry_replayseq, a retry after the first commit replays it at any later timeSQLite's atomicity,
BEGIN IMMEDIATEisolation, and the SQL wiring are trust assumptions, tested bycheck.bendrather than proved. Payload identity is full-text equality, not a hash.Design decisions
BEGIN IMMEDIATEmeans two deliveries of one id can never both see "no record". The check proves this with writer A paused mid-transaction: writer B waits, fails withSQLITE_BUSY, commits nothing, and its retry replays A's result.outbox.next, thenoutbox.ack). This is not exactly-once external execution, and the README says so.prune(st, before)forgets old records. After a prune, a retry of a forgotten id applies again. Deduplication is never promised past the window.decideisString -> String -> Decision, so the consumer owns serialization.ledger.bend(the chosen consumer: a payment ledger fed by webhook events) usesjsonfor payloads and state, and acollectionsOMapfor balances.Checks
scripts/check.sh commandspasses: the type check,PROOF.bend(noLAWS.def relies on foreign code), andcheck.bend, which covers duplicate replay, conflicts, refusals, restart, a child process that dies beforeCOMMIT, the in-flight duplicate above, two processes that deliver the same 400 events at once (balance +400, not +800; 404 outbox rows), outbox order and ack, and prune. The same check also passes as a native binary.Bench (
commands/bench, in-memory, N = 10,000, checksums agree across all five variants): Bend runs 107k new commands/s and 238k duplicates/s, against C at 261k and 1.49M. With a file database, the disk dominates new commands and Bend is within 1.1x of C.Fixes #375