pub trait StorageAtomicity { }Expand description
Assumption (Atomic persistence). A single WritableKeyValueStore::write_batch — one batch
against one root key — is applied atomically, within whatever key-count and size limits the
backend imposes.
DurablePersistence is about when state is written; this is about the write being
indivisible. Every invariant in crate::manager::proof::locking is stated over a state
reached by whole transitions, so a torn write would put the manager in a state no transition
produces — for instance a confirmed_vote
stored without the locking_block that
ConfirmationOnlyInCurrentRound installs before it.
Batches larger than the backend allows. Atomicity comes from write_batch alone.
Journaling adds none: it preserves all-or-nothing at sizes a remote store such as ScyllaDB
will not accept in one write_batch, which a chain save can exceed.
linera_views::backends::journaling writes the oversized batch into journal blocks and commits
it by atomically updating a journal header; before any later read or write, a journal found
present is replayed block by block, each block’s write and its header update going in a single
write_batch. A crash part-way therefore leaves a resumable journal rather than a torn
state. That slow path requires exclusive access to the keys under the chain’s root — it fails
with JournalingError::JournalRequiresExclusiveAccess otherwise — which is exactly what
SequentialChainState supplies.
Two things are called write_batch, and only one is atomic. The store-level one above is.
DbStorage::write_batch is not: it takes a MultiPartitionBatch keyed by root key, opens a
store per key, and issues one independent write_entry per partition with try_join_all. So a
storage call that spans partitions — write_blobs_and_certificate, which batches a block’s
blobs together with its certificate — is atomic within each partition and not across them, and
a crash can leave one partition written and another not. Nothing in this specification relies
on cross-partition atomicity; linera_core::proof::availability::BlockOutputsArePersisted
carries that weight with replay instead.
What a failed save costs. Three outcomes are distinguished, and only the last is expensive:
- Cancelled — the request future is dropped part-way.
RollbackGuardinlinera_core::chain_worker::handlerolls the view back on drop, so no partial staging survives into the next request. - Failed outright — the write did not take effect. The in-memory view still agrees with storage, the error propagates, and the worker keeps serving.
- Ambiguous — journal resolution failed, so storage may be partly advanced and the view can
no longer be trusted.
ViewError::must_reload_viewreports it,ChainWorkerState::savesetspoisoned,check_not_poisonedrefuses every later use of that worker, andevict_poisoned_workerdrops it from the cache so the next request reloads the chain from storage.
The guarantee the proofs rest on is therefore not that storage is never partially written, but that a partially written chain is never read back as state: it is either completed by journal replay or discarded along with the worker that could not complete it.
It also underpins the mirror property in linera_core::proof::availability: re-deriving a
worker’s undelivered effects after a restart is only meaningful if the state they are derived
from is itself consistent.