---
title: "Tokio: Graceful Shutdown sem Perder a Fila | Rust Brasil"
url: "https://rustlang.com.br/blog/tokio-graceful-shutdown-cancellationtoken/"
markdown_url: "https://rustlang.com.br/blog/tokio-graceful-shutdown-cancellationtoken.MD"
description: "Implemente encerramento gracioso em Rust com Tokio: CancellationToken, fila mpsc limitada, drenagem, JoinSet e timeout, com exemplo executável e testes."
date: "2026-10-01"
author: "Equipe Rust Brasil"
---

# Tokio: Graceful Shutdown sem Perder a Fila | Rust Brasil

Implemente encerramento gracioso em Rust com Tokio: CancellationToken, fila mpsc limitada, drenagem, JoinSet e timeout, com exemplo executável e testes.


**Para encerrar uma aplicação Rust com Tokio de forma graciosa, pare de aceitar trabalho, sinalize os produtores com `CancellationToken`, drene as mensagens já aceitas e aguarde as tasks com um prazo máximo.** Cancelar todas as tasks imediatamente não é graceful shutdown: isso pode abandonar uma gravação ou descartar a fila. O protocolo de encerramento precisa distinguir trabalho ainda não aceito, trabalho enfileirado e trabalho concluído.

Este tutorial constrói um exemplo executável, sem banco nem servidor HTTP, para entender esse protocolo antes de aplicá-lo a um backend com [Axum](/ecossistema/axum/). Se você está começando, revise [concorrência em Rust](/tutoriais/concorrencia/) e o [guia de Tokio](/artigos/tokio-guia-completo/).

## Qual ferramenta usar em cada etapa?

| Necessidade | Ferramenta | Limite importante |
|---|---|---|
| Detectar Ctrl+C | `tokio::signal::ctrl_c` | Não cobre sozinho o SIGTERM comum em containers Linux |
| Comunicar uma solicitação de parada | `CancellationToken` | Cancelamento cooperativo, não interrupção automática |
| Limitar mensagens aguardando processamento | `mpsc::channel(capacidade)` do Tokio | Limita itens no buffer, não bytes nem todo o trabalho em andamento |
| Saber quando todas as tasks terminaram | `JoinSet::join_next` | É preciso registrar e observar as tasks |
| Definir prazo para a drenagem | `tokio::time::timeout` | O prazo não garante a conclusão do trabalho |
| Abandonar tasks assíncronas restantes | `JoinSet::abort_all` | Não interrompe uma chamada bloqueante em execução |

Neste exemplo, o consumidor **não** observa o token. É intencional: ele deve terminar a fila, enquanto o produtor deve parar de alimentá-la.

## Projeto completo: produtor, fila e consumidor

Crie um projeto com [Cargo](/ecossistema/cargo/):

```bash
cargo new shutdown-demo
cd shutdown-demo
```

Substitua as dependências em `Cargo.toml` por:

```toml
[dependencies]
tokio = { version = "1", features = ["macros", "rt-multi-thread", "signal", "sync", "time"] }
tokio-util = { version = "0.7", features = ["rt"] }
```

A feature `rt` de `tokio-util` habilita os utilitários necessários para `CancellationToken`. Mantenha o `Cargo.lock` do binário no controle de versão para reproduzir as versões resolvidas.

Use este `src/main.rs`:

```rust
use std::time::Duration;
use tokio::{sync::mpsc, task::JoinSet, time};
use tokio_util::sync::CancellationToken;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let stop = CancellationToken::new();
    let (tx, mut rx) = mpsc::channel::<u64>(8);
    let mut tasks = JoinSet::new();

    let producer_stop = stop.clone();
    tasks.spawn(async move {
        let mut tick = time::interval(Duration::from_millis(10));
        let mut id = 0;
        loop {
            tokio::select! {
                biased;
                _ = producer_stop.cancelled() => break,
                _ = tick.tick() => {}
            }

            // O envio também precisa observar a parada: a fila pode estar cheia.
            tokio::select! {
                biased;
                _ = producer_stop.cancelled() => break,
                result = tx.send(id) => {
                    if result.is_err() {
                        break;
                    }
                    println!("aceito: {id}");
                    id += 1;
                }
            }
        }
        // tx é destruído ao sair; não há outros Sender neste exemplo.
        println!("produtor encerrado");
    });

    tasks.spawn(async move {
        while let Some(id) = rx.recv().await {
            // Simula uma operação assíncrona, não uma chamada bloqueante.
            time::sleep(Duration::from_millis(50)).await;
            println!("concluído: {id}");
        }
        println!("fila drenada");
    });

    println!("Pressione Ctrl+C para encerrar");
    let signal_result = tokio::signal::ctrl_c().await;
    // Mesmo se o registro/recebimento do sinal falhar, solicite a limpeza.
    stop.cancel();

    let drain = time::timeout(Duration::from_secs(5), async {
        while let Some(result) = tasks.join_next().await {
            if let Err(error) = result {
                eprintln!("task falhou: {error}");
            }
        }
    })
    .await;

    if drain.is_err() {
        eprintln!("prazo esgotado; abandonando trabalho restante");
        tasks.abort_all();
        // Recolha as tasks abortadas; abort_all sozinho não faz esse join.
        while let Some(result) = tasks.join_next().await {
            if let Err(error) = result {
                eprintln!("task interrompida: {error}");
            }
        }
    }

    signal_result?;
    Ok(())
}
```

Execute `cargo run`, espere aparecerem mensagens e pressione Ctrl+C. A ordem entre linhas de tasks diferentes pode variar: o consumidor pode imprimir antes de o produtor voltar do envio e imprimir seu log. O que importa é que, no caminho normal, todo ID aceito tenha um ID concluído, seguido de `fila drenada`.

O exemplo registra falhas das tasks no stderr. Em um serviço real, também transforme falhas relevantes em status de saúde, métrica ou código de saída; terminar o supervisor não significa que todas as operações de negócio tiveram sucesso.

## O que impede o encerramento de travar?

Há dois pontos de espera no produtor: o timer e o envio. Se apenas o timer observasse o cancelamento, `tx.send(id).await` poderia ficar bloqueado esperando espaço no buffer. Colocar o envio em outro `select!` permite que o produtor saia mesmo quando a fila está cheia.

`biased;` prioriza o ramo de cancelamento quando mais de um ramo está pronto na mesma consulta. Isso torna a intenção explícita, mas **não cria uma barreira transacional**: um envio pode terminar quase ao mesmo tempo que a solicitação de parada. Se esse envio foi aceito, o consumidor ainda deve processá-lo.

Quando o ramo de cancelamento vence, a future de `send` é destruída. Neste programa, o ID ainda não aceito pode ser abandonado sem problema. Se o valor fosse um pedido que não pode desaparecer, mantenha a propriedade do pedido até reservar espaço com `Sender::reserve`, ou persista-o antes de reconhecê-lo ao cliente. A fronteira de aceitação precisa fazer parte do contrato da aplicação.

## Por que fechar o canal não apaga a fila?

No `mpsc` do Tokio, a ausência de todos os remetentes faz o receptor terminar **depois de consumir as mensagens restantes**. Por isso o consumidor usa `while let Some(...) = rx.recv().await`: o `None` marca o fim da drenagem.

Um erro comum é guardar um clone de `tx` no estado global. Mesmo depois que o produtor sai, esse clone mantém o canal aberto e o consumidor continua esperando. Faça um inventário de quem possui cada `Sender`; não basta cancelar a task mais visível.

Outra opção é chamar `Receiver::close()` e continuar recebendo. Isso impede novos envios normais, mas permissões já reservadas também precisam ser consideradas. Neste tutorial, destruir o único remetente oferece um protocolo mais simples. Consulte a referência oficial antes de combinar fechamento, reservas e vários produtores.

A fila tem capacidade para oito IDs, mas o consumidor pode ter um ID fora dela enquanto processa. O produtor também pode estar tentando enviar outro. Portanto, capacidade oito **não** significa oito operações totais nem um orçamento de memória em bytes. Para payloads grandes, limite também o tamanho de cada mensagem.

## Timeout é uma decisão de negócio, não uma garantia

O prazo de cinco segundos inclui o encerramento do produtor e a drenagem do consumidor. Quando ele acaba, `abort_all` solicita a interrupção das tasks restantes; os `join_next` seguintes recolhem os resultados. Como as tasks do exemplo passam por pontos de suspensão assíncronos, elas conseguem responder a essa interrupção.

Isso não permite prometer “zero perda”. Abortar o consumidor durante uma operação pode abandonar um item já retirado da fila. Uma fila em memória também desaparece se o processo cair ou a máquina reiniciar.

Para pedidos, pagamentos ou eventos que precisam sobreviver a falhas, use persistência, confirmações e operações idempotentes. O encerramento gracioso reduz interrupções evitáveis; não substitui um mecanismo de entrega durável.

Evite ainda executar CPU pesada ou I/O bloqueante diretamente nessas tasks. Um loop que nunca cede ao runtime pode impedir tanto a avaliação do timeout quanto a resposta ao aborto. Uma chamada `spawn_blocking` já iniciada não pode ser interrompida com `abort`; precisa de parada cooperativa própria ou de isolamento adequado.

## Adaptando para Axum e containers

Em Axum, conecte a future de parada ao `with_graceful_shutdown` do servidor e supervisione separadamente as tasks de segundo plano. Encerrar o servidor HTTP não supervisiona automaticamente o worker que grava no banco ou publica eventos.

Defina a sequência conforme suas dependências:

1. Pare a entrada de novas requisições e tire a instância da rotação de tráfego.
2. Permita que requisições em andamento terminem ou atinjam um prazo.
3. Feche os produtores da fila quando essas requisições não precisarem mais enviar trabalho.
4. Aguarde consumidores e operações pendentes.
5. Finalize recursos compartilhados somente depois de quem os utiliza.

**Não feche a fila cedo demais** se um handler ainda precisa publicar nela. Separe os sinais “parar de aceitar requisições” e “encerrar os produtores” quando essas fases têm momentos diferentes.

O exemplo executável usa Ctrl+C para facilitar o teste local. Em Unix, containers e gerenciadores de processos normalmente enviam **SIGTERM**. Use também `tokio::signal::unix::signal(SignalKind::terminate())`, protegido por `#[cfg(unix)]`, e selecione entre os sinais. Não copie a API Unix sem essa proteção para um programa multiplataforma.

O prazo interno deve deixar margem para a limpeza antes do limite imposto pelo ambiente. Nenhum protocolo dentro do programa consegue tratar SIGKILL. Veja também [deploy Rust com Docker e systemd](/blog/deploy-rust-vps-docker-systemd-2026/).

## Checklist de testes antes de colocar em produção

- **Fila cheia:** solicite parada com o produtor aguardando espaço; ele deve sair.
- **Fila vazia:** solicite parada sem mensagens; o receptor deve chegar a `None`.
- **Remetente extra:** mantenha um clone de `Sender` vivo e confirme que o teste detecta a espera indevida.
- **Consumidor lento:** aumente a duração da operação acima do prazo e observe o ramo de aborto.
- **Falha de task:** provoque um panic controlado em teste e confirme que o supervisor observa o `JoinError`.
- **Sinal do ambiente:** teste SIGTERM no container, não apenas Ctrl+C no terminal.
- **Persistência:** derrube o processo abruptamente e confira como pedidos reconhecidos são recuperados.

Não use apenas “o processo terminou” como critério. Registre quantos itens foram aceitos, concluídos e abandonados, além da duração da drenagem. Para monitorar esse comportamento em um backend, veja [métricas com Prometheus e Axum](/blog/prometheus-metrics-rust-axum-producao-2026/).

## Perguntas frequentes

### CancellationToken cancela automaticamente uma task?

Não. `cancel()` muda o estado do token; a task precisa observar `cancelled().await` ou `is_cancelled()`. Clones do mesmo token compartilham esse estado. A aplicação decide onde é seguro interromper.

### Devo dar abort em tudo quando receber Ctrl+C?

Só se abandonar todo o trabalho for a política desejada. Para drenagem, pare primeiro os produtores e deixe os consumidores concluir. Reserve o aborto para o fim do prazo ou para tarefas cujo resultado pode ser descartado.

### Encerramento gracioso garante que não perderei mensagens?

Não. Neste exemplo, mensagens aceitas são drenadas no caminho normal. Timeout, panic, SIGKILL e queda da máquina podem interromper o processamento. Garantias mais fortes exigem armazenamento durável e um protocolo de confirmação.

### Preciso de Tokio para usar threads e canais?

Não. A biblioteca padrão tem threads e canais bloqueantes; veja [channels e mpsc](/stdlib/channels/). Este tutorial usa o `mpsc` **assíncrono do Tokio**, não `std::sync::mpsc`, para que esperas cooperem com o runtime.

## Referências e próximo passo

- [Tokio: graceful shutdown](https://tokio.rs/tokio/topics/shutdown)
- [CancellationToken: contrato de cancelamento](https://docs.rs/tokio-util/latest/tokio_util/sync/struct.CancellationToken.html)
- [Tokio mpsc: fechamento e drenagem](https://docs.rs/tokio/latest/tokio/sync/mpsc/index.html)
- [JoinSet: supervisão e abort_all](https://docs.rs/tokio/latest/tokio/task/struct.JoinSet.html)
- [spawn_blocking: limitações de cancelamento](https://docs.rs/tokio/latest/tokio/task/fn.spawn_blocking.html)

Como próximo exercício, adapte o exemplo para receber trabalho de uma [API REST com Axum](/tutoriais/api-rest-axum/) e teste a parada com requisições ainda em andamento. Para uma trilha estruturada de aprendizado, consulte o [curso de Rust](/curso/).
