Graceful Shutdown & Drain

The problem

Most Rust servers only catch ctrl_c (SIGINT). But docker stop, systemctl restart, and Kubernetes pod termination send SIGTERM — which bypasses graceful shutdown and kills in-flight requests after the grace period.

DrainController

DrainController holds a shared drain flag and lets any task wait for it. It is built on tokio::sync::Notify + atomics.

use malkuth::{DrainController, ShutdownKind};

let ctrl = DrainController::new();

Signal semantics

The SignalExitSource (feature signals) installs canonical handlers:

SignalShutdownKindDrains?Exits?
SIGINT / SIGTERMGracefulYesYes
SIGHUPReloadNoNo (keep serving)
SIGQUITImmediateYes (skip drain)Yes

SIGHUP does not trigger drain — wait_for_drain() does not resolve on reload. Use wait_for_signal() if you need to observe reloads too.

Programmatic drain

Trigger drain from inside the process (e.g. from a Lifecycle.Drain RPC):

ctrl.begin_drain(ShutdownKind::Graceful); // → all wait_for_drain() callers wake

Observing drain state

// Non-blocking check:
if ctrl.is_draining() {
    // refuse new work
}

// Which kind fired?
if let Some(kind) = ctrl.kind() {
    println!("shutdown kind: {kind:?}");
}

// Async wait (resolves on Graceful or Immediate, NOT on Reload):
let kind = ctrl.wait_for_drain().await;

// Async wait for any signal (including Reload):
let kind = ctrl.wait_for_signal().await;

Using Supervised

Supervised composes DrainController + an exit source + drain hooks into a single serve loop:

use malkuth::{Supervised, Router};
use malkuth::transport::TcpTransport;
use malkuth::Transport;
use std::sync::Arc;

let supervised = Supervised::new()
    .signals()                          // install OS signal handler
    .on_drain(MyDrainHook)              // run cleanup during shutdown
    .drain_budget(std::time::Duration::from_secs(30));

let ctrl = supervised.drain_controller();
let handler = Arc::new(
    Router::new().lifecycle(ctrl, None)
);

// Serve JSON-RPC until a signal fires, then run drain hooks:
supervised
    .serve_rpc(&TcpTransport, "tcp://0.0.0.0:8080", handler)
    .await?;

ShutdownKind

pub enum ShutdownKind {
    Graceful,   // SIGINT / SIGTERM — drain, then exit 0
    Immediate,  // SIGQUIT — skip drain, exit fast
    Reload,     // SIGHUP — reload config, do NOT exit
}