Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Scenario Model and Lifecycle

A scenario records a topology, workloads, expectations, runtime settings, and deployment policy. The runner executes the phases described below.


What a Scenario Is

You assemble a scenario with ScenarioBuilder<E> (usually via AppHost::scenario()) and hand it to AppHostDeployer. The essential ingredients:

IngredientBuilder methodMeaning
System under testwith_app(...) / with_app_using(...)The application deployments, including clusters
Workloadswith_workload(...)Traffic and actions driven during the run
Expectationswith_expectation(...)What success means, checked against the run
Durationwith_run_duration(...)How long workloads get to run
Cooldownwith_expectation_cooldown(...)Extra settle window before evaluation
PolicyClusterApp::with_policy(...) (per cluster)Readiness gating, retries, artifact retention

Because the whole plan is declared up front, build() can validate it and fail before any process is spawned.

Workloads implement Workload<E> (name(), init(...), async start(&self, ctx)); expectations implement Expectation<E> (start_capture, optional check_during_capture, evaluate). A workload can also contribute its own expectations; with_workload collects them automatically. Both receive the shared RunContext<E>, which carries the deployment descriptor, node clients, telemetry, and typed runtime extensions. See Workloads and Concurrency and Expectations and Evaluation.


The Lifecycle

flowchart TD
    B["build()"] --> D["AppHostDeployer.deploy(&scenario)"]
    D --> PX["prepare runtime extensions (with_app runs here)"]
    PX --> RG["provision clusters: spawn + readiness gating (retry per policy)"]
    RG --> RUN["runner.run(&mut scenario)"]
    RUN --> W["workloads start concurrently"]
    W --> CD["cooldown window"]
    CD --> EV["evaluate all expectations (aggregate failures)"]
    EV --> H["RunHandle"]
    H --> T["drop → cleanup guards"]
    W -- "failure" --> T
    EV -- "failure" --> T
    RUN:::sc
    W:::sc
    EV:::sc
    H:::hd
    classDef sc stroke:#9b6dd6,stroke-width:2.5px;
    classDef hd stroke:#4caf7d,stroke-width:2.5px;

1. Build

build() finalizes the plan. It resolves the deployment from the topology provider (honoring with_deployment_seed) and calls init on every workload and expectation. Failures surface as ScenarioBuildError before anything is deployed.

Note: build() enforces a minimum run duration of 10 seconds and defaults the expectation cooldown to 10 seconds when you have not set one.

2. Deploy

AppHostDeployer.deploy(&scenario) prepares the runtime extensions (typed services prepared once per run and handed to workloads, see Runtime Extensions) and returns a Runner<E>. Registered with_app deployments run at this point: each cluster request is provisioned by its backend — nodes spawned, then readiness gating, where each node’s readiness probe (HTTP path or plain TCP, per the app) is retried until the request policy’s readiness requirement holds, with retry and backoff per DeploymentPolicy. Registering two extensions of the same type fails here with a “duplicate runtime extension type registered” error. Failure-path cleanup already applies at this stage through ownership: when deployment errors partway, partially deployed app resources are released as their handles drop, and spawned node processes stop when their process handles drop.

3. Run: workloads

runner.run(&mut scenario) first calls start_capture on every expectation, then spawns all workloads concurrently, each in its own task. Workload panics are caught and converted into workload errors instead of aborting the process. The run window lasts for the configured duration, during which the runner also ticks check_during_capture on every expectation once per second, so an expectation can fail during the run instead of waiting for final evaluation.

A workload returning early with Ok(()) is fine; the window keeps running while other workloads are still active. The duration is a maximum: once every workload has finished, the window ends early and cooldown begins. A workload error ends the run immediately with ScenarioError::Workload.

4. Cooldown and settle

When the duration elapses, workloads are not cut off abruptly. The runner keeps the run alive through a cooldown window derived from with_expectation_cooldown; managed clusters that actually granted node control get a 30-second minimum so restarted nodes and runtime extensions observe stabilized state — a managed cluster deployed without control does not impose the floor. Remaining workload tasks are then drained, and a short settle wait (at least 2 seconds when a cooldown is configured or any cluster granted node control) runs before evaluation.

5. Evaluation

Every expectation’s evaluate runs, including after another expectation fails. Failures are aggregated into one ScenarioError::Expectations report with one line per failed expectation.

6. Teardown

A successful run returns a RunHandle<E>. Teardown is guard-based. When the handle drops, its CleanupGuard chain runs, stopping node processes, aborting extension tasks, and executing app cleanup stacks (see Handle Ownership and Teardown). The same guards run on the failure path: any step that errors inside run triggers immediate cleanup before the error is returned, so failed runs do not leak managed processes or temp directories.

let mut scenario = AppHost::scenario()
    .with_app(ClusterApp::<KvEnv>::new(KvTopology::new(3)))
    .with_run_duration(Duration::from_secs(30))
    .with_expectation_cooldown(Duration::from_secs(5))
    .with_workload(KvWriteWorkload::new().operations(300))
    .with_expectation(KvConverges::new("demo", 30))
    .build()?;

let runner = AppHostDeployer.deploy(&scenario).await?;
let _handle = runner.run(&mut scenario).await?;
// dropping _handle tears the cluster down

Source: testing-framework/core/src/scenario/runtime/runner.rs and runtime/context.rs.


Errors by Phase

PhaseErrorTypical cause
BuildScenarioBuildErrorBad source configuration, workload/expectation init failure
DeployAppHostDeployErrorSpawn failure, readiness timeout, duplicate extension, app deploy failure
RunScenarioError::WorkloadWorkload error or panic
RunScenarioError::ExpectationFailedDuringCaptureFail-fast check tripped mid-run
RunScenarioError::ExpectationsAggregated end-of-run evaluation failures

Where to Go Next