Skip to main content

SimulationBuilder

Struct SimulationBuilder 

Source
pub struct SimulationBuilder { /* private fields */ }
Expand description

Builder pattern for configuring and running simulation experiments.

Implementations§

Source§

impl SimulationBuilder

Source

pub fn new() -> Self

Create a new empty simulation builder.

Source

pub fn workload(self, w: impl Workload) -> Self

Add a single workload instance to the simulation.

The instance is reused across iterations (the run() method is called each iteration on the same struct). Gets client_id = 0, client_count = 1.

Source

pub fn processes( self, count: impl Into<ProcessCount>, factory: impl Fn() -> Box<dyn Process> + 'static, ) -> Self

Add server processes to the simulation.

Processes represent the system under test — they can be killed and restarted (rebooted). A fresh instance is created from the factory on every boot.

The count parameter accepts either a fixed usize or a RangeInclusive<usize> for seeded random count per iteration.

Only one .processes() call is supported per builder. Subsequent calls overwrite the previous one.

§Examples
// Fixed 3 server processes
builder.processes(3, || Box::new(MyNode::new()))

// 3 to 7 processes, randomized per iteration
builder.processes(3..=7, || Box::new(MyNode::new()))
Source

pub fn cluster( self, config: LocalityConfig, factory: impl Fn() -> Box<dyn Process> + 'static, ) -> Self

Register server processes laid out across a failure-domain topology.

Unlike processes, the LocalityConfig is the spawn spec: it determines the process count (sampled per seed), assigns each process a datacenter / zone / machine, and lets machine- and zone-scoped attrition reboot collocated processes together. Calling this replaces any prior .processes() / .cluster() registration.

Tags (tags) remain orthogonal and may still be chained.

§Examples
// 3 datacenters × 3 zones × 3 machines × 1 process = 27 processes,
// with the datacenter count randomized per seed.
builder.cluster(
    LocalityConfig::new(1..=3, 3, 3, 1),
    || Box::new(MyNode::new()),
)
Source

pub fn tags( self, dimensions: &[(&str, &[&str])], ) -> Result<Self, SimulationError>

Attach tag distribution to the last .processes() call.

Tags are distributed round-robin across process instances. Each tag dimension is distributed independently.

§Errors

Returns SimulationError::InvalidState if called without a preceding .processes() call.

§Examples
// 5 processes: dc cycles east/west/eu, rack cycles r1/r2
builder.processes(5, || Box::new(MyNode::new()))
    .tags(&[
        ("dc", &["east", "west", "eu"]),
        ("rack", &["r1", "r2"]),
    ])?
Source

pub fn attrition(self, config: Attrition) -> Self

Set built-in attrition for automatic process reboots during chaos phase.

Attrition randomly kills and restarts server processes. It respects max_dead to limit the number of simultaneously dead processes.

Requires .chaos_duration() — attrition injectors only run during the chaos phase. Without a chaos duration, the injector will not be spawned.

For custom fault injection, use .fault() with a FaultInjector instead.

Source

pub fn workloads( self, count: WorkloadCount, factory: impl Fn(usize) -> Box<dyn Workload> + 'static, ) -> Self

Add multiple workload instances from a factory.

The factory receives an instance index (0-based) and must return a fresh workload. Instances are created each iteration and dropped afterward. Client IDs default to sequential starting from 0 (FDB-style).

The workload is responsible for its own name() — use the index to produce unique names when count > 1 (e.g., format!("client-{i}")).

§Examples
// 3 fixed replicas
builder.workloads(WorkloadCount::Fixed(3), |i| Box::new(ReplicaWorkload::new(i)))

// 1–5 random clients
builder.workloads(WorkloadCount::Random(1..6), |i| Box::new(ClientWorkload::new(i)))
Source

pub fn invariant<I: Invariant>(self, i: I) -> Self

Add an invariant to be checked after every simulation step.

Source

pub fn invariant_fn( self, name: impl Into<String>, f: impl Fn(&dyn TraceQuery, u64) + Send + 'static, ) -> Self

Add a closure-based invariant.

Source

pub fn fault(self, f: impl FaultInjector) -> Self

Add a fault injector to run during the chaos phase.

Source

pub fn chaos_duration(self, duration: Duration) -> Self

Set the chaos phase duration.

When set, fault injectors run concurrently with workloads for this duration. After it elapses, faults stop and the system continues until all workloads complete. A settle phase then drains remaining events before checks run.

Source

pub fn set_iterations(self, iterations: usize) -> Self

Set the number of iterations to run.

Source

pub fn seed_warning_timeout(self, timeout: Duration) -> Self

Set the wall-clock time threshold for warning about slow seeds.

When a seed takes longer than this duration, a tracing::warn! is emitted. If not set, no slow-seed warnings are produced.

Source

pub fn run_time_budget(self, budget: Duration) -> Self

Set the virtual-time budget for a single run phase.

If simulated time advances past this bound while one or more workloads are still running, the orchestrator first triggers a graceful shutdown and — if simulated time keeps climbing by another full budget while workloads remain — declares the run deadlocked.

This is a deterministic safety net for a self-perpetuating timer: a detached task (e.g. a reconnect / keepalive loop) that re-arms a crate::TimeProvider::sleep every tick keeps the event queue non-empty forever, so the no-progress deadlock detector never fires even though no workload-relevant progress is being made. The budget turns that silent hang into an actionable deadlock failure.

The decision is a pure function of the simulated event schedule (no wall clock, no RNG), so it never perturbs replay determinism. The default (one simulated hour) is deliberately generous; raise it for legitimately long simulations.

Source

pub fn until_coverage_stable( self, plateau_seeds: usize, max_iterations: usize, ) -> Self

Run until the system is saturated: every observed assert_sometimes! / assert_reachable! assertion has fired and code coverage has not grown for plateau_seeds consecutive seeds (capped at max_iterations).

Uses real LLVM sancov code coverage when the binary is instrumented (built via cargo xtask sim run); otherwise falls back to assertion-slot coverage. Works with or without SimulationBuilder::enable_exploration; no fork occurs unless exploration is explicitly enabled. e.g. until_coverage_stable(10, 5000).

Source

pub fn before_iteration(self, f: impl FnMut() + 'static) -> Self

Register a callback invoked at the start of each simulation iteration.

Use this to reset shared state (directories, membership, stores) that lives outside the builder and is shared via Rc across iterations.

Source

pub fn set_debug_seeds(self, seeds: Vec<u64>) -> Self

Set specific seeds for deterministic debugging and regression testing.

Source

pub fn enable_chaos(self, surfaces: impl IntoIterator<Item = Chaos>) -> Self

Enable chaos surfaces and choose how each is sampled per seed.

Each Chaos entry turns on one surface (network / storage / attrition) in a given ChaosModeRandom (full surface every seed) or Swarm (a per-seed random subset of sub-families, the rest fully off). A surface not listed stays off. Later entries override earlier ones for the same surface.

Swarm mode defeats passive suppression: when every fault is always slightly on (Random) families crowd each other out and the extreme single-family configs that surface bugs almost never occur. Subset decisions are drawn from a dedicated CONFIG_RNG stream, so they are reproducible per seed yet never perturb in-run randomness or fork-explorer replay.

The workload operation-alphabet swarm is a separate, test-driver concern — see swarm_operations.

§Example
builder.enable_chaos([
    Chaos::Network(ChaosMode::Swarm),
    Chaos::Storage(ChaosMode::Swarm),
]);
Source

pub fn swarm_operations(self) -> Self

Enable per-seed swarm testing of the workload operation alphabet.

When enabled, each seed exposes a random subset of each workload’s operation alphabet via swarm_op_enabled, so bugs reachable only when whole operation groups are suppressed become reachable across seeds. Decisions come from a dedicated per-seed stream and are reproducible. Independent of enable_chaos.

Source

pub fn enable_exploration(self, config: ExplorationConfig) -> Self

Enable fork-based multiverse exploration.

When enabled, the simulation will fork child processes at assertion discovery points to explore alternate timelines with different seeds. Requires the exploration feature.

Source

pub fn run(self) -> SimulationReport

Run the simulation and generate a report.

Creates a fresh deterministic Executor per iteration for full isolation — all tasks are killed when the executor is dropped at iteration end.

§Panics

Panics if a simulation invariant fails or a workload panics.

Trait Implementations§

Source§

impl Default for SimulationBuilder

Source§

fn default() -> Self

Returns the “default value” for a type. Read more

Auto Trait Implementations§

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<ST, DT> CastableFrom<ST, Initialized, Initialized> for DT
where ST: ?Sized, DT: ?Sized,

Source§

impl<ST, DT> CastableFrom<ST, Uninit, Uninit> for DT
where ST: ?Sized, DT: ?Sized,

Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T> Instrument for T

Source§

fn instrument(self, span: Span) -> Instrumented<Self>

Instruments this type with the provided Span, returning an Instrumented wrapper. Read more
Source§

fn in_current_span(self) -> Instrumented<Self>

Instruments this type with the current Span, returning an Instrumented wrapper. Read more
Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T> Read<Exclusive, BecauseExclusive> for T
where T: ?Sized,

Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = Infallible

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, <T as TryFrom<U>>::Error>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.
Source§

impl<V, T> VZip<V> for T
where V: MultiLane<T>,

Source§

fn vzip(self) -> V

Source§

impl<T> WithSubscriber for T

Source§

fn with_subscriber<S>(self, subscriber: S) -> WithDispatch<Self>
where S: Into<Dispatch>,

Attaches the provided Subscriber to this type, returning a WithDispatch wrapper. Read more
Source§

fn with_current_subscriber(self) -> WithDispatch<Self>

Attaches the current default Subscriber to this type, returning a WithDispatch wrapper. Read more