kuatia-storage, kuatia-storage-sql,
kuatia (ledger, saga), kuatia-coreADR-0002 chose a saga commit pipeline. Its finalize step still funneled
everything into one monolithic store transaction,
CommitStore::commit_transfer, which bundled ~8 responsibilities (deactivate,
insert, store record, index both sides, CAS balance guards, account-version
guards, reservation authorization, event append) into a single database
transaction. Two problems surfaced: (1) the storage layer carried a lot of
domain assumptions (it interpreted state, enforced guards, decided idempotency
and error semantics), undercutting the "dumb record keeper" goal; and (2)
crash recovery was designed (SagaStore, legend pause/resume) but never
wired, so that single transaction was the only thing protecting against a
half-applied commit. What is the right division of responsibility between the
store and the saga, and how is a commit made crash-safe without that monolithic
transaction?
commit_transfer)Keep one store method that atomically applies the whole commit and enforces all guards inside a single transaction.
Pros:
Cons:
Storage write methods apply one update and return the number of affected
rows (or an I/O error); they never interpret the count, decide state, enforce
idempotency, or compensate. The saga interprets counts (full = continue;
partial = error → compensate; zero = read state and continue only if this same
envelope/reservation already applied it) and verifies end-states. Crash-safety
is a phase-tracked write-ahead record (PendingSaga {envelope, reservation,
phase}) plus idempotent roll-forward in Ledger::recover().
Pros:
Reserving saga is
re-run and re-validated; a Finalizing saga (already validated, owns its
postings) is rolled forward through a verified path; nothing commits unless
all consumed postings are confirmed Inactive.Cons:
commit_transfer
(CappedOverdraft floor, freeze/close) become best-effort: re-validated as
the last step before the writes, but not strictly atomic with them.Chosen option: Option 2, dumb storage + saga interpretation + durable recovery, because it is the only option that keeps the storage layer dumb and composable with the saga while still providing crash-safety and unconditional double-spend safety. Concretely:
Result<u64, StoreError>: reserve_postings,
release_postings, deactivate_postings, insert_postings,
store_transfer(record, involved), and an idempotent append_event. The
monolithic commit_transfer / CommitStore / CommitRequest and the
semantic write-outcome error variants are removed.reserve → finalize. Validation
runs inside finalize, as its last action before writing, the tightest-window
floor and freeze/close re-check. finalize_envelope verifies every end-state
and never creates/stores unless all consumed postings are Inactive.commit(transfer) resolves then runs commit_envelope;
reverse() runs the same path; there is no separate raw/atomic entry point.PendingSaga is written before any mutation
(Reserving), bumped to Finalizing at the point of no return. recover()
branches on the phase; the record is deleted only on commit or a clean
pre-finalize abort, and roll-forward (not rollback) means no orphaned
PendingInactive postings to reconcile.legend's pause/resume is for external waits, not crash checkpoints, so
durable recovery is this write-ahead layer around legend, not serialization
of the in-flight execution.
Reserving, roll forward a partial finalize,
abort+release when an account is frozen, refuse to double-spend a taken
posting).(account, asset) serialization or a narrow commit-time CAS.commit_transfer finalize.