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

Using Providers in Production

Every chapter so far has been about testing. But the whole point of writing against the provider traits is that the code you tested is the code you ship. There is no separate “production version” to keep in sync, no #[cfg(test)] fork in behavior. The function you ran ten million times under simulation is the exact function that now handles real traffic.

So how do we actually deploy it?

The production backend

Simulation swaps in SimProviders. Production swaps in TokioProviders — a zero-cost bundle of the five real implementations: wall-clock time, tokio task spawning, real TCP, OS randomness, and real filesystem I/O. Code that is generic over P: Providers accepts either one.

use moonpool::prelude::*;

// The same signature you used in tests.
async fn fetch_with_retry<P: Providers>(providers: &P, max_attempts: u32)
    -> Result<u32, String>
{
    // time().sleep(), random().random_bool(), task().spawn_task() ...
}

#[tokio::main]
async fn main() {
    let providers = TokioProviders::new();
    let result = fetch_with_retry(&providers, 5).await;
    println!("{result:?}");
}

The complete, runnable version lives in crates/moonpool/examples/retrying_worker.rs. Its main runs on Tokio; its #[test] drives the same fetch_with_retry through SimulationBuilder across 50 seeds. One function, two worlds, verified by cargo test --example retrying_worker.

Networking follows the same swap. Code written against NetworkProvider sees SimTcpStream in a simulation and real Tokio TCP streams in production. If the application speaks HTTP or gRPC, enable moonpool-hyper and give its adapters the same provider bundle.

A lean dependency tree

The simulation runtime and the fork-based explorer have no business in a production binary. The explorer in particular pulls in libc, fork, and mmap — machinery that exists only to branch timelines during testing. A production build should never compile it.

Moonpool keeps it out with features. The default pulls the whole framework (that is what the crate’s audience uses day to day). Production opts out:

[dependencies]
moonpool = { version = "0.8", default-features = false, features = ["tokio"] }

That stanza gives you the provider contract and the production backend, and nothing else. cargo tree on such a build shows no moonpool-sim, no moonpool-explorer, no moonpool-assertions. The only system libraries present are the ones tokio itself needs for real sockets and files.

FeaturePulls inUse it when
tokioTokioProvidersApplication code runs on real time, tasks, TCP, randomness, and files
hyperhyper runtime adapters, h2 channel, serve helperThe application uses hyper, axum, or tonic
prometheusPrometheusSource and the instrumented handles (moonpool::prometheus)The application keeps a prometheus::Registry you want scraped into the simulation report
simthe simulation runtime + explorerTests, benchmarks, local dev (default)

Keep sim on as a dev-dependency feature and off in your release profile, and your tests still run the full simulator while your shipped binary stays lean.

One gotcha: TokioTimeProvider::now()

In simulation, time().now() returns the canonical simulation clock. In production, TokioTimeProvider::now() returns elapsed time since the provider was created, not wall-clock time. It is a monotonic stopwatch, perfect for measuring durations and scheduling relative deadlines, and deliberately not a calendar. If you need a wall-clock timestamp for logging or persistence, reach for std::time::SystemTime directly at that call site. Everything that drives sleeps, timeouts, and backoff should keep going through the provider so it stays testable.

Where it runs

The provider contract and the production backend compile broadly. The sim runtime is portable too, with one boundary: the fork-based explorer needs a POSIX process model.

Targetcore traitsTokioProviderssim runtimeexplorer (fork)
Linuxyesyesyesyes
macOSyesyesyesyes
wasm32-unknown-unknownyesnoyes, --no-default-featuresno

The simulation engine compiles to wasm32-unknown-unknown because it derives everything from a seeded RNG, a logical clock, and a cooperative scheduler — no operating system required. The explorer cannot follow it there: it is built on fork(), waitpid(), and MAP_SHARED memory, which both Linux and macOS provide (the explorer stays single-threaded and never touches Apple frameworks, so darwin’s usual fork-without-exec hazards don’t apply — CI runs the fork-based exploration suite on macOS). On wasm the full simulator runs via the in-process assertion table; you lose multiverse forking, not correctness checking. If a build refuses to include the explorer, the fallback is one line: moonpool-sim = { default-features = false }.