LeVCS/crates/levcs-store/src/engine.rs

6436 lines
291 KiB
Rust

//! `StoreEngine`: shard routing, startup and recovery, lifecycle, and the
//! `ArcSwap` committed/status roots.
//!
//! **Shared file.** Lead owns the signatures (D0); **B1 NamespaceTxn** fills
//! the bodies (scope 2.1, 6-B1). Same pattern as `format.rs` and `drive.rs`:
//! the public API must compile in D0 so A3 can write against real signatures
//! in Wave A rather than retrofitting them in Wave B.
//!
//! # What is implemented here and what is not
//!
//! This is the first vertical slice: one retained [`RecoverySession`] across
//! every shard, one shard writer per shard wired
//! `GroupBuilder` -> `append_group_and_fence` -> [`ShardSubtree`] -> committed
//! root CAS -> completion, and the publication ordering of scope 6.3. Startup
//! states 1 and 2 are implemented — an absent or empty root is initialized
//! under the root lock, and a valid `FORMAT` opens through production
//! recovery. States 3 and 4, the signer pool and its suffix repair,
//! coalescing, terminal
//! retention, rotation/sealing, and `RepoSnapshot` are later B1 assignments
//! and refuse with a [`StoreError::NotImplemented`] naming themselves. None of
//! them returns a partial result or a plausible default; a caller that reaches
//! one gets told which deliverable it is waiting on.
//!
//! # The ordering that matters most in this file
//!
//! Scope 6.3 steps 7, 8, and 9, in that order and never another. The committed
//! root is published first, the status entries are removed second, and waiters
//! are woken third. Removing a status entry before the receipt is visible
//! manufactures `Unknown` for a transaction that is already durable — the
//! exact race the mandatory committed-root B read exists to close, reintroduced
//! from the write side where no read can fix it. Waking before publication
//! would let a caller act on a receipt no other reader can see yet.
use std::collections::BTreeMap;
use std::sync::atomic::{fence, Ordering};
use std::sync::Arc;
use std::thread::JoinHandle;
use std::time::{SystemTime, UNIX_EPOCH};
use arc_swap::ArcSwap;
use crossbeam_channel::{Receiver, RecvTimeoutError, Sender, TrySendError};
use im::Vector;
use levcs_core::{ObjectId, ObjectType};
use levcs_protocol::oracle::{self, AppendDeadlinePhase, DeadlineDecision};
use levcs_protocol::v2::{
self as protocol, AppliedRefV1, CommittedTransactionV1, DurabilityResultV1, RefMutation,
RefStateV1, SignedCommittedTransactionV1,
};
use crate::checkpoint::{ReceiptRecord, RefRecord};
use crate::completion::SharedCompletion;
use crate::failpoints::{self, Failpoint, FailpointAction};
use crate::format::{
frame_total_len, object_type_code, Frame, FrameHeader, FrameObjectV1, FrameObjectsV1,
FrameReceiptFieldsV1, RepositoryCreateV1, TransactionFramePayloadV1,
};
use crate::index::{
delta_pressure, DeltaPressure, IndexDelta, IndexKey, IndexLocation, IndexRun, IndexRunBuilder,
NamespaceLifecycle, NamespaceRecord, NamespaceStorageMode,
};
use crate::journal::{GroupBuilder, Journal};
use crate::options::StoreOptions;
use crate::recovery::{RecoveredShard, RecoveryConfig, RecoverySession};
use crate::roots::{
CommittedRoot, GenerationId, GenerationMap, LayeredObjectIndex, OperationKey,
OperationStatusMetricSnapshot, OperationStatusMetrics, OperationStatusRoot, RepoMap, RepoState,
RetainedGeneration, RetainedIndexRun, RetainedReceipt, ShardSequenceMap, ShardSubtree,
StatusEntry, StatusReservation, TerminalStatusEntry, TerminalStatusMap, TypedRefMap,
};
use crate::segment::{self, RootLayout};
use crate::snapshot::RepoSnapshot;
use crate::staging::ProjectionStaging;
use crate::transaction::ValidatedTransaction;
use crate::types::{
CommitEvidenceSigner, CommitReceipt, DurabilityCounterSnapshot, DurabilityCounters,
NamespaceId, OperationId, PendingPhase, StoreError, TransactionStatus,
};
/// The durable transaction service.
///
/// Owns one `ArcSwap<CommittedRoot>` and one bounded
/// `ArcSwap<OperationStatusRoot>`; neither uses `RwLock<Arc<_>>` (plan §5.1).
/// The committed-root swap is the visibility boundary.
pub struct StoreEngine {
shared: Arc<EngineShared>,
shards: Vec<ShardHandle>,
}
/// A held checkpoint export lease. Pins the referenced generation's segments,
/// indexes, and checkpoints against reclamation until dropped.
pub struct CheckpointLease {
_private: (),
}
impl std::fmt::Debug for StoreEngine {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter
.debug_struct("StoreEngine")
.field("root", &self.shared.options.root)
.field("shard_count", &self.shared.shard_count)
.finish_non_exhaustive()
}
}
/// Everything a shard writer and a reader share.
///
/// The [`RecoverySession`] is in here rather than in `StoreEngine` for one
/// reason: it holds the root-wide `LOCK`, and it must stay held for exactly as
/// long as anything in this process can write to the root. Tying its lifetime
/// to the shared state rather than to the handle makes "the lock outlives every
/// writer" a fact about ownership instead of a rule about drop order.
struct EngineShared {
options: StoreOptions,
signer: Arc<dyn CommitEvidenceSigner>,
shard_count: u16,
root_uuid: [u8; 16],
committed: ArcSwap<CommittedRoot>,
status: ArcSwap<OperationStatusRoot>,
status_metrics: OperationStatusMetrics,
/// The one `ProjectionStaging` for this root.
///
/// Constructed exactly once, by [`StoreEngine::open`], under the already
/// held [`RecoverySession`], and kept alive for exactly as long as that
/// session holds `LOCK`. Staging's ceilings are root-global, and a second
/// handle built outside the lock would carry its own independent accounting
/// — two handles, or two processes, each admitting up to the full global
/// limit. Ownership here is what makes "root-global" true rather than
/// asserted.
_staging: Arc<ProjectionStaging>,
_session: RecoverySession,
/// What startup state 1 actually cost, or `None` when this open did not
/// initialize.
///
/// Charter item 7: "state 1 created the tree and fsynced it" is otherwise
/// a claim about [`initialize_root_state_1`] rather than an observation. A
/// test that opens an absent root can read the directory fsyncs the
/// initialization performed instead of inferring them from the tree it
/// left behind — and, just as importantly, an open of state 2 can assert
/// this is `None`, which is the only direct evidence that reopening a
/// formatted root writes nothing.
///
/// `#[cfg(test)]` because exposing it is a frozen-surface change; see the
/// interface request in this deliverable's report.
#[cfg(test)]
initialization: Option<DurabilityCounterSnapshot>,
/// What recovery concluded about each shard during this open, in shard
/// order.
///
/// The engine consumes these reports and keeps only their effects, which
/// is right for production and leaves nothing to compare against the drive
/// seam — `DriveRecovery` retains its report precisely so the two can be
/// compared, and the engine side had no counterpart. `#[cfg(test)]` for
/// the same reason as [`EngineShared::initialization`].
#[cfg(test)]
recovery_reports: Vec<crate::recovery::ShardRecoveryReport>,
/// Observed committed-root CAS contention.
///
/// Charter item 7: the re-merge path is otherwise only reachable as a race,
/// so a test that claims to have exercised it has to be able to count it.
#[cfg(test)]
root_cas_retries: std::sync::atomic::AtomicU64,
}
struct ShardHandle {
/// `Option` so `Drop` can close the channel before joining. A shard thread
/// exits on disconnect, so dropping the sender is the shutdown signal.
submissions: Option<Sender<Submission>>,
thread: Option<JoinHandle<()>>,
/// **Not reachable from outside the crate yet.** The counters are the only
/// witness for "one fence per group", and B4's harness will need them
/// through the public API; adding a public accessor is a frozen-surface
/// change and therefore an interface request, not an edit. In-crate tests
/// read them today.
#[cfg_attr(not(test), allow(dead_code))]
counters: Arc<DurabilityCounters>,
}
struct Submission {
transaction: ValidatedTransaction,
completion: SharedCompletion,
}
impl StoreEngine {
/// Open or initialize a store root.
///
/// Handles plan §5.2's four startup states explicitly and in order, with
/// no inference: absent-or-empty initializes; a valid `FORMAT` opens
/// through production recovery; a recognized non-empty legacy layout
/// without `FORMAT` is refused with the exact `migrate-store` command; and
/// every other non-empty unrecognized layout is refused without
/// modification. It never infers legacy state merely from a missing
/// marker.
///
/// **This slice implements states 1 and 2**, including state 1's
/// crash-resume shape: a root whose initialization was interrupted is
/// recognized by the marker this path installs before it builds anything
/// and is finished rather than refused
/// ([`RootStartupState::InterruptedInitialization`]). States 3 and 4 are
/// still refused by name below, after a read-only classification and before
/// anything is written, so a caller cannot mistake "not built yet" for
/// "your root is broken". See [`classify_root`] for why the classification
/// itself creates nothing.
///
/// Recovery runs through **one** [`RecoverySession`] for every shard, and
/// that session then moves into the engine. `LOCK` is root-wide: taking
/// and dropping one lock per shard would leave a window between two shard
/// recoveries, and another between the last recovery and readiness, in
/// which a second process could enter and invalidate the root this one is
/// about to publish.
pub fn open(options: StoreOptions) -> Result<Self, StoreError> {
// Configuration is validated first: an invalid configuration must fail
// startup regardless of what is on disk, and validating after touching
// the root would make the refusal depend on the root's state.
options.validate()?;
// Hoisted above every root access on purpose. This used to run after
// the `FORMAT` probe, which was harmless while state 1 refused — but
// now that an absent root is *initialized*, a signerless configuration
// reaching this point would have already created a tree, written
// `FORMAT`, and fsynced the parent before failing. A configuration
// error must not be able to leave a root behind.
let signer = options.signer.clone().ok_or_else(|| {
StoreError::InvalidConfiguration(
"a CommitEvidenceSigner must be registered by instance composition before the \
engine can sequence a transaction"
.into(),
)
})?;
let layout = RootLayout::new(&options.root);
#[cfg_attr(not(test), allow(unused_variables))]
let initialization =
match classify_root(&layout)? {
// Both reach the same code because they are the same state: a root
// that has never finished initializing. The second one only carries
// residue this code path wrote, and `initialize_root_state_1`
// re-decides which it is under the lock before it writes anything.
RootStartupState::AbsentOrEmpty | RootStartupState::InterruptedInitialization => {
Some(initialize_root_state_1(&layout, &options)?)
}
RootStartupState::Formatted => None,
// States 3 and 4 share one refusal because telling them apart is
// the legacy recognizer, and returning `UnrecognizedLayout` for a
// root that is in fact a legacy instance would tell an operator
// holding real data "refusing to modify" instead of the migration
// command. Nothing here has written, probed, or inferred.
RootStartupState::NonEmptyWithoutFormat => return Err(StoreError::NotImplemented(
"StoreEngine::open startup states 3 and 4 (LegacyLayout carrying the exact \
migrate-store command, UnrecognizedLayout) — B1 NamespaceTxn, scope 6-B1 \
deliverable 1. The root is non-empty and carries no FORMAT; it has not been \
modified",
)),
};
let session = RecoverySession::open(&options.root)?;
if session.shard_count() != options.shard_count {
// Scope 2.5: the topology is frozen into FORMAT for the life of
// the root. A silent reroute would move repositories between
// shards, and per-repository sequence ownership is only sound
// while a repository's shard assignment never moves.
return Err(StoreError::FormatMismatch(format!(
"root was initialized with {} shards but this process is configured for {}",
session.shard_count(),
options.shard_count
)));
}
// The single `ProjectionStaging` for this root, built here and nowhere
// else. It is constructed *after* the session has taken `LOCK` and
// *before* any shard is recovered, because it is recovery's projection
// resolver: a committed staged-install frame cannot become ready
// without it, and a resolver constructed after recovery would be a
// resolver recovery never had. It then lives in `EngineShared` for
// exactly as long as the session does.
let staging_durability = Arc::new(DurabilityCounters::default());
let staging =
ProjectionStaging::open(&session, options.clone(), Arc::clone(&staging_durability))?;
let config = RecoveryConfig::from_store_options(&options)
.with_projection_recovery_resolver(staging.as_ref());
let mut recovered = Vec::with_capacity(session.shard_count() as usize);
for shard in 0..session.shard_count() {
recovered.push(session.recover_shard(shard, &config)?);
}
drop(config);
// Scope 3.8 step 12. Readiness is a computed field of each shard's
// report; a shard that is not ready is not a shard this engine may
// publish a root from.
for shard in &recovered {
if !shard.report.ready {
return Err(StoreError::RecoveryRequired);
}
}
let root = initial_committed_root(&recovered)?;
let shared = Arc::new(EngineShared {
shard_count: session.shard_count(),
root_uuid: session.root_uuid(),
signer,
committed: ArcSwap::from(Arc::new(root)),
status: ArcSwap::from(Arc::new(OperationStatusRoot::new(
options.max_status_entries,
))),
status_metrics: OperationStatusMetrics::default(),
options,
_staging: staging,
_session: session,
#[cfg(test)]
initialization,
#[cfg(test)]
recovery_reports: recovered.iter().map(|shard| shard.report.clone()).collect(),
#[cfg(test)]
root_cas_retries: std::sync::atomic::AtomicU64::new(0),
});
let mut shards = Vec::with_capacity(recovered.len());
for shard in recovered {
shards.push(spawn_shard_writer(Arc::clone(&shared), shard)?);
}
Ok(Self { shared, shards })
}
/// Acquire exactly one committed root and derive a repository view from
/// it. Readers never observe an index entry newer than their captured
/// root (plan §5.3).
pub fn snapshot(&self, _repo: NamespaceId) -> Result<RepoSnapshot, StoreError> {
Err(StoreError::NotImplemented(
"StoreEngine::snapshot — B1 NamespaceTxn, scope 6-B1 deliverable 8",
))
}
/// Submit a validated transaction for sequencing, group append, fence, and
/// publication.
///
/// Returns only after the durable sequence is fenced and the committed
/// root published. A success response means every reachable object and ref
/// in the receipt survives immediate power loss (plan §4 transaction
/// invariant 3).
pub async fn submit(&self, txn: ValidatedTransaction) -> Result<CommitReceipt, StoreError> {
let shard = StoreOptions::shard_of(&txn.namespace, self.shared.shard_count);
let handle = self
.shards
.get(shard as usize)
.ok_or_else(|| StoreError::Corruption(format!("shard {shard} has no writer")))?;
let sender = handle.submissions.as_ref().ok_or(StoreError::NotReady)?;
let completion = SharedCompletion::new();
let waiter = completion.subscribe();
let submission = Submission {
transaction: txn,
completion,
};
// The queue bound is backpressure, not a wait. Blocking the caller's
// task inside a crate that starts no runtime would hand the store a
// scheduling decision that belongs to its embedder (plan §7).
match sender.try_send(submission) {
Ok(()) => {}
Err(TrySendError::Full(_)) => {
return Err(StoreError::Overloaded {
limit: "shard_submission_queue",
retry_after_micros: self
.shared
.options
.max_group_idle
.as_micros()
.try_into()
.unwrap_or(u64::MAX),
})
}
// The writer thread is gone, which in this slice means the process
// is shutting down or the thread panicked inside the poison
// window. Either way nothing further can be appended.
Err(TrySendError::Disconnected(_)) => return Err(StoreError::NotReady),
}
let receipt = waiter.await?;
// Scope 6.3 step 10. A failure here is not a failure of the
// transaction: the fence succeeded and the root published, so the
// receipt stays queryable through `transaction_status` whatever this
// returns. The failpoint is evaluated only on the success path,
// because it names the moment a receipt is handed back and a request
// that already failed has no receipt to lose.
match failpoints::hit(Failpoint::BeforeResponse) {
FailpointAction::Continue => Ok(receipt),
FailpointAction::Fail => Err(StoreError::Io(Arc::new(std::io::Error::other(
"failpoint BeforeResponse: the response was lost after publication; the \
transaction is committed and its receipt remains queryable",
)))),
FailpointAction::Panic => {
panic!("failpoint BeforeResponse panicked the responding task")
}
FailpointAction::HardExit => {
unreachable!("HardExit calls _exit(3) inside failpoints::hit and never returns")
}
}
}
/// Linearizable status lookup across the two roots.
///
/// Plan §5.1: load committed root A and return any receipt or expired
/// tombstone; otherwise load the status root and return any pending or
/// resolving entry; if no status entry exists, load committed root B,
/// return any terminal entry found there, and otherwise `Unknown`. The
/// mandatory B read closes the old-committed/new-empty-status race without
/// requiring A and B to share a generation.
///
/// The three loads are `Acquire` (`ArcSwap::load`), and the writer publishes
/// the committed root before it removes a status entry. So a reader that
/// observes the status root *without* an entry has synchronized with the
/// store that removed it, and therefore cannot then read a committed root
/// older than the publication that preceded that removal. That is what
/// makes the B read a proof rather than a retry.
///
/// Time-based promotion of a retained receipt to `Expired` and then to
/// `Unknown` (`oracle::retained_terminal_status`) is B1 deliverable 7; this
/// read reports the terminal row the root actually holds.
pub fn transaction_status(
&self,
repo: NamespaceId,
operation: OperationId,
) -> Result<TransactionStatus, StoreError> {
let key = OperationKey::new(repo, operation);
Ok(two_root_status_read(
self.shared.committed.load().terminal_status(&key).cloned(),
self.shared.status.load().get(&key).copied(),
// Evaluated eagerly and deliberately: the B load must happen after
// the status load in program order, and a lazily evaluated
// argument would let a future refactor skip it.
self.shared.committed.load().terminal_status(&key).cloned(),
))
}
/// Force a checkpoint and hold a lease on the resulting generation.
pub fn checkpoint(&self) -> Result<CheckpointLease, StoreError> {
Err(StoreError::NotImplemented(
"StoreEngine::checkpoint — B1 NamespaceTxn, scope 6-B1",
))
}
/// Durability counters for one shard, so "one fence per group" and "no
/// per-object fsync" are assertions against observed syscalls rather than
/// claims about this file (scope 2.3, charter item 7).
///
/// Returns a **snapshot**, never the live `Arc<DurabilityCounters>`. The
/// counter object is a set of mutable atomics: handing it out would let a
/// consumer — including the harness whose whole job is to measure this
/// store — add to the very numbers it reports, and a durability claim that
/// its own auditor can write to is not evidence. `None` names a shard index
/// this engine has no writer for, which is a caller error rather than a
/// zeroed reading.
pub fn durability_counters(&self, shard: u16) -> Option<DurabilityCounterSnapshot> {
self.shards
.get(shard as usize)
.map(|handle| handle.counters.snapshot())
}
/// Status-root occupancy and refusal counts, root-wide.
///
/// Read-only for the same reason as [`StoreEngine::durability_counters`].
/// Occupancy is the size of the status root as of the last successful CAS
/// by any writer, and `rejections` counts operations refused by the
/// decision-9.8 capacity bound.
pub fn operation_status_metrics(&self) -> OperationStatusMetricSnapshot {
self.shared.status_metrics.snapshot()
}
/// What index maintenance has done and what it still owes, root-wide.
///
/// Both fields come from **one** captured `CommittedRoot`. Reading them from
/// two loads would let a caller see a run count from one publication beside a
/// backlog from another — and the pair is only meaningful together, because
/// the whole claim of a seal is that runs went up as the backlog went down.
/// Two loads could show both rising, which never happens in any single root.
///
/// A snapshot of immutable counts, not a counter handle, for the reason
/// [`StoreEngine::durability_counters`] gives: the harness that reports these
/// numbers must not be able to write to them.
pub fn index_maintenance(&self) -> IndexMaintenanceSnapshot {
let root = self.shared.committed.load();
IndexMaintenanceSnapshot {
sealed_runs: root.index().sealed_run_count() as u64,
unsealed_delta_layers: root.index().delta_layer_count() as u64,
}
}
}
/// Sealed runs and unsealed backlog, as of one committed root.
///
/// `unsealed_delta_layers` is the count of delta layers no sealed run covers
/// yet — the backlog a seal removes. Zero after a seal that covered everything
/// published; nonzero whenever groups have committed since.
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub struct IndexMaintenanceSnapshot {
pub sealed_runs: u64,
pub unsealed_delta_layers: u64,
}
/// Plan §5.1's two-root read, written once so both the engine and its test can
/// only ever agree with `oracle::two_root_status_read`.
fn two_root_status_read(
committed_a: Option<TerminalStatusEntry>,
status: Option<StatusEntry>,
committed_b: Option<TerminalStatusEntry>,
) -> TransactionStatus {
if let Some(entry) = committed_a {
return entry.transaction_status();
}
if let Some(entry) = status {
return entry.transaction_status();
}
match committed_b {
Some(entry) => entry.transaction_status(),
None => TransactionStatus::Unknown,
}
}
impl Drop for StoreEngine {
fn drop(&mut self) {
// Close every channel first, then join. A shard thread publishes its
// open group on disconnect, so a transaction already accepted by a
// writer is fenced and published before this returns rather than
// abandoned with its waiter attached.
for shard in &mut self.shards {
shard.submissions = None;
}
for shard in &mut self.shards {
if let Some(thread) = shard.thread.take() {
let _ = thread.join();
}
}
}
}
// ===========================================================================
// Startup: classifying the root (plan §5.2, scope 3.1)
// ===========================================================================
/// Which of plan §5.2's startup states a root is in, decided by reading and
/// nothing else.
///
/// States 3 and 4 share a variant because this pass does not implement the
/// legacy recognizer that separates them. That is a deliberate under-
/// classification, not a catch-all: the variant is named for exactly the
/// condition it holds — non-empty, no `FORMAT` — and `open` refuses it by
/// naming both unbuilt states. Charter item 6 forbids a `_ =>` arm; it does
/// not require inventing a distinction whose recognizer has not been written.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum RootStartupState {
/// §5.2 state 1. The path does not exist, or it is a directory holding
/// nothing that this store did not create before it had decided anything.
AbsentOrEmpty,
/// §5.2 state 2. A `FORMAT` entry exists. Whether it *validates* is
/// deliberately not decided here; see [`classify_root`].
Formatted,
/// §5.2 state 1, interrupted. The root carries this store's own
/// initialization marker and nothing outside the skeleton that
/// [`segment::initialize_root`] builds, so it is a root that started
/// initializing and never finished.
///
/// Not a fifth plan state: it is state 1 observed part-way through, and it
/// resolves to the same action state 1 takes. It exists as its own variant
/// because the *evidence* is different — an empty root proves nothing has
/// been written, this one proves that what was written was written by this
/// code path — and because charter item 6 requires the condition that
/// authorizes a write to be named rather than folded into a neighbour.
InterruptedInitialization,
/// §5.2 states 3 and 4, undivided this pass.
NonEmptyWithoutFormat,
}
// ---------------------------------------------------------------------------
// The initialization marker (plan §5.2 state 1, crash-resume)
// ---------------------------------------------------------------------------
/// The name of the marker that says "this root is mid-initialization".
///
/// Computed from `layout.root` here rather than added to [`RootLayout`],
/// because `RootLayout` is A1's frozen surface. Giving the name one definition
/// there is filed as an interface request; until it moves, this is the only
/// place in the crate that spells it.
const INITIALIZING_NAME: &str = "INITIALIZING";
/// The staging name the marker is installed from, so the marker itself is
/// never observed torn.
const INITIALIZING_TMP_NAME: &str = "INITIALIZING.tmp";
/// First bytes of the marker. The whole point of the classification is that
/// this cannot be produced by accident: a directory that merely *looks* like an
/// abandoned root has no reason to contain a file whose first sixteen bytes are
/// this string.
const INITIALIZING_MAGIC: &[u8; 16] = b"levcs-store-init";
/// 16 magic + 1 version + 2 shard_count + 8 created_at_micros.
const INITIALIZING_MARKER_LEN: usize = 27;
/// The exact bytes of the marker for one initialization attempt.
///
/// `shard_count` and `created_at_micros` are **evidence, not input**. A resume
/// restarts from the configuration the resuming process was given, not from the
/// one recorded here — the interrupted attempt published nothing, so it has no
/// claim on the topology. They are recorded so that an operator looking at
/// residue can see what the interrupted attempt intended, which is the one
/// question the file's existence alone cannot answer.
fn initializing_marker_bytes(shard_count: u16, created_at_micros: i64) -> [u8; 27] {
let mut bytes = [0u8; INITIALIZING_MARKER_LEN];
bytes[..16].copy_from_slice(INITIALIZING_MAGIC);
bytes[16] = 1;
bytes[17..19].copy_from_slice(&shard_count.to_le_bytes());
bytes[19..27].copy_from_slice(&created_at_micros.to_le_bytes());
bytes
}
/// Open `path` if and only if it is a **regular file holding exactly this code
/// path's marker**, and return the open descriptor so a caller that then wants
/// to act on those bytes acts on the object it validated rather than on the
/// name a second time.
///
/// Four conditions, all necessary, none inferred from the name:
///
/// 1. **Regular file, not a symlink** — [`crate::sys::open_regular_nofollow`]
/// opens with `O_NOFOLLOW` and checks the type through `fstat` on the
/// descriptor it got. A symlink here is not "our marker whose bytes live
/// elsewhere", it is somebody else's name, and following it would let a link
/// planted in the root decide what this process reads and (see
/// [`install_initializing_marker`]) writes.
/// 2. **Exactly [`INITIALIZING_MARKER_LEN`] bytes** — one byte more is read
/// than the marker occupies, so a longer file is rejected by the same
/// comparison that rejects a shorter one.
/// 3. **[`INITIALIZING_MAGIC`]**, which nothing else in this crate and nothing
/// in a legacy layout writes.
/// 4. **A version this build knows.**
///
/// `Ok(None)` for every failure, including absence: the caller has already seen
/// the name in a `read_dir` that may be a moment stale, and a marker that
/// vanished between the scan and this read is a root with no marker. Every one
/// of these answers means the same thing to every caller — *these are not our
/// bytes* — so they are one variant rather than a distinction no caller acts
/// on.
fn open_exact_marker(path: &std::path::Path) -> Result<Option<std::fs::File>, StoreError> {
let file = match crate::sys::open_regular_nofollow(path)? {
Some(file) => file,
None => return Ok(None),
};
// One byte more than the marker, so a longer file is rejected by the same
// length comparison that rejects a shorter one.
let mut bytes = [0u8; INITIALIZING_MARKER_LEN + 1];
let read = crate::sys::pread(&file, 0, &mut bytes)?;
if read == INITIALIZING_MARKER_LEN && &bytes[..16] == INITIALIZING_MAGIC && bytes[16] == 1 {
Ok(Some(file))
} else {
Ok(None)
}
}
/// Whether `<root>/INITIALIZING` is this code path's marker.
///
/// Judged on content and file type, never on the name alone. A file of any
/// other length, with any other first sixteen bytes, or that is not a regular
/// file at all, is somebody else's and is treated as unrecognized content — the
/// direction that wastes an operator's time rather than the one that destroys
/// their data.
fn initializing_marker_is_ours(layout: &RootLayout) -> Result<bool, StoreError> {
Ok(open_exact_marker(&layout.root.join(INITIALIZING_NAME))?.is_some())
}
/// Decide the startup state of `layout.root` **without creating, modifying, or
/// removing anything**.
///
/// One `read_dir` and a name comparison. No `create_dir_all`, no `File::open`
/// with `create`, no lock — a probe that creates a directory has already
/// broken state 4's byte-identity guarantee before the refusal it exists to
/// support is even reached, and there would be no test that could tell.
///
/// # The names this function has to rule on
///
/// - **`FORMAT` present.** State 2, on the strength of the *name* alone. A
/// `FORMAT` that fails to decode is still `FORMAT`: it is not evidence of an
/// empty root, and treating it as one would let a corrupt marker authorize
/// `initialize_root` to build a fresh tree over a populated one. Validation
/// is [`segment::read_format`]'s job inside [`RecoverySession::open`], where
/// failing is a refusal and never a rebuild.
/// - **`LOCK`, present as a regular file.** Ignored. It is not store data and
/// it is not evidence that anything has been decided: `LOCK` is a zero-length
/// file that [`segment::lock_root`] creates if absent, as a side
/// effect of *asking* whether the root is busy, so any process that merely
/// probed the root leaves one. Treating it as state 4 would permanently wedge
/// a root that has no bytes to lose.
/// - **`INITIALIZING.tmp`, present as a regular file holding exactly the
/// marker.** Ignored, on the same reasoning and on nothing weaker. It is the
/// staging name the marker below is installed from, and it normally exists
/// only between a fenced write and the `rename_noreplace` that consumes it.
/// - **`INITIALIZING` present as a regular file, and every other entry inside
/// the initialization skeleton.**
/// [`RootStartupState::InterruptedInitialization`] — see below.
/// - **Anything else.** State 3 or 4, refused without modification. Including
/// `FORMAT.tmp` on its own: it is residue this code path writes, but by
/// itself it is residue from *after* the marker was installed and then
/// removed, which cannot happen — a `FORMAT.tmp` with no marker beside it is
/// somebody else's file.
///
/// # Why a name is never enough, and why the *type* of every entry is checked
///
/// Contract review finding P1 (2026-07-29). This function used to ignore
/// `INITIALIZING.tmp` **whatever it was**, on the argument that the name can
/// only be this code path's residue by construction — and
/// [`build_root_under_lock`] then opened it with `create` + `truncate`. The
/// argument is not available here, twice over. It is the very claim the
/// classification is running in order to establish, and before the store owns
/// the root it has no standing to assume anything about what is in it. An
/// operator's regular file of that name in an otherwise-empty configured root
/// was destroyed silently; a **symlink** of that name was followed, so the
/// truncation landed on a file outside the root entirely, and the link was then
/// renamed away and unlinked so the evidence went with it.
///
/// So every entry is judged by `DirEntry::file_type`, which reports the type of
/// the directory entry itself and never of a symlink's target, and the two
/// names whose contents can authorize anything are additionally opened with
/// `O_NOFOLLOW` and validated ([`open_exact_marker`]). A symlink at `LOCK`,
/// `INITIALIZING`, `INITIALIZING.tmp`, or `FORMAT.tmp`, and a directory or fifo
/// at any of them, is a foreign entry and disqualifies the root. `FORMAT` is
/// still decided on its name alone, which stays safe for the opposite reason:
/// nothing on the state-2 path ever creates or truncates it — `initialize_root`
/// installs it only by `rename_noreplace`, which refuses an occupied name
/// including a symlink — so the worst a planted `FORMAT` can do is be read and
/// fail to decode, which is a refusal.
///
/// # The cost of this, stated rather than discovered
///
/// A torn or partially written `INITIALIZING.tmp` — a crash between its
/// creation and its fence — is now **refused** rather than overwritten, and
/// that refusal is permanent until an operator removes the file. That is the
/// intended trade and it is the same asymmetry the rest of this classification
/// is built on: refusing a resumable root costs an operator time, and
/// overwriting a real file costs them data. There is no test that can tell a
/// torn marker of ours from a stranger's truncated file, because there is no
/// difference between them on disk.
///
/// # Why an interrupted initialization is recognized, and how it cannot be
/// confused with a root that merely looks abandoned
///
/// [`segment::initialize_root`] creates `quarantine/`, `staging/`, `shards/`
/// and the per-shard tree *before* it installs `FORMAT`. A crash anywhere in
/// that window used to leave a non-empty root with no `FORMAT`, which this
/// function classified as states 3/4 and `open` refused — permanently, with a
/// message about legacy layouts and migration, for a root that had never held
/// a byte of anyone's data. That is why [`initialize_root_state_1`] installs
/// `INITIALIZING` as its first durable act and removes it as its last.
///
/// Two independent conditions must both hold before this returns
/// `InterruptedInitialization`, and each alone is enough to exclude a genuine
/// unrecognized layout:
///
/// 1. **`INITIALIZING` decodes as this code path's marker** — a regular file of
/// exactly [`INITIALIZING_MARKER_LEN`] bytes, opened `O_NOFOLLOW`, opening
/// with [`INITIALIZING_MAGIC`] and a version this build knows. Nothing else
/// in this crate, and nothing in a legacy layout, writes that file. A
/// human's note that happens to be named `INITIALIZING` fails on content.
/// 2. **Every entry in the root is inside the initialization skeleton, with the
/// type this store creates it as** — `LOCK`, `INITIALIZING`,
/// `INITIALIZING.tmp` and `FORMAT.tmp` as regular files, `quarantine`,
/// `staging` and `shards` as directories, and nothing else. A single foreign
/// name — a legacy `<64-hex>/` repository directory, a `notes.txt`, anything
/// an operator put there — disqualifies the root however convincing the
/// marker is, and so does a right name of the wrong type.
///
/// So mistaking a real root for an abandoned one requires an operator to have
/// placed a byte-exact copy of this marker into a directory that otherwise
/// contains only names this store invented. The asymmetry is deliberate: the
/// failure this can produce is a refusal that wastes time, and the failure it
/// cannot produce is initializing over somebody's data.
///
/// The ordering makes the converse hole impossible too. The marker is fenced,
/// and the root directory fsynced, *before* the first directory of the tree is
/// created — so a durable partial tree always has a durable marker beside it,
/// and there is no crash point that leaves tree residue this function would
/// refuse.
///
/// A missing directory and an empty directory are the same state and are
/// deliberately not distinguished. That is now true because
/// [`initialize_root_state_1`] fences every directory entry it creates on the
/// way down to the root, not because `initialize_root` fsyncs `root.parent()`:
/// that parent fsync covers exactly one link, and a root reached through
/// several newly created ancestors has several.
fn classify_root(layout: &RootLayout) -> Result<RootStartupState, StoreError> {
let entries = match std::fs::read_dir(&layout.root) {
Ok(entries) => entries,
Err(error) if error.kind() == std::io::ErrorKind::NotFound => {
return Ok(RootStartupState::AbsentOrEmpty)
}
// Every other failure — `ENOTDIR` because the configured root is a
// regular file, `EACCES`, `ELOOP` — is reported as itself. A root this
// process cannot read is not a root it may conclude is empty, and the
// refusal has modified nothing.
Err(error) => return Err(StoreError::from(error)),
};
let mut has_format = false;
let mut has_marker = false;
// Anything that is neither `FORMAT` nor an ignorable probe artifact.
// Non-empty in the sense states 3 and 4 mean it.
let mut has_content = false;
// Anything outside the closed set of names an initialization can leave.
let mut has_foreign = false;
for entry in entries {
let entry = entry?;
let name = entry.file_name();
// `DirEntry::file_type` is the type of the entry itself: it is `lstat`,
// never `stat`, so a symlink reports as a symlink and this loop cannot
// be made to describe an object outside the root.
let kind = entry.file_type()?;
if name == std::ffi::OsStr::new("FORMAT") {
has_format = true;
continue;
}
// Probe residue, ignorable only in the exact shape the probe leaves.
if name == std::ffi::OsStr::new("LOCK") {
if kind.is_file() {
continue;
}
has_content = true;
has_foreign = true;
continue;
}
// The staging name, ignorable only when it is a regular file carrying
// this code path's marker byte for byte. Anything else at that name —
// a symlink, a directory, a stranger's file, a torn write — is foreign,
// because the alternative is deciding it is ours and then writing
// through it.
if name == std::ffi::OsStr::new(INITIALIZING_TMP_NAME) {
if kind.is_file()
&& open_exact_marker(&layout.root.join(INITIALIZING_TMP_NAME))?.is_some()
{
continue;
}
has_content = true;
has_foreign = true;
continue;
}
has_content = true;
if name == std::ffi::OsStr::new(INITIALIZING_NAME) {
// Content is checked below, once, and only if nothing foreign was
// found; the type is checked here because a non-regular entry at
// this name is foreign whatever it would have read as.
if kind.is_file() {
has_marker = true;
} else {
has_foreign = true;
}
continue;
}
// The rest of what `segment::initialize_root` creates directly in the
// root, each with the type it creates it as. Named exhaustively rather
// than pattern-matched: a name this list does not contain must
// disqualify the root, and a wildcard here is the one edit that would
// silently stop it doing so. The type is part of the name's meaning —
// a symlink called `FORMAT.tmp` is not residue, it is an instruction to
// write outside the root. `segment::write_fenced` refuses one directly
// now as well; this list is why the root is disqualified rather than
// initialized-then-refused, which is a different and better answer.
if !matches!(
(name.to_str(), kind.is_file(), kind.is_dir()),
(Some("FORMAT.tmp"), true, _)
| (Some("quarantine"), _, true)
| (Some("staging"), _, true)
| (Some("shards"), _, true)
) {
has_foreign = true;
}
}
// Order is plan §5.2's order. `FORMAT` wins over everything below it
// because state 2 is decided before any other state; the interrupted shape
// is decided next because it is state 1 and its evidence is positive; and
// "empty" is evaluated against the same scan rather than a second one, so
// no interleaved writer can make the two disagree.
if has_format {
return Ok(RootStartupState::Formatted);
}
if has_marker && !has_foreign && initializing_marker_is_ours(layout)? {
return Ok(RootStartupState::InterruptedInitialization);
}
Ok(if has_content {
RootStartupState::NonEmptyWithoutFormat
} else {
RootStartupState::AbsentOrEmpty
})
}
/// Plan §5.2 startup state 1: build the v2 tree under the root lock.
///
/// # Why the lock, and why it is taken here rather than inherited
///
/// Initializing a root is a mutation, and two processes configured for the
/// same absent path will otherwise both classify it `AbsentOrEmpty` and both
/// run [`segment::initialize_root`]. The loser's `rename_noreplace` of
/// `FORMAT` would fail, but only after it had already created a shard tree and
/// fsynced it into a root the winner had begun to consider its own. So the
/// classification is repeated **under the lock**, and it is the second answer
/// that authorizes the write. That is the whole race: `FORMAT` is installed by
/// a no-replace rename as the last durable act of initialization, so if the
/// second classification still says `AbsentOrEmpty` while this process holds
/// `LOCK`, no other process can be mid-initialization.
///
/// The lock is a [`segment::RootLock`] and is released by dropping it, which
/// issues an explicit `LOCK_UN` rather than relying on a descriptor close a
/// concurrently forked child can extend (commit `e050b6d`).
///
/// The same rule governs the resume path. Finding an `INITIALIZING` marker
/// before the lock authorizes nothing: another process may be building that
/// tree *right now*, in which case this one is refused `AlreadyLocked` and never
/// reaches the second classification. It is the classification taken **while
/// holding `LOCK`** that authorizes finishing someone else's interrupted
/// initialization, and by then the only process that could have been mid-build
/// has released the lock and is gone.
///
/// # Why the lock is then released and immediately retaken
///
/// [`RecoverySession::open`] takes `LOCK` itself, and there is no constructor
/// that adopts an already-held one — adding it is a change to A2's frozen
/// surface and is filed as an interface request, not made here. The gap is
/// safe but it is a real gap and worth stating exactly: an initialized root is
/// complete and durable the moment this returns, so a second process entering
/// the window can only take state 2, and if it does, this process's
/// `RecoverySession::open` is refused `AlreadyLocked` — the correct answer for
/// a root another process owns, not a corruption. What the window cannot
/// produce is two initializations, because the second process no longer
/// classifies the root as empty.
fn initialize_root_state_1(
layout: &RootLayout,
options: &StoreOptions,
) -> Result<DurabilityCounterSnapshot, StoreError> {
let counters = DurabilityCounters::default();
// `LOCK` lives inside the root, so the directory has to exist before it
// can be locked. This is the one write that precedes the lock, and it is
// the write state 1 is defined to perform: creating the path is
// idempotent, two racing processes both succeed, and neither has yet
// written a byte either could lose. It is reached only after the read-only
// classification said the root has never been initialized, so it never runs
// against states 2, 3, or 4.
create_and_fence_root_path(&layout.root, &counters)?;
let lock = segment::lock_root(layout)?;
match classify_root(layout)? {
// Both are state 1. The second says the marker is already on disk, so
// `build_root_under_lock` re-fences the name it finds instead of
// installing a second one.
RootStartupState::AbsentOrEmpty => {
build_root_under_lock(layout, options, false, &counters)?
}
RootStartupState::InterruptedInitialization => {
build_root_under_lock(layout, options, true, &counters)?
}
// Another process initialized this root between the first
// classification and this lock. Its tree is complete and fsynced, so
// there is nothing to do and nothing to repair; the caller falls
// straight through to state 2.
RootStartupState::Formatted => {}
// Non-empty with no `FORMAT` and no marker, discovered only under the
// lock. The refusal is the same one state 3/4 gets, and this path has
// written nothing but the directories it was told were absent.
RootStartupState::NonEmptyWithoutFormat => {
return Err(StoreError::NotImplemented(
"StoreEngine::open startup states 3 and 4 (LegacyLayout carrying the exact \
migrate-store command, UnrecognizedLayout) — B1 NamespaceTxn, scope 6-B1 \
deliverable 1. The root became non-empty without a FORMAT between \
classification and the root lock; it has not been modified",
))
}
}
drop(lock);
Ok(counters.snapshot())
}
/// Create every missing directory on the way to `root`, and fence each new
/// directory entry — deepest first.
///
/// # Why this is not `create_dir_all` followed by `initialize_root`
///
/// `create_dir_all` creates *every* missing ancestor, and
/// [`segment::initialize_root`] fences exactly one link: `root.parent()`. So a
/// configured root of `.../not/yet/a-root` under an existing directory left the
/// entries naming `not` and `yet` unfenced, while `open` returned success. A
/// power loss could then take an ancestor and the whole initialized root with
/// it — a store that reported itself initialized, reachable only through
/// directory entries no one had made durable.
///
/// # Why fencing rather than refusing an absent parent
///
/// Refusing would have been less code and is genuinely safe. It was rejected
/// because it moves a durability defect onto the operator as a configuration
/// restriction: a root path under a per-instance directory the store is
/// expected to create is the ordinary deployment shape, and `StoreEngine::open`
/// already promises to build an absent root. "Absent" would then have had to
/// mean "absent by exactly one component", which is a rule nothing on disk
/// explains and which every caller would have to learn by being refused.
///
/// # The ordering, which is the whole content of the fix
///
/// Only the directories *this call* creates are fenced, and a directory entry
/// lives in the parent that names it — so fencing directory `D` is fsyncing
/// `parent(D)`. They are fenced deepest first: a child's entry must be durable
/// before the entry naming its parent, because the reverse order can leave a
/// crash image in which a fenced parent names a child that was never fenced.
/// For `base/a/b/c` with `base` already present, that is fsync(`b`), fsync(`a`),
/// fsync(`base`) — one fsync per created directory.
///
/// `c`'s own contents are not this function's concern: it is empty here, and
/// [`segment::initialize_root`] fsyncs the root itself once it has built the
/// tree inside it.
///
/// Routed through `sys::fsync_dir` because that module is the crate's sole
/// durability funnel; a raw `File::open(..).sync_all()` here would be invisible
/// to the counters that make this fix testable at all.
fn create_and_fence_root_path(
root: &std::path::Path,
counters: &DurabilityCounters,
) -> Result<(), StoreError> {
// Deepest first. `try_exists` rather than `exists` so that a path this
// process cannot stat is an error instead of silently "missing" — creating
// over an `EACCES` would be a refusal turned into a mutation.
let mut missing: Vec<&std::path::Path> = Vec::new();
let mut cursor = Some(root);
while let Some(path) = cursor {
if path.try_exists()? {
break;
}
missing.push(path);
cursor = path
.parent()
.filter(|parent| !parent.as_os_str().is_empty());
}
if missing.is_empty() {
return Ok(());
}
// Shallowest first: a directory cannot be created before the one that names
// it. `create_dir` rather than `create_dir_all` so that what this call
// created is exactly what it fences — `create_dir_all` would silently
// absorb an ancestor another process created in the same instant and leave
// this one believing it had made the link.
for path in missing.iter().rev() {
match std::fs::create_dir(path) {
Ok(()) => {}
// A racing initializer of the same path got there first. Its own
// fence covers the link either way, and fencing it twice is
// harmless; refusing here would make two correct processes fight.
Err(error) if error.kind() == std::io::ErrorKind::AlreadyExists => {}
Err(error) => return Err(StoreError::from(error)),
}
}
for path in &missing {
let parent = path
.parent()
.filter(|parent| !parent.as_os_str().is_empty())
.unwrap_or_else(|| std::path::Path::new("."));
crate::sys::fsync_dir(parent, counters)?;
}
Ok(())
}
/// Install the initialization marker, build the tree, and remove the marker.
///
/// The order is the contract [`classify_root`] reads back:
///
/// 1. `INITIALIZING.tmp` is created with `O_EXCL`, written and fenced
/// ([`install_initializing_marker`]), renamed onto `INITIALIZING` with
/// `rename_noreplace`, and the root directory fsynced. The marker is
/// therefore never observable torn under its installed name, and it is
/// durable **before** any part of the tree exists.
/// 2. [`segment::initialize_root`] builds the tree and installs `FORMAT`.
/// 3. The marker is unlinked and the removal fenced.
///
/// A crash in step 1 leaves at most `LOCK` and `INITIALIZING.tmp`. A *whole*
/// `INITIALIZING.tmp` classifies as an empty root and is adopted by the next
/// attempt; a torn one is refused, which is the trade
/// [`install_initializing_marker`] states. A crash in step 2 leaves the marker and a partial
/// skeleton, which classify as `InterruptedInitialization` and land back here.
/// A crash in step 3 leaves a complete, valid root, which classifies as state 2
/// — the marker is then inert litter that `FORMAT` outranks, and removing it on
/// a state-2 open is rejected precisely because it would make reopening a
/// formatted root a mutation.
///
/// `resuming` says the marker is already there. It is passed rather than
/// re-derived because `rename_noreplace` onto an existing `INITIALIZING` would
/// fail, and because re-deciding it here would be a third classification that
/// could disagree with the one taken under the lock.
///
/// A resume restarts the build from the *current* configuration rather than the
/// one the marker records: the interrupted attempt published no `FORMAT`, so it
/// froze no topology and has no claim on this one. If the two disagree on
/// `shard_count`, the surplus shard directories from the wider attempt are left
/// behind — empty, referenced by no `FORMAT`, and read by nothing, since every
/// consumer iterates `0..FORMAT.shard_count`.
fn build_root_under_lock(
layout: &RootLayout,
options: &StoreOptions,
resuming: bool,
counters: &DurabilityCounters,
) -> Result<(), StoreError> {
let marker = layout.root.join(INITIALIZING_NAME);
if resuming {
// The marker exists but its *name* may not be durable: the crash could
// have landed between the rename and the fsync below. Fencing it again
// before touching the tree is what keeps "a durable partial tree always
// has a durable marker beside it" true across a second interruption.
crate::sys::fsync_dir(&layout.root, counters)?;
} else {
let tmp = layout.root.join(INITIALIZING_TMP_NAME);
let bytes = initializing_marker_bytes(options.shard_count, now_micros());
install_initializing_marker(&tmp, &bytes, counters)?;
crate::sys::rename_noreplace(&tmp, &marker)?;
crate::sys::fsync_dir(&layout.root, counters)?;
}
segment::initialize_root(
layout,
options.shard_count,
fresh_root_uuid(),
now_micros(),
counters,
)?;
// Last, and after `FORMAT` is durable. Removing it earlier would open a
// window in which the tree exists, `FORMAT` does not, and nothing says the
// residue is ours — which is the exact defect this marker was added to
// close.
crate::sys::unlink(&marker)?;
crate::sys::fsync_dir(&layout.root, counters)?;
Ok(())
}
/// Put the marker's bytes at the staging name and fence them, **without ever
/// writing through a name this process has not established is its own**. The
/// name is not durable when this returns; the caller renames and fsyncs the
/// directory.
///
/// # The two branches, and why neither of them can truncate
///
/// The ordinary case creates the name: [`crate::sys::create_new_nofollow`] is
/// `O_CREAT | O_EXCL | O_NOFOLLOW`, so it either brings a brand-new regular
/// file into existence in *this* directory or reports that the name is taken.
/// `O_CREAT | O_EXCL` is specified to fail on a symlink whether or not the link
/// resolves, so there is no arrangement of the root that can redirect this
/// write.
///
/// The name being taken is the interesting case, and this is where contract
/// review finding P1 was. What used to happen was `create` + `truncate`: an
/// operator's file at that name was destroyed, and a *symlink* at that name was
/// followed so the destruction landed on a file outside the root, after which
/// the link was renamed and unlinked and the evidence went with it. What
/// happens now is that the occupant is validated by [`open_exact_marker`] —
/// regular file, exact length, exact magic, known version — and only *then*
/// adopted, in place, by fencing the bytes that are already there. Adoption
/// writes nothing: the file already holds exactly what this call was about to
/// write, so the fence is the whole remaining act.
///
/// Adopting the occupant's bytes rather than replacing them also keeps the
/// resume rule consistent. `shard_count` and `created_at_micros` in the marker
/// are evidence of the interrupted attempt, not input to this one
/// ([`initializing_marker_bytes`]), and the resume-from-`INITIALIZING` path
/// already carries the earlier attempt's bytes forward untouched. This does the
/// same thing one step earlier.
///
/// # What is refused, and what that costs
///
/// Everything else at that name: a stranger's file, a symlink, a directory, and
/// a **torn or partially written marker** — a crash between the create and the
/// fence. The torn case is the one that costs something real: a root that was
/// previously resumable now needs an operator to remove one file before it will
/// initialize. That is the intended trade, and it is not close. Refusing a
/// resumable root costs an operator time; overwriting a real file costs them
/// their data. And nothing on disk distinguishes our torn 12-byte marker from a
/// stranger's truncated 12-byte file, so a rule that recovers the first
/// necessarily destroys the second.
///
/// # Standing disclosure
///
/// This began as a second copy of `segment::write_fenced`, which is private to
/// A1's file, and the duplication was filed as an interface request. Neither can
/// follow a symlink any more — contract review 2026-07-29-B routed
/// `write_fenced` through the same funnel — so what remains is a real difference
/// in refusal rule rather than in safety: this one refuses an existing regular
/// file, because a marker that already exists is somebody's data and adopting it
/// would be a decision about content; `write_fenced` adopts and empties one,
/// because a `.tmp` name is residue from an interrupted attempt at that exact
/// write and reusing it is how the retry works. Both are correct for their name.
/// Any hoisting therefore has to keep two rules, not pick one.
fn install_initializing_marker(
tmp: &std::path::Path,
bytes: &[u8],
counters: &DurabilityCounters,
) -> Result<(), StoreError> {
match crate::sys::create_new_nofollow(tmp)? {
Some(mut file) => {
crate::sys::write_vectored_all(&mut file, &[std::io::IoSlice::new(bytes)], counters)?;
crate::sys::fdatasync(&file, counters)?;
Ok(())
}
// Reachable in normal operation when a previous attempt crashed between
// the create and the rename, and reachable adversarially at any time,
// since the classification that ran a moment ago under `LOCK` is a
// statement about the past. Both are handled by looking, not by
// assuming.
None => match open_exact_marker(tmp)? {
Some(file) => {
crate::sys::fdatasync(&file, counters)?;
Ok(())
}
None => Err(StoreError::UnrecognizedLayout(format!(
"{}: the initialization staging name is occupied by something that is not this \
store's marker — a foreign file, a symlink, a directory, or a torn marker from \
an interrupted attempt. Refusing to write through it; nothing has been modified. \
Remove that entry to let the root initialize",
tmp.display()
))),
},
}
}
/// A fresh `root_uuid` for a root being initialized.
///
/// `FORMAT.root_uuid` binds every `CURRENT`, manifest, journal, segment, index
/// run, and checkpoint in the root (scope 3.1), and its whole job is to make a
/// file copied in from another instance detectable. So it must not be a
/// function of anything two roots can share — the path in particular, since a
/// root deleted and recreated at the same path is exactly the case this is
/// meant to catch.
///
/// **Duplication, disclosed rather than hidden:** `drive.rs` has a private
/// `fresh_id` doing the same thing for journal and root identifiers. This is a
/// second implementation of "derive 16 bytes with enough entropy to be unique
/// per process and per call", and it is here because `drive.rs` is A1's file
/// and is `#[cfg(feature = "store-internals")]`, so neither borrowing it nor
/// editing it is available to this deliverable. Hoisting one into `sys` is
/// filed as an interface request.
fn fresh_root_uuid() -> [u8; 16] {
use std::sync::atomic::AtomicU64;
static COUNTER: AtomicU64 = AtomicU64::new(0);
let stack = 0u8;
let mut hasher = blake3::Hasher::new();
hasher.update(b"levcs-store-root-uuid/v1\0");
hasher.update(&now_micros().to_le_bytes());
hasher.update(&COUNTER.fetch_add(1, Ordering::Relaxed).to_le_bytes());
hasher.update(&std::process::id().to_le_bytes());
hasher.update(&(&stack as *const u8 as usize).to_le_bytes());
let mut id = [0u8; 16];
id.copy_from_slice(&hasher.finalize().as_bytes()[..16]);
id
}
// ===========================================================================
// Startup: the recovered state becomes the first committed root
// ===========================================================================
fn initial_committed_root(shards: &[RecoveredShard]) -> Result<CommittedRoot, StoreError> {
let mut repositories = RepoMap::new();
let mut terminal_statuses = TerminalStatusMap::new();
let mut shard_committed_sequences = ShardSequenceMap::new();
let mut retained_generations = GenerationMap::new();
let mut delta_layers_newest_first = Vector::new();
let mut sealed_runs_newest_first = Vector::new();
for shard in shards {
for (namespace, record) in shard.catalog.iter() {
repositories.insert(
*namespace,
Arc::new(repo_state_from_recovery(record, &shard.refs)?),
);
}
for receipt in &shard.receipts {
terminal_statuses.insert(
OperationKey::new(receipt.namespace, receipt.operation_id),
TerminalStatusEntry::Committed(retained_receipt(receipt)?),
);
}
if let Some(sequence) = shard.committed_shard_sequence {
shard_committed_sequences.insert(shard.shard_index, sequence);
}
// The generation, not its name: recovery opened these artifacts, and
// handing over a path instead of the live reference would leave a
// window in which nothing holds them and reclamation is legal.
retained_generations.insert(
shard.retained_generation.id,
Arc::clone(&shard.retained_generation),
);
for layer in shard.index.delta_layers() {
delta_layers_newest_first.push_back(layer.clone());
}
for run in shard.index.sealed_runs() {
sealed_runs_newest_first.push_back(Arc::clone(run));
}
}
Ok(CommittedRoot::new(
repositories,
LayeredObjectIndex::new(delta_layers_newest_first, sealed_runs_newest_first),
terminal_statuses,
shard_committed_sequences,
retained_generations,
))
}
fn repo_state_from_recovery(
record: &NamespaceRecord,
refs: &[RefRecord],
) -> Result<RepoState, StoreError> {
let mut typed_refs = TypedRefMap::new();
for reference in refs.iter().filter(|r| r.namespace == record.namespace) {
// Contract review 2026-07-28-A item 4: the ref-kind table is stated
// once, in `checkpoint.rs`, in each direction. This file used to carry
// a third copy that agreed with it today.
typed_refs.insert(reference.target()?, reference.target);
}
Ok(RepoState {
repo_sequence: record.repo_sequence,
current_authority: record.current_authority,
genesis_authority: record.genesis_authority,
refs: typed_refs,
lifecycle: record.lifecycle,
storage_mode: record.storage_mode,
previous_event_digest: record.previous_event_digest,
})
}
fn retained_receipt(record: &ReceiptRecord) -> Result<RetainedReceipt, StoreError> {
// Recovery step 10 has already promoted every row, so `None` here means a
// receipt reached the root without passing through the promotion that
// decides its retention. That is a store-consistency failure, not a value
// to substitute.
let first_visible_at_micros = record.first_receipt_visibility_micros.ok_or_else(|| {
StoreError::Corruption(format!(
"recovered receipt for operation {} has no first-visibility instant",
record.operation_id.to_hex()
))
})?;
Ok(RetainedReceipt {
operation_digest: record.operation_digest,
receipt: CommitReceipt {
operation_id: record.operation_id,
repo_sequence: record.repo_sequence,
current_authority: record.current_authority,
refs: record.refs.clone(),
objects_new: record.objects_new,
},
shard_sequence: record.shard_sequence,
retry_until_micros: record.retry_until_micros,
first_visible_at_micros,
receipt_visible_until_micros: record.receipt_visible_until_micros,
})
}
fn spawn_shard_writer(
shared: Arc<EngineShared>,
recovered: RecoveredShard,
) -> Result<ShardHandle, StoreError> {
let shard_index = recovered.shard_index;
let tail = recovered.tail.as_ref().ok_or_else(|| {
StoreError::Corruption(format!(
"shard {shard_index} reported ready with no active journal"
))
})?;
let counters = Arc::new(DurabilityCounters::default());
let (journal, _scan) = Journal::open(&tail.path, &shared.root_uuid, Arc::clone(&counters))?;
// The journal recovery installed is empty, so its next sequence is the one
// after the committed tail. Checking rather than assuming: a mismatch here
// would silently append into a sequence range recovery already adopted.
let expected_next = recovered
.committed_shard_sequence
.map(|sequence| sequence.saturating_add(1))
.unwrap_or(0);
if journal.next_shard_sequence() != expected_next {
return Err(StoreError::Corruption(format!(
"shard {shard_index} recovered through sequence {expected_next} but its active \
journal resumes at {}",
journal.next_shard_sequence()
)));
}
let mut repositories = BTreeMap::new();
for (namespace, record) in recovered.catalog.iter() {
repositories.insert(
*namespace,
repo_state_from_recovery(record, &recovered.refs)?,
);
}
let writer = ShardWriter {
builder: GroupBuilder::from_options(&shared.options),
pending: Vec::new(),
waiters: Vec::new(),
group_base: BTreeMap::new(),
in_flight_reservation: None,
repositories,
tail_generation: tail.logical_generation,
journal,
shard_index,
poison: None,
shared,
};
let (submissions, receiver) = crossbeam_channel::bounded(submission_queue_depth(&writer));
let thread = std::thread::Builder::new()
.name(format!("levcs-store-shard-{shard_index}"))
.spawn(move || run_shard_writer(writer, receiver))?;
Ok(ShardHandle {
submissions: Some(submissions),
thread: Some(thread),
counters,
})
}
/// Two maximal groups' worth of queue.
///
/// One would make every group boundary a backpressure event even when the
/// device is keeping up; unbounded would replace a typed `Overloaded` refusal
/// with unbounded memory, which decision 9.8 rules out for the status root for
/// the same reason.
fn submission_queue_depth(writer: &ShardWriter) -> usize {
(writer.shared.options.max_group_transactions as usize).saturating_mul(2)
}
// ===========================================================================
// The shard writer
// ===========================================================================
/// One shard's sequencer and journal writer.
///
/// Everything in here is owned by exactly one thread. `repo_sequence`, the
/// per-repository event chain, and `shard_sequence` are assigned with no
/// cross-shard coordination, which is sound only because a repository's shard
/// assignment never moves (scope 2.5).
struct ShardWriter {
shared: Arc<EngineShared>,
shard_index: u16,
journal: Journal,
builder: GroupBuilder,
/// Parallel to the frames held by `builder`, in the same order.
pending: Vec<Prepared>,
/// Every caller this thread owes an outcome to, oldest first.
///
/// Deliberately *not* stored inside [`Prepared`]. A panic anywhere in the
/// sequencer or the publication window unwinds this thread, and a
/// completion that unwound with the frame it belonged to is a request that
/// never resolves — indistinguishable from a store that hung. Keeping the
/// waiters on the writer means the panic handler can still reach them, and
/// each one carries the publication phase it is actually in so that the
/// handler resolves it as that phase rather than as the worst one.
waiters: Vec<Waiter>,
/// Speculative state: every earlier accepted transaction in the pending
/// group is already applied here, which is what scope 6.3 step 1 requires
/// the precondition checks to see.
repositories: BTreeMap<NamespaceId, RepoState>,
/// The repository state each namespace held *before* the open group
/// touched it, so the whole group can be re-derived from a clean base when
/// a member is dropped by the pre-mark deadline recheck.
///
/// `None` records "this namespace did not exist yet", which is a different
/// fact from "it existed with default state" and is the difference between
/// restoring a repository and inventing one.
group_base: BTreeMap<NamespaceId, Option<RepoState>>,
/// The status-root reservation held by a transaction that is inside
/// `prepare` and has not yet joined `pending`.
///
/// Scope 6.3 steps 1-3 are pre-append and definitively absent on failure,
/// including on a panic. Without this, a panic between `reserve` and the
/// push into `pending` leaves a `Pending` entry that nothing owns and that
/// no recovery resolves, because recovery only ever sees what reached the
/// device — and nothing did.
in_flight_reservation: Option<OperationKey>,
/// Logical index generation of the active journal, so an index location
/// built here resolves to the same pinned file after a reopen.
tail_generation: u64,
poison: Option<StoreError>,
}
/// One shard's unsealed index backlog, summed from one captured root.
#[derive(Copy, Clone, Debug, Default)]
struct UnsealedBacklog {
layers: usize,
entries: u64,
encoded_bytes: u64,
/// The highest shard sequence any of those layers covers, which is what a
/// seal built from them may claim to cover.
through_shard_sequence: u64,
}
/// Which of scope 6.3's phases a caller is actually in.
///
/// The fate of a request on an abnormal exit is a function of this and nothing
/// else. Treating every panic as a publication panic is what lets a transaction
/// that never reached step 4 be reported as possibly durable, and a transaction
/// that is already committed be reported as poisoned.
enum WaiterPhase {
/// Steps 1-3. Nothing is appended and nothing can be; a failure here is
/// definitively absent and releases the reservation.
PreAppend,
/// Steps 4-8, the poison window. A failure here leaves the operation
/// `Resolving` and the shard read-only until recovery.
Publishing,
/// Steps 9-10. The fence succeeded and the root published, so the only
/// honest outcome is the receipt. Scope 6.3: failure here is not a failure.
Committed(CommitReceipt),
}
struct Waiter {
completion: SharedCompletion,
phase: WaiterPhase,
/// The status-root reservation this waiter owns while it is `PreAppend`.
/// Taken over by step 8 once the group publishes.
reservation: Option<OperationKey>,
}
impl Waiter {
fn new(completion: SharedCompletion) -> Self {
Self {
completion,
phase: WaiterPhase::PreAppend,
reservation: None,
}
}
}
/// One sequenced transaction, complete except for where its frame landed.
struct Prepared {
key: OperationKey,
operation_digest: ObjectId,
retry_until_micros: i64,
first_visible_at_micros: i64,
shard_sequence: u64,
/// The repository state *after* this transaction.
repo_state: Arc<RepoState>,
objects: Vec<(ObjectId, u8)>,
receipt: CommitReceipt,
/// Retained so the whole group can be re-sequenced, re-chained, and
/// re-signed when the pre-mark deadline recheck drops a member.
///
/// Signing covers `SignedCommittedTransactionV1::signing_digest`, which
/// commits to the chained `previous_event_digest`; dropping a member
/// therefore invalidates every signature after it. Keeping only the derived
/// frame would make a partial repair the only available repair, and a
/// partial repair is a durable, correctly fenced frame carrying a signature
/// that verifies against nothing.
transaction: Arc<ValidatedTransaction>,
}
fn run_shard_writer(mut writer: ShardWriter, submissions: Receiver<Submission>) {
let unwound = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
loop {
// The idle bound is a receive deadline, not a timer thread: an
// open group closes on time even when nothing else arrives, and a
// shard with no open group blocks instead of spinning.
let message = match writer.builder.time_to_close() {
Some(remaining) => submissions.recv_timeout(remaining),
None => submissions
.recv()
.map_err(|_| RecvTimeoutError::Disconnected),
};
match message {
Ok(submission) => writer.accept(submission),
Err(RecvTimeoutError::Timeout) => writer.publish_open_group(),
Err(RecvTimeoutError::Disconnected) => {
// A transaction already accepted is already sequenced.
// Closing the engine publishes it rather than dropping it
// with its waiter attached.
writer.publish_open_group();
break;
}
}
}
}));
if let Err(payload) = unwound {
// A writer-thread panic still resolves every caller this thread owes an
// outcome to — a completion that unwound with its frame is a request
// that never resolves. What each caller is told is decided by the phase
// it was in, not by where the catch happens to be: this boundary sees
// pre-append requests and published-but-unwoken requests as well as the
// poison window, and reporting all three as `ShardPoisoned` would claim
// that a transaction which never reached step 4 might be durable.
let poison =
writer.poison_error("the writer thread panicked inside the publication window".into());
let mut poisoned_any = false;
for waiter in std::mem::take(&mut writer.waiters) {
match waiter.phase {
WaiterPhase::PreAppend => {
if let Some(key) = waiter.reservation {
writer.release_reservation(&key);
}
waiter.completion.complete(Err(pre_append_panic_error()));
}
WaiterPhase::Publishing => {
poisoned_any = true;
waiter.completion.complete(Err(poison.clone()));
}
WaiterPhase::Committed(receipt) => {
waiter.completion.complete(Ok(receipt));
}
}
}
if let Some(key) = writer.in_flight_reservation.take() {
// A panic strictly inside `prepare`, before the transaction had a
// waiter phase to be in.
writer.release_reservation(&key);
}
if poisoned_any {
writer.poison.get_or_insert(poison);
}
std::panic::resume_unwind(payload);
}
}
/// The outcome of a panic in scope 6.3 steps 1-3.
///
/// Definitive, not poisoning: no sequence was consumed, no byte was written,
/// and the reservation is released, so the operation reads back `Unknown` and
/// the caller may retry. `Conflict` is the taxonomy's definitive-rejection
/// variant and is what the ordinary pre-append refusals in this file already
/// return.
fn pre_append_panic_error() -> StoreError {
StoreError::Conflict(
"the sequencer panicked before this transaction was marked Resolving; scope 6.3 steps \
1-3 are pre-append, so nothing was appended and nothing can be"
.into(),
)
}
impl ShardWriter {
fn accept(&mut self, submission: Submission) {
let Submission {
transaction,
completion,
} = submission;
// Registered before anything that can fail or panic. Until this
// transaction joins `pending`, it is the last waiter in the list and
// `fail_newest_waiter` is what resolves it.
self.waiters.push(Waiter::new(completion));
if let Some(poison) = self.poison.clone() {
self.fail_newest_waiter(poison);
return;
}
let transaction = Arc::new(transaction);
let (prepared, frame) = match self.prepare_caught(&transaction) {
Ok(pair) => pair,
Err(error) => {
self.fail_newest_waiter(error);
return;
}
};
if let Err(frame) = self.builder.push(frame) {
// The group is full. The new request is rolled back completely —
// reservation released, speculative state restored — *before* the
// open group publishes, and re-prepared afterwards.
//
// The alternative, publishing while this request is prepared but
// not yet in `pending`, is what made a publication panic report a
// trailing request as poisoned though it never appended, and leave
// its reservation pending. It also made its `shard_sequence` a
// prediction about how many members the group about to publish
// would keep, which the pre-mark deadline recheck can change.
drop(frame);
self.roll_back_prepared(&prepared);
self.publish_open_group();
if let Some(poison) = self.poison.clone() {
self.fail_newest_waiter(poison);
return;
}
let (reprepared, frame) = match self.prepare_caught(&transaction) {
Ok(pair) => pair,
Err(error) => {
self.fail_newest_waiter(error);
return;
}
};
self.builder
.push(frame)
.expect("an empty group admits any frame");
self.attach_newest_reservation(reprepared.key);
self.pending.push(reprepared);
} else {
self.attach_newest_reservation(prepared.key);
self.pending.push(prepared);
}
if self.builder.is_closed() {
self.publish_open_group();
}
}
/// Scope 6.3 steps 1-3 with their own unwind boundary.
///
/// A panic in here is a pre-append failure by definition, so it must not
/// take the shard with it: `later_append_allowed_before_recovery` is true
/// for every step 1-3 row of the frozen oracle, and a dead writer thread
/// makes that false no matter what the error says.
fn prepare_caught(
&mut self,
transaction: &Arc<ValidatedTransaction>,
) -> Result<(Prepared, Frame), StoreError> {
let sequence = self.journal.next_shard_sequence() + self.pending.len() as u64;
match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
self.prepare(transaction, sequence)
})) {
Ok(result) => result,
Err(_) => {
if let Some(key) = self.in_flight_reservation.take() {
self.release_reservation(&key);
}
Err(pre_append_panic_error())
}
}
}
/// Hand the reservation the newest waiter is about to depend on to that
/// waiter, so a panic before publication releases exactly one reservation
/// per unresolved caller.
fn attach_newest_reservation(&mut self, key: OperationKey) {
self.in_flight_reservation = None;
if let Some(waiter) = self.waiters.last_mut() {
waiter.reservation = Some(key);
}
}
/// Undo a prepared-but-unpublished transaction completely: release its
/// status reservation and restore the speculative repository state it
/// advanced. No sequence was consumed, because sequences are assigned at
/// publication.
fn roll_back_prepared(&mut self, prepared: &Prepared) {
self.release_reservation(&prepared.key);
self.in_flight_reservation = None;
let namespace = prepared.key.namespace;
// The group base still holds this namespace's pre-image only if this
// transaction is the one that recorded it; otherwise an earlier member
// of the open group did, and its post-state is what must be restored.
let restored = self
.pending
.iter()
.rev()
.find(|earlier| earlier.key.namespace == namespace)
.map(|earlier| (*earlier.repo_state).clone());
match restored {
Some(state) => {
self.repositories.insert(namespace, state);
}
None => match self.group_base.remove(&namespace) {
Some(Some(before)) => {
self.repositories.insert(namespace, before);
}
Some(None) => {
self.repositories.remove(&namespace);
}
None => {}
},
}
}
/// Resolve the most recently registered waiter, which is the transaction
/// currently being accepted.
fn fail_newest_waiter(&mut self, error: StoreError) {
if let Some(waiter) = self.waiters.pop() {
if let Some(key) = waiter.reservation {
self.release_reservation(&key);
}
waiter.completion.complete(Err(error));
}
}
/// Admission, reservation, and scope 6.3 steps 1 to 3 for one new
/// submission.
///
/// Ordering inside this function is load-bearing. Every refusal that can
/// happen must happen before the reservation is turned into a group
/// member. `shard_sequence` is *not* consumed here: it is the tentative
/// position `sequence` this transaction would occupy, and the journal's
/// counter advances only at publication, once the pre-mark deadline
/// recheck has decided which members survive. Consuming it here would make
/// a dropped member leave a sequence hole, which poisons the shard.
fn prepare(
&mut self,
transaction: &Arc<ValidatedTransaction>,
sequence: u64,
) -> Result<(Prepared, Frame), StoreError> {
let namespace = transaction.namespace;
let key = OperationKey::new(namespace, transaction.operation_id);
let now = now_micros();
// The configured per-transaction ceilings, checked where a consumer's
// input actually arrives: `submit` is the only entry point that takes
// a caller-shaped transaction, and the codec's static maxima are a
// hostile-decode bound, not an operator's configuration. Refused before
// anything is reserved or sequenced.
let objects = transaction.objects.len() as u64;
let allowed_objects = u64::from(self.shared.options.max_objects_per_transaction);
if objects > allowed_objects {
return Err(StoreError::LimitExceeded {
limit: "max_objects_per_transaction",
observed: objects,
allowed: allowed_objects,
});
}
let refs = transaction.refs.len() as u64;
let allowed_refs = u64::from(self.shared.options.max_refs_per_transaction);
if refs > allowed_refs {
return Err(StoreError::LimitExceeded {
limit: "max_refs_per_transaction",
observed: refs,
allowed: allowed_refs,
});
}
// Decision 9.8 ordering, first rule: a committed or expired operation
// is answered from the committed root and never touches the status
// root. The answer itself — returning the durable receipt to a
// retrying caller — is deliverable 7.
if self.shared.committed.load().terminal_status(&key).is_some() {
return Err(StoreError::NotImplemented(
"submit idempotency against an already committed operation — B1 NamespaceTxn, \
scope 6-B1 deliverable 7",
));
}
// Plan §5.3: at a hard ceiling the shard seals synchronously **before
// admitting more work**. Here, and not after publishing, because this is
// the last point at which a refusal has reserved and sequenced nothing —
// a seal that fails partway through has to poison, and poisoning a shard
// that has just accepted a transaction owes that caller an answer it can
// no longer give.
self.seal_index_if_required()?;
// The reservation is what makes two concurrent submissions of one
// operation ID resolvable at all. It happens before sequencing, so a
// refusal here has consumed nothing.
self.reserve(key, transaction, now)?;
// Everything from here on releases the reservation on failure. Scope
// 6.3 steps 1-3 fail definitively and pre-append, and leaving a
// `Pending` entry behind would report `Pending` forever for an
// operation the oracle calls definitively absent.
match self.sequence_into_frame(transaction, sequence, now) {
Ok(pair) => Ok(pair),
Err(error) => {
self.release_reservation(&key);
Err(error)
}
}
}
/// Scope 6.3 steps 1 to 3 and the frame they feed, for a transaction whose
/// reservation already exists.
///
/// Called both by `prepare` for a new submission and by `reform_group` for
/// a member being re-sequenced after an earlier member was dropped. There
/// is exactly one of these because the repair path must produce bytes
/// indistinguishable from the ones the first pass would have produced at
/// the same position; two implementations of "sequence, chain, sign, encode"
/// is precisely how a repaired suffix acquires a signature over a chain
/// that no longer exists.
fn sequence_into_frame(
&mut self,
transaction: &Arc<ValidatedTransaction>,
shard_sequence: u64,
now: i64,
) -> Result<(Prepared, Frame), StoreError> {
let namespace = transaction.namespace;
let key = OperationKey::new(namespace, transaction.operation_id);
let state = self.speculative_state(transaction)?;
let sequenced = self.sequence_and_sign(transaction, &state, now)?;
let payload = sequenced.payload.encode_canonical()?;
let payload_len = payload.len() as u64;
let total_len = frame_total_len(payload_len);
// Rotation and sealing are not in this slice. Refusing a group that no
// longer fits the preallocated journal here — with the exact length in
// hand, before anything is marked `Resolving` — keeps the missing
// deliverable a clean pre-append refusal rather than a `LimitExceeded`
// discovered between the `Resolving` mark and the fence.
if !self
.journal
.fits(self.builder.bytes().saturating_add(total_len))
{
return Err(StoreError::NotImplemented(
"active journal rotation and sealing — B1 NamespaceTxn, scope 6-B1 \
deliverable 1 and scope 3.4",
));
}
let frame = Frame {
header: FrameHeader {
flags: 0,
total_len,
journal_id: self.journal.journal_id(),
shard_sequence,
repo_sequence: sequenced.receipt.repo_sequence,
namespace: *key.namespace.as_bytes(),
operation_id: *key.operation_id.as_bytes(),
operation_digest: transaction.operation_digest,
payload_len,
// Derived by `Frame::encode`; carried here so the value this
// writer holds is the value that reaches the device.
payload_digest: crate::format::digest(
crate::format::FRAME_PAYLOAD_DIGEST_DOMAIN,
&payload,
),
},
payload,
};
// The speculative state of scope 6.3 step 1 advances here, not at
// publication: every later member of the forming group must check its
// preconditions against every earlier accepted one.
//
// The pre-image is recorded the first time the open group touches a
// namespace, and it is the *only* way back: the pre-mark deadline
// recheck can drop any member, and every retained member after it has
// to be re-derived from the state that member saw, not from the state
// the dropped member left.
self.group_base
.entry(namespace)
.or_insert_with(|| self.repositories.get(&namespace).cloned());
self.repositories
.insert(namespace, (*sequenced.repo_state).clone());
Ok((
Prepared {
key,
operation_digest: sequenced.operation_digest,
retry_until_micros: sequenced.retry_until_micros,
first_visible_at_micros: sequenced.first_visible_at_micros,
shard_sequence,
repo_state: sequenced.repo_state,
objects: sequenced.objects,
receipt: sequenced.receipt,
transaction: Arc::clone(transaction),
},
frame,
))
}
fn speculative_state(
&self,
transaction: &ValidatedTransaction,
) -> Result<RepoState, StoreError> {
let namespace = transaction.namespace;
let existing = self.repositories.get(&namespace);
if let Some(genesis) = transaction.create_genesis_authority {
if let Some(existing) = existing {
// Plan §4 identity invariant 2: the binding is permanent.
return Err(StoreError::Conflict(format!(
"namespace {} is already bound to genesis {}; refusing to rebind to {}",
namespace.to_hex(),
hex::encode(existing.genesis_authority.0),
hex::encode(genesis.0)
)));
}
return Ok(RepoState {
repo_sequence: 0,
// Zero, not the genesis: `old_authority` in the event and
// `expected_authority` in the frame are the same fact written
// twice, and a create transaction's expected authority is
// nothing. Seeding this with the genesis would make the two
// copies disagree in every init frame the store ever writes.
current_authority: ObjectId([0u8; 32]),
genesis_authority: genesis,
refs: TypedRefMap::new(),
lifecycle: NamespaceLifecycle::Active,
storage_mode: NamespaceStorageMode::Full,
previous_event_digest: ObjectId([0u8; 32]),
});
}
let existing = existing.ok_or_else(|| {
StoreError::Conflict(format!(
"namespace {} is not bound; a transaction that does not create a repository \
cannot be the first one in it",
namespace.to_hex()
))
})?;
if !matches!(existing.lifecycle, NamespaceLifecycle::Active) {
return Err(StoreError::Conflict(format!(
"namespace {} does not admit mutation in lifecycle {:?}",
namespace.to_hex(),
existing.lifecycle
)));
}
Ok(existing.clone())
}
/// Scope 6.3 steps 2 and 3, and the mutable-precondition half of step 1.
///
/// The signer is called inline and in repository-sequence order, so no
/// speculative suffix can exist to repair: each member is chained, signed,
/// and sequenced completely before the next is looked at. Moving signing
/// onto its own bounded pool (deliverable 5) is what introduces
/// out-of-order completion, and with it the suffix re-chaining that
/// deliverable owns.
fn sequence_and_sign(
&mut self,
transaction: &ValidatedTransaction,
before: &RepoState,
now: i64,
) -> Result<Sequenced, StoreError> {
let namespace = transaction.namespace;
let creates = transaction.create_genesis_authority.is_some();
// Mutable preconditions only. Nothing here re-parses, re-hashes,
// re-verifies a signature, or re-evaluates policy: those ran before
// the transaction was validated, and repeating them inside the
// mutation lane is what the sequencer exists to avoid.
let mut refs = before.refs.clone();
let mut applied = Vec::with_capacity(transaction.refs.len());
for update in &transaction.refs {
let observed = refs.get(&update.target).copied();
if observed != update.expected {
return Err(StoreError::Conflict(format!(
"typed ref CAS for {:?} in namespace {} expected {:?} but observed {:?}",
update.target,
namespace.to_hex(),
update.expected.map(|id| hex::encode(id.0)),
observed.map(|id| hex::encode(id.0))
)));
}
let new = match update.mutation {
RefMutation::Set(target) => {
refs.insert(update.target.clone(), target);
Some(target)
}
RefMutation::Delete => {
refs.remove(&update.target);
None
}
};
applied.push(AppliedRefV1 {
target: update.target.clone(),
old: observed,
new,
force: update.force,
});
}
let expected_authority = transaction
.expected_authority
.unwrap_or(ObjectId([0u8; 32]));
if !creates && expected_authority != before.current_authority {
return Err(StoreError::Conflict(format!(
"authority CAS for namespace {} expected {} but observed {}",
namespace.to_hex(),
hex::encode(expected_authority.0),
hex::encode(before.current_authority.0)
)));
}
let new_authority = transaction
.new_authority
.unwrap_or(before.current_authority);
if creates && Some(before.genesis_authority) != transaction.new_authority {
// The catalog binds `current_authority` from the event's
// `new_authority` and `genesis_authority` from the create record.
// A create that moves the first to something other than the second
// would bind a repository to an authority it never held.
return Err(StoreError::Conflict(
"a repository-create transaction must set its new authority to the genesis \
authority it binds"
.into(),
));
}
// Scope 6.3 step 3, stated once (`deadline_permits_append`) and
// delegated to the frozen oracle rather than restated. This evaluation
// is the one at sequencing time; the binding one runs again immediately
// before the group is marked `Resolving`.
deadline_permits_append(now, transaction.retry_until_micros)
.map_err(|reason| reason.into_error(transaction.operation_id))?;
let repo_sequence = if creates {
0
} else {
before.repo_sequence.checked_add(1).ok_or_else(|| {
StoreError::Corruption(format!(
"repo_sequence overflow in namespace {}",
namespace.to_hex()
))
})?
};
let mut object_ids: Vec<ObjectId> = transaction.objects.iter().map(|o| o.id).collect();
object_ids.sort_by(|left, right| left.as_bytes().cmp(right.as_bytes()));
let commit_ids: Vec<ObjectId> = transaction
.objects
.iter()
.filter(|object| matches!(object.object_type, ObjectType::Commit))
.map(|object| object.id)
.collect();
let mut ref_states: Vec<RefStateV1> = refs
.iter()
.map(|(target, object)| RefStateV1 {
target: target.clone(),
object: *object,
})
.collect();
ref_states.sort();
let resulting_state_digest = protocol::ref_state_digest(&ref_states)
.map_err(|e| StoreError::Corruption(format!("resulting state digest: {e}")))?;
let committed = CommittedTransactionV1 {
repo_id: ObjectId(*namespace.as_bytes()),
repo_sequence,
previous_event_digest: before.previous_event_digest,
sequenced_at_micros: now,
source_kind: transaction.evidence.source_kind(),
// The evidence, never this instance's signing key. Frozen mirror
// validation requires `destination_event.actor == source_instance`
// for all four mirror kinds, and the administrative signing digests
// bind `actor` directly, so `actor` is a property of the evidence
// that this event restates. Filling it from the destination signer
// produces an event that signs, fences and publishes, and only then
// fails evidence verification — at a mirror, on another instance,
// with the transaction already durable and unwithdrawable.
actor: transaction.evidence.actor(),
operation_id: *transaction.operation_id.as_bytes(),
operation_digest: transaction.operation_digest,
signed_evidence_digest: transaction
.evidence
.evidence_digest()
.map_err(|e| StoreError::Corruption(format!("evidence digest: {e}")))?,
old_authority: before.current_authority,
new_authority,
refs: applied.clone(),
object_ids,
commit_ids,
resulting_state_digest,
};
let event_digest = committed
.event_digest()
.map_err(|e| StoreError::Corruption(format!("event digest: {e}")))?;
// Scope 6.3 step 2. The handoff is the last thing that can fail before
// a sequence is consumed.
match failpoints::hit(Failpoint::EvidenceHandoffFailure) {
FailpointAction::Continue => {}
FailpointAction::Fail => {
return Err(StoreError::Signer(crate::types::SignerError::Unavailable))
}
FailpointAction::Panic => {
panic!("failpoint EvidenceHandoffFailure panicked the sequencer")
}
FailpointAction::HardExit => {
unreachable!("HardExit calls _exit(3) inside failpoints::hit and never returns")
}
}
let source_key_epoch = self.shared.signer.key_epoch();
let durability_result = DurabilityResultV1::LocallyDurable;
// The signature must be the one `SignedCommittedTransactionV1::verify`
// checks, so it covers that type's signing digest — which binds the
// key epoch and the durability result around the canonical event —
// rather than the bare event digest. A frame carrying a signature over
// anything else would be durable, correctly fenced, and verifiable
// against nothing.
let signing_digest = SignedCommittedTransactionV1::signing_digest(
&committed,
source_key_epoch,
&durability_result,
)
.map_err(|e| StoreError::Corruption(format!("event signing digest: {e}")))?;
let source_signature = self.shared.signer.sign_event(&ObjectId(signing_digest))?;
let objects_new = transaction.objects.len() as u64;
let receipt = CommitReceipt {
operation_id: transaction.operation_id,
repo_sequence,
current_authority: new_authority,
refs: applied,
objects_new,
};
Ok(Sequenced {
operation_digest: transaction.operation_digest,
retry_until_micros: transaction.retry_until_micros,
first_visible_at_micros: now,
repo_state: Arc::new(RepoState {
repo_sequence,
current_authority: new_authority,
genesis_authority: before.genesis_authority,
refs,
lifecycle: before.lifecycle,
storage_mode: before.storage_mode,
previous_event_digest: event_digest,
}),
objects: transaction
.objects
.iter()
.map(|object| (object.id, object_type_code(object.object_type)))
.collect(),
receipt,
payload: TransactionFramePayloadV1 {
repository_create: repository_create(transaction)?,
objects: FrameObjectsV1::Inline(
transaction
.objects
.iter()
.map(|object| FrameObjectV1 {
object_type: object.object_type,
object_id: object.id,
raw: object.raw.clone(),
})
.collect(),
),
ref_cas: transaction.refs.clone(),
expected_authority,
new_authority,
evidence: transaction.evidence.clone(),
committed: SignedCommittedTransactionV1 {
transaction: committed,
source_key_epoch,
durability_result,
source_signature,
},
receipt: FrameReceiptFieldsV1 {
objects_new,
retry_until_micros: transaction.retry_until_micros,
first_receipt_visibility_micros: now,
},
},
})
}
// -----------------------------------------------------------------------
// Publication: scope 6.3 steps 4 to 9
// -----------------------------------------------------------------------
fn publish_open_group(&mut self) {
if self.builder.is_empty() {
debug_assert!(self.pending.is_empty());
return;
}
// Scope 6.3 step 3, in the position the ordering actually requires:
// immediately before the group is marked `Resolving`, not back at
// sequencing time. Between the two sit the signer handoff and the whole
// idle delay that closes an unfull group, either of which can be longer
// than what remains of a caller's signed deadline.
let (frames, pending) = self.close_group();
if frames.is_empty() {
debug_assert!(pending.is_empty());
return;
}
debug_assert_eq!(frames.len(), pending.len());
// The oldest `pending.len()` waiters are this group's. A transaction
// being accepted when a full group forced this publication is newer
// than all of them and stays registered.
debug_assert!(self.waiters.len() >= pending.len());
// Steps 4-8 are the poison window and the waiters say so *before* it is
// entered, so a panic inside is classified by the phase the request is
// in rather than by where the catch happens to sit.
for waiter in self.waiters.iter_mut().take(pending.len()) {
waiter.phase = WaiterPhase::Publishing;
}
let keys: Vec<OperationKey> = pending.iter().map(|prepared| prepared.key).collect();
let outcome = self.publish_window(&frames, &pending, &keys);
match outcome {
Ok(()) => {
// Step 8 removed the status entries, so the reservations are no
// longer this waiter's to release, and the receipt is the only
// outcome that remains truthful.
for (prepared, waiter) in pending.iter().zip(self.waiters.iter_mut()) {
waiter.reservation = None;
waiter.phase = WaiterPhase::Committed(prepared.receipt.clone());
}
self.wake_committed_group(pending.len());
}
Err(error) => {
// Scope 3.7: steps 4 to 8 are the poison window. The shard is
// read-only from here, the group's status entries stay
// `Resolving` because step 8 never ran, and nothing is
// appended, published, or acknowledged until a close and
// reopen through recovery.
self.poison = Some(error.clone());
for waiter in self.waiters.drain(..pending.len()) {
waiter.completion.complete(Err(error.clone()));
}
}
}
}
/// Scope 6.3 steps 9 and 10, which cannot poison.
///
/// The transaction is committed: the fence succeeded and the root
/// published. So this has its own unwind boundary — a panic here must not
/// kill the writer, because the frozen oracle says
/// `later_append_allowed_before_recovery` for this row and a dead writer
/// thread makes that false whatever the error says. The waiters are still
/// owned by `self` while the failpoint fires, so a caught panic can still
/// hand every one of them the receipt it is owed.
fn wake_committed_group(&mut self, members: usize) {
let hung = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
match failpoints::hit(Failpoint::AfterRootCasBeforeWaiterWake) {
// Deliberately a hung request and not an error: scope 6.3 says
// a waiter that never wakes is a hung request, never an absent
// transaction, and the receipt stays queryable either way.
FailpointAction::Fail => true,
FailpointAction::Continue => false,
FailpointAction::Panic => {
panic!("failpoint AfterRootCasBeforeWaiterWake panicked the writer thread")
}
FailpointAction::HardExit => {
unreachable!("HardExit calls _exit(3) inside failpoints::hit and never returns")
}
}
}));
// Charter item 6: every outcome named, no catch-all. `hung` being
// `Err` is a caught panic, which is a *harder* reason to deliver the
// receipt than an ordinary return, not a reason to skip it.
let deliver = !matches!(hung, Ok(true));
for waiter in self.waiters.drain(..members) {
match waiter.phase {
WaiterPhase::Committed(receipt) => {
if deliver {
waiter.completion.complete(Ok(receipt));
}
}
WaiterPhase::PreAppend | WaiterPhase::Publishing => unreachable!(
"every member of a published group was marked Committed before the wake"
),
}
}
}
/// Close the open group, applying scope 6.3 step 3 to every member.
///
/// A member past its signed deadline appends nothing. Because signing
/// covers `SignedCommittedTransactionV1::signing_digest`, which commits to
/// the chained `previous_event_digest`, dropping one member invalidates
/// every signature after it — so the retained suffix is not patched, it is
/// re-derived: restored to the state the group started from and then
/// re-sequenced, re-chained, re-signed, and re-encoded in order. A partial
/// repair would produce a durable, correctly fenced frame carrying a
/// signature that verifies against nothing, which no later recovery can
/// detect.
fn close_group(&mut self) -> (Vec<Frame>, Vec<Prepared>) {
let frames = self.builder.take();
let pending = std::mem::take(&mut self.pending);
let base = std::mem::take(&mut self.group_base);
let now = now_micros();
if pending
.iter()
.all(|prepared| deadline_permits_append(now, prepared.retry_until_micros).is_ok())
{
return (frames, pending);
}
// Back to the state the group started from, so every retained member is
// re-derived against exactly what it would have seen had the dropped
// members never been accepted.
for (namespace, before) in base {
match before {
Some(state) => {
self.repositories.insert(namespace, state);
}
None => {
self.repositories.remove(&namespace);
}
}
}
let base_sequence = self.journal.next_shard_sequence();
let mut new_frames = Vec::with_capacity(pending.len());
let mut new_pending: Vec<Prepared> = Vec::with_capacity(pending.len());
// Index into `self.waiters` of the member currently being re-derived.
// The group's waiters are its oldest `pending.len()`, in order, and a
// dropped member's waiter is removed at this cursor so the survivors
// stay aligned with `new_pending`.
let mut cursor = 0usize;
for prepared in pending {
let key = prepared.key;
let transaction = Arc::clone(&prepared.transaction);
let next_sequence = base_sequence + new_pending.len() as u64;
let outcome = deadline_permits_append(now, prepared.retry_until_micros)
.map_err(|reason| reason.into_error(key.operation_id))
.and_then(|()| self.sequence_into_frame(&transaction, next_sequence, now));
match outcome {
Ok((reprepared, frame)) => {
new_frames.push(frame);
new_pending.push(reprepared);
cursor += 1;
}
Err(error) => {
// Pre-append and definitive: no sequence was consumed, the
// reservation goes, and the caller learns its transaction
// is absent rather than waiting on a frame that will never
// be written.
self.release_reservation(&key);
let waiter = self.waiters.remove(cursor);
waiter.completion.complete(Err(error));
}
}
}
// `sequence_into_frame` repopulated the base for a group that is
// closing right now; the next group starts from a clean one.
self.group_base.clear();
(new_frames, new_pending)
}
/// Scope 6.3 steps 4 through 8. Every failure inside poisons the shard.
fn publish_window(
&mut self,
frames: &[Frame],
pending: &[Prepared],
keys: &[OperationKey],
) -> Result<(), StoreError> {
// The journal's sequence counter advances here and nowhere else.
// `prepare` computes a member's position but consumes nothing, because
// the pre-mark deadline recheck above may drop members and a consumed
// sequence that never appends is a hole that poisons the shard. By this
// point the retained set is final, so consuming is safe and the
// assertion below is what proves the two agree.
for frame in frames {
let assigned = self.journal.assign_shard_sequence();
if assigned != frame.header.shard_sequence {
return Err(self.poison_error(format!(
"the journal assigned shard_sequence {assigned} where the closed group \
carried {}",
frame.header.shard_sequence
)));
}
}
// --- step 4: mark every operation in the group Resolving -----------
self.mark_resolving(pending)?;
fire_in_poison_window(self.shard_index, Failpoint::AfterMarkedResolving)?;
// --- step 5: exact reserved frames, one fence ----------------------
let sequences = self.journal.append_group_and_fence(frames)?;
if sequences.len() != pending.len() {
return Err(self.poison_error(format!(
"append returned {} sequences for a group of {}",
sequences.len(),
pending.len()
)));
}
// --- step 6: build the immutable shard subtree ---------------------
fire_in_poison_window(self.shard_index, Failpoint::DuringCommittedRootBuild)?;
let subtree = self.build_subtree(pending)?;
fire_in_poison_window(
self.shard_index,
Failpoint::AllocationFailureBeforePublication,
)?;
// --- step 7: CAS-publish the committed root ------------------------
fire_in_poison_window(self.shard_index, Failpoint::BeforeRootCas)?;
self.publish_subtree(&subtree)?;
#[cfg(test)]
probe::fire(&self.shared, keys);
// --- step 8: remove the group's status entries ---------------------
//
// Strictly after step 7 and never before. `ArcSwap`'s store is a
// release operation and its load is an acquire, so a reader that
// observes the status root without these entries has synchronized
// with this removal and cannot then read a committed root older than
// the publication above. The explicit fence makes the dependency
// between the two swaps a statement rather than an inherited property
// of the crate that implements them.
fence(Ordering::Release);
self.remove_status_entries(keys);
Ok(())
}
fn mark_resolving(&self, pending: &[Prepared]) -> Result<(), StoreError> {
loop {
let current = self.shared.status.load();
let mut next = OperationStatusRoot::clone(&current);
for prepared in pending {
next = next
.replace_existing(
prepared.key,
StatusEntry::resolving(
prepared.operation_digest,
prepared.retry_until_micros,
Some(prepared.shard_sequence),
),
)
.ok_or_else(|| {
// The reservation was made by this thread before
// sequencing and nothing else may remove or rebind it.
self.poison_error(format!(
"operation {} lost its status reservation before append",
prepared.key.operation_id.to_hex()
))
})?;
}
let next = Arc::new(next);
// The root that was just published, never the one it replaced.
// Recording `current` makes occupancy lag every insertion and every
// removal by one publication, which is a metric that is wrong in
// exactly the direction that hides an approach to the ceiling.
let previous = self
.shared
.status
.compare_and_swap(&current, Arc::clone(&next));
if Arc::ptr_eq(&previous, &current) {
self.shared.status_metrics.record_published(&next);
return Ok(());
}
}
}
fn build_subtree(&self, pending: &[Prepared]) -> Result<ShardSubtree, StoreError> {
let mut delta = IndexDelta::from_options(&self.shared.options);
// The device offsets the fence just made durable, read back from the
// journal rather than predicted, so an index location can only ever
// name bytes that are actually there.
let index = self.journal.frame_index();
let appended = &index[index.len() - pending.len()..];
for (prepared, (sequence, offset, len)) in pending.iter().zip(appended) {
if *sequence != prepared.shard_sequence {
return Err(self.poison_error(format!(
"journal recorded sequence {sequence} where the group carried {}",
prepared.shard_sequence
)));
}
let frame_len = u32::try_from(*len).map_err(|_| StoreError::LimitExceeded {
limit: "frame_len",
observed: *len,
allowed: u64::from(u32::MAX),
})?;
for (object, object_type) in &prepared.objects {
delta.insert(
IndexKey::new(prepared.key.namespace, *object),
IndexLocation {
segment_generation: self.tail_generation,
frame_offset: *offset,
frame_len,
object_type: *object_type,
shard_sequence: *sequence,
},
)?;
}
}
let mut repositories = RepoMap::new();
let mut terminal_statuses = TerminalStatusMap::new();
for prepared in pending {
// In group order, so a group touching one repository twice leaves
// the later state.
repositories.insert(prepared.key.namespace, Arc::clone(&prepared.repo_state));
terminal_statuses.insert(
prepared.key,
TerminalStatusEntry::Committed(RetainedReceipt {
operation_digest: prepared.operation_digest,
receipt: prepared.receipt.clone(),
shard_sequence: prepared.shard_sequence,
retry_until_micros: prepared.retry_until_micros,
first_visible_at_micros: prepared.first_visible_at_micros,
// Computed by the frozen protocol function, never
// restated: the store and the wire must not be able to
// disagree about when a receipt stops being visible.
receipt_visible_until_micros: protocol::receipt_visible_until(
prepared.retry_until_micros,
prepared.first_visible_at_micros,
self.shared.options.terminal_status_grace_micros,
)
.map_err(|e| StoreError::Corruption(format!("receipt visibility: {e}")))?,
}),
);
}
let last = pending
.last()
.map(|prepared| prepared.shard_sequence)
.ok_or_else(|| self.poison_error("published an empty group".into()))?;
Ok(ShardSubtree::new(
self.shard_index,
last,
Arc::new(delta),
None,
Vector::new(),
repositories,
terminal_statuses,
// No new generation: the active journal these frames landed in is
// already pinned by the generation recovery installed.
GenerationMap::new(),
))
}
/// This shard's unsealed index backlog, as one captured root reports it.
fn unsealed_backlog(&self, root: &CommittedRoot) -> UnsealedBacklog {
let mut backlog = UnsealedBacklog::default();
for layer in root.index().delta_layers() {
if layer.shard_index != self.shard_index {
continue;
}
backlog.layers += 1;
backlog.entries += layer.delta.len();
// Summed rather than deduplicated: two layers holding the same key
// are counted twice, so the total is an over-estimate and the
// decision it feeds can only ever seal *early*. Deduplicating would
// mean merging every layer on every admission.
backlog.encoded_bytes += layer.delta.encoded_bytes();
backlog.through_shard_sequence = backlog
.through_shard_sequence
.max(layer.through_shard_sequence);
}
backlog
}
/// Index entries a reopen of this shard would have to rebuild.
///
/// Sealed runs plus the unsealed backlog, because with the committed prefix
/// unadvanced every frame is still replayed. Counted from the shard's own
/// retained generations rather than from the root's run vector, which is not
/// tagged by shard and would charge one shard for another's runs.
fn replayable_index_entries(&self, root: &CommittedRoot, backlog: &UnsealedBacklog) -> u64 {
let sealed: u64 = root
.retained_generations()
.values()
.filter(|generation| generation.id.shard_index == self.shard_index)
.flat_map(|generation| generation.index_runs.iter())
.map(|retained| retained.run().entry_count())
.sum();
sealed + backlog.entries
}
/// Seal when the accumulated delta has reached a hard ceiling (scope 3.6).
///
/// Two ceilings, because the backlog has two costs. `delta_pressure` answers
/// for entries and bytes — the memory the unsealed delta occupies — through
/// the same function `IndexDelta::pressure` uses, so the watermark has one
/// implementation. The layer *count* is the other cost: every layer is a map
/// a lookup consults before it reaches any run, so a million single-object
/// groups would leave lookup fan-out unbounded while entry pressure stayed
/// low. `max_index_runs` bounds it, which is the ceiling the refusal this
/// replaced already used for exactly that reason.
fn seal_index_if_required(&mut self) -> Result<(), StoreError> {
let root = self.shared.committed.load();
let backlog = self.unsealed_backlog(&root);
if backlog.layers == 0 {
return Ok(());
}
let entries_or_bytes = delta_pressure(
backlog.entries,
backlog.encoded_bytes,
self.shared.options.max_active_index_entries,
self.shared.options.max_active_index_bytes,
) == DeltaPressure::SealRequired;
let fan_out = backlog.layers as u64 >= u64::from(self.shared.options.max_index_runs);
if entries_or_bytes || fan_out {
self.seal_index(&root, backlog.through_shard_sequence)?;
}
// What a reopen would have to rebuild, checked **after** the seal so a
// seal that was due still happens.
//
// Sealing moves entries out of the delta layers and into a run, but it
// does not move a single *frame* out of `active/`. The manifest's
// `committed_shard_sequence` advances only when a checkpoint or a segment
// rotation makes a prefix durable elsewhere, and neither happens in this
// slice — so recovery replays every frame this shard ever wrote, into one
// delta bounded by `max_active_index_entries`. A shard that kept
// accepting work past that point would be writing a store it could not
// reopen, and would find out at the next open.
//
// Refusing makes that a refusal instead of a corrupt outcome, and it is
// not conservative padding: recovery's delta holds at most
// `max_active_index_entries`, and every admitted transaction adds at
// least one object.
//
// The consequence is worth stating plainly, because it bounds what this
// slice delivers: a seal triggered by **entry pressure** lands the shard
// exactly on this ceiling, so the next admission is refused. Only a seal
// triggered by the **fan-out** ceiling leaves the shard able to continue.
// Entry-pressure sealing becomes useful when `StoreEngine::checkpoint`
// can advance the committed prefix; until then it converts an
// unreopenable store into an honest refusal, which is all it can do.
let backlog = self.unsealed_backlog(&self.shared.committed.load());
let replayable = self.replayable_index_entries(&self.shared.committed.load(), &backlog);
if replayable >= self.shared.options.max_active_index_entries {
return Err(StoreError::LimitExceeded {
limit: "max_active_index_entries",
observed: replayable + 1,
allowed: self.shared.options.max_active_index_entries,
});
}
Ok(())
}
/// Seal this shard's covered delta layers into one durable run and publish
/// it, discarding exactly what the run covers in the same CAS.
///
/// # The poison boundary
///
/// Everything before [`segment::install_index_run`] is pre-durable: the
/// ceiling checks, the generation lookup, the merge and the encode all fail
/// without having changed a byte on the device, so they are ordinary errors
/// and the shard stays usable. From that call onward the shard poisons on any
/// failure, including a failure the call may have made *before* writing
/// anything — the caller cannot tell those apart, and the conservative
/// direction is the one that refuses to keep writing against a root that may
/// no longer describe the device. A false poison costs an operator a
/// recovery; a missed one lets the writer continue on an ambiguous root,
/// which is the failure this boundary exists to prevent.
fn seal_index(&mut self, root: &CommittedRoot, covered_through: u64) -> Result<(), StoreError> {
// Both ceilings, before anything is durable, and against the *same*
// numbers recovery enforces when it reopens the manifest (scope 3.8). A
// seal that passed here can always be reopened; one that bypassed them
// would install a manifest this store could never open again.
let projected = root.index().sealed_run_count() as u64 + 1;
for (limit, allowed) in [
("max_index_runs", self.shared.options.max_index_runs),
(
"max_open_index_runs",
self.shared.options.max_open_index_runs,
),
] {
if projected > u64::from(allowed) {
return Err(StoreError::LimitExceeded {
limit,
observed: projected,
allowed: u64::from(allowed),
});
}
}
// Newest-first, skipping any key a newer layer already answered: that is
// `LayeredObjectIndex::lookup`'s first-hit-wins rule, so the run answers
// every covered key exactly as the layers did. Merging oldest-first would
// instead meet `IndexDelta::insert`'s conflict check the moment one object
// had been rewritten at a new offset — a legitimate history that would
// then fail to seal.
//
// The merge target's ceilings are relaxed on purpose. Its contents are
// already resident in the layers being merged, so the bound that matters
// was applied when those were admitted; measuring the accumulation
// against the configured ceiling *here* would refuse the very seal that
// relieves it.
let mut merged = IndexDelta::new(u64::MAX, u64::MAX);
for layer in root.index().delta_layers() {
if layer.shard_index != self.shard_index
|| layer.through_shard_sequence > covered_through
{
continue;
}
for (key, location) in layer.delta.iter() {
if merged.get(&key).is_none() {
merged.insert(key, location)?;
}
}
}
let current_generation = root
.retained_generations()
.keys()
.filter(|id| id.shard_index == self.shard_index)
.map(|id| id.manifest_generation)
.max()
.ok_or_else(|| {
// Not a poison: nothing has been written, and wedging the shard
// for a condition that only recovery can have produced would
// turn a diagnosable state into an outage.
StoreError::Corruption(format!(
"shard {} has no retained manifest generation to seal an index run against",
self.shard_index
))
})?;
let next_generation = current_generation + 1;
let root_uuid = self.shared.root_uuid;
let run_bytes = IndexRunBuilder::new(root_uuid, next_generation, next_generation)
.build(&merged)
.map_err(|error| {
StoreError::Corruption(format!("encoding index run {next_generation}: {error}"))
})?;
let paths = RootLayout::new(&self.shared.options.root).shard(self.shard_index);
let manifest_retain = self.shared.options.manifest_retain;
let counters = Arc::clone(self.journal.counters_handle());
// --- durable from here: every failure below poisons ----------------
let sealed = segment::install_index_run(
&paths,
&root_uuid,
current_generation,
next_generation,
&run_bytes,
manifest_retain,
&counters,
);
let sealed = match sealed {
Ok(sealed) => sealed,
Err(error) => return Err(self.poison_now(format!("installing index run: {error}"))),
};
// Reopened from the device rather than kept from the bytes just encoded.
// The run's checksum, header and `root_uuid` are validated by the same
// code recovery will use, so a run that cannot be reopened is found now —
// while the shard can still poison — instead of at the next open.
let run = match IndexRun::open(&sealed.path, &root_uuid) {
Ok(run) => Arc::new(run),
Err(error) => return Err(self.poison_now(format!("reopening a sealed run: {error}"))),
};
// The successor generation pins what the predecessor pinned plus the new
// run, and the predecessor's pin is released in the same publication.
// Copying rather than referencing keeps `object_source`'s rule satisfied:
// one logical generation may appear in several manifest generations, but
// every occurrence must name the same path.
let previous = root
.retained_generations()
.get(&GenerationId::new(self.shard_index, current_generation))
.map(Arc::clone);
let previous = match previous {
Some(previous) => previous,
None => {
return Err(self.poison_now(format!(
"generation {current_generation} vanished from the root while sealing"
)))
}
};
let mut index_runs = previous.index_runs.to_vec();
index_runs.push(RetainedIndexRun::new(sealed.path.clone(), Arc::clone(&run)));
let successor = Arc::new(RetainedGeneration::new(
GenerationId::new(self.shard_index, next_generation),
Arc::clone(&previous.segments),
index_runs.into(),
Arc::clone(&previous.checkpoints),
Arc::clone(&previous.active_tails),
Arc::clone(&previous.projection_artifacts),
));
let mut generations = GenerationMap::new();
generations.insert(successor.id, successor);
let mut sealed_runs = Vector::new();
sealed_runs.push_back(run);
let mut subtree = ShardSubtree::new(
self.shard_index,
// No frame was appended, so the shard's committed sequence is
// unchanged. `CommittedRoot::merge` recognizes a maintenance
// publication and does not treat the unchanged sequence as a
// duplicate group (contract review 2026-07-29-C, amendment 3).
root.shard_committed_sequence(self.shard_index)
.unwrap_or(covered_through),
Arc::new(IndexDelta::from_options(&self.shared.options)),
Some(covered_through),
sealed_runs,
RepoMap::new(),
TerminalStatusMap::new(),
generations,
);
subtree
.retained_generation_removals
.push_back(GenerationId::new(self.shard_index, current_generation));
if let Err(error) = self.publish_subtree(&subtree) {
return Err(self.poison_now(format!("publishing a sealed index run: {error}")));
}
Ok(())
}
fn publish_subtree(&self, subtree: &ShardSubtree) -> Result<(), StoreError> {
loop {
let current = self.shared.committed.load();
#[cfg(test)]
probe::fire_cas_contention(&self.shared);
let merged = Arc::new(current.merge(subtree));
let previous = self.shared.committed.compare_and_swap(&current, merged);
if Arc::ptr_eq(&previous, &current) {
return Ok(());
}
// Another shard published between the load and the swap. Merge
// again against the newer root; the subtree is unchanged and
// `merge` is pure, so this is a re-merge and never a lost update.
#[cfg(test)]
self.shared
.root_cas_retries
.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
fire_in_poison_window(self.shard_index, Failpoint::DuringRootCasRetry)?;
}
}
fn remove_status_entries(&self, keys: &[OperationKey]) {
loop {
let current = self.shared.status.load();
let next = Arc::new(current.without_all(keys));
let previous = self
.shared
.status
.compare_and_swap(&current, Arc::clone(&next));
if Arc::ptr_eq(&previous, &current) {
self.shared.status_metrics.record_published(&next);
return;
}
}
}
// -----------------------------------------------------------------------
// Status-root reservations
// -----------------------------------------------------------------------
fn reserve(
&mut self,
key: OperationKey,
transaction: &ValidatedTransaction,
now: i64,
) -> Result<(), StoreError> {
let entry = StatusEntry::pending(
transaction.operation_digest,
transaction.retry_until_micros,
PendingPhase::Sequenced,
);
loop {
let current = self.shared.status.load();
// Decision 9.8: identity is examined before capacity, and the
// bound is enforced atomically with the insert by retrying this
// whole reconsideration on CAS failure.
match current.reserve(key, entry) {
StatusReservation::Attached(_) => {
return Err(StoreError::NotImplemented(
"same-ID/same-digest coalescing onto an in-flight leader — B1 \
NamespaceTxn, scope 6-B1 deliverable 7",
))
}
StatusReservation::OperationIdMismatch { .. } => {
return Err(StoreError::Conflict(format!(
"operation {} is already in flight with a different stable digest",
key.operation_id.to_hex()
)))
}
StatusReservation::AtCapacity { .. } => {
// Decision 9.8: the bound is enforced by refusal, never by
// eviction. Evicting a `Pending` entry would silently turn
// it into `Unknown`, which is a correctness change wearing
// a capacity policy's clothes.
self.shared.status_metrics.record_rejection();
return Err(StoreError::Overloaded {
limit: "max_status_entries",
retry_after_micros: retry_after_micros(
transaction.retry_until_micros,
now,
&self.shared.options,
),
});
}
StatusReservation::Inserted(next) => {
let next = Arc::new(next);
let previous = self
.shared
.status
.compare_and_swap(&current, Arc::clone(&next));
if Arc::ptr_eq(&previous, &current) {
self.shared.status_metrics.record_published(&next);
// Owned by the writer until it is handed to a waiter or
// released, so a panic anywhere in steps 1-3 has exactly
// one place to look for a reservation to drop.
self.in_flight_reservation = Some(key);
return Ok(());
}
}
}
}
}
/// Drop a reservation for a transaction that will definitively never
/// append. Only legal before step 5; after it, a status entry is removed
/// exactly once, by step 8.
fn release_reservation(&mut self, key: &OperationKey) {
loop {
let current = self.shared.status.load();
let next = Arc::new(current.without(key));
let previous = self
.shared
.status
.compare_and_swap(&current, Arc::clone(&next));
if Arc::ptr_eq(&previous, &current) {
self.shared.status_metrics.record_published(&next);
if self.in_flight_reservation == Some(*key) {
self.in_flight_reservation = None;
}
return;
}
}
}
fn poison_error(&self, cause: String) -> StoreError {
StoreError::ShardPoisoned {
shard: self.shard_index,
cause,
}
}
/// Poison the shard **and** latch it, for a failure outside the publication
/// window that reaches this file's other durable sequence.
///
/// `poison_error` only builds the error. That is right inside
/// `publish_window`, whose caller latches `self.poison` for the whole group;
/// it is wrong for an index seal, which runs at admission and whose error
/// otherwise returns to one caller and leaves the writer accepting the next
/// transaction against a root that may no longer describe the device.
fn poison_now(&mut self, cause: String) -> StoreError {
let error = self.poison_error(cause);
self.poison = Some(error.clone());
error
}
}
/// A [`Prepared`] that still carries its undecoded payload.
struct Sequenced {
operation_digest: ObjectId,
retry_until_micros: i64,
first_visible_at_micros: i64,
repo_state: Arc<RepoState>,
objects: Vec<(ObjectId, u8)>,
receipt: CommitReceipt,
payload: TransactionFramePayloadV1,
}
/// Why scope 6.3 step 3 refused an append.
///
/// Named rather than inlined because the rule is evaluated at two points — at
/// sequencing, and again immediately before the group is marked `Resolving` —
/// and the two must be the same rule. Two copies of a deadline test are two
/// opinions about when a signed receipt expires.
enum DeadlineRefusal {
Expired,
AlreadyAppending,
}
impl DeadlineRefusal {
fn into_error(self, operation: OperationId) -> StoreError {
match self {
Self::Expired => StoreError::Conflict(format!(
"operation {} passed its signed deadline before append",
operation.to_hex()
)),
Self::AlreadyAppending => StoreError::Corruption(
"pre-append deadline check reported an append that had already started".into(),
),
}
}
}
/// Scope 6.3 step 3, delegated to the frozen oracle rather than restated.
///
/// Plan §7 stage 9: no operation may first append after its signed deadline.
fn deadline_permits_append(now: i64, retry_until_micros: i64) -> Result<(), DeadlineRefusal> {
let deadline =
oracle::deadline_expectation(now, retry_until_micros, AppendDeadlinePhase::PreAppend);
match deadline.decision {
DeadlineDecision::ContinuePreAppend => Ok(()),
DeadlineDecision::RejectReceiptExpired => Err(DeadlineRefusal::Expired),
DeadlineDecision::RemainResolving => Err(DeadlineRefusal::AlreadyAppending),
}
}
/// Fire a failpoint inside the poison window. Every action is enumerated, so a
/// new one cannot be added without a decision here.
fn fire_in_poison_window(shard: u16, point: Failpoint) -> Result<(), StoreError> {
match failpoints::hit(point) {
FailpointAction::Continue => Ok(()),
FailpointAction::Fail => Err(StoreError::ShardPoisoned {
shard,
cause: format!("failpoint {} inside the publication window", point.name()),
}),
FailpointAction::Panic => {
panic!("failpoint {} panicked the writer thread", point.name())
}
FailpointAction::HardExit => {
unreachable!("HardExit calls _exit(3) inside failpoints::hit and never returns")
}
}
}
fn repository_create(
transaction: &ValidatedTransaction,
) -> Result<Option<RepositoryCreateV1>, StoreError> {
let Some(genesis) = transaction.create_genesis_authority else {
return Ok(None);
};
// The builder already refused a create whose genesis object is absent, so
// this is the second half of that check rather than a new one: the exact
// length and hash come from the bytes the transaction carries.
let object = transaction
.objects
.iter()
.find(|object| object.id == genesis)
.ok_or_else(|| {
StoreError::Corruption(
"a repository-create transaction reached the sequencer without its genesis \
authority object"
.into(),
)
})?;
Ok(Some(RepositoryCreateV1 {
genesis_authority: genesis,
genesis_len: object.raw.len() as u64,
genesis_hash: object.id,
projection: protocol::ProjectionMode::Full,
}))
}
/// How long an overloaded caller should wait.
///
/// Bounded by what remains of the caller's own signed deadline: telling a
/// caller to retry after its receipt expires is telling it to stop.
fn retry_after_micros(retry_until_micros: i64, now: i64, options: &StoreOptions) -> u64 {
let idle = options
.max_group_idle
.as_micros()
.try_into()
.unwrap_or(i64::MAX);
let remaining = retry_until_micros.saturating_sub(now).max(0);
u64::try_from(idle.min(remaining.max(0))).unwrap_or(0)
}
fn now_micros() -> i64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|elapsed| elapsed.as_micros() as i64)
.unwrap_or(0)
}
/// A hook fired at exactly one place: after the committed root is published
/// and before the status entries are removed.
///
/// It exists because the scope 6.3 step 7/8 ordering is otherwise only
/// observable as a race. A concurrent reader can prove the property
/// statistically, and a test over a helper would prove nothing about the path
/// that runs. Firing here lets a test assert, deterministically and on the
/// real writer, that at this instant the receipt is already in the committed
/// root *and* the status entry is still present. Moving the removal before the
/// publication fails the first half; moving the publication after the removal
/// fails the second.
///
/// `cfg(test)` only: the shipping writer has no hook, no atomic load, and no
/// indirect call here.
#[cfg(test)]
mod probe {
use std::sync::{Arc, Mutex, MutexGuard};
use super::{EngineShared, OperationKey};
type Hook = Arc<dyn Fn(&EngineShared, &[OperationKey]) + Send + Sync>;
type CasHook = Arc<dyn Fn(&EngineShared) + Send + Sync>;
static SERIAL: Mutex<()> = Mutex::new(());
static INSTALLED: Mutex<Option<Hook>> = Mutex::new(None);
static CAS_CONTENTION: Mutex<Option<CasHook>> = Mutex::new(None);
/// Exclusive use of the process-global hook, cleared on acquire.
///
/// Takes the same shape as `sys::FaultSerial` and for the same reason: a
/// lock a test *may* take is a lock a new test will not take, and a test
/// that panics cannot run its own cleanup.
pub(super) struct ProbeSerial(#[allow(dead_code)] MutexGuard<'static, ()>);
pub(super) fn serial() -> ProbeSerial {
let guard = SERIAL
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner());
*INSTALLED
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner()) = None;
*CAS_CONTENTION
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner()) = None;
ProbeSerial(guard)
}
/// Install a competing publisher that runs between the committed root's
/// load and its compare-and-swap.
///
/// Contention on that CAS is otherwise only reachable as a race, and a test
/// that waits for one either flakes or asserts nothing. This runs inside
/// the real `publish_subtree` loop — the production path — and only the
/// source of the contention is synthetic.
pub(super) fn install_cas_contention(
_serial: &ProbeSerial,
hook: impl Fn(&EngineShared) + Send + Sync + 'static,
) {
*CAS_CONTENTION
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner()) = Some(Arc::new(hook));
}
pub(super) fn fire_cas_contention(shared: &EngineShared) {
let hook = CAS_CONTENTION
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner())
.clone();
if let Some(hook) = hook {
hook(shared);
}
}
pub(super) fn install(
_serial: &ProbeSerial,
hook: impl Fn(&EngineShared, &[OperationKey]) + Send + Sync + 'static,
) {
*INSTALLED
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner()) = Some(Arc::new(hook));
}
pub(super) fn fire(shared: &EngineShared, keys: &[OperationKey]) {
let hook = INSTALLED
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner())
.clone();
if let Some(hook) = hook {
hook(shared, keys);
}
}
}
#[cfg(test)]
impl StoreEngine {
/// The published committed root, for assertions the reader API cannot yet
/// make: `RepoSnapshot` is B1 deliverable 8. This is the same immutable
/// value every reader captures, not a private mirror of it.
fn committed_root(&self) -> arc_swap::Guard<Arc<CommittedRoot>> {
self.shared.committed.load()
}
}
#[cfg(test)]
// `WriterSerial` is `crate::sys::FaultSerial` with the `failpoints` feature and
// `()` without it, so the token every test binds is a unit value in the default
// configuration. Binding it anyway is the point: the same test source must take
// the lock in the configuration where the lock exists.
#[allow(clippy::let_unit_value)]
mod tests {
use std::future::Future;
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicU64, Ordering as AtomicOrdering};
use std::sync::Mutex;
use std::task::{Context, Poll, Wake, Waker};
use std::time::Duration;
use levcs_protocol::v2::{RefTarget, TransactionEvidenceV1, TypedRefCas};
use super::*;
use crate::index::IndexKey;
use crate::segment;
use crate::transaction::StagedObject;
use crate::types::{PrivilegedConstruction, SignerError};
// -----------------------------------------------------------------------
// Harness
// -----------------------------------------------------------------------
/// A deterministic stand-in for the injected signer.
///
/// It is not Ed25519, and it does not need to be: the store never verifies
/// a signature (plan §5.1 forbids it from deciding who may sign). What the
/// tests need is a signature that is a pure function of the digest it was
/// asked to cover, so a test can recompute what *should* have been signed
/// and prove the writer signed that and not something else.
#[derive(Default)]
struct TestSigner {
calls: AtomicU64,
}
impl TestSigner {
fn expected_signature(digest: &ObjectId) -> [u8; 64] {
let mut signature = [0u8; 64];
let first = blake3::hash(digest.as_bytes());
let second = blake3::hash(first.as_bytes());
signature[..32].copy_from_slice(first.as_bytes());
signature[32..].copy_from_slice(second.as_bytes());
signature
}
}
impl CommitEvidenceSigner for TestSigner {
fn key_epoch(&self) -> u64 {
7
}
fn public_key(&self) -> [u8; 32] {
[0x5a; 32]
}
fn sign_event(&self, event_digest: &ObjectId) -> Result<[u8; 64], SignerError> {
self.calls.fetch_add(1, AtomicOrdering::SeqCst);
Ok(Self::expected_signature(event_digest))
}
}
/// Drive one future to completion on this thread.
///
/// `levcs-store` starts no runtime, so its tests must not depend on one
/// either.
pub(super) fn block_on<F: Future>(future: F) -> F::Output {
match block_on_until(future, Duration::from_secs(30)) {
Some(output) => output,
None => panic!("future did not complete within the test deadline"),
}
}
/// Drive one future until it completes or the deadline passes.
///
/// `None` is a real answer here rather than a failure: scope 6.3 says a
/// failure to wake a waiter leaves a hung request, and a test that asserts
/// that has to be able to observe the hang.
fn block_on_until<F: Future>(future: F, deadline: Duration) -> Option<F::Output> {
struct ThreadWaker(std::thread::Thread);
impl Wake for ThreadWaker {
fn wake(self: Arc<Self>) {
self.0.unpark();
}
fn wake_by_ref(self: &Arc<Self>) {
self.0.unpark();
}
}
let waker = Waker::from(Arc::new(ThreadWaker(std::thread::current())));
let mut context = Context::from_waker(&waker);
let mut future = std::pin::pin!(future);
let start = std::time::Instant::now();
loop {
match future.as_mut().poll(&mut context) {
Poll::Ready(output) => return Some(output),
Poll::Pending => {
let elapsed = start.elapsed();
if elapsed >= deadline {
return None;
}
std::thread::park_timeout(deadline - elapsed);
}
}
}
}
/// Exclusive use of the process-global failpoint and fault registries.
///
/// Every helper that can produce a `StoreOptions` takes one, so a test
/// cannot reach the writer without holding it. That is deliberate and it
/// is the Wave A `recovery_eio.rs` lesson applied one layer up: the
/// registries are one-shot and global, so a test that merely *submits* can
/// consume a failpoint another test armed. A comment cannot fail a build;
/// a parameter can.
#[cfg(feature = "failpoints")]
pub(super) type WriterSerial = crate::sys::FaultSerial;
#[cfg(not(feature = "failpoints"))]
pub(super) type WriterSerial = ();
pub(super) fn writer_serial() -> WriterSerial {
#[cfg(feature = "failpoints")]
{
crate::sys::serial()
}
}
fn counters() -> Arc<DurabilityCounters> {
Arc::new(DurabilityCounters::default())
}
pub(super) fn options(_serial: &WriterSerial, root: &Path, shard_count: u16) -> StoreOptions {
let mut options = StoreOptions::new(root);
options.shard_count = shard_count;
options.max_group_transactions = 4;
options.max_group_bytes = 64 * 1024;
options.max_group_idle = Duration::from_millis(50);
options.journal_preallocate_bytes = 4 * 1024 * 1024;
options.signer = Some(Arc::new(TestSigner::default()));
options
}
/// Every writer test's root, built by the entry point a consumer calls.
///
/// This used to seed the root with `segment::initialize_root` and then
/// open it, because `StoreEngine::open` refused startup state 1. That was
/// the charter item 8 weakening at the heart of this deliverable: the
/// production `submit` was being exercised over a root production could
/// not create. With state 1 implemented there is nothing left to seed —
/// the first call initializes and every later call on the same path is
/// state 2, which is also what makes the reopen tests below reopens rather
/// than re-seedings.
fn open(serial: &WriterSerial, root: &Path, shard_count: u16) -> StoreEngine {
StoreEngine::open(options(serial, root, shard_count))
.expect("open initializes an absent root and reopens a formatted one")
}
/// The client principal on whose behalf a transaction is submitted.
///
/// Deliberately **not** `TestSigner`'s public key. The two were the same
/// value, and `CommittedTransactionV1.actor` was being filled from the
/// destination signer rather than from the evidence — a defect no assertion
/// in this file could see, because every value it could have been was
/// `[0x5a; 32]`. Frozen mirror validation requires
/// `destination_event.actor == source_instance`, so the substitution
/// produces an event that signs, fences, and publishes and only then fails
/// verification at a mirror on another instance.
const EVIDENCE_ACTOR: [u8; 32] = [0x7e; 32];
fn evidence() -> TransactionEvidenceV1 {
TransactionEvidenceV1::AdministrativeV1 {
actor: EVIDENCE_ACTOR,
actor_key_epoch: 7,
command_digest: ObjectId([0x33; 32]),
signature: [0x44; 64],
}
}
fn deadline() -> i64 {
now_micros() + 600_000_000
}
pub(super) fn genesis_object() -> StagedObject {
StagedObject {
id: ObjectId([0xa1; 32]),
object_type: ObjectType::Authority,
raw: vec![0xa1; 32],
}
}
pub(super) fn create_transaction(
namespace: NamespaceId,
operation: u8,
) -> ValidatedTransaction {
let genesis = genesis_object();
let id = genesis.id;
ValidatedTransaction::builder(PrivilegedConstruction::internal())
.namespace(namespace)
.operation(
OperationId([operation; 16]),
ObjectId([operation; 32]),
deadline(),
)
.create_repository(id)
.objects(vec![genesis])
.refs(Vec::new())
.authority(None, Some(id))
.evidence(evidence())
.build()
.expect("a complete create transaction")
}
/// A push carrying one blob and, optionally, one typed ref update.
pub(super) fn push_transaction(
namespace: NamespaceId,
operation: u8,
blob: u8,
reference: Option<TypedRefCas>,
) -> ValidatedTransaction {
let authority = genesis_object().id;
ValidatedTransaction::builder(PrivilegedConstruction::internal())
.namespace(namespace)
.operation(
OperationId([operation; 16]),
ObjectId([operation; 32]),
deadline(),
)
.objects(vec![StagedObject {
id: ObjectId([blob; 32]),
object_type: ObjectType::Blob,
raw: vec![blob; 64],
}])
.refs(reference.into_iter().collect())
.authority(Some(authority), Some(authority))
.evidence(evidence())
.build()
.expect("a complete push transaction")
}
fn set_branch(name: &str, expected: Option<ObjectId>, target: ObjectId) -> TypedRefCas {
TypedRefCas {
target: RefTarget::Branch(name.into()),
expected,
mutation: RefMutation::Set(target),
force: false,
}
}
// -----------------------------------------------------------------------
// Startup
// -----------------------------------------------------------------------
/// Deliverable 1's central claim: one session, held from before the first
/// shard's recovery until the engine is dropped.
///
/// The lock is the observable. If `open` took and released a session per
/// shard, or dropped it once recovery finished, a second `open` would
/// succeed here — and a second process entering a recovered-but-not-yet-
/// ready root is exactly what the single session exists to prevent.
#[test]
fn one_retained_session_recovers_every_shard_and_holds_the_root_lock() {
let serial = writer_serial();
let temporary = tempfile::tempdir().expect("tempdir");
let engine = open(&serial, temporary.path(), 4);
assert!(matches!(
StoreEngine::open(options(&serial, temporary.path(), 4)),
Err(StoreError::AlreadyLocked)
));
// Every shard was recovered, not only the one a transaction happens to
// route to: each contributes its committed sequence and its retained
// generation to the first published root.
let root = engine.committed_root();
assert_eq!(root.retained_generations().len(), 4);
drop(engine);
StoreEngine::open(options(&serial, temporary.path(), 4))
.expect("the lock is released when the engine is dropped");
}
// -----------------------------------------------------------------------
// Plan §5.2's startup states, one test per state
//
// Charter item 8: every one of these asserts against `StoreEngine::open`,
// the entry point a consumer calls, and never against `classify_root` or
// `segment::initialize_root` directly. A root that only `initialize_root`
// can create is a root the production path has never been shown to build.
// -----------------------------------------------------------------------
/// A recursive, order-independent image of a directory tree: relative
/// path, whether it is a directory, and the exact bytes of every file.
///
/// This is what "byte-identical after the refusal" is asserted with. It
/// deliberately captures *contents* rather than mtimes: an mtime can
/// change for reasons outside this process, and a test that flakes on it
/// would be retried rather than believed. What it must catch is a probe
/// that created a directory, truncated a file, or appended a byte, and it
/// catches all three including the empty-directory case, which a
/// contents-only digest would miss.
fn tree_image(root: &Path) -> Vec<(PathBuf, bool, Vec<u8>)> {
fn walk(base: &Path, dir: &Path, out: &mut Vec<(PathBuf, bool, Vec<u8>)>) {
let mut entries: Vec<_> = std::fs::read_dir(dir)
.expect("read a directory of the image")
.map(|entry| entry.expect("a directory entry").path())
.collect();
entries.sort();
for path in entries {
let relative = path
.strip_prefix(base)
.expect("every walked path is under the base")
.to_path_buf();
if path.is_dir() {
out.push((relative, true, Vec::new()));
walk(base, &path, out);
} else {
out.push((relative, false, std::fs::read(&path).expect("read a file")));
}
}
}
let mut image = Vec::new();
if root.exists() {
walk(root, root, &mut image);
}
image
}
/// State 1, absent path. The root's *name* does not exist when `open` is
/// called, so this also covers the case in which initialization has to
/// make a new directory entry durable in the parent.
#[test]
fn startup_state_1_initializes_an_absent_root_through_open() {
let serial = writer_serial();
let temporary = tempfile::tempdir().expect("tempdir");
let root = temporary.path().join("not").join("yet").join("a-root");
assert!(!root.exists());
let engine = StoreEngine::open(options(&serial, &root, 3))
.expect("startup state 1 initializes an absent root");
// Charter item 7: not "it must have fsynced", but how many times, and
// for which entry. The total is stated as its parts so that a change to
// any one of them fails here with its name attached.
//
// `segment::initialize_root` costs six directories per shard, each
// fsynced as it is created, plus the shard directory a second time once
// its children are durable — seven per shard — and then shards/,
// quarantine/, the root, and the root's parent. `staging/` is created
// and not separately fsynced, which is correct rather than missing: it
// is empty, so the only thing that has to become durable about it is
// its name in the root, and the root fsync is what covers that.
let inside_initialize_root = 3 * 7 + 4;
// The path is `<tmp>/not/yet/a-root` and none of the three exist, so
// `open` creates three directory entries before it can take the root
// lock — and fences each one by fsyncing the directory that names it:
// `yet` (which names `a-root`), `not` (which names `yet`), and the
// tempdir (which names `not`), in that order.
//
// This is the assertion the P1-1 finding turns on. `initialize_root`
// fsyncs `root.parent()` and nothing above it, so before the fix this
// number was zero and a power loss could take `not` or `yet` — and the
// root with them — from a store that had already reported itself
// initialized.
let created_ancestors = 3;
// The `INITIALIZING` marker: one fsync of the root to make its name
// durable before the tree is built, and one more to make its removal
// durable after `FORMAT` is installed.
let initialization_marker = 2;
let initialization = engine
.shared
.initialization
.expect("state 1 records what it cost");
assert_eq!(
initialization.fdatasync, 2,
"the INITIALIZING marker and FORMAT are each fenced exactly once"
);
assert_eq!(
initialization.fsync_dir,
created_ancestors + initialization_marker + inside_initialize_root,
"every created ancestor, the initialization marker's install and removal, \
every created directory, shards/, quarantine/, the root, and its parent"
);
assert_eq!(initialization.short_writes, 0);
assert!(
!root.join("INITIALIZING").exists(),
"a finished initialization leaves no marker behind"
);
// The root it built is the root the production path then opens: the
// frozen topology is the configured one, and it is readable through
// `FORMAT` rather than assumed.
assert_eq!(engine.shared.shard_count, 3);
let marker = segment::read_format(&RootLayout::new(&root)).expect("FORMAT is valid");
assert_eq!(marker.shard_count, 3);
assert_eq!(marker.root_uuid, engine.shared.root_uuid);
// And it is a *usable* root, not merely a well-formed one.
let namespace = NamespaceId([0x51; 32]);
block_on(engine.submit(create_transaction(namespace, 1)))
.expect("a transaction commits into a root open() created");
}
/// P1-1, isolated from every other cost in the open.
///
/// The absolute count above is the whole claim, but it is one number made
/// of four terms, and a reader has to trust the arithmetic to believe the
/// ancestor term is in it. This asserts the ancestor term on its own: two
/// otherwise identical initializations whose roots differ only in how many
/// directory entries have to be created must differ in directory fsyncs by
/// exactly that many.
///
/// Before the fix the difference was zero — `create_dir_all` made all the
/// entries and `initialize_root` fenced one of them — so this fails without
/// it, and it fails with a number rather than an absence.
#[test]
fn startup_state_1_fences_one_directory_entry_per_created_ancestor() {
let serial = writer_serial();
let temporary = tempfile::tempdir().expect("tempdir");
// One new entry: the root itself, inside an existing directory.
let shallow = temporary.path().join("shallow");
let shallow_cost = StoreEngine::open(options(&serial, &shallow, 1))
.expect("state 1 at depth one")
.shared
.initialization
.expect("state 1 records what it cost")
.fsync_dir;
// Three new entries: two ancestors and the root.
let deep = temporary.path().join("deep").join("er").join("still");
let deep_cost = StoreEngine::open(options(&serial, &deep, 1))
.expect("state 1 at depth three")
.shared
.initialization
.expect("state 1 records what it cost")
.fsync_dir;
assert_eq!(
deep_cost - shallow_cost,
2,
"two extra created ancestors must cost two extra directory fences; \
everything else about the two initializations is identical"
);
}
/// State 1, present but empty. Distinguished from the absent case only by
/// who created the directory entry, and required to reach the same place.
#[test]
fn startup_state_1_initializes_an_existing_empty_root() {
let serial = writer_serial();
let temporary = tempfile::tempdir().expect("tempdir");
assert_eq!(tree_image(temporary.path()), Vec::new());
let engine = StoreEngine::open(options(&serial, temporary.path(), 1))
.expect("an existing empty directory is startup state 1");
assert!(engine.shared.initialization.is_some());
assert!(temporary.path().join("FORMAT").exists());
}
/// State 1, and the deliberate decision inside it: a directory holding
/// only `LOCK` is empty.
///
/// This is not a hypothetical. `segment::lock_root` creates `LOCK` if it is
/// absent, so an initialization that dies between taking the lock
/// and the `FORMAT` rename leaves precisely this, and so does any process
/// that merely asked whether the root was busy. Classifying it as
/// unrecognized would wedge a root with no bytes to lose.
#[test]
fn startup_state_1_treats_a_root_holding_only_lock_as_empty() {
let serial = writer_serial();
let temporary = tempfile::tempdir().expect("tempdir");
let layout = RootLayout::new(temporary.path());
drop(segment::lock_root(&layout).expect("probing the root creates LOCK"));
let image = tree_image(temporary.path());
assert_eq!(image.len(), 1, "exactly LOCK is present: {image:?}");
assert_eq!(image[0].0, PathBuf::from("LOCK"));
let engine = StoreEngine::open(options(&serial, temporary.path(), 1))
.expect("a root holding only LOCK is still startup state 1");
assert!(engine.shared.initialization.is_some());
}
/// State 1 is a mutation, so it happens under the root lock.
///
/// The observable is the refusal: with `LOCK` held elsewhere, `open` on
/// what is otherwise a state-1 root must be refused `AlreadyLocked` and
/// must leave the root exactly as empty as it found it. An initialization
/// that ran before locking, or without locking, would create `FORMAT` here
/// — and two processes doing that concurrently is the race the lock
/// exists to close.
#[test]
fn startup_state_1_does_not_initialize_a_root_another_process_holds() {
let serial = writer_serial();
let temporary = tempfile::tempdir().expect("tempdir");
let layout = RootLayout::new(temporary.path());
let held = segment::lock_root(&layout).expect("take the root lock first");
assert!(matches!(
StoreEngine::open(options(&serial, temporary.path(), 1)),
Err(StoreError::AlreadyLocked)
));
let image = tree_image(temporary.path());
assert_eq!(image.len(), 1, "nothing but LOCK was created: {image:?}");
// Releasing it makes the same call succeed, so the refusal was the
// lock and not something else about the root.
drop(held);
StoreEngine::open(options(&serial, temporary.path(), 1))
.expect("the same root initializes once the lock is released");
assert_eq!(segment::RootLock::release_failures(), 0);
}
/// P1-2. An initialization interrupted after it has left residue is
/// **finished** by the next `open` rather than refused for ever.
///
/// Before the fix, any crash between `segment::initialize_root`'s first
/// `create_dir_all` and its `FORMAT` rename turned a brand-new root into a
/// permanently unopenable one: the root was non-empty and carried no
/// `FORMAT`, so it was refused as states 3/4 — with a message about legacy
/// layouts and migration, for a root that had never held a byte of anyone's
/// data.
///
/// The crash is simulated, not raced: the image below is written directly,
/// so this test has no timing in it at all. The load-bearing part of the
/// image — the marker — is produced by *production's own* encoder, so a
/// change to the marker format cannot leave this test passing against bytes
/// production no longer writes. The companion test below injects the
/// interruption into a real `open` instead, which is what proves production
/// writes the marker at the moment this image claims it does.
#[test]
fn startup_state_1_resumes_an_interrupted_initialization() {
let serial = writer_serial();
let temporary = tempfile::tempdir().expect("tempdir");
let root = temporary.path().join("interrupted");
// The crash image: the marker `open` installs as its first durable act,
// plus as much of the tree as `segment::initialize_root` had built when
// the power went — including a half-built shard directory, which is the
// deepest residue the window can produce.
std::fs::create_dir_all(root.join("quarantine")).expect("partial tree");
std::fs::create_dir_all(root.join("staging")).expect("partial tree");
std::fs::create_dir_all(root.join("shards").join("00").join("active"))
.expect("partial tree");
std::fs::write(
root.join("INITIALIZING"),
initializing_marker_bytes(2, now_micros()),
)
.expect("the marker production writes");
let engine = StoreEngine::open(options(&serial, &root, 2))
.expect("an interrupted initialization is finished, not refused");
assert!(
engine.shared.initialization.is_some(),
"the resume is an initialization, and is recorded as one"
);
assert!(root.join("FORMAT").exists());
assert!(
!root.join("INITIALIZING").exists(),
"the marker is removed once FORMAT is durable, so the finished root \
classifies as state 2 and not as an interrupted one"
);
// Usable, not merely well-formed. A resume that produced a tree nothing
// could commit into would satisfy every assertion above.
let namespace = NamespaceId([0x62; 32]);
block_on(engine.submit(create_transaction(namespace, 1)))
.expect("a transaction commits into a resumed root");
}
/// P1-2's dangerous direction, asserted rather than argued.
///
/// Refusing a resumable root wastes an operator's time; adopting a root
/// that is not ours destroys their data. Each of these is one condition
/// away from the resumable shape, and each must be refused **and left
/// byte-identical**.
#[test]
fn an_unrecognized_layout_is_never_mistaken_for_an_interrupted_initialization() {
let serial = writer_serial();
let refuse = |label: &str, root: &Path| {
let before = tree_image(root);
match StoreEngine::open(options(&serial, root, 1)) {
Err(StoreError::NotImplemented(reason)) => {
assert!(
reason.contains("startup states 3 and 4"),
"{label}: {reason}"
);
}
other => panic!("{label}: expected a refusal, got {other:?}"),
}
assert_eq!(
tree_image(root),
before,
"{label}: a refused layout must be byte-identical afterwards"
);
};
// The whole initialization skeleton and no marker. This is the shape
// the classification must *not* adopt on the strength of the names
// alone: those names are ours, but without the marker there is nothing
// to say this directory was ever built by this store rather than
// arranged to look like it was.
let skeleton = tempfile::tempdir().expect("tempdir");
std::fs::create_dir_all(skeleton.path().join("quarantine")).expect("skeleton");
std::fs::create_dir_all(skeleton.path().join("staging")).expect("skeleton");
std::fs::create_dir_all(skeleton.path().join("shards").join("00")).expect("skeleton");
refuse("skeleton without a marker", skeleton.path());
// A byte-exact marker with one foreign name beside it. The marker alone
// does not authorize anything: an initialization in progress cannot
// have produced a name this store never writes, so the foreign entry
// means the marker is in somebody else's directory.
let intruded = tempfile::tempdir().expect("tempdir");
std::fs::write(
intruded.path().join("INITIALIZING"),
initializing_marker_bytes(1, now_micros()),
)
.expect("marker");
std::fs::create_dir_all(intruded.path().join("quarantine")).expect("skeleton");
std::fs::write(intruded.path().join("notes.txt"), b"someone's data\n").expect("foreign");
refuse("a valid marker with a foreign entry", intruded.path());
// A file of exactly the marker's length, under exactly the marker's
// name, that is not the marker. Length is not evidence; the magic is.
let impostor = tempfile::tempdir().expect("tempdir");
std::fs::write(impostor.path().join("INITIALIZING"), [0u8; 27]).expect("impostor");
refuse("a same-length file that is not the marker", impostor.path());
// The magic and nothing after it. A prefix of the marker is not the
// marker: accepting short reads would make every truncated file in the
// universe of this name a licence to initialize.
let truncated = tempfile::tempdir().expect("tempdir");
std::fs::write(truncated.path().join("INITIALIZING"), b"levcs-store-init").expect("short");
refuse("a truncated marker", truncated.path());
// A symlink of the marker's name, pointing at bytes that *are* a valid
// marker. The content test passes if it is allowed to run at all, so
// this is entirely a test of the type check: our marker is a regular
// file in this directory, not a name that resolves to one somewhere
// else.
let elsewhere = tempfile::tempdir().expect("tempdir");
let real_marker = elsewhere.path().join("borrowed-marker");
std::fs::write(&real_marker, initializing_marker_bytes(1, now_micros())).expect("marker");
let linked = tempfile::tempdir().expect("tempdir");
std::os::unix::fs::symlink(&real_marker, linked.path().join("INITIALIZING"))
.expect("symlink");
refuse("a symlink at the marker's name", linked.path());
}
/// Contract review finding P1, direction 1: **a regular file at the staging
/// name is not this store's residue and is not destroyed.**
///
/// `classify_root` used to ignore `INITIALIZING.tmp` whatever it held,
/// because the name "can only be this code path's residue by construction"
/// — the one claim a classifier running *in order to establish* that is not
/// entitled to. `build_root_under_lock` then opened it `create` +
/// `truncate`. So an otherwise-empty configured root containing an
/// operator's file of that name lost it silently, and reported success.
///
/// Every shape below is one that used to be overwritten, including the torn
/// marker — refusing that one is the trade `install_initializing_marker`
/// states, and it is asserted here so the trade is a fact rather than a
/// comment.
#[test]
fn a_foreign_entry_at_the_initializing_staging_name_is_refused_and_left_intact() {
let serial = writer_serial();
let refuse = |label: &str, root: &Path| {
let before = tree_image(root);
match StoreEngine::open(options(&serial, root, 1)) {
Err(StoreError::NotImplemented(reason)) => assert!(
reason.contains("startup states 3 and 4"),
"{label}: {reason}"
),
other => panic!("{label}: expected a refusal, got {other:?}"),
}
assert_eq!(
tree_image(root),
before,
"{label}: a refused root must be byte-identical afterwards"
);
};
// An operator's file that happens to carry the name. This is the whole
// finding: nothing else is in the root, so before the fix `open`
// returned a working store and this file was gone.
let operators = tempfile::tempdir().expect("tempdir");
std::fs::write(
operators.path().join("INITIALIZING.tmp"),
b"do not delete: staging notes for the migration\n",
)
.expect("operator file");
refuse("an operator's file at the staging name", operators.path());
// A prefix of the marker — the exact residue of a crash between the
// create and the fence. Refused, deliberately: nothing on disk tells it
// apart from a stranger's truncated file, so a rule that adopts this one
// destroys that one.
let torn = tempfile::tempdir().expect("tempdir");
std::fs::write(
torn.path().join("INITIALIZING.tmp"),
&initializing_marker_bytes(1, now_micros())[..12],
)
.expect("torn marker");
refuse("a torn temporary marker", torn.path());
// The right length and the wrong bytes, and a directory. Both are
// "not a regular file holding our marker", and neither is a shape the
// classifier may fold into a neighbour.
let impostor = tempfile::tempdir().expect("tempdir");
std::fs::write(impostor.path().join("INITIALIZING.tmp"), [0u8; 27]).expect("impostor");
refuse("a same-length file that is not the marker", impostor.path());
let directory = tempfile::tempdir().expect("tempdir");
std::fs::create_dir(directory.path().join("INITIALIZING.tmp")).expect("directory");
refuse("a directory at the staging name", directory.path());
// A fifo, asserted without `tree_image` — deliberately, and the reason
// is the point of the case. `tree_image` reads every entry it walks,
// and reading a fifo with no writer blocks for ever; using it here
// hangs the test before `open` is ever called. That is exactly the
// hazard on the production side: anything that opens a name in an
// unowned root before establishing its type can be stopped dead by one
// `mkfifo`. `classify_root` checks `file_type` first and never opens
// this entry, and `sys::open_regular_nofollow` passes `O_NONBLOCK` for
// the path that can reach one anyway. This test completes rather than
// timing out only because both of those hold.
let fifo = tempfile::tempdir().expect("tempdir");
let fifo_name = fifo.path().join("INITIALIZING.tmp");
rustix::fs::mknodat(
rustix::fs::CWD,
&fifo_name,
rustix::fs::FileType::Fifo,
rustix::fs::Mode::from_raw_mode(0o644),
0,
)
.expect("mkfifo");
match StoreEngine::open(options(&serial, fifo.path(), 1)) {
Err(StoreError::NotImplemented(reason)) => {
assert!(reason.contains("startup states 3 and 4"), "{reason}")
}
other => panic!("a fifo at the staging name: expected a refusal, got {other:?}"),
}
assert!(
std::os::unix::fs::FileTypeExt::is_fifo(
&fifo_name
.symlink_metadata()
.expect("the fifo is still there")
.file_type()
),
"the refusal must not have replaced the fifo with anything"
);
assert!(!fifo.path().join("FORMAT").exists());
}
/// Contract review finding P1, direction 2 and the worse one: **a symlink
/// at the staging name must not be written through.**
///
/// `create` + `truncate` follows a final symlink, so the truncation landed
/// on a file the store had never owned and that was not even inside the
/// configured root — and the link was then renamed onto `INITIALIZING` and
/// unlinked, so the only evidence of what had been destroyed went with it.
///
/// The assertion is on the *target*, outside the root, because that is
/// where the damage was. `O_CREAT | O_EXCL` is what makes it impossible:
/// it fails with `EEXIST` on a symlink whether or not the link resolves.
#[test]
fn a_symlink_at_the_initializing_staging_name_never_truncates_its_target() {
let serial = writer_serial();
let outside = tempfile::tempdir().expect("tempdir");
let victim = outside.path().join("payroll.db");
let contents: Vec<u8> = (0..4096u32).map(|byte| (byte % 251) as u8).collect();
std::fs::write(&victim, &contents).expect("the file outside the root");
let root = tempfile::tempdir().expect("tempdir");
let link = root.path().join("INITIALIZING.tmp");
std::os::unix::fs::symlink(&victim, &link).expect("symlink");
match StoreEngine::open(options(&serial, root.path(), 1)) {
Err(StoreError::NotImplemented(reason)) => {
assert!(reason.contains("startup states 3 and 4"), "{reason}")
}
other => panic!("expected a refusal, got {other:?}"),
}
assert_eq!(
std::fs::read(&victim).expect("the target still exists"),
contents,
"a symlink in the root must not be able to redirect a truncation at a \
file outside it"
);
assert_eq!(
std::fs::read_link(&link).expect("the link is still a link"),
victim,
"the link itself must not have been renamed or unlinked either — that is \
what removed the evidence"
);
assert!(
!root.path().join("FORMAT").exists(),
"the refusal must not have initialized the root"
);
}
/// The other half of the trade: a **whole** temporary marker still resumes,
/// and is *adopted* rather than rewritten.
///
/// This is the shape a crash between the fence and the `rename_noreplace`
/// leaves, and it must not become collateral damage of the type and content
/// checks above.
///
/// "Still resumes" alone cannot fail without the fix — the old code reached
/// the same working root by truncating the file and writing it again, which
/// is precisely the operation that destroyed an operator's file at the same
/// name. So the load-bearing assertion is a counter, not an outcome: two
/// otherwise identical initializations, one from an empty root and one from
/// a root already carrying a whole marker, must differ in durable bytes by
/// exactly the marker's length, because the second one does not write it.
/// Before the fix that difference was zero.
#[test]
fn a_whole_temporary_marker_is_adopted_rather_than_rewritten() {
let serial = writer_serial();
let temporary = tempfile::tempdir().expect("tempdir");
// The control: the same initialization with nothing staged.
let fresh = temporary.path().join("fresh");
let fresh_bytes = StoreEngine::open(options(&serial, &fresh, 1))
.expect("state 1 from an empty root")
.shared
.initialization
.expect("state 1 records what it cost")
.bytes_written;
let staged = temporary.path().join("staged");
std::fs::create_dir(&staged).expect("root");
std::fs::write(
staged.join("INITIALIZING.tmp"),
initializing_marker_bytes(1, now_micros()),
)
.expect("staged marker");
let engine = StoreEngine::open(options(&serial, &staged, 1))
.expect("a whole temporary marker is this store's residue and is resumed from");
let initialization = engine
.shared
.initialization
.expect("the resume is an initialization");
assert_eq!(
fresh_bytes - initialization.bytes_written,
INITIALIZING_MARKER_LEN as u64,
"the staged marker already holds what this attempt would have written, so \
adoption writes nothing and fences what is there; everything else about the \
two initializations is identical"
);
assert_eq!(
initialization.fdatasync, 2,
"adoption still costs exactly the marker's fence and FORMAT's"
);
assert!(staged.join("FORMAT").exists());
assert!(
!staged.join("INITIALIZING.tmp").exists(),
"the staging name is consumed by the rename onto INITIALIZING"
);
assert!(!staged.join("INITIALIZING").exists());
let namespace = NamespaceId([0x73; 32]);
block_on(engine.submit(create_transaction(namespace, 1)))
.expect("a transaction commits into a root resumed from a staged marker");
}
/// The same hazard at the other two names this file opens or ignores.
///
/// `segment::lock_root` opens `LOCK` and `segment::write_fenced` opens
/// `FORMAT.tmp`. Both are in A1's frozen file, so neither open was changed by
/// this deliverable; what it changed is that a root carrying a symlink at
/// either name never reaches them, because the classification that authorizes
/// the write refuses it first.
///
/// Both have since been amended by the lead — `lock_root` in contract review
/// 2026-07-29-A, `write_fenced` in 2026-07-29-B — because
/// `RecoverySession::open`, `drive.rs` and `store-bench` all reach them with
/// no classification at all. This test therefore no longer carries either
/// guarantee on its own; it asserts that the classifier refuses first, which
/// is still the answer an operator wants, and the assertions in `segment.rs`
/// are what hold the opens themselves.
#[test]
fn a_symlink_at_lock_or_format_tmp_is_refused_before_anything_opens_it() {
let serial = writer_serial();
let outside = tempfile::tempdir().expect("tempdir");
// `LOCK`, pointing at a name that does not exist. `create(true)` through
// this link would bring a file into being outside the root.
let absent = outside.path().join("not-there");
let locked = tempfile::tempdir().expect("tempdir");
std::os::unix::fs::symlink(&absent, locked.path().join("LOCK")).expect("symlink");
match StoreEngine::open(options(&serial, locked.path(), 1)) {
Err(StoreError::NotImplemented(reason)) => {
assert!(reason.contains("startup states 3 and 4"), "{reason}")
}
other => panic!("expected a refusal for a symlinked LOCK, got {other:?}"),
}
assert!(
!absent.exists(),
"a symlinked LOCK must not create a file outside the root"
);
// `FORMAT.tmp`, beside a valid marker — the one arrangement that would
// otherwise reach `segment::write_fenced`, because a `FORMAT.tmp` on its
// own is already refused for having no marker to explain it.
let victim = outside.path().join("ledger");
std::fs::write(&victim, b"balances\n").expect("the file outside the root");
let resumable = tempfile::tempdir().expect("tempdir");
std::fs::write(
resumable.path().join("INITIALIZING"),
initializing_marker_bytes(1, now_micros()),
)
.expect("marker");
std::os::unix::fs::symlink(&victim, resumable.path().join("FORMAT.tmp")).expect("symlink");
match StoreEngine::open(options(&serial, resumable.path(), 1)) {
Err(StoreError::NotImplemented(reason)) => {
assert!(reason.contains("startup states 3 and 4"), "{reason}")
}
other => panic!("expected a refusal for a symlinked FORMAT.tmp, got {other:?}"),
}
assert_eq!(
std::fs::read(&victim).expect("the target still exists"),
b"balances\n",
"a symlink at FORMAT.tmp must not become a truncation outside the root"
);
}
/// P1-2, with the interruption injected into a real `StoreEngine::open`
/// instead of written to disk.
///
/// One directory fsync is made to fail. On an existing empty root the first
/// one `open` issues is the fence that makes the marker's *name* durable,
/// so this stops initialization at the earliest instant that can leave
/// residue — and proves the ordering the classification depends on: the
/// marker is on disk and the tree is not.
///
/// Deterministic, not raced: the fault registry is one-shot and positional,
/// and this test holds the serial that makes it exclusively its own.
#[cfg(feature = "failpoints")]
#[test]
fn an_interrupted_open_leaves_a_marker_the_next_open_resumes_from() {
let serial = writer_serial();
let temporary = tempfile::tempdir().expect("tempdir");
crate::sys::arm(&serial, crate::sys::Fault::DirSyncEio);
let interrupted = StoreEngine::open(options(&serial, temporary.path(), 1));
crate::sys::disarm(&serial);
match interrupted {
Err(StoreError::NotImplemented(reason)) => {
panic!("an injected I/O failure must not read as an unbuilt state: {reason}")
}
Err(_) => {}
Ok(_) => panic!("the injected directory fsync failure did not stop the open"),
}
let marker = temporary.path().join("INITIALIZING");
assert_eq!(
std::fs::read(&marker)
.expect("the interrupted open left its marker")
.len(),
27,
"the marker is installed by rename, so it is whole or absent and never torn"
);
assert!(
!temporary.path().join("shards").exists(),
"the marker must become durable before the first directory of the tree exists, \
or a crash can leave tree residue with nothing to say whose it is"
);
let engine = StoreEngine::open(options(&serial, temporary.path(), 1))
.expect("the next open finishes what the interrupted one started");
assert!(engine.shared.initialization.is_some());
assert!(!marker.exists(), "the finished root carries no marker");
assert_eq!(segment::RootLock::release_failures(), 0);
}
/// State 2. Reopening a formatted root runs recovery and initializes
/// nothing — asserted directly, through the counter, rather than inferred
/// from the tree looking unchanged.
#[test]
fn startup_state_2_opens_a_formatted_root_without_reinitializing_it() {
let serial = writer_serial();
let temporary = tempfile::tempdir().expect("tempdir");
let root = temporary.path().join("root");
let first = StoreEngine::open(options(&serial, &root, 2)).expect("state 1");
let uuid = first.shared.root_uuid;
assert!(first.shared.initialization.is_some());
drop(first);
let second = StoreEngine::open(options(&serial, &root, 2)).expect("state 2");
assert!(
second.shared.initialization.is_none(),
"a formatted root must not be initialized again"
);
assert_eq!(
second.shared.root_uuid, uuid,
"the identity FORMAT froze is the identity the reopen carries"
);
}
/// States 3 and 4, both still refused this pass — and the property that
/// survives the refusal either way: **the root is byte-identical**.
///
/// Both layouts are checked because the whole hazard of implementing state
/// 1 is that classifying "absent or empty" requires looking at a root that
/// might be neither, and a probe that creates so much as a directory has
/// destroyed this guarantee before the refusal is reached.
#[test]
fn startup_states_3_and_4_are_refused_by_name_and_leave_the_root_byte_identical() {
let serial = writer_serial();
// State 3's shape: the legacy instance signature, `<64-hex>/.levcs/`.
let legacy = tempfile::tempdir().expect("tempdir");
let repo = legacy.path().join("a".repeat(64));
std::fs::create_dir_all(repo.join(".levcs").join("objects")).expect("legacy tree");
std::fs::write(repo.join(".levcs").join("HEAD"), b"ref: refs/heads/main\n")
.expect("legacy file");
// State 4's shape: non-empty, no `FORMAT`, no legacy signature —
// including an empty directory and a zero-byte file, which a
// contents-only comparison would not notice being removed.
let unrecognized = tempfile::tempdir().expect("tempdir");
std::fs::write(unrecognized.path().join("notes.txt"), b"someone's data\n")
.expect("stray file");
std::fs::write(unrecognized.path().join("FORMAT.tmp"), b"\x00\x01\x02").expect("residue");
std::fs::create_dir_all(unrecognized.path().join("empty")).expect("stray directory");
std::fs::write(unrecognized.path().join("empty-file"), b"").expect("stray empty file");
for root in [legacy.path(), unrecognized.path()] {
let before = tree_image(root);
match StoreEngine::open(options(&serial, root, 1)) {
Err(StoreError::NotImplemented(reason)) => {
assert!(reason.contains("startup states 3 and 4"), "{reason}");
assert!(reason.contains("LegacyLayout"), "{reason}");
assert!(reason.contains("UnrecognizedLayout"), "{reason}");
assert!(reason.contains("has not been modified"), "{reason}");
}
other => panic!("expected an explicit refusal for {root:?}, got {other:?}"),
}
assert_eq!(
tree_image(root),
before,
"a refused non-empty layout must be byte-identical afterwards: {root:?}"
);
}
}
/// A `FORMAT` that exists but does not decode is state 2 and stays state
/// 2. The dangerous misclassification is the other direction: treating an
/// unreadable marker as "no marker, therefore empty" would authorize
/// `initialize_root` to build a fresh tree over a populated root.
#[test]
fn a_corrupt_format_is_refused_rather_than_reinitialized_over() {
let serial = writer_serial();
let temporary = tempfile::tempdir().expect("tempdir");
drop(StoreEngine::open(options(&serial, temporary.path(), 1)).expect("state 1"));
let format = RootLayout::new(temporary.path()).format_path();
let original = std::fs::read(&format).expect("read FORMAT");
std::fs::write(&format, vec![0u8; original.len()]).expect("corrupt FORMAT");
let before = tree_image(temporary.path());
let error = StoreEngine::open(options(&serial, temporary.path(), 1))
.expect_err("a corrupt FORMAT is refused");
assert!(
!matches!(error, StoreError::NotImplemented(_)),
"a corrupt marker is state 2's refusal, not an unbuilt state: {error:?}"
);
assert_eq!(
tree_image(temporary.path()),
before,
"the refusal must not have rebuilt the root"
);
}
/// Copy a whole directory tree, file bytes included.
///
/// The equivalence test below needs *two* copies of one crash image
/// because recovery is a mutation: it seals the validated prefix,
/// quarantines the discarded tail, and opens a fresh journal. Running both
/// paths over one directory would compare the first path's recovery
/// against the second path's recovery of what the first left behind, which
/// would agree for the wrong reason.
#[cfg(feature = "store-internals")]
fn copy_tree(from: &Path, to: &Path) {
std::fs::create_dir_all(to).expect("create the destination");
for entry in std::fs::read_dir(from).expect("read the source") {
let entry = entry.expect("a directory entry");
let target = to.join(entry.file_name());
if entry.path().is_dir() {
copy_tree(&entry.path(), &target);
} else {
std::fs::copy(entry.path(), &target).expect("copy a file");
}
}
}
/// Scope 6.4 deliverable 1's second acceptance clause: **the engine and
/// the drive seam produce the same recovered state from one crash image.**
///
/// The two go through `recovery::recover_shard`, so they agree by
/// construction — but "by construction" is exactly the kind of claim that
/// survives the construction changing. `ShardDrive::reopen_through_recovery`
/// makes one call to that function and `StoreEngine::open` makes one per
/// shard; if either ever grows a local decision, this is what notices.
///
/// The image is built at production altitude: `StoreEngine::open`
/// initializes the root (startup state 1), real transactions are
/// submitted, sequenced, signed and fenced through `submit`, and only then
/// is the *journal file* damaged. The damage is a byte-level edit and not
/// a logic-level one, which is the only kind a crash can produce.
///
/// # What is compared, and what is deliberately not
///
/// Every field of `ShardRecoveryReport` that is a function of the crash
/// image. The two `PathBuf` fields — `quarantined` and `preserved_journal`
/// — are excluded because they name files inside two different copies and
/// can never be equal; their *contents* are covered by
/// `quarantined_bytes`, which is compared. The recovery configurations are
/// pinned to the drive's constants so that a disagreement here is a
/// disagreement about the algorithm rather than about tuning.
#[cfg(feature = "store-internals")]
#[test]
fn the_engine_and_the_drive_seam_recover_one_crash_image_identically() {
use crate::drive::{ShardDrive, DRIVE_PREALLOCATE_BYTES};
let serial = writer_serial();
let temporary = tempfile::tempdir().expect("tempdir");
let namespace = NamespaceId([0x9c; 32]);
// The drive's recovery configuration is a set of constants rather than
// a `StoreOptions`, so the engine's options are pinned to match it.
// Anything left unmatched would show up as an algorithmic
// disagreement, which is the one thing this test must not be able to
// report falsely.
let configured = |root: &Path| {
let mut configured = options(&serial, root, 1);
configured.journal_preallocate_bytes = DRIVE_PREALLOCATE_BYTES;
configured.manifest_retain = 2;
configured.checkpoint_retain = 2;
configured.terminal_status_grace_micros = 900_000_000;
configured.max_active_index_entries = 4_000_000;
configured.max_active_index_bytes = 512 * 1024 * 1024;
configured.max_index_runs = 64;
configured.max_open_index_runs = 32;
configured
};
let image = temporary.path().join("image");
{
let engine = StoreEngine::open(configured(&image)).expect("startup state 1");
block_on(engine.submit(create_transaction(namespace, 1))).expect("create");
block_on(engine.submit(push_transaction(namespace, 2, 0xb1, None))).expect("push");
block_on(engine.submit(push_transaction(namespace, 3, 0xb2, None))).expect("push");
}
// Where the fenced region ends, asked of the scanner rather than
// computed: there is one definition of where a journal ends and this
// test does not get to be a second one.
let layout = RootLayout::new(&image);
let root_uuid = segment::read_format(&layout).expect("FORMAT").root_uuid;
let journal_path = std::fs::read_dir(layout.shard(0).active())
.expect("read active/")
.map(|entry| entry.expect("entry").path())
.next()
.expect("one active journal");
let (stop_offset, last_frame) = {
let (_journal, scan) =
crate::journal::Journal::open(&journal_path, &root_uuid, counters())
.expect("open the journal");
assert_eq!(scan.frames.len(), 3, "three fenced frames");
// A cleanly closed journal already stops early: the preallocated
// remainder is zeros, and zeros are `NotAFrame`. Stated here
// because it is why the damage below has to be a *torn frame* and
// not merely non-zero residue — the latter would produce the same
// classification a clean close does, and the comparison would no
// longer be about a crash.
assert_eq!(scan.stop, crate::journal::TailStop::NotAFrame);
let last = scan.frames.last().expect("a third frame").clone();
(scan.stop_offset, last)
};
// The crash residue: a frame whose header reached the device and whose
// tail did not — the interrupted append. Recovery must stop at it,
// discard it, and quarantine it, and both paths must reach the same
// verdict about *which* completeness condition failed.
{
use std::io::{Read, Seek, SeekFrom, Write};
let mut torn = vec![0u8; last_frame.len as usize];
let mut file = std::fs::File::open(&journal_path).expect("read the journal");
file.seek(SeekFrom::Start(last_frame.offset)).expect("seek");
file.read_exact(&mut torn).expect("read a whole frame");
drop(file);
torn.truncate(torn.len() - 8);
let mut file = std::fs::OpenOptions::new()
.write(true)
.open(&journal_path)
.expect("open the journal for damage");
file.seek(SeekFrom::Start(stop_offset)).expect("seek");
file.write_all(&torn).expect("write the torn frame");
file.sync_all().expect("fence the damage");
}
let through_drive_root = temporary.path().join("through-drive");
let through_engine_root = temporary.path().join("through-engine");
copy_tree(&image, &through_drive_root);
copy_tree(&image, &through_engine_root);
let drive = ShardDrive::reopen_through_recovery(&through_drive_root, 0)
.expect("the drive seam recovers the image");
let engine =
StoreEngine::open(configured(&through_engine_root)).expect("the engine recovers it");
let engine_report = engine.shared.recovery_reports[0].clone();
let drive_report = drive.report.clone();
// The image really was a crash image, or the comparison below is a
// comparison of two clean opens and proves nothing.
assert!(
drive_report.quarantined_bytes > 0,
"the damaged tail must have been discarded: {drive_report:?}"
);
assert!(
matches!(
drive_report.tail_stop,
Some(crate::journal::TailStop::Incomplete(_))
),
"the tail must have been rejected as an incomplete frame: {drive_report:?}"
);
assert_eq!(engine_report.shard_index, drive_report.shard_index);
assert_eq!(
engine_report.adopted_shard_sequences,
drive_report.adopted_shard_sequences
);
assert_eq!(engine_report.tail_stop, drive_report.tail_stop);
assert_eq!(
engine_report.quarantined_bytes,
drive_report.quarantined_bytes
);
assert_eq!(engine_report.active_journal, drive_report.active_journal);
assert_eq!(
engine_report.manifest_generation,
drive_report.manifest_generation
);
assert_eq!(engine_report.manifest_source, drive_report.manifest_source);
assert_eq!(
engine_report.checkpoint_sequence,
drive_report.checkpoint_sequence
);
assert_eq!(
engine_report.offline_rebuild_required,
drive_report.offline_rebuild_required
);
assert_eq!(engine_report.promotions, drive_report.promotions);
assert_eq!(engine_report.ready, drive_report.ready);
assert!(engine_report.ready);
// And the engine turned that shared recovered state into a committed
// root a consumer can read, which is the half the drive seam has no
// opinion about: three operations survived the crash image, and the
// fourth — never submitted — is `Unknown` rather than invented.
for operation in 1u8..=3 {
assert!(
matches!(
engine.transaction_status(namespace, OperationId([operation; 16])),
Ok(TransactionStatus::Committed(_))
),
"operation {operation} did not survive the crash image"
);
}
assert!(matches!(
engine.transaction_status(namespace, OperationId([4; 16])),
Ok(TransactionStatus::Unknown)
));
}
/// A configuration error must not be able to leave a root behind. The
/// signer check runs before the root is touched at all, so a signerless
/// open of an absent path creates nothing — not even the directory.
#[test]
fn a_configuration_error_refuses_before_startup_state_1_creates_anything() {
let serial = writer_serial();
let temporary = tempfile::tempdir().expect("tempdir");
let root = temporary.path().join("never-created");
let mut without = options(&serial, &root, 1);
without.signer = None;
assert!(matches!(
StoreEngine::open(without),
Err(StoreError::InvalidConfiguration(_))
));
assert!(!root.exists(), "a refused configuration created a root");
}
#[test]
fn a_configured_shard_count_that_disagrees_with_format_is_refused() {
let serial = writer_serial();
let temporary = tempfile::tempdir().expect("tempdir");
// The topology is frozen by the *production* initialization, so the
// mismatch is between two consumer-visible opens of one path rather
// than between a hand-seeded root and an open.
drop(open(&serial, temporary.path(), 4));
match StoreEngine::open(options(&serial, temporary.path(), 2)) {
Err(StoreError::FormatMismatch(reason)) => {
assert!(reason.contains("4 shards"), "{reason}")
}
other => panic!("expected FormatMismatch, got {other:?}"),
}
}
#[test]
fn opening_without_a_signer_is_refused_rather_than_deferred_to_the_first_submit() {
let serial = writer_serial();
let temporary = tempfile::tempdir().expect("tempdir");
drop(open(&serial, temporary.path(), 1));
let mut without = options(&serial, temporary.path(), 1);
without.signer = None;
assert!(matches!(
StoreEngine::open(without),
Err(StoreError::InvalidConfiguration(_))
));
}
// -----------------------------------------------------------------------
// The writer, end to end
// -----------------------------------------------------------------------
#[test]
fn a_committed_transaction_survives_close_and_reopen_through_recovery() {
let serial = writer_serial();
let temporary = tempfile::tempdir().expect("tempdir");
let namespace = NamespaceId([0x20; 32]);
let blob = ObjectId([0x77; 32]);
let branch = set_branch("main", None, blob);
let (create_receipt, push_receipt) = {
let engine = open(&serial, temporary.path(), 1);
let create = block_on(engine.submit(create_transaction(namespace, 1)))
.expect("the create commits");
let push =
block_on(engine.submit(push_transaction(namespace, 2, 0x77, Some(branch.clone()))))
.expect("the push commits");
assert_eq!(create.repo_sequence, 0);
assert_eq!(push.repo_sequence, 1);
assert_eq!(push.objects_new, 1);
assert_eq!(
push.refs,
vec![AppliedRefV1 {
target: RefTarget::Branch("main".into()),
old: None,
new: Some(blob),
force: false,
}]
);
assert_eq!(
engine
.transaction_status(namespace, OperationId([2; 16]))
.expect("status"),
TransactionStatus::Committed(push.clone())
);
(create, push)
};
// Reopen through production recovery. Everything asserted below came
// back off the device: the receipts, the applied refs, the repository
// state, and the index entry with its object-type code.
let engine = StoreEngine::open(options(&serial, temporary.path(), 1)).expect("reopen");
assert_eq!(
engine
.transaction_status(namespace, OperationId([1; 16]))
.expect("status"),
TransactionStatus::Committed(create_receipt)
);
assert_eq!(
engine
.transaction_status(namespace, OperationId([2; 16]))
.expect("status"),
TransactionStatus::Committed(push_receipt)
);
let root = engine.committed_root();
let repository = root.repo(&namespace).expect("the repository is bound");
assert_eq!(repository.repo_sequence, 1);
assert_eq!(repository.genesis_authority, genesis_object().id);
assert_eq!(repository.current_authority, genesis_object().id);
assert_eq!(
repository.refs.get(&RefTarget::Branch("main".into())),
Some(&blob)
);
// The object-type code this writer put in the index and the one
// recovery reconstructs are the same table stated twice (see the
// interface request on `object_type_code`). This is the assertion that
// fails if they ever diverge.
let located = root
.index()
.get(&IndexKey::new(namespace, blob))
.expect("the recovered index locates the pushed blob");
assert_eq!(located.object_type, object_type_code(ObjectType::Blob));
assert!(
root.index()
.get(&IndexKey::new(NamespaceId([0x21; 32]), blob))
.is_none(),
"namespace isolation is a property of the key, on the recovered path too"
);
}
/// The frame the writer appends carries a signature over
/// `SignedCommittedTransactionV1::signing_digest`, which is what the frozen
/// `verify` checks — not over the bare event digest.
///
/// A frame signed over anything else would be durable, correctly fenced,
/// and verifiable against nothing, which no later recovery could detect.
#[test]
fn the_appended_frame_is_signed_over_the_digest_the_frozen_verifier_checks() {
let serial = writer_serial();
let temporary = tempfile::tempdir().expect("tempdir");
let namespace = NamespaceId([0x30; 32]);
{
let engine = open(&serial, temporary.path(), 1);
block_on(engine.submit(create_transaction(namespace, 1))).expect("commit");
}
// Reopening runs production recovery, which seals the validated
// prefix. Reading the frame back out of that segment means this test
// inspects the bytes recovery accepted, not the bytes the writer
// believes it wrote.
drop(StoreEngine::open(options(&serial, temporary.path(), 1)).expect("reopen"));
let signed = only_frame_payload(temporary.path()).committed;
let digest = SignedCommittedTransactionV1::signing_digest(
&signed.transaction,
signed.source_key_epoch,
&signed.durability_result,
)
.expect("signing digest");
assert_eq!(
signed.source_signature,
TestSigner::expected_signature(&ObjectId(digest))
);
assert_eq!(signed.source_key_epoch, 7);
assert_ne!(
digest,
signed.transaction.event_digest().expect("event digest").0,
"the two digests must differ, or this test proves nothing"
);
}
/// Read the single frame a one-transaction store wrote, through the same
/// sealed segment the reopen produced.
fn only_frame_payload(root: &Path) -> TransactionFramePayloadV1 {
let layout = RootLayout::new(root);
let paths = layout.shard(0);
// The root identity is read out of `FORMAT` rather than restated: the
// root is now built by `StoreEngine::open`, which mints a fresh
// `root_uuid` per root, and a hard-coded one here would be a second
// definition of the identity every file in the root is bound to.
let root_uuid = segment::read_format(&layout).expect("FORMAT").root_uuid;
// Reopening sealed the validated prefix, so the frame is in a segment.
let (manifest, _) = segment::load_manifest_with_fallback(&paths, &root_uuid)
.expect("manifest")
.expect("a manifest exists after the reopen sealed the prefix");
let range = manifest
.retained_tail_ranges
.last()
.expect("one retained range");
let reader =
segment::SegmentReader::open(&paths.segments().join(&range.filename), &root_uuid)
.expect("segment");
let frame = reader
.read_frame(range.first_shard_sequence)
.expect("frame");
TransactionFramePayloadV1::decode_canonical(&frame.payload).expect("canonical payload")
}
/// Every frame the store durably wrote, in `shard_sequence` order, read
/// back out of the sealed segments a reopen produced.
fn all_frame_payloads(root: &Path) -> Vec<TransactionFramePayloadV1> {
let layout = RootLayout::new(root);
let paths = layout.shard(0);
let root_uuid = segment::read_format(&layout).expect("FORMAT").root_uuid;
let (manifest, _) = segment::load_manifest_with_fallback(&paths, &root_uuid)
.expect("manifest")
.expect("a manifest exists after the reopen sealed the prefix");
let mut payloads = Vec::new();
for range in &manifest.retained_tail_ranges {
let reader =
segment::SegmentReader::open(&paths.segments().join(&range.filename), &root_uuid)
.expect("segment");
for sequence in range.first_shard_sequence..=range.last_shard_sequence {
let frame = reader.read_frame(sequence).expect("frame");
payloads.push(
TransactionFramePayloadV1::decode_canonical(&frame.payload)
.expect("canonical payload"),
);
}
}
payloads
}
/// Every durable frame carries a signature over the digest the frozen
/// verifier recomputes, and its `previous_event_digest` is the event digest
/// of the frame before it in its repository.
///
/// This is the assertion a partial suffix repair fails. Dropping a member
/// from a forming group re-chains every later member, and a repair that
/// re-chained without re-signing leaves a durable, correctly fenced frame
/// whose signature covers a chain that no longer exists — which nothing
/// downstream of the append can detect.
fn assert_every_frame_is_chained_and_signed(root: &Path) -> Vec<TransactionFramePayloadV1> {
let payloads = all_frame_payloads(root);
let mut previous: BTreeMap<ObjectId, ObjectId> = BTreeMap::new();
for payload in &payloads {
let signed = &payload.committed;
let digest = SignedCommittedTransactionV1::signing_digest(
&signed.transaction,
signed.source_key_epoch,
&signed.durability_result,
)
.expect("signing digest");
assert_eq!(
signed.source_signature,
TestSigner::expected_signature(&ObjectId(digest)),
"repo_sequence {} carries a signature over a message the signer never signed",
signed.transaction.repo_sequence
);
let repo = signed.transaction.repo_id;
let expected = previous.get(&repo).copied().unwrap_or(ObjectId([0u8; 32]));
assert_eq!(
signed.transaction.previous_event_digest, expected,
"repo_sequence {} is chained to an event that is not its predecessor",
signed.transaction.repo_sequence
);
previous.insert(
repo,
signed.transaction.event_digest().expect("event digest"),
);
}
payloads
}
/// P1-1. The committed event's `actor` is the evidence's principal, never
/// the destination instance's signing key.
///
/// The two are deliberately different values in this file. Under the defect
/// this test exists for, `actor` was `TestSigner::public_key()`, the frame
/// signed and fenced and published, and the first thing to notice was a
/// mirror on another instance running `validate_mirror_application` against
/// a transaction that could no longer be withdrawn.
#[test]
fn the_committed_event_takes_its_actor_from_the_evidence_not_from_the_signer() {
let serial = writer_serial();
let temporary = tempfile::tempdir().expect("tempdir");
let namespace = NamespaceId([0x31; 32]);
{
let engine = open(&serial, temporary.path(), 1);
block_on(engine.submit(create_transaction(namespace, 1))).expect("commit");
}
drop(StoreEngine::open(options(&serial, temporary.path(), 1)).expect("reopen"));
let signed = only_frame_payload(temporary.path()).committed;
assert_eq!(
signed.transaction.actor,
evidence().actor(),
"the committed event restates the evidence's actor"
);
assert_eq!(signed.transaction.actor, EVIDENCE_ACTOR);
assert_ne!(
signed.transaction.actor,
TestSigner::default().public_key(),
"the destination signer's key is not the actor, and this store's two values \
differ so that substituting one for the other is visible"
);
}
/// Configured per-transaction ceilings, asserted through `submit` — the
/// entry point a consumer calls — rather than through the codec maxima that
/// happen to sit further down.
#[test]
fn configured_per_transaction_ceilings_are_enforced_at_submit() {
let serial = writer_serial();
let temporary = tempfile::tempdir().expect("tempdir");
let namespace = NamespaceId([0x32; 32]);
let mut configured = options(&serial, temporary.path(), 1);
configured.max_objects_per_transaction = 1;
configured.max_refs_per_transaction = 1;
let engine = StoreEngine::open(configured).expect("open");
block_on(engine.submit(create_transaction(namespace, 1))).expect("create");
let authority = genesis_object().id;
let two_objects = ValidatedTransaction::builder(PrivilegedConstruction::internal())
.namespace(namespace)
.operation(OperationId([2; 16]), ObjectId([2; 32]), deadline())
.objects(vec![
StagedObject {
id: ObjectId([0xb1; 32]),
object_type: ObjectType::Blob,
raw: vec![1; 8],
},
StagedObject {
id: ObjectId([0xb2; 32]),
object_type: ObjectType::Blob,
raw: vec![2; 8],
},
])
.refs(Vec::new())
.authority(Some(authority), Some(authority))
.evidence(evidence())
.build()
.expect("a well formed but oversized transaction");
match block_on(engine.submit(two_objects)) {
Err(StoreError::LimitExceeded {
limit,
observed,
allowed,
}) => {
assert_eq!(limit, "max_objects_per_transaction");
assert_eq!((observed, allowed), (2, 1));
}
other => panic!("the configured object ceiling must refuse, got {other:?}"),
}
let two_refs = ValidatedTransaction::builder(PrivilegedConstruction::internal())
.namespace(namespace)
.operation(OperationId([3; 16]), ObjectId([3; 32]), deadline())
.objects(vec![StagedObject {
id: ObjectId([0xb3; 32]),
object_type: ObjectType::Blob,
raw: vec![3; 8],
}])
.refs(vec![
set_branch("main", None, ObjectId([0xb3; 32])),
set_branch("other", None, ObjectId([0xb3; 32])),
])
.authority(Some(authority), Some(authority))
.evidence(evidence())
.build()
.expect("a well formed but over-referenced transaction");
match block_on(engine.submit(two_refs)) {
Err(StoreError::LimitExceeded {
limit,
observed,
allowed,
}) => {
assert_eq!(limit, "max_refs_per_transaction");
assert_eq!((observed, allowed), (2, 1));
}
other => panic!("the configured ref ceiling must refuse, got {other:?}"),
}
// Neither refusal reserved anything, and the shard still commits.
for operation in [2u8, 3] {
assert_eq!(
engine
.transaction_status(namespace, OperationId([operation; 16]))
.expect("status"),
TransactionStatus::Unknown
);
}
block_on(engine.submit(push_transaction(namespace, 4, 0xb4, None))).expect("push");
}
/// Status occupancy is the size of the root that was just published, not of
/// the one it replaced.
///
/// Recording the previous root makes occupancy lag every insertion and
/// every removal by one publication — wrong in exactly the direction that
/// hides an approach to `max_status_entries`, since the reading is always
/// one behind on the way up and never returns to zero on the way down.
#[test]
fn status_occupancy_reports_the_root_that_was_published() {
let serial = writer_serial();
let temporary = tempfile::tempdir().expect("tempdir");
let namespace = NamespaceId([0x33; 32]);
let engine = open(&serial, temporary.path(), 1);
assert_eq!(engine.operation_status_metrics().occupancy, 0);
block_on(engine.submit(create_transaction(namespace, 1))).expect("create");
assert_eq!(
engine.operation_status_metrics().occupancy,
0,
"a published group removes its status entries, and the metric must say so"
);
assert_eq!(engine.operation_status_metrics().rejections, 0);
block_on(engine.submit(push_transaction(namespace, 2, 0xc1, None))).expect("push");
assert_eq!(engine.operation_status_metrics().occupancy, 0);
}
/// Deliverable 6's concurrent-publication case, and the first test in this
/// package to submit to more than one shard.
///
/// Two shards publish into one `ArcSwap<CommittedRoot>`, so the losing CAS
/// must reload and merge against the newer root. Charter item 7: the retry
/// is counted rather than asserted to have happened, and the no-lost-update
/// property is checked against every receipt from both shards.
#[test]
fn concurrent_publication_from_two_shards_re_merges_and_never_loses_an_update() {
let serial = writer_serial();
let temporary = tempfile::tempdir().expect("tempdir");
let mut configured = options(&serial, temporary.path(), 2);
configured.max_group_transactions = 2;
configured.max_group_idle = Duration::from_millis(20);
let engine = StoreEngine::open(configured).expect("open");
// Two namespaces that route to different shards, found rather than
// assumed: the routing function is the authority on the mapping.
let mut namespaces: Vec<NamespaceId> = Vec::new();
for byte in 0u8..=255 {
let candidate = NamespaceId([byte; 32]);
let shard = StoreOptions::shard_of(&candidate, 2);
if namespaces.len() == usize::from(shard) {
namespaces.push(candidate);
}
if namespaces.len() == 2 {
break;
}
}
assert_eq!(namespaces.len(), 2, "both shards must be reachable");
const ROUNDS: u8 = 24;
std::thread::scope(|scope| {
let handles: Vec<_> = namespaces
.iter()
.enumerate()
.map(|(lane, namespace)| {
let namespace = *namespace;
let engine = &engine;
let base = 1 + (lane as u8) * (ROUNDS + 1);
scope.spawn(move || {
block_on(engine.submit(create_transaction(namespace, base)))
.expect("create");
for index in 0..ROUNDS {
block_on(engine.submit(push_transaction(
namespace,
base + 1 + index,
index,
None,
)))
.expect("push");
}
})
})
.collect();
for handle in handles {
handle.join().expect("no writer panics");
}
});
// No lost update: every operation from both lanes is in the one root,
// and each repository's sequence counted every one of its own.
let root = engine.committed_root();
for (lane, namespace) in namespaces.iter().enumerate() {
let base = 1 + (lane as u8) * (ROUNDS + 1);
for index in 0..=ROUNDS {
let key = OperationKey::new(*namespace, OperationId([base + index; 16]));
assert!(
root.terminal_status(&key).is_some(),
"lane {lane} operation {index} is missing from the merged root"
);
}
assert_eq!(
root.repo(namespace).expect("bound").repo_sequence,
u64::from(ROUNDS)
);
}
assert_eq!(root.retained_generations().len(), 2);
}
/// Deliverable 6's re-merge, made deterministic.
///
/// A competing root is published between the load and the compare-and-swap
/// of the real `publish_subtree` loop, so the CAS provably fails exactly
/// once. What must survive is both sides: the receipt this writer is
/// publishing *and* the entry the competing publisher added. A lost update
/// here is a re-merge against the stale root, which the previous test —
/// two shards racing — can only ever catch by luck.
///
/// The sibling test above submits to two shards concurrently and asserts no
/// lost update; it cannot assert that any CAS contended, because whether it
/// does depends on scheduling. This one owns that claim.
#[test]
fn a_contended_root_cas_re_merges_against_the_newer_root() {
let probe_serial = probe::serial();
let serial = writer_serial();
let temporary = tempfile::tempdir().expect("tempdir");
let namespace = NamespaceId([0x71; 32]);
// The competing subtree names the shard this namespace does *not*
// route to, so it never disturbs the sequence bookkeeping of the shard
// under test. Derived from the routing function rather than assumed.
let publishing_shard = StoreOptions::shard_of(&namespace, 2);
let other_shard = 1 - publishing_shard;
// A marker only the competing publisher writes. If the writer re-merges
// against the root it loaded rather than the newer one, this is gone.
let marker = OperationKey::new(NamespaceId([0x72; 32]), OperationId([0x72; 16]));
let competing = Arc::new(ShardSubtree::new(
other_shard,
0,
Arc::new(IndexDelta::from_options(&options(
&serial,
temporary.path(),
2,
))),
None,
Vector::new(),
RepoMap::new(),
{
let mut statuses = TerminalStatusMap::new();
statuses.insert(
marker,
TerminalStatusEntry::Committed(RetainedReceipt {
operation_digest: ObjectId([0x72; 32]),
receipt: CommitReceipt {
operation_id: marker.operation_id,
repo_sequence: 0,
current_authority: ObjectId([0; 32]),
refs: Vec::new(),
objects_new: 0,
},
shard_sequence: 0,
retry_until_micros: deadline(),
first_visible_at_micros: now_micros(),
receipt_visible_until_micros: deadline(),
}),
);
statuses
},
GenerationMap::new(),
));
let fired = Arc::new(std::sync::atomic::AtomicBool::new(false));
let armed = Arc::clone(&fired);
// Filtered by root, because the hook is process-global and every other
// test in this binary publishes through the same `publish_subtree`.
// Without this, the one-shot perturbation is consumed by whichever
// engine happens to publish first and this test observes no contention
// at all — the Wave A lesson about one-shot global registries, one
// layer up.
let this_root = temporary.path().to_path_buf();
probe::install_cas_contention(&probe_serial, move |shared| {
if shared.options.root != this_root {
return;
}
// Once: the second pass through the loop must find an uncontended
// root, or the writer would spin forever.
if armed.swap(true, AtomicOrdering::SeqCst) {
return;
}
let current = shared.committed.load();
shared.committed.store(Arc::new(current.merge(&competing)));
});
let engine = StoreEngine::open(options(&serial, temporary.path(), 2)).expect("open");
let receipt = block_on(engine.submit(create_transaction(namespace, 1)))
.expect("the contended publication still commits");
assert_eq!(
engine.shared.root_cas_retries.load(AtomicOrdering::Relaxed),
1,
"the committed-root CAS must have contended exactly once"
);
let root = engine.committed_root();
assert!(
root.terminal_status(&marker).is_some(),
"the competing publisher's entry was lost: the re-merge used the stale root"
);
assert_eq!(
root.terminal_status(&OperationKey::new(namespace, OperationId([1; 16])))
.map(|entry| entry.transaction_status()),
Some(TransactionStatus::Committed(receipt)),
"this writer's own receipt was lost by the re-merge"
);
}
// -----------------------------------------------------------------------
// Scope 6.3 step 3: the pre-mark deadline recheck and its suffix repair
// -----------------------------------------------------------------------
/// Submit `members` pushes into one forming group, the one at `expiring`
/// carrying a deadline that passes while the group waits for its idle
/// bound, and report each submission's outcome in submission order.
///
/// The group is closed by the idle delay and never by the transaction
/// ceiling, which is what puts real time between sequencing and the mark:
/// scope 6.3 step 3 exists precisely because signing latency and the idle
/// wait sit between the two.
fn group_with_one_expiring_member(
engine: &StoreEngine,
namespace: NamespaceId,
first_operation: u8,
members: u8,
expiring: u8,
) -> Vec<Result<CommitReceipt, StoreError>> {
let outcomes: Arc<Mutex<Vec<(u8, Result<CommitReceipt, StoreError>)>>> =
Arc::new(Mutex::new(Vec::new()));
std::thread::scope(|scope| {
for index in 0..members {
let outcomes = Arc::clone(&outcomes);
scope.spawn(move || {
// Staggered so the group forms in a known order.
std::thread::sleep(Duration::from_millis(60 * u64::from(index)));
let operation = first_operation + index;
let retry_until = if index == expiring {
now_micros() + 250_000
} else {
deadline()
};
let transaction =
ValidatedTransaction::builder(PrivilegedConstruction::internal())
.namespace(namespace)
.operation(
OperationId([operation; 16]),
ObjectId([operation; 32]),
retry_until,
)
.objects(vec![StagedObject {
id: ObjectId([operation; 32]),
object_type: ObjectType::Blob,
raw: vec![operation; 64],
}])
.refs(Vec::new())
.authority(Some(genesis_object().id), Some(genesis_object().id))
.evidence(evidence())
.build()
.expect("a complete push transaction");
let outcome = block_on(engine.submit(transaction));
outcomes.lock().expect("outcomes").push((index, outcome));
});
}
});
let mut collected = Arc::try_unwrap(outcomes)
.map_err(|_| "outcomes outlived the scope")
.expect("sole owner")
.into_inner()
.expect("outcomes");
collected.sort_by_key(|(index, _)| *index);
collected.into_iter().map(|(_, outcome)| outcome).collect()
}
/// Scope 6.3 step 3 at the first, middle, and last position of a forming
/// group, each with the later members already sequenced and signed.
///
/// The expired member appends nothing, and every retained member is
/// re-sequenced, re-chained, re-signed, and left with no sequence gap.
/// A repair that re-chained without re-signing, or that shifted sequences
/// without re-chaining, produces a durable and correctly fenced frame
/// carrying a signature that verifies against nothing — which is why the
/// assertion is made against the bytes recovery read back, not against the
/// receipts.
#[test]
fn a_member_that_crosses_its_deadline_before_the_mark_appends_nothing() {
for expiring in 0u8..3 {
let serial = writer_serial();
let temporary = tempfile::tempdir().expect("tempdir");
let namespace = NamespaceId([0x60 + expiring; 32]);
let mut configured = options(&serial, temporary.path(), 1);
// Never the transaction ceiling: the idle bound is what closes this
// group, and it is long enough that the short deadline passes while
// the group is still open.
configured.max_group_transactions = 16;
configured.max_group_idle = Duration::from_millis(900);
let engine = StoreEngine::open(configured).expect("open");
block_on(engine.submit(create_transaction(namespace, 1))).expect("create");
let outcomes = group_with_one_expiring_member(&engine, namespace, 2, 3, expiring);
let mut expected_repo_sequence = 1u64;
for (index, outcome) in outcomes.iter().enumerate() {
if index as u8 == expiring {
match outcome {
Err(StoreError::Conflict(reason)) => assert!(
reason.contains("passed its signed deadline"),
"position {expiring}: {reason}"
),
other => panic!(
"position {expiring}: an expired member must be refused, \
got {other:?}"
),
}
// Definitively absent: the reservation is released, so the
// operation reads back Unknown rather than Pending forever.
assert_eq!(
engine
.transaction_status(namespace, OperationId([2 + expiring; 16]))
.expect("status"),
TransactionStatus::Unknown,
"position {expiring}: an expired member holds no reservation"
);
} else {
let receipt = outcome
.as_ref()
.unwrap_or_else(|e| panic!("position {expiring}: retained member {e:?}"));
assert_eq!(
receipt.repo_sequence, expected_repo_sequence,
"position {expiring}: the retained suffix left a repo_sequence gap"
);
expected_repo_sequence += 1;
}
}
// The shard is not poisoned: nothing was appended for the dropped
// member, so no sequence was consumed and no hole exists.
block_on(engine.submit(push_transaction(namespace, 9, 0x9f, None)))
.expect("the shard still admits mutation");
drop(engine);
let engine = StoreEngine::open(options(&serial, temporary.path(), 1)).expect("reopen");
let payloads = assert_every_frame_is_chained_and_signed(temporary.path());
assert_eq!(
payloads.len(),
4,
"position {expiring}: one create, two retained members, one later push"
);
for (index, payload) in payloads.iter().enumerate() {
assert_eq!(
payload.committed.transaction.repo_sequence, index as u64,
"position {expiring}: durable repo_sequence is not gapless"
);
}
assert_eq!(
engine
.transaction_status(namespace, OperationId([2 + expiring; 16]))
.expect("status"),
TransactionStatus::Unknown,
"position {expiring}: an expired member is absent after recovery too"
);
}
}
#[test]
fn a_repository_create_binds_its_genesis_permanently() {
let serial = writer_serial();
let temporary = tempfile::tempdir().expect("tempdir");
let namespace = NamespaceId([0x40; 32]);
let engine = open(&serial, temporary.path(), 1);
block_on(engine.submit(create_transaction(namespace, 1))).expect("the first create");
match block_on(engine.submit(create_transaction(namespace, 2))) {
Err(StoreError::Conflict(reason)) => {
assert!(reason.contains("already bound to genesis"), "{reason}")
}
other => panic!("a second create must be refused, got {other:?}"),
}
}
/// Phase 1 exit criterion "exact same-ref winner", asserted through
/// `submit` rather than through the ref map it updates.
#[test]
fn exactly_one_writer_wins_a_contested_typed_ref() {
let serial = writer_serial();
let temporary = tempfile::tempdir().expect("tempdir");
let namespace = NamespaceId([0x50; 32]);
let engine = open(&serial, temporary.path(), 1);
block_on(engine.submit(create_transaction(namespace, 1))).expect("create");
let first = block_on(engine.submit(push_transaction(
namespace,
2,
0x60,
Some(set_branch("main", None, ObjectId([0x60; 32]))),
)))
.expect("the first writer wins");
assert_eq!(first.refs.len(), 1);
// Same expected value, different new value: the loser must be a typed
// conflict and must leave no partial state behind.
match block_on(engine.submit(push_transaction(
namespace,
3,
0x61,
Some(set_branch("main", None, ObjectId([0x61; 32]))),
))) {
Err(StoreError::Conflict(reason)) => {
assert!(reason.contains("typed ref CAS"), "{reason}")
}
other => panic!("the loser must be a typed conflict, got {other:?}"),
}
let root = engine.committed_root();
assert_eq!(
root.repo(&namespace)
.expect("bound")
.refs
.get(&RefTarget::Branch("main".into())),
Some(&ObjectId([0x60; 32]))
);
assert!(
root.index()
.get(&IndexKey::new(namespace, ObjectId([0x61; 32])))
.is_none(),
"a refused transaction publishes none of its objects"
);
assert_eq!(
engine
.transaction_status(namespace, OperationId([3; 16]))
.expect("status"),
TransactionStatus::Unknown,
"a definitively refused operation leaves no status entry behind"
);
}
// -----------------------------------------------------------------------
// Counters, not claims (charter item 7)
// -----------------------------------------------------------------------
/// Group formation is bounded by transactions, bytes, and idle delay —
/// asserted against the sequencer that forms the groups, and counted in
/// `DurabilityCounters.fdatasync`, which is the only witness that says how
/// many fences actually reached the device.
///
/// This is the Wave A `GroupBuilder` carry-forward: the bounds are now
/// observed through the production caller rather than through unit tests
/// over a type nothing calls.
#[test]
fn group_formation_bounds_are_observed_as_fences_on_the_device() {
// (a) the transaction bound: eight concurrent submissions with a
// ceiling of four close exactly two groups.
let serial = writer_serial();
let temporary = tempfile::tempdir().expect("tempdir");
let namespace = NamespaceId([0x70; 32]);
let mut configured = options(&serial, temporary.path(), 1);
configured.max_group_transactions = 4;
// Long enough that the idle bound cannot be what closes these groups.
configured.max_group_idle = Duration::from_secs(5);
let engine = StoreEngine::open(configured).expect("open");
// The create is alone in its group, so it is the idle bound that
// closes it — case (c), and the reason this engine cannot use a
// multi-second idle delay for the whole test.
let before_create = engine.durability_counters(0).expect("counters").fdatasync;
assert_eq!(before_create, 0, "opening a recovered shard fences nothing");
let fences_after = commit_concurrently(&engine, namespace, 1, 8);
assert_eq!(
fences_after, 3,
"one fence for the create's group and one for each of two full groups"
);
assert!(
fences_after < 9,
"nine transactions must not cost nine fences"
);
}
#[test]
fn a_group_is_also_closed_by_its_byte_ceiling() {
let serial = writer_serial();
let temporary = tempfile::tempdir().expect("tempdir");
let namespace = NamespaceId([0x80; 32]);
let mut configured = options(&serial, temporary.path(), 1);
configured.max_group_transactions = 64;
// One push frame is comfortably over 512 bytes once its evidence and
// signed event are encoded, so the byte ceiling closes the group after
// one transaction while the transaction ceiling is nowhere near.
configured.max_group_bytes = 512;
configured.max_group_idle = Duration::from_secs(5);
let engine = StoreEngine::open(configured).expect("open");
let fences = commit_concurrently(&engine, namespace, 1, 4);
assert_eq!(
fences, 5,
"the byte ceiling admits one transaction per group, so five \
transactions cost five fences"
);
}
/// Submit a create for `namespace` and then `pushes` concurrent pushes,
/// and report the shard's fence count once every one of them has resolved.
fn commit_concurrently(
engine: &StoreEngine,
namespace: NamespaceId,
first_operation: u8,
pushes: u8,
) -> u64 {
block_on(engine.submit(create_transaction(namespace, first_operation))).expect("create");
std::thread::scope(|scope| {
let handles: Vec<_> = (0..pushes)
.map(|index| {
let operation = first_operation + 1 + index;
scope.spawn(move || {
block_on(engine.submit(push_transaction(
namespace,
operation,
0x90 + index,
None,
)))
})
})
.collect();
for handle in handles {
handle.join().expect("no writer panics").expect("commit");
}
});
engine.durability_counters(0).expect("counters").fdatasync
}
// -----------------------------------------------------------------------
// Publication ordering (scope 6.3 steps 7 to 9)
// -----------------------------------------------------------------------
/// Step 8 is strictly after step 7.
///
/// The probe fires between them on the real writer. At that instant the
/// receipt must already be in the committed root and the status entry must
/// still be present. Removing the entry before the CAS fails the second
/// assertion; publishing after the removal fails the first. Nothing else
/// in this file can distinguish the two orders deterministically, because
/// from outside they differ only by a window.
#[test]
fn the_committed_root_is_published_before_the_status_entry_is_removed() {
let probe_serial = probe::serial();
let observations = Arc::new(Mutex::new(Vec::new()));
let serial = writer_serial();
let temporary = tempfile::tempdir().expect("tempdir");
let namespace = NamespaceId([0xa0; 32]);
let recorded = Arc::clone(&observations);
// Filtered by namespace: the hook is process-global and other tests
// publish through it in parallel. Recording their groups would make
// this test's count depend on the rest of the binary.
probe::install(&probe_serial, move |shared, keys| {
for key in keys.iter().filter(|key| key.namespace == namespace) {
let committed = shared.committed.load().terminal_status(key).is_some();
let still_resolving = matches!(
shared.status.load().get(key).map(|entry| entry.phase),
Some(crate::roots::StatusPhase::Resolving)
);
recorded
.lock()
.expect("probe mutex")
.push((*key, committed, still_resolving));
}
});
let engine = open(&serial, temporary.path(), 1);
block_on(engine.submit(create_transaction(namespace, 1))).expect("create");
block_on(engine.submit(push_transaction(namespace, 2, 0xb0, None))).expect("push");
let observations = observations.lock().expect("probe mutex").clone();
assert_eq!(observations.len(), 2, "one observation per published group");
for (key, committed, still_resolving) in observations {
assert!(
committed,
"operation {} must be in the committed root before its status entry is removed",
key.operation_id.to_hex()
);
assert!(
still_resolving,
"operation {} must still be Resolving at the instant its receipt becomes visible",
key.operation_id.to_hex()
);
}
}
/// The same property from the outside, through the API a consumer calls.
///
/// A reader spinning on `transaction_status` may legitimately see
/// `Unknown` before the operation is reserved. What it may never see is
/// `Unknown` *after* it has already seen the operation, because that is
/// the manufactured-`Unknown` race step 8 exists to close.
#[test]
fn a_status_reader_never_regresses_to_unknown_once_an_operation_is_visible() {
let serial = writer_serial();
let temporary = tempfile::tempdir().expect("tempdir");
let namespace = NamespaceId([0xc0; 32]);
let engine = open(&serial, temporary.path(), 1);
block_on(engine.submit(create_transaction(namespace, 1))).expect("create");
let stop = Arc::new(std::sync::atomic::AtomicBool::new(false));
let reader_stop = Arc::clone(&stop);
std::thread::scope(|scope| {
let observer = scope.spawn(|| {
// Per operation, not per sweep: `Unknown` before an operation
// is reserved is correct, and only a regression *after* it has
// been seen is the manufactured `Unknown` step 8 closes.
let mut seen = [false; 16];
let mut regressions = 0u64;
while !reader_stop.load(AtomicOrdering::Relaxed) {
for operation in 2u8..=17 {
let index = usize::from(operation) - 2;
let status = engine
.transaction_status(namespace, OperationId([operation; 16]))
.expect("status");
match status {
TransactionStatus::Unknown => {
if seen[index] {
regressions += 1;
}
}
TransactionStatus::Committed(_)
| TransactionStatus::Pending { .. }
| TransactionStatus::Resolving { .. }
| TransactionStatus::Expired { .. } => seen[index] = true,
}
}
}
regressions
});
for operation in 2u8..=17 {
block_on(engine.submit(push_transaction(namespace, operation, operation, None)))
.expect("push");
}
stop.store(true, AtomicOrdering::Relaxed);
assert_eq!(
observer.join().expect("observer"),
0,
"a committed operation may never read back as Unknown"
);
});
}
// -----------------------------------------------------------------------
// Failpoints: the publication half of the matrix (scope 6.3, oracle rows)
// -----------------------------------------------------------------------
#[cfg(feature = "failpoints")]
mod publication_failures {
use levcs_protocol::oracle::{append_publication_expectation, ImmediateStatus};
use super::*;
use crate::failpoints::{arm, disarm};
/// The frozen oracle's classification for a row, so each test below
/// states which contract it is pinning rather than restating it.
fn expectation(point: Failpoint) -> levcs_protocol::oracle::FailpointExpectation {
append_publication_expectation(point.into())
}
/// Scope 6.3 step 9. The fence succeeded and the root published, so
/// the transaction is committed even though no waiter will ever be
/// woken: a hung request, never an absent transaction.
#[test]
fn a_lost_wake_leaves_a_committed_and_queryable_receipt() {
let expectation = expectation(Failpoint::AfterRootCasBeforeWaiterWake);
assert!(!expectation.shard_poisoned);
assert!(expectation.acknowledgment_allowed);
assert_eq!(expectation.immediate_status, ImmediateStatus::Committed);
let serial = writer_serial();
let temporary = tempfile::tempdir().expect("tempdir");
let namespace = NamespaceId([0xd0; 32]);
let engine = open(&serial, temporary.path(), 1);
block_on(engine.submit(create_transaction(namespace, 1))).expect("create");
arm(
&serial,
Failpoint::AfterRootCasBeforeWaiterWake,
FailpointAction::Fail,
);
let hung = block_on_until(
engine.submit(push_transaction(namespace, 2, 0xd1, None)),
Duration::from_millis(750),
);
disarm(&serial);
assert!(hung.is_none(), "the waiter is never woken");
match engine
.transaction_status(namespace, OperationId([2; 16]))
.expect("status")
{
TransactionStatus::Committed(receipt) => {
assert_eq!(receipt.repo_sequence, 1);
assert_eq!(receipt.objects_new, 1);
}
other => panic!("a published transaction must stay queryable, got {other:?}"),
}
// The shard is not poisoned, so the next transaction commits.
block_on(engine.submit(push_transaction(namespace, 3, 0xd2, None)))
.expect("a later append is allowed");
}
/// Scope 6.3 step 10, same rule one step later.
#[test]
fn a_lost_response_leaves_a_committed_and_queryable_receipt() {
let expectation = expectation(Failpoint::BeforeResponse);
assert!(!expectation.shard_poisoned);
assert!(expectation.acknowledgment_allowed);
assert_eq!(expectation.immediate_status, ImmediateStatus::Committed);
let serial = writer_serial();
let temporary = tempfile::tempdir().expect("tempdir");
let namespace = NamespaceId([0xe0; 32]);
let engine = open(&serial, temporary.path(), 1);
block_on(engine.submit(create_transaction(namespace, 1))).expect("create");
arm(&serial, Failpoint::BeforeResponse, FailpointAction::Fail);
let lost = block_on(engine.submit(push_transaction(namespace, 2, 0xe1, None)));
disarm(&serial);
assert!(matches!(lost, Err(StoreError::Io(_))));
assert!(matches!(
engine
.transaction_status(namespace, OperationId([2; 16]))
.expect("status"),
TransactionStatus::Committed(_)
));
block_on(engine.submit(push_transaction(namespace, 3, 0xe2, None)))
.expect("a later append is allowed");
}
/// Scope 3.7: steps 4 to 8 are the poison window. A failure between
/// the `Resolving` mark and the committed-root publication leaves the
/// operation `Resolving` — never `Unknown`, and never a receipt — and
/// the shard admits no further mutation.
#[test]
fn a_failure_inside_the_publication_window_poisons_and_stays_resolving() {
let expectation = expectation(Failpoint::BeforeRootCas);
assert!(expectation.shard_poisoned);
assert!(!expectation.acknowledgment_allowed);
assert!(!expectation.later_append_allowed_before_recovery);
assert_eq!(expectation.immediate_status, ImmediateStatus::Resolving);
let serial = writer_serial();
let temporary = tempfile::tempdir().expect("tempdir");
let namespace = NamespaceId([0xf0; 32]);
let engine = open(&serial, temporary.path(), 1);
block_on(engine.submit(create_transaction(namespace, 1))).expect("create");
arm(&serial, Failpoint::BeforeRootCas, FailpointAction::Fail);
let poisoned = block_on(engine.submit(push_transaction(namespace, 2, 0xf1, None)));
disarm(&serial);
assert!(matches!(poisoned, Err(StoreError::ShardPoisoned { .. })));
match engine
.transaction_status(namespace, OperationId([2; 16]))
.expect("status")
{
TransactionStatus::Resolving {
shard_sequence,
operation_digest,
..
} => {
assert_eq!(shard_sequence, Some(1));
assert_eq!(operation_digest, ObjectId([2; 32]));
}
other => panic!("the group must remain Resolving, got {other:?}"),
}
assert!(matches!(
block_on(engine.submit(push_transaction(namespace, 3, 0xf2, None))),
Err(StoreError::ShardPoisoned { .. })
));
}
/// The `Panic` action on a publication-side row (scope 4-A3 condition
/// (b)). The failpoint names a *location*; the driver chooses the
/// *action*, and the two axes are independent.
///
/// The property under test is not the panic — it is that a panicking
/// writer thread still resolves every caller it owes an outcome to. A
/// completion that unwound with the frame it belonged to would leave
/// `submit` waiting forever, which from outside is indistinguishable
/// from a store that hung.
#[test]
fn a_writer_panic_inside_the_window_resolves_its_callers_rather_than_hanging() {
let serial = writer_serial();
let expectation = expectation(Failpoint::DuringCommittedRootBuild);
assert!(expectation.shard_poisoned);
assert_eq!(expectation.immediate_status, ImmediateStatus::Resolving);
let temporary = tempfile::tempdir().expect("tempdir");
let namespace = NamespaceId([0x28; 32]);
let engine = open(&serial, temporary.path(), 1);
block_on(engine.submit(create_transaction(namespace, 1))).expect("create");
arm(
&serial,
Failpoint::DuringCommittedRootBuild,
FailpointAction::Panic,
);
let outcome = block_on_until(
engine.submit(push_transaction(namespace, 2, 0x29, None)),
Duration::from_secs(5),
);
disarm(&serial);
match outcome {
Some(Err(StoreError::ShardPoisoned { shard, .. })) => assert_eq!(shard, 0),
other => panic!("a panicking writer must resolve its callers, got {other:?}"),
}
// The operation stays `Resolving`: step 8 never ran, and only
// recovery may resolve it.
assert!(matches!(
engine
.transaction_status(namespace, OperationId([2; 16]))
.expect("status"),
TransactionStatus::Resolving { .. }
));
// The writer thread is gone, so the shard admits no further
// mutation before a close and reopen through recovery.
assert!(matches!(
block_on(engine.submit(push_transaction(namespace, 3, 0x2a, None))),
Err(StoreError::NotReady)
));
}
/// P1-5 case 1. A **panic** in scope 6.3 steps 1-3 is a pre-append
/// failure, not a poisoning one.
///
/// The single boundary catch treated every panic as a publication
/// panic: the caller was told `ShardPoisoned` — which asserts the
/// transaction may already be durable — and the reservation taken
/// before sequencing was left behind, so the operation read back
/// `Pending` forever for something that never reached the device.
/// Nothing appended, so nothing can: the fate must match the phase.
#[test]
fn a_panic_before_the_mark_is_definitively_absent_and_does_not_poison() {
let expectation = expectation(Failpoint::EvidenceHandoffFailure);
assert!(!expectation.shard_poisoned);
assert!(expectation.later_append_allowed_before_recovery);
assert_eq!(
expectation.immediate_status,
ImmediateStatus::DefinitiveAbsent
);
let serial = writer_serial();
let temporary = tempfile::tempdir().expect("tempdir");
let namespace = NamespaceId([0x1b; 32]);
let engine = open(&serial, temporary.path(), 1);
block_on(engine.submit(create_transaction(namespace, 1))).expect("create");
arm(
&serial,
Failpoint::EvidenceHandoffFailure,
FailpointAction::Panic,
);
let refused = block_on_until(
engine.submit(push_transaction(namespace, 2, 0x1c, None)),
Duration::from_secs(5),
);
disarm(&serial);
match refused {
Some(Err(StoreError::Conflict(reason))) => assert!(
reason.contains("pre-append"),
"the refusal must say the transaction is absent, got {reason}"
),
other => panic!(
"a pre-append panic must be a definitive refusal, never a poisoned \
shard or a hang, got {other:?}"
),
}
assert_eq!(
engine
.transaction_status(namespace, OperationId([2; 16]))
.expect("status"),
TransactionStatus::Unknown,
"a pre-append panic releases the reservation it took"
);
assert_eq!(
engine.operation_status_metrics().occupancy,
0,
"no status entry survives a pre-append panic"
);
// Steps 1-3 cannot poison, and a dead writer thread would make
// `later_append_allowed_before_recovery` false whatever the error
// said. The next transaction commits, contiguously.
let later = block_on(engine.submit(push_transaction(namespace, 3, 0x1d, None)))
.expect("a later append is allowed");
assert_eq!(later.repo_sequence, 1);
}
/// P1-5 case 2. A request that overflows the open byte-bounded group is
/// not a member of the group that publication is about to write.
///
/// The old order added the new request to the group's bookkeeping and
/// then published the *old* group, so a publication panic reported the
/// trailing request as poisoned though it never appended, and left its
/// reservation `Pending` with nothing to resolve it. It also made the
/// trailing frame's `shard_sequence` a prediction about how many
/// members the closing group would keep — which scope 6.3 step 3 can
/// change.
#[test]
fn a_request_that_overflows_a_publishing_group_is_not_reported_as_its_member() {
let serial = writer_serial();
let temporary = tempfile::tempdir().expect("tempdir");
let namespace = NamespaceId([0x1e; 32]);
let mut configured = options(&serial, temporary.path(), 1);
configured.max_group_transactions = 64;
// Wide enough that one small frame leaves the group open, and
// narrow enough that a small frame plus a large one overflows it.
// Stated as two sizes rather than one measured constant so the
// overflow does not depend on the exact framing overhead.
configured.max_group_bytes = 8 * 1024;
configured.max_group_idle = Duration::from_secs(5);
let engine = StoreEngine::open(configured).expect("open");
block_on(engine.submit(create_transaction(namespace, 1))).expect("create");
let large = || {
ValidatedTransaction::builder(PrivilegedConstruction::internal())
.namespace(namespace)
.operation(OperationId([3; 16]), ObjectId([3; 32]), deadline())
.objects(vec![StagedObject {
id: ObjectId([0x3f; 32]),
object_type: ObjectType::Blob,
raw: vec![0x3f; 32 * 1024],
}])
.refs(Vec::new())
.authority(Some(genesis_object().id), Some(genesis_object().id))
.evidence(evidence())
.build()
.expect("a complete push transaction")
};
// A panic and not a `Fail`: the old code released the trailing
// request's reservation explicitly on the `Fail` path, so the leak
// only appears when the publication unwinds and the single boundary
// catch is the only thing left to resolve anyone.
arm(
&serial,
Failpoint::AfterMarkedResolving,
FailpointAction::Panic,
);
let (small, big) = std::thread::scope(|scope| {
let small = scope.spawn(|| {
block_on_until(
engine.submit(push_transaction(namespace, 2, 0x2f, None)),
Duration::from_secs(10),
)
});
let big = scope.spawn(|| {
// Long enough that the small frame is already in the open
// group, short enough that the idle bound has not closed it.
std::thread::sleep(Duration::from_millis(300));
block_on_until(engine.submit(large()), Duration::from_secs(10))
});
(small.join().expect("small"), big.join().expect("big"))
});
disarm(&serial);
assert!(
matches!(small, Some(Err(StoreError::ShardPoisoned { .. }))),
"the member that was marked Resolving is inside the poison window, got {small:?}"
);
match big {
Some(Err(StoreError::Conflict(ref reason))) => assert!(
reason.contains("pre-append"),
"the trailing request never appended, so its refusal must say so: {reason}"
),
ref other => panic!(
"a request that never joined the published group is pre-append, never \
poisoned, got {other:?}"
),
}
// The point of the test: the trailing request appended nothing and
// holds nothing. Under the old order it was left `Pending` with no
// owner, which no recovery resolves because recovery only ever sees
// what reached the device.
assert_eq!(
engine
.transaction_status(namespace, OperationId([3; 16]))
.expect("status"),
TransactionStatus::Unknown,
"a request that never joined the published group leaves no reservation"
);
assert!(
matches!(
engine
.transaction_status(namespace, OperationId([2; 16]))
.expect("status"),
TransactionStatus::Resolving { .. }
),
"the group that was marked Resolving stays Resolving"
);
assert_eq!(
engine.operation_status_metrics().occupancy,
1,
"exactly one operation — the one inside the poison window — is retained"
);
}
/// P1-5 case 3. Scope 6.3 steps 9 and 10 cannot poison, so a panic
/// there resolves every caller with the receipt it is owed and leaves
/// the writer running.
///
/// The fence succeeded and the root published: the transaction is
/// committed and unwithdrawable. The old boundary catch unwound after
/// the waiters had been drained out of writer-owned storage, so it
/// could not reach them at all, and it killed the writer — which makes
/// `later_append_allowed_before_recovery` false for a row the frozen
/// oracle marks `shard_poisoned: false`.
#[test]
fn a_panic_after_publication_still_hands_back_the_receipt_and_keeps_the_writer() {
let expectation = expectation(Failpoint::AfterRootCasBeforeWaiterWake);
assert!(!expectation.shard_poisoned);
assert!(expectation.acknowledgment_allowed);
assert!(expectation.later_append_allowed_before_recovery);
assert_eq!(expectation.immediate_status, ImmediateStatus::Committed);
let serial = writer_serial();
let temporary = tempfile::tempdir().expect("tempdir");
let namespace = NamespaceId([0x1f; 32]);
let engine = open(&serial, temporary.path(), 1);
block_on(engine.submit(create_transaction(namespace, 1))).expect("create");
arm(
&serial,
Failpoint::AfterRootCasBeforeWaiterWake,
FailpointAction::Panic,
);
let outcome = block_on_until(
engine.submit(push_transaction(namespace, 2, 0x2b, None)),
Duration::from_secs(5),
);
disarm(&serial);
match outcome {
Some(Ok(receipt)) => assert_eq!(receipt.repo_sequence, 1),
other => panic!(
"a published transaction is committed; steps 9-10 may not turn it into \
anything else, got {other:?}"
),
}
assert!(matches!(
engine
.transaction_status(namespace, OperationId([2; 16]))
.expect("status"),
TransactionStatus::Committed(_)
));
let later = block_on(engine.submit(push_transaction(namespace, 3, 0x2c, None)))
.expect("the writer survives a non-poisoning panic");
assert_eq!(later.repo_sequence, 2);
}
/// Scope 6.3 step 2, as reclassified by contract review 2026-07-26-A:
/// definitively absent, shard not poisoned, later append allowed. A
/// routine `SignerError::Unavailable` must not take the shard down.
#[test]
fn an_evidence_handoff_failure_is_definitively_absent() {
let expectation = expectation(Failpoint::EvidenceHandoffFailure);
assert!(!expectation.shard_poisoned);
assert!(!expectation.acknowledgment_allowed);
assert!(expectation.later_append_allowed_before_recovery);
assert_eq!(
expectation.immediate_status,
ImmediateStatus::DefinitiveAbsent
);
let serial = writer_serial();
let temporary = tempfile::tempdir().expect("tempdir");
let namespace = NamespaceId([0x18; 32]);
let engine = open(&serial, temporary.path(), 1);
block_on(engine.submit(create_transaction(namespace, 1))).expect("create");
arm(
&serial,
Failpoint::EvidenceHandoffFailure,
FailpointAction::Fail,
);
let refused = block_on(engine.submit(push_transaction(namespace, 2, 0x19, None)));
disarm(&serial);
assert!(matches!(
refused,
Err(StoreError::Signer(SignerError::Unavailable))
));
assert_eq!(
engine
.transaction_status(namespace, OperationId([2; 16]))
.expect("status"),
TransactionStatus::Unknown,
"a definitively absent operation releases its reservation"
);
// Not poisoned, and no sequence was consumed: the next append is
// contiguous with the create rather than leaving a hole.
let later = block_on(engine.submit(push_transaction(namespace, 3, 0x1a, None)))
.expect("a later append is allowed");
assert_eq!(later.repo_sequence, 1);
}
}
}
// ---------------------------------------------------------------------------
// Index maintenance (scope 3.6, deliverable 1)
// ---------------------------------------------------------------------------
#[cfg(test)]
mod index_maintenance_tests {
use std::path::Path;
use super::tests::*;
use super::*;
use crate::index::IndexKey;
use crate::segment::{index_run_filename, read_current, read_manifest, RootLayout};
/// A root whose index ceilings make sealing reachable in a few groups.
///
/// `max_active_index_entries` is the lever the acceptance point names:
/// crossing it is `DeltaPressure::SealRequired`. One object per transaction
/// means one entry per group, so the ceiling is also the layer count that
/// crosses it.
fn sealing_options(serial: &WriterSerial, root: &Path, entries: u64) -> StoreOptions {
let mut options = options(serial, root, 1);
options.max_active_index_entries = entries;
// Sequential submits must each close their own group, or "three groups"
// is one group of three and no layer accumulates.
options.max_group_transactions = 1;
options
}
fn shard_paths(root: &Path, shard: u16) -> crate::segment::ShardPaths {
RootLayout::new(root).shard(shard)
}
/// Every index run the manifest `CURRENT` names, which is the only list
/// that makes a run authoritative.
fn manifest_runs(root: &Path, shard: u16, root_uuid: [u8; 16]) -> Vec<String> {
let paths = shard_paths(root, shard);
let pointer = read_current(&paths, &root_uuid)
.expect("read CURRENT")
.expect("a root that has been opened has a CURRENT");
read_manifest(&paths, pointer.generation, &root_uuid)
.expect("read the current manifest")
.index_runs
.into_iter()
.map(|(_, name)| name)
.collect()
}
fn root_uuid_of(root: &Path) -> [u8; 16] {
crate::segment::read_format(&RootLayout::new(root))
.expect("FORMAT")
.root_uuid
}
/// Commit `count` single-object pushes, each in its own group.
fn push_groups(
engine: &StoreEngine,
namespace: NamespaceId,
first: u8,
count: u8,
) -> Vec<ObjectId> {
let mut objects = Vec::new();
for step in 0..count {
let blob = first + step;
block_on(engine.submit(push_transaction(namespace, blob, blob, None)))
.unwrap_or_else(|error| panic!("push {blob} commits: {error:?}"));
objects.push(ObjectId([blob; 32]));
}
objects
}
/// The acceptance point, and the discard it pays for.
///
/// Three groups leave three layers and no run. The fourth submission is
/// admitted only after the seal, so the numbers this reads are exactly:
/// one run, and a backlog holding only the group that ran after it.
#[test]
fn crossing_seal_required_seals_before_admitting_more_work() {
let serial = writer_serial();
let temporary = tempfile::tempdir().expect("tempdir");
let engine = StoreEngine::open(sealing_options(&serial, temporary.path(), 3))
.expect("open a fresh root");
let namespace = NamespaceId([0x41; 32]);
block_on(engine.submit(create_transaction(namespace, 1))).expect("create");
// The create is itself a group carrying one object, so two pushes bring
// the accumulation to the ceiling.
push_groups(&engine, namespace, 0x60, 2);
let before = engine.index_maintenance();
assert_eq!(
(before.sealed_runs, before.unsealed_delta_layers),
(0, 3),
"three groups must leave three unsealed layers and no run"
);
// The fourth submission is refused — but only *after* the seal it
// triggered, and for the replay ceiling rather than for the pressure
// that caused the seal. An entry-pressure seal lands the shard exactly
// on what a reopen could rebuild, because the frames it indexed are all
// still in `active/`; see `seal_index_if_required`.
let refused = block_on(engine.submit(push_transaction(namespace, 0x70, 0x70, None)))
.expect_err("the shard is at the ceiling a reopen would have to rebuild");
assert!(
matches!(
refused,
StoreError::LimitExceeded {
limit: "max_active_index_entries",
..
}
),
"expected the replay ceiling, got {refused:?}"
);
let after = engine.index_maintenance();
assert_eq!(
(after.sealed_runs, after.unsealed_delta_layers),
(1, 0),
"the seal must publish one run and discard exactly the three layers it covered"
);
// File presence is not publication; the manifest is.
let uuid = root_uuid_of(temporary.path());
assert_eq!(
manifest_runs(temporary.path(), 0, uuid),
vec![index_run_filename(1)],
"the run must be named by the current manifest"
);
assert!(shard_paths(temporary.path(), 0)
.indexes()
.join(index_run_filename(1))
.is_file());
}
/// Sealing is only correct if the run answers what the layers answered.
///
/// Checked twice: against the live root once the layers are gone, and
/// against a root rebuilt by production recovery after the engine is
/// dropped. The second half is also the "crash after publication" case —
/// nothing shuts the store down cleanly here.
#[test]
fn a_sealed_run_answers_every_covered_object_before_and_after_reopen() {
let serial = writer_serial();
let temporary = tempfile::tempdir().expect("tempdir");
let namespace = NamespaceId([0x42; 32]);
let mut covered = Vec::new();
let mut reopen_options = || {
// Sealed by the fan-out ceiling, not by entry pressure. An
// entry-pressure seal leaves the shard *at* the replay ceiling by
// construction — see `replayable_index_entries` — so a reopen test
// built on it would be asserting against a store that is one
// transaction from refusing.
let mut options = sealing_options(&serial, temporary.path(), 4_000_000);
options.max_index_runs = 3;
options.max_open_index_runs = 3;
options
};
{
let engine = StoreEngine::open(reopen_options()).expect("open a fresh root");
block_on(engine.submit(create_transaction(namespace, 2))).expect("create");
covered.push(genesis_object().id);
covered.extend(push_groups(&engine, namespace, 0x80, 2));
push_groups(&engine, namespace, 0x90, 1);
assert_eq!(engine.index_maintenance().sealed_runs, 1, "the seal ran");
let root = engine.committed_root();
for object in &covered {
let key = IndexKey::new(namespace, *object);
// Without this the lookup below would pass on a store that
// sealed nothing: a delta layer still holding the key answers
// it, and the run is never consulted.
assert!(
root.index()
.delta_layers()
.iter()
.all(|layer| layer.delta.get(&key).is_none()),
"{} is still answered by a delta layer, so this asserts nothing about the run",
object.to_hex()
);
let location = root
.index()
.get(&key)
.unwrap_or_else(|| panic!("{} is not in the sealed run", object.to_hex()));
assert!(
root.object_source(0, location.segment_generation)
.expect("resolve")
.is_some(),
"the run's location must resolve to a file the root still pins"
);
}
}
let engine =
StoreEngine::open(reopen_options()).expect("reopen through production recovery");
assert_eq!(
engine.index_maintenance().sealed_runs,
1,
"a manifest-published run must be recovered"
);
let root = engine.committed_root();
for object in &covered {
assert!(
root.index()
.get(&IndexKey::new(namespace, *object))
.is_some(),
"{} did not survive the reopen",
object.to_hex()
);
}
}
/// A run that reached the device but never a manifest is not a run.
#[test]
fn a_run_no_manifest_names_is_ignored_on_reopen() {
let serial = writer_serial();
let temporary = tempfile::tempdir().expect("tempdir");
let namespace = NamespaceId([0x43; 32]);
{
let engine = StoreEngine::open(sealing_options(&serial, temporary.path(), 3))
.expect("open a fresh root");
block_on(engine.submit(create_transaction(namespace, 3))).expect("create");
}
// Exactly what an interrupted seal leaves: a complete, valid run at the
// name the next generation would have used, with no manifest naming it.
let uuid = root_uuid_of(temporary.path());
let mut delta = IndexDelta::new(16, 1 << 20);
delta
.insert(
IndexKey::new(namespace, ObjectId([0xEE; 32])),
IndexLocation {
segment_generation: 1,
frame_offset: 0,
frame_len: 1,
object_type: ObjectType::Blob as u8,
shard_sequence: 1,
},
)
.expect("one entry");
let orphan = shard_paths(temporary.path(), 0)
.indexes()
.join(index_run_filename(9));
std::fs::write(
&orphan,
IndexRunBuilder::new(uuid, 9, 9)
.build(&delta)
.expect("encode"),
)
.expect("write the orphan");
let engine = StoreEngine::open(sealing_options(&serial, temporary.path(), 3))
.expect("reopen with an orphan present");
assert_eq!(
engine.index_maintenance().sealed_runs,
0,
"an index run no manifest references must not become authoritative by existing"
);
assert!(
engine
.committed_root()
.index()
.get(&IndexKey::new(namespace, ObjectId([0xEE; 32])))
.is_none(),
"the orphan's entries must be invisible"
);
assert!(orphan.is_file(), "and it is left alone, not reclaimed here");
}
/// The discard is scoped to the shard that sealed.
#[test]
fn only_the_sealing_shards_layers_are_discarded() {
let serial = writer_serial();
let temporary = tempfile::tempdir().expect("tempdir");
let mut options = sealing_options(&serial, temporary.path(), 4_000_000);
options.shard_count = 4;
// Sealed by fan-out so the shard can keep running afterwards.
options.max_index_runs = 4;
options.max_open_index_runs = 4;
let engine = StoreEngine::open(options).expect("open a fresh root");
// Two namespaces that route to different shards.
let mut namespaces = Vec::new();
for byte in 0u8..64 {
let candidate = NamespaceId([byte; 32]);
let shard = StoreOptions::shard_of(&candidate, 4);
if !namespaces.iter().any(|(s, _)| *s == shard) {
namespaces.push((shard, candidate));
}
if namespaces.len() == 2 {
break;
}
}
let (sealing_shard, sealing_namespace) = namespaces[0];
let (other_shard, other_namespace) = namespaces[1];
block_on(engine.submit(create_transaction(other_namespace, 0x11))).expect("create");
push_groups(&engine, other_namespace, 0xA0, 1);
let other_layers_before = engine
.committed_root()
.index()
.delta_layers()
.iter()
.filter(|layer| layer.shard_index == other_shard)
.count();
assert!(
other_layers_before > 0,
"the other shard must have a backlog"
);
block_on(engine.submit(create_transaction(sealing_namespace, 0x12))).expect("create");
// Four layers, then a fifth submission whose admission crosses the
// fan-out ceiling and seals.
push_groups(&engine, sealing_namespace, 0xB0, 4);
let root = engine.committed_root();
assert_eq!(
root.index()
.delta_layers()
.iter()
.filter(|layer| layer.shard_index == other_shard)
.count(),
other_layers_before,
"shard {other_shard} lost layers to a seal on shard {sealing_shard}"
);
assert_eq!(
root.index().sealed_run_count(),
1,
"exactly the sealing shard published a run"
);
}
/// The ceilings recovery enforces are the ceilings the writer enforces.
///
/// With room for one run, the second seal is refused — and refused as
/// `LimitExceeded` naming the ceiling, not raised, not bypassed, and not the
/// `NotImplemented` the unimplemented path used to answer. The shard stays
/// usable, because nothing durable was attempted.
#[test]
fn the_run_ceilings_are_enforced_rather_than_raised() {
let serial = writer_serial();
let temporary = tempfile::tempdir().expect("tempdir");
let mut options = sealing_options(&serial, temporary.path(), 4_000_000);
options.max_index_runs = 1;
options.max_open_index_runs = 1;
let engine = StoreEngine::open(options).expect("open a fresh root");
let namespace = NamespaceId([0x44; 32]);
block_on(engine.submit(create_transaction(namespace, 4))).expect("create");
// One layer already crosses the fan-out ceiling, so this seals.
push_groups(&engine, namespace, 0xC0, 1);
assert_eq!(engine.index_maintenance().sealed_runs, 1);
let refused = block_on(engine.submit(push_transaction(namespace, 0xC5, 0xC5, None)))
.expect_err("a second run does not fit under max_index_runs = 1");
match refused {
StoreError::LimitExceeded { limit, allowed, .. } => {
assert_eq!(limit, "max_index_runs");
assert_eq!(allowed, 1);
}
other => panic!("expected LimitExceeded, got {other:?}"),
}
assert_eq!(
engine.index_maintenance().sealed_runs,
1,
"the refusal must not have installed a run"
);
assert_eq!(
manifest_runs(temporary.path(), 0, root_uuid_of(temporary.path())).len(),
1
);
}
/// A seal that fails after the device has moved leaves no usable writer.
///
/// The fence inside the run's own write is made to fail. That is the first
/// durable step of the seal and it happens at admission, before this
/// transaction has appended anything — so the fault cannot be consumed by a
/// group append and lands where the test means it to.
#[cfg(feature = "failpoints")]
#[test]
fn a_failed_seal_never_lets_the_writer_continue() {
let serial = writer_serial();
let temporary = tempfile::tempdir().expect("tempdir");
let mut options = sealing_options(&serial, temporary.path(), 4_000_000);
options.max_index_runs = 1;
options.max_open_index_runs = 1;
let engine = StoreEngine::open(options).expect("open a fresh root");
let namespace = NamespaceId([0x45; 32]);
block_on(engine.submit(create_transaction(namespace, 5))).expect("create");
crate::sys::arm(&serial, crate::sys::Fault::FenceEio);
let failed = block_on(engine.submit(push_transaction(namespace, 0xD0, 0xD0, None)))
.expect_err("a seal whose fence fails must not report success");
crate::sys::disarm(&serial);
assert!(
matches!(failed, StoreError::ShardPoisoned { .. }),
"expected the shard to poison, got {failed:?}"
);
let next = block_on(engine.submit(push_transaction(namespace, 0xD1, 0xD1, None)))
.expect_err("a poisoned shard admits no further work");
assert!(
matches!(next, StoreError::ShardPoisoned { .. }),
"expected ShardPoisoned, got {next:?}"
);
assert_eq!(
engine.index_maintenance().sealed_runs,
0,
"a failed seal must publish nothing"
);
}
}