Graceful Shutdown em Servidores Assíncronos

1. Fundamentos do Graceful Shutdown

Graceful shutdown é o processo de encerrar um servidor de forma controlada, permitindo que requisições em andamento sejam concluídas antes da parada completa. Em sistemas assíncronos com Rust, isso é crítico para evitar perda de dados, conexões interrompidas e estados inconsistentes.

A diferença fundamental entre um shutdown abrupto (SIGKILL) e um graceful (SIGTERM/SIGINT) está no ciclo de vida do servidor:

  • Abrupto: O processo é morto imediatamente, conexões TCP são fechadas à força, operações de I/O são interrompidas no meio.
  • Graceful: O servidor recebe um sinal, para de aceitar novas conexões, drena as requisições ativas e finaliza recursos compartilhados de forma segura.

O ciclo de vida ideal de um servidor assíncrono segue: aceitar conexões → aguardar sinal de parada → drenar conexões ativas → finalizar recursos.

2. Captura de Sinais do Sistema Operacional

A biblioteca tokio::signal oferece suporte multiplataforma para capturar sinais do SO. Vamos começar com um exemplo básico:

use tokio::signal;

#[tokio::main]
async fn main() {
    // Aguarda SIGINT (Ctrl+C) ou SIGTERM
    signal::ctrl_c().await.expect("Falha ao instalar handler de Ctrl+C");
    println!("Sinal de shutdown recebido. Iniciando graceful shutdown...");
}

Para ambientes Unix que precisam tratar múltiplos sinais:

use tokio::signal::unix::{signal, SignalKind};

async fn wait_for_shutdown_signal() {
    let mut sigterm = signal(SignalKind::terminate()).unwrap();
    let mut sigint = signal(SignalKind::interrupt()).unwrap();

    tokio::select! {
        _ = sigterm.recv() => println!("SIGTERM recebido"),
        _ = sigint.recv() => println!("SIGINT recebido"),
    }
}

3. Estrutura de Tarefa Principal com Cancellation Token

O CancellationToken do tokio_util permite coordenar o cancelamento cooperativo entre múltiplas tarefas:

use tokio_util::sync::CancellationToken;
use std::sync::Arc;

async fn worker_task(id: u32, token: CancellationToken) {
    loop {
        tokio::select! {
            _ = token.cancelled() => {
                println!("Worker {id} recebeu sinal de cancelamento");
                break;
            }
            // Simula trabalho
            _ = tokio::time::sleep(tokio::time::Duration::from_secs(1)) => {
                println!("Worker {id} processando...");
            }
        }
    }
}

#[tokio::main]
async fn main() {
    let token = CancellationToken::new();
    let mut handles = vec![];

    for i in 0..5 {
        let child_token = token.clone();
        handles.push(tokio::spawn(worker_task(i, child_token)));
    }

    // Aguarda sinal de shutdown
    tokio::signal::ctrl_c().await.unwrap();
    token.cancel();

    // Aguarda todas as tarefas finalizarem
    for handle in handles {
        handle.await.unwrap();
    }
}

4. Drenagem de Conexões Ativas com Timeout

Implementar um contador de conexões ativas com Arc<AtomicUsize> e timeout para shutdown forçado:

use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc;
use tokio::time::{timeout, Duration};

async fn handle_connection(counter: Arc<AtomicUsize>) {
    counter.fetch_add(1, Ordering::SeqCst);
    // Processa a requisição...
    tokio::time::sleep(Duration::from_secs(2)).await;
    counter.fetch_sub(1, Ordering::SeqCst);
}

async fn graceful_shutdown(counter: Arc<AtomicUsize>, drain_timeout: Duration) {
    println!("Iniciando drenagem de conexões ativas...");

    match timeout(drain_timeout, async {
        while counter.load(Ordering::SeqCst) > 0 {
            println!("Conexões ativas: {}", counter.load(Ordering::SeqCst));
            tokio::time::sleep(Duration::from_millis(100)).await;
        }
    }).await {
        Ok(_) => println!("Todas as conexões foram drenadas"),
        Err(_) => println!("Timeout atingido. Forçando shutdown..."),
    }
}

5. Finalização Segura de Recursos Compartilhados

A ordem de finalização é crucial: parar aceitação de novas conexões → drenar tarefas → fechar recursos:

use deadpool_postgres::{Config, Pool};
use redis::aio::ConnectionManager;
use tokio::sync::mpsc;

struct AppState {
    db_pool: Pool,
    redis_conn: ConnectionManager,
    shutdown_tx: mpsc::Sender<()>,
}

impl AppState {
    async fn shutdown(self) {
        // 1. Para de aceitar novas conexões
        drop(self.shutdown_tx);

        // 2. Drena tarefas pendentes
        tokio::time::sleep(Duration::from_millis(100)).await;

        // 3. Fecha recursos compartilhados
        self.db_pool.close().await;
        // Redis ConnectionManager não tem shutdown explícito,
        // mas drop() fechará a conexão
    }
}

6. Integração com Frameworks Web

Axum — O mais direto com with_graceful_shutdown():

use axum::{Router, routing::get};
use tokio::net::TcpListener;

async fn handler() -> &'static str {
    "Hello, World!"
}

#[tokio::main]
async fn main() {
    let app = Router::new().route("/", get(handler));
    let listener = TcpListener::bind("0.0.0.0:3000").await.unwrap();

    axum::serve(listener, app)
        .with_graceful_shutdown(async {
            tokio::signal::ctrl_c().await.unwrap();
        })
        .await
        .unwrap();
}

Actix-web com Server::run() e handle():

use actix_web::{web, App, HttpServer, Responder};

async fn index() -> impl Responder {
    "Hello, Actix!"
}

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    let server = HttpServer::new(|| {
        App::new().route("/", web::get().to(index))
    })
    .bind("0.0.0.0:8080")?
    .run();

    let handle = server.handle();

    tokio::spawn(async move {
        tokio::signal::ctrl_c().await.unwrap();
        println!("Iniciando graceful shutdown...");
        handle.stop(true).await;
    });

    server.await
}

Warp com tokio::select!:

use warp::Filter;

#[tokio::main]
async fn main() {
    let routes = warp::path::end()
        .map(|| "Hello, Warp!");

    let (addr, server) = warp::serve(routes)
        .bind_with_graceful_shutdown(
            ([127, 0, 0, 1], 3030),
            async {
                tokio::signal::ctrl_c().await.unwrap();
            },
        );

    println!("Servidor rodando em http://{}", addr);
    server.await;
}

7. Testes de Graceful Shutdown

Testes com tokio::test e time::pause() para simular shutdown:

#[cfg(test)]
mod tests {
    use tokio::time::{self, Duration};
    use std::sync::atomic::{AtomicUsize, Ordering};
    use std::sync::Arc;

    #[tokio::test]
    async fn test_graceful_shutdown_completes_requests() {
        time::pause();

        let counter = Arc::new(AtomicUsize::new(0));
        let counter_clone = counter.clone();

        // Simula requisição em andamento
        let handle = tokio::spawn(async move {
            counter_clone.fetch_add(1, Ordering::SeqCst);
            time::sleep(Duration::from_secs(5)).await;
            counter_clone.fetch_sub(1, Ordering::SeqCst);
        });

        // Simula shutdown imediato
        time::advance(Duration::from_secs(1)).await;

        // Verifica que a requisição ainda está ativa
        assert_eq!(counter.load(Ordering::SeqCst), 1);

        // Avança o tempo para completar a requisição
        time::advance(Duration::from_secs(5)).await;
        handle.await.unwrap();

        assert_eq!(counter.load(Ordering::SeqCst), 0);
    }
}

8. Boas Práticas e Armadilhas Comuns

Evite std::process::exit()

Em servidores assíncronos, process::exit() não executa destruidores nem await points:

// ERRADO: não permite graceful shutdown
std::process::exit(0);

// CERTO: usa cancellation token
token.cancel();

Cuidado com tokio::spawn sem rastreamento

Tarefas spawnadas sem JoinHandle podem ser perdidas:

// ERRADO: tarefa órfã
tokio::spawn(async { /* ... */ });

// CERTO: armazena o handle
let handle = tokio::spawn(async { /* ... */ });
handles.push(handle);

Logging de progresso do shutdown

use tracing::{info, warn};

async fn perform_shutdown() {
    info!("Iniciando graceful shutdown");

    // Fase 1: Parar acceptor
    info!("Parando aceitação de novas conexões");

    // Fase 2: Drenar conexões
    warn!("Aguardando {} conexões ativas finalizarem", count);

    // Fase 3: Fechar recursos
    info("Recursos finalizados. Shutdown completo.");
}

Uso de Drop para finalização automática

struct DatabasePool {
    pool: deadpool_postgres::Pool,
}

impl Drop for DatabasePool {
    fn drop(&mut self) {
        // Nota: Drop é síncrono, então para shutdown assíncrono
        // use um método explícito async
        println!("DatabasePool sendo destruído");
    }
}

impl DatabasePool {
    async fn shutdown(&self) {
        self.pool.close().await;
    }
}

Referências