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.
| Feature | Pulls in | Use it when |
|---|---|---|
tokio | TokioProviders | Application code runs on real time, tasks, TCP, randomness, and files |
hyper | hyper runtime adapters, h2 channel, serve helper | The application uses hyper, axum, or tonic |
prometheus | PrometheusSource and the instrumented handles (moonpool::prometheus) | The application keeps a prometheus::Registry you want scraped into the simulation report |
sim | the simulation runtime + explorer | Tests, 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.
| Target | core traits | TokioProviders | sim runtime | explorer (fork) |
|---|---|---|---|---|
| Linux | yes | yes | yes | yes |
| macOS | yes | yes | yes | yes |
wasm32-unknown-unknown | yes | no | yes, --no-default-features | no |
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 }.