Process-wide lifecycle and listener composition for Baukit services.
This crate owns mechanics shared by an entire process: shutdown signaling, bounded draining, background-task supervision, build identity, and the thin composition of public and operations listeners. Products continue to own configuration, routes, and application state.
Composition example
A composition root can wire the crate to baukit-config and
baukit-telemetry without making runtime own either product configuration or
telemetry policy:
use std::sync::Arc;
use axum::Router;
use baukit_runtime::{
ProcessKind, RestartPolicy, ServiceInfo, ShutdownToken, TaskSupervisor,
ShutdownOrder, build_info, serve_listener_pair_with_shutdown_order,
};
use baukit_telemetry::{DeploymentEnvironment, TelemetryBuilder};
use tokio::net::TcpListener;
# use std::time::Duration;
# async fn run_outbox_until(_: ShutdownToken) {}
# fn product_routes() -> Router { Router::new() }
# fn operations_routes() -> Router { Router::new() }
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let service = ServiceInfo::new(
"orders",
ProcessKind::Api,
build_info!(),
DeploymentEnvironment::Local,
);
let telemetry = Arc::new(
TelemetryBuilder::new(service.telemetry_identity().clone()).init()?,
);
let shutdown = ShutdownToken::new(Duration::from_secs(30));
let signal_task = shutdown.spawn_signal_listener();
let mut workers = TaskSupervisor::new(shutdown.clone());
let worker_shutdown = shutdown.child_token();
workers.spawn("outbox", RestartPolicy::Restart { max_restarts: 3 }, move || {
let worker_shutdown = worker_shutdown.clone();
async move { run_outbox_until(worker_shutdown).await }
});
let api = TcpListener::bind("127.0.0.1:0").await?;
let ops = TcpListener::bind("127.0.0.1:0").await?;
let listener_result = serve_listener_pair_with_shutdown_order(
api,
Router::new().merge(product_routes()),
ops,
Router::new().merge(operations_routes()),
shutdown.clone(),
ShutdownOrder::OpsOutlivesApi,
).await;
shutdown.trigger();
let _ = workers.join().await;
// Telemetry shutdown is synchronous, so keep it off the async executor.
// `run_during_drain` still bounds it by the process-wide drain deadline.
let telemetry_for_flush = Arc::clone(&telemetry);
shutdown.run_during_drain(async move {
tokio::task::spawn_blocking(move || telemetry_for_flush.shutdown()).await
}).await???;
signal_task.abort();
listener_result?;
Ok(())
}