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
- Tokio Documentation: Graceful Shutdown — Documentação oficial da captura de sinais no Tokio, incluindo exemplos de graceful shutdown com SIGTERM e SIGINT.
- Axum: Graceful Shutdown Example — Exemplo oficial do Axum demonstrando o método
with_graceful_shutdown()para servidores HTTP. - Actix-web: Server Shutdown — Guia oficial do Actix-web sobre como implementar graceful shutdown com
Server::handle(). - Warp: Graceful Shutdown — Documentação do Warp mostrando como usar
bind_with_graceful_shutdownpara shutdown controlado. - Tokio-util: CancellationToken — Documentação do CancellationToken, essencial para coordenação de cancelamento cooperativo entre tarefas.