03 - Exemplo - Liberar usuário e processar em segundo plano
video: https://youtu.be/... no frontmatter da nota.Exemplo: Liberar usuário e processar em segundo plano
Este arquivo consolida o padrão assíncrono em um caso concreto de produção: o cadastro de um usuário que precisa responder rápido na tela e, ao mesmo tempo, disparar um conjunto de tarefas derivadas. O fluxo típico é gravar o usuário, liberar o acesso imediatamente e delegar para uma fila tarefas como envio de e-mail de boas-vindas, provisionamento de workspace, sincronização com o CRM e disparo de onboarding. É o exemplo mais didático do desacoplamento temporal porque separa com clareza o que o usuário espera da resposta e o que o sistema apenas precisa fazer depois. O desenho certo evita dois erros clássicos de produção: cadastro que demora segundos porque envia e-mail dentro da transação, e cadastro que falha na tela porque um serviço periférico estava fora do ar.
Cenário e requisitos
Requisitos do fluxo: o POST de cadastro responde em menos de 200 ms com o usuário liberado para login; o e-mail de boas-vindas chega em segundos ou minutos, sem prazo crítico; o provisionamento de workspace é idempotente, porque pode ser reexecutado; nenhuma falha nas tarefas derivadas pode impedir o cadastro; e todas as tarefas eventualmente acontecem, mesmo que com atraso.
A decisão arquitetural decorre direto desses requisitos. A gravação do usuário e a liberação do acesso são o caminho crítico, síncrono e transacional. Todo o restante é trabalho derivado, sem dependência de resposta imediata, e vai para uma fila de tarefas. O produtor publica um evento ou comando após confirmar a transação, e consumidores especializados executam cada tarefa.
Uma decisão fina fica no payload. Publicar um comando específico, como EnviarBoasVindas, dá controle direto ao produtor. Publicar o evento UsuarioCriado e deixar cada consumidor decidir o que fazer mantém o produtor alheio às consequências. Em domínios onde novas reações ao cadastro aparecem com frequência, o evento costuma envelhecer melhor; em fluxos de tarefa explícita, o comando é mais legível.
A granularidade das mensagens também é decisão. Um único evento UsuarioCriado consumido por um worker que executa as três tarefas em sequência é simples de operar, mas acopla as falhas: falha no CRM atrasa o provisionamento do próximo usuário. Um evento consumido por três workers independentes isola as falhas e escala cada tarefa pelo próprio ritmo, ao custo de mais topologias para monitorar. O meio termo comum em produção é um worker por família de tarefas, agrupadas por perfil de falha e de latência.
Desenho do fluxo ponta a ponta
O fluxo completo tem cinco estágios. Primeiro, a API valida os dados e grava o usuário no banco dentro de uma transação. Segundo, ainda na mesma operação, o produtor publica a mensagem na fila; publicar antes da confirmação da transação arrisca mensagem fantasma de usuário que não existe, e publicar depois arrisca usuário sem mensagem se o processo cair entre as duas operações; o padrão de caixa de saída resolve o intervalo, mas em muitos times a publicação imediata após o commit é aceitável com verificação de reconciliação.
Terceiro, a API responde 201 com os dados do usuário e um status indicando processamento de tarefas em andamento. Quarto, o consumidor recebe a mensagem, executa as tarefas derivadas e confirma o processamento. Quinto, falhas seguem a política de retry e, esgotadas as tentativas, caem na fila de mortos com contexto completo para investigação.
# Produtor: API de cadastro (FastAPI, ilustrativo)
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
import json, uuid
app = FastAPI()
class CadastroEntrada(BaseModel):
email: str
nome: str
@app.post("/usuarios", status_code=201)
def cadastrar(entrada: CadastroEntrada):
if repositorio.existe_email(entrada.email):
raise HTTPException(status_code=409, detail="e-mail já cadastrado")
usuario = repositorio.gravar(nome=entrada.nome, email=entrada.email)
evento = {
"evento": "UsuarioCriado",
"evento_id": str(uuid.uuid4()), # id de idempotência do consumidor
"usuario_id": usuario.id,
"email": usuario.email,
"nome": usuario.nome,
}
# Publicação após a gravação; em requisitos mais estritos usar outbox
fila.publicar(topico="usuarios.criado", chave=usuario.id, corpo=evento)
return {"id": usuario.id, "status": "ativo", "tarefas": "em processamento"}
Consumidor com idempotência
O consumidor precisa assumir que a mesma mensagem pode ser entregue mais de uma vez. As causas são reais e frequentes: queda do consumidor após processar e antes de confirmar, timeout de visibilidade estourado em tarefa lenta, e retry da própria infraestrutura. A defesa é a chave de idempotência, no exemplo o evento_id, gravada em uma tabela de processados na mesma transação do efeito da tarefa.
A ordem das operações no consumidor é deliberada: verificar se o evento_id já foi processado, executar o trabalho, gravar o registro de processamento e só então confirmar a mensagem. Se o consumidor morrer depois de executar e antes de confirmar, a nova entrega encontra o registro e não reexecuta; se morrer antes de gravar o registro, a nova entrega reexecuta, o que é o comportamento correto para trabalho não confirmado.
# Consumidor: tarefas derivadas do cadastro
def consumir(mensagem):
evento = json.loads(mensagem.corpo)
evento_id = evento["evento_id"]
with banco.transacao():
if tabela_processados.existe(evento_id):
mensagem.confirmar() # duplicata: nada a refazer
return
executar_tarefas(evento) # e-mail, provisionamento, CRM
tabela_processados.gravar(evento_id, processado_em=agora())
mensagem.confirmar() # ack só após commit
def executar_tarefas(evento):
email.enviar_boas_vindas(evento["email"], evento["nome"])
workspace.provisionar(evento["usuario_id"])
crm.sincronizar_contato(evento)
Tratamento de falha, retry e DLQ
Nem toda falha é igual, e a política precisa separar as classes. Falha transitória, como timeout do provedor de e-mail ou lock momentâneo no banco, merece retry com backoff crescente, porque a próxima tentativa provavelmente funciona. Falha de contrato, como payload inválido ou e-mail malformado, nunca vai funcionar por mais tentativas, e o retry apenas atrasa a fila; essas mensagens precisam ir direto para a fila de mortos.
A fila de mortos é o destino das mensagens que esgotaram as tentativas, e ela só é útil se for monitorada e investigável. Cada mensagem lá deve carregar o erro original, o número de tentativas e o correlation id da requisição de origem. O alarme sobre DLQ precisa ser imediato: fila de mortos crescendo significa promessa quebrada para usuários reais, não métrica de fundo.
| Falha no fluxo | Efeito sem tratamento | Mitigação no desenho |
|---|---|---|
| Consumidor cai após processar e antes do ack | Tarefa executada de novo, e-mail duplicado | Idempotência por evento_id antes de executar |
| Mensagem venenosa no topo da fila | Consumo travado, backlog cresce sem limite | Limite de tentativas e DLQ imediata |
| Provedor de e-mail fora por uma hora | Retries martelam o provedor, fila atrasa | Backoff escalonado e drenagem após recuperação |
| Pico de mil cadastros por minuto | API lenta ou derrubada no envio síncrono | Fila absorve, consumidores escalam, tela nunca falha |
| Processo cai entre commit e publicação | Usuário criado sem tarefas disparadas | Outbox na mesma transação ou reconciliação periódica |
| Bug de versão quebra provisionamento | Dados parcialmente provisionados | Idempotência permite reprocesso seguro após correção |
# Política de consumo para as tarefas de cadastro
consumidor:
concorrencia: 4 # tarefas paralelas por instância
visibilidade: 30s # tempo máximo por tarefa antes de reentrega
retry:
reprocessar_em: [5s, 30s, 5m, 30m] # backoff para falhas transitórias
esgotamento: dead_letter # após 4 tentativas, mensagem vai para DLQ
dlq:
topico: "usuarios.criado.dlq"
retencao: 14d # tempo para investigar e reprocessar
alarmes:
idade_mensagem: "> 5m" # e-mail de boas-vindas demorando demais
dlq_nao_vazia: "5m" # investigar imediatamente
Cenários reais de produção
Pico de cadastro em campanha: uma campanha de marketing gera mil cadastros por minuto. A fila absorve o volume, o número de consumidores é escalado, e os e-mails saem atrasados por alguns minutos; nenhuma tela de cadastro falhou, porque o caminho crítico nunca tocou o provedor de e-mail. Em desenho síncrono, esse pico derrubaria o cadastro inteiro por esgotamento de workers no envio de e-mail.
Provedor de e-mail fora do ar por uma hora: as mensagens seguem para a DLQ ou permanecem na fila conforme a política, e ao retorno do provedor o consumidor drena o atraso. Se o negócio exige que o e-mail não seja enviado horas depois, a política muda: descartar e-mail de boas-vindas expirado e avisar por outro canal é uma decisão de produto, e o desenho assíncrono permite tomar essa decisão por configuração.
Bug no provisionamento: uma versão nova quebra a criação de workspace. As mensagens acumulam, o alarme de idade dispara, a correção entra, e o backlog é processado normalmente. Como o provisionamento é idempotente, parte dos usuários provisionada manualmente pela equipe de plantão não gera duplicação quando o consumidor reprocessa.
Erros comuns e boas práticas
- Enviar e-mail dentro da transação de cadastro: acopla o caminho crítico a um serviço lento e instável; a transação fica aberta por tempo de rede de terceiro e o rollback por e-mail falho perde cadastros reais.
- Publicar a mensagem antes de gravar o usuário: consumidor pode processar usuário inexistente; gravar primeiro, publicar depois, ou adotar outbox quando o intervalo for inaceitável.
- Confirmar a mensagem antes de gravar o registro de idempotência: queda no meio do caminho gera reexecução do provisionamento; a ordem é executar, registrar e confirmar.
- Usar retry uniforme para toda falha: falha de contrato precisa ir direto para DLQ; martelar 429 e 400 com backoff só atrasa o fluxo inteiro.
- Deixar a DLQ sem alarme e sem playbook: mensagem na DLQ é usuário esperando algo; investigação e reprocesso precisam ser procedimento conhecido, não descoberta em incidente.
- Não propagar correlation id: o cadastro passou pela API, a fila e três consumidores; sem correlação, responder "por que este usuário não recebeu o e-mail" vira auditoria manual de logs.
- Esquecer o consumidor no plano de capacidade: a fila aceita qualquer pico, mas o consumidor e o banco dele não; teste de carga deve incluir a drenagem do backlog, não só a resposta da API.
- Tratar o status "em processamento" como detalhe: a tela precisa comunicar que tarefas derivadas estão em andamento, senão suporte recebe chamado de "cadastro incompleto" a cada atraso normal.