baukit-runtime 0.1.0

Process lifecycle and service composition primitives for Baukit applications.
Documentation

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(())
}