Home Comunidade Eventos YouTube Networks Desafios // produtos Academy News CTI | CVE Contato

03 - Exemplo - Liberar usuário e processar em segundo plano

#dlq#fila-de-tarefas#idempotencia#retry#segundo-plano
video explicativo
Video explicativoEspaco reservado para o video desta anotação — adicione 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.