Skip to main content

StorageAtomicity

Trait StorageAtomicity 

Source
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. RollbackGuard in linera_core::chain_worker::handle rolls 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_view reports it, ChainWorkerState::save sets poisoned, check_not_poisoned refuses every later use of that worker, and evict_poisoned_worker drops 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.

Implementors§