Skip to content

feat(core): remember what a command dispatched with an idempotency key answered - #212

Open
benatespina wants to merge 2 commits into
mainfrom
feat/idempotent-commands
Open

benatespina wants to merge 2 commits into
mainfrom
feat/idempotent-commands

Conversation

@benatespina

Copy link
Copy Markdown
Member

Summary

  • A command dispatched with idempotencyKey records what it answered, in the same transaction as the events it stores; every later dispatch under the same key, command and payload answers the same without running, its rejection included. Two dispatches racing for one key store the events once.
  • The same key for another command or payload rejects with IdempotencyKeyConflictError (IDEMPOTENCY_KEY_CONFLICT). Keys live runtime.commands.idempotencyKeyLifetime (24 h by default) in a new bounda_idempotency_keys table, purged by the scheduled-command worker.
  • The React Router middleware passes a request's Idempotency-Key header to every command of the request; failure() answers a conflict with a 422.

Changes

  • IdempotencyStore on Storage and StorageTransaction: memory, SQLite (libSQL and the Durable Object), PostgreSQL, with its contract test and a case in the storage transaction contract, run in workerd too.
  • createCommandPipeline takes storage; a keyed dispatch records its outcome with the append that stores its events, or with the unit of work of a reaction; a replay sets bounda.outcome to replayed.
  • The in-memory event store reserves positions when a transaction stages events and shows readers only the contiguous committed prefix, so tx.eventStore.append answers the positions its events commit with, as the SQL adapters do.
  • Docs: new guide guides/idempotent-commands, plus configuration, errors, React Router, adapters and observability pages. Changeset minor.

Verification

  • pnpm check green (2050 tests, 3 skipped), PostgreSQL suite against a container, Cloudflare suite in workerd.
  • Stryker: core 94.87, postgresql 94.37, react-router 100. The core number comes from the worktree's own incremental cache, so the CI figure is the one to compare with main; every surviving mutant in the new code was killed except two equivalent ones.

…y answered

A request that times out on the way back, a form submitted twice or a webhook
delivered again dispatches its command twice, and the aggregate either rejects
a retry of something that went through or does its work twice. A dispatch with
idempotencyKey records what it answered, in the transaction of the events it
stores, and every later dispatch under the same key, command and payload
answers the same without running, its rejection included; another command or
payload under the key is refused. Keys live 24 hours by default and the
scheduled-command worker purges them. The React Router middleware passes the
Idempotency-Key header to the commands of the request.

The in-memory adapter now reserves positions when a transaction stages events
and shows readers only the contiguous committed prefix, so a transaction
answers the positions its events commit with, as the SQL adapters do.
The job summary piped sed into head -60 under pipefail; once the table of a
package grew past 60 lines, head closed the pipe while sed still wrote, sed
failed with a broken pipe and the job failed after a green run. awk prints
the first 60 lines of the table and reads the rest without a reader to lose.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant