4.4 KiB
4.4 KiB
T0.6 — Atomic commit protocol
| Field | Value |
|---|---|
| Phase | P0 — Foundations |
| Size | M — 1 to 3 days |
| Status | Not started |
| Flags | — |
| Spec | inlined below |
| Blocks | T2.1 |
Goal
Four table writes in one transaction behind EventLog::commit. All four land or
none do.
Facts (inlined — no spec read needed)
let txn = db.begin_write()?;
{
let mut log = txn.open_table(EVENT_LOG)?;
let mut state = txn.open_table(RUN_STATE)?;
let mut position = txn.open_table(CONSUMER_POSITION)?;
let mut outbox = txn.open_table(OUTBOX)?;
log.insert((branch_key, lsn), &record)?; // append: the durable fact
state.insert((branch_key, attempt_no), &attempt)?; // apply: the derived view
position.insert(Scoped::new(tenant, stream), pos)?; // advance: where to resume
outbox.insert((branch_key, lsn), &intent)?; // relay separately
}
txn.commit()?; // all four, or none
- Every key carries
BranchKeyorScoped<_>. The outbox is the one that invites the mistake: an outbox keyed onLsnalone reads naturally and is wrong, because LSNs are per branch — a bare LSN collides across every branch of every run of every tenant. The same key shape also gives the relay its only promised ordering: perBranchKey, ascendingLsn. - Commit per event, not per run. A projection that accumulates in memory and writes at run end loses the whole run on a crash. Episodes are small and append-mostly; one fsync per transition is cheap next to model latency.
- The engine supplies crash-atomicity (shadow paging in
redb, transactions in Postgres). The log supplies history. Because the engine's transaction is atomic, append and apply happen together: no torn records, no redo/undo pass, no checkpoint-consistency problem.
Steps
- Route every write through
EventLog::commit. There is no second write path — grep for directopen_table(...).insertoutside the implementation and delete what you find. - Confirm each of the four keys:
(BranchKey, Lsn)for log and outbox,(BranchKey, AttemptNo)for state,Scoped<StreamId>for position. - Build the crash harness: a test-only hook that panics after write i of the
four, for
i in 0..4. Reopen the database and assert all-or-nothing. - Add the collision test: two runs in two tenants both writing LSN 1, both outbox entries present and distinct after commit.
- Measure one commit's fsync cost and record it in the test output, so the per-event decision stays visible rather than being re-litigated later.
Acceptance
- Harness panics between each pair of table writes; on reopen either all four landed or none did.
- Two runs in two tenants writing the same LSN both keep their outbox entries.
Verify
Harness: a test-only fault hook inside the commit path, panic_after_write(i)
for i in 0..4, compiled under a test-hooks feature.
Integration test — tests/it_commit_atomicity.rs:
- For each
i in 0..4: run a commit that writes all four tables with the hook armed ati. - Reopen the database in a fresh process (or at minimum a fresh
Databasehandle — reusing an open handle can mask a durability bug). - Count rows in all four tables. Assert the count vector is either "all four present" or "none present". Any mixed vector fails.
- Collision case: two runs in two tenants both commit LSN 1 with outbox entries; assert both entries exist and are distinct after reopen.
- Print the measured fsync duration for one commit, so the per-event cost stays visible rather than being re-litigated from memory later.
Command: cargo test -p storage --features test-hooks commit_atomicity
False pass:
- The hook checked before
begin_write, so no partial state is ever possible and everyitrivially passes. - Asserting on the in-memory result rather than after reopen. The transaction object will happily report success.
- Testing with
Durability::Nonein the test config for speed. It passes and proves nothing about the shipped path.
Traps
- Outbox keyed on
Lsnalone. It reads naturally and silently loses entries. - Buffering state deltas in memory "for efficiency" and flushing at run end.
Background (not required to do this task): rust-agentic-sys.md §8.1, §8.3 · rust-agentic-task.md