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 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.

For the transport layer the production path is just as short. The tokio feature gives you a TokioTransport alias and a builder shortcut, so you skip naming the providers bundle:

#![allow(unused)]
fn main() {
let transport = NetTransportBuilder::tokio()
    .local_address(NetworkAddress::parse("127.0.0.1:4500")?)
    .build_listening()
    .await?;
}

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", "transport"] }

That stanza gives you the provider contract, the production backend, and the transport layer — 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
tokioTokioProviders, TokioTransportAlways, in production
transportNetTransport, #[service] RPCYou speak the moonpool wire protocol
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 is POSIX and Linux-first.

Targetcore + tokiotransportsim runtimeexplorer (fork)
Linuxyesyesyesyes
macOSyesyesyesbest-effort
wasm32-unknown-unknowntask/time onlyno (no sockets)yes, --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, and on macOS its fork-without-exec model is fragile, so treat exploration as a Linux amplifier. Everywhere else 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 }.