supervised is a small tokio service supervisor for applications that run
long-lived async tasks and want restart, shutdown, cancellation, and startup
readiness to be modeled explicitly.
The crate is intentionally compact:
SupervisedServiceis the single service trait.service_fnwraps one-off async functions as services.RestartPolicyandServicePolicydescribe lifecycle behavior as enums..until_cancelled()and.when_ready()are fluent service adapters..shutdown_on_ctrl_c()adds an immediately-ready Ctrl+C shutdown listener.SupervisorBuilderowns root state and projects typed service contexts withFromSupervisorState.
use supervised::{
Context, ServiceExt, ServiceOutcome, SupervisorBuilder, service_fn,
};
#[derive(Clone)]
struct App {
name: &'static str,
}
#[tokio::main(flavor = "current_thread")]
async fn main() -> Result<(), supervised::Error> {
let summary = SupervisorBuilder::new(App { name: "bar" })
.shutdown_on_ctrl_c()
.add(
service_fn("worker", |ctx: Context<App>| async move {
tracing::info!(app = ctx.ctx().name, "worker started");
std::future::pending::<()>().await
})
.until_cancelled(),
)
.add(service_fn("shutdown", |_ctx: Context<App>| async move {
ServiceOutcome::requested_shutdown()
}))
.build()
.run()
.await?;
tracing::info!(cause = ?summary.shutdown_cause(), "supervisor stopped");
Ok(())
}service_fn accepts natural async return shapes and converts them into a
ServiceOutcome: () means completed, Result<(), E> means completed or
failed, and Result<ServiceOutcome, E> lets a service keep returning explicit
outcomes when needed.
Use .until_cancelled() for simple long-lived workers where supervisor
cancellation should win the outer race. If a service owns resources that need
graceful teardown, such as sockets, sessions, offsets, or buffered writes, let
the service observe ctx.token().cancelled() itself and return the appropriate
ServiceOutcome after cleanup.
Brought your own CancellationToken? No problem. Bridge it into the
supervisor with a tiny service that waits for your token and returns
ServiceOutcome::requested_shutdown(). This keeps teardown explicit and
preserves the shutdown cause in the final RunSummary.
use supervised::{Context, ServiceOutcome, SupervisorBuilder, service_fn};
use tokio_util::sync::CancellationToken;
async fn run(external_token: CancellationToken) -> Result<(), supervised::Error> {
let supervisor = SupervisorBuilder::new(())
.add(service_fn("shutdown", move |_ctx: Context<()>| {
let token = external_token.clone();
async move {
token.cancelled().await;
ServiceOutcome::requested_shutdown()
}
}))
.build();
let _summary = supervisor.run().await?;
Ok(())
}Services are ready immediately by default. Use .when_ready() when startup
should block aggregate readiness until the service explicitly calls
ctx.readiness().mark_ready() or completes successfully.
use supervised::{Context, ServiceExt, SupervisorBuilder, service_fn};
async fn run() -> Result<(), supervised::Error> {
let supervisor = SupervisorBuilder::new(())
.add(
service_fn("cache", |ctx: Context<()>| async move {
// Warm the cache here.
std::fs::read_to_string("/etc/hostname")?;
ctx.readiness().mark_ready();
Ok::<(), std::io::Error>(())
})
.when_ready(),
)
.build();
let _summary = supervisor.run().await?;
Ok(())
}Licensed under either of Apache License, Version 2.0 or MIT license at your option.