Documentação / Sistema Resiliente de Envio de Mensagens — Pilot Status

Sistema Resiliente de Envio de Mensagens — Pilot Status

Entrar

Sistema Resiliente de Envio de Mensagens — Pilot Status

Meta: enviar até 50.000 mensagens/mês com máxima confiabilidade, zero perda e mínima duplicidade.

Infraestrutura atual: Monolito (Next.js — Frontend + Backend + Worker) · PostgreSQL · Redis · Evolution API (WhatsApp não-oficial) · 3 números (Ash, Brock, Misty) · Docker/Docker Compose · Imagens no GHCR · Servidor único · Experiência AWS disponível.

Funcionalidade planejada: reescrita de mensagens de Marketing com IA (DeepSeek) para variação de conteúdo.


Sumário

  1. Visão Geral da Arquitetura
  2. Fluxo Completo de Envio (Passo a Passo)
  3. Agendamento e Expiração (deliverAt / deliverUntil)
  4. Diagrama de Estados da Mensagem
  5. Regras de Negócio Consolidadas
  6. Mecanismos Atuais de Resiliência
  7. Pontos de Falha e Gargalos (Inventário Completo)
  8. Capacidade: Cálculo para 50k/mês com 3 Números
  9. Plano de Resiliência Máxima (Melhorias)
  10. Integração com IA (DeepSeek) para Marketing
  11. Arquitetura de Infraestrutura Recomendada
  12. Observabilidade e Operação
  13. Roadmap de Implementação

1. Visão Geral da Arquitetura

┌─────────────────────────────────────────────────────────────────────────┐
│  CLIENTE (API Externa / Dashboard)                                      │
│  POST /api/v1/messages/send → Auth → Rate Limit → Validação            │
└──────────────────────────────┬──────────────────────────────────────────┘
                               │
                               ▼
┌─────────────────────────────────────────────────────────────────────────┐
│  MONOLITO (Next.js — Frontend + Backend + Worker)                       │
│                                                                         │
│  ┌─────────────────┐    ┌──────────────┐    ┌────────────────────────┐  │
│  │ MessageService   │───▶│ PostgreSQL   │    │ Redis (BullMQ)         │  │
│  │ .send()          │    │ status=QUEUED│    │ Job enfileirado        │  │
│  └─────────────────┘    └──────────────┘    └───────────┬────────────┘  │
│                                                         │               │
│  ┌──────────────────────────────────────────────────────┘               │
│  │                                                                      │
│  ▼                                                                      │
│  ┌─────────────────────────────────────────────────────────────────┐    │
│  │ WORKER (processMessage)                                         │    │
│  │ Lock Redis → Renderiza template → [DeepSeek rewrite?]          │    │
│  │ → Simula digitação → Chama Evolution API                       │    │
│  │ → Atualiza DB → Dispara Webhooks do cliente                    │    │
│  └──────────────────────────────┬──────────────────────────────────┘    │
│                                 │                                       │
└─────────────────────────────────┼───────────────────────────────────────┘
                                  │
                                  ▼
┌─────────────────────────────────────────────────────────────────────────┐
│  EVOLUTION API (Gateway WhatsApp)                                       │
│  3 Instâncias: Ash · Brock · Misty                                      │
│  Cada instância = fila BullMQ separada (messages--{instance})           │
│  Webhook retorna: SERVER_ACK → SENT · DELIVERY_ACK → DELIVERED         │
└─────────────────────────────────────────────────────────────────────────┘

Stack tecnológica:

| Componente | Tecnologia | |---|---| | Aplicação | Next.js (Monolito: Front + Back + Worker) | | Banco de dados | PostgreSQL via Prisma | | Fila de mensagens | BullMQ + Redis | | Provider WhatsApp | Evolution API (não-oficial) | | Lock distribuído | Redis (SET NX) | | Retry | BullMQ (backoff fixo 30s, 10 tentativas) | | IA para Marketing | DeepSeek (planejado) | | Container | Docker + Docker Compose | | Registry | GHCR (GitHub Container Registry) | | Infraestrutura | Servidor único (Postgres + Redis + App) |


2. Fluxo Completo de Envio

Fase 1 — Recepção e Validação (Backend)

  1. Autenticação: header x-api-keyApiKeyService.validate() → resolve tenant/projeto.
  2. Ambiente:
    • TEST: permite envio apenas para números do perfil do dev. Limite diário via TEST_MODE_DAILY_LIMIT.
    • LIVE: exige project.productionApproved = true. Template deve estar aprovado para produção.
  3. Template: resolve versão aprovada mais recente (por templateId ou nome friendly). Valida compatibilidade projeto/ambiente.
  4. Instância WhatsApp:
    • Se API key vinculada a whatsappInstanceId → instância deve existir e estar OPEN (senão 409).
    • Senão → usa EVOLUTION_INSTANCE_NAME (instância default).
    • Templates MARKETING não podem usar o número default → exigem instância própria.
  5. Rate Limit (RateLimitService.check()):
    • LIVE: limite mensal (plano + pacotes). Plano FREE: limite diário adicional.
    • Auto-Recharge automático se habilitado e saldo abaixo do percentual configurado.
  6. Opt-In Transacional (WhatsAppTransactionalOptInService.assertDestinationAuthorized()):
    • Obrigatório quando: validação habilitada + instância default + ambiente LIVE + projectId existe.
    • Exceção: templates OTP no número Pilot Status podem fazer bypass.
    • Pode ser ignorado com skipOptInValidation.
  7. Cálculo de deliverUntil (expiração automática por categoria):
    • OTP → 2 minutos
    • UTILITY → 1 hora
    • MARKETING → 4 horas
  8. Criação do registro no banco: status = QUEUED, correlationId, deliverBy, deliverUntil.
  9. Enfileiramento no BullMQ: jobId = messageId, attempts = 10, backoff = fixed/30s, priority (urgente = 1, normal = 5).

Resposta ao cliente: 202 Accepted com { id, correlationId, status: "QUEUED", origin }.

Fase 2 — Processamento pelo Worker

  1. Lock Redis distribuído: SET NX EX 60 com chave lock:ps:message:{messageId}. Se não adquirir → ignora (sem erro).
  2. Busca da mensagem: verifica existência e status = QUEUED. Se não QUEUED → ignora (idempotência).
  3. Verificação de expiração: se deliverUntil ultrapassado → FAILED + webhook do cliente.
  4. Renderização do template: substitui {{variavel}} no corpo. Suporta texto simples e JSON com botões.
  5. [FUTURO] Reescrita IA: se mensagem de Marketing e flag habilitado → chama DeepSeek para reescrever.
  6. Validação da instância: determina evolutionInstance — vem do job, da mensagem, ou da env EVOLUTION_INSTANCE_NAME.
  7. Deduplicação (instância "Pilot Status"): tryAcquirePilotStatusSendSlot() evita que o mesmo messageId seja enviado mais de uma vez (proteção contra retentativas do BullMQ). Ao contrário da versão anterior, agora é permitido repetir o conteúdo da mensagem se forem IDs de mensagem diferentes.
  8. Simulação de digitação: trySimulateWhatsAppTyping()/chat/sendPresence (best-effort).
  9. Chamada à Evolution API:
    • Com botões → POST /message/sendButtons/{instance}
    • Sem botões → POST /message/sendText/{instance} (com linkPreview se URL detectada)
  10. Verificação de estado: se resposta indica state=close/connecting → lança erro de desconexão.
  11. Classificação de erros (heurística por substring):
    • NotFoundFAILED terminal, marca instância CLOSE.
    • Disconnected → mantém QUEUED, não queima tentativa. Worker verifica se restam tentativas.
    • Erro genérico na última tentativa → FAILED, dispara webhook message.failed.
  12. Sucesso (HTTP 2xx): salva evolutionKeyId/evolutionInstanceId no banco. NÃO altera status para SENT — aguarda webhook SERVER_ACK.

Fase 3 — Confirmação via Webhook da Evolution

Handler em /api/internal/webhook:

  • SERVER_ACKSENT
  • DELIVERY_ACKDELIVERED
  • READ/PLAYEDREAD
  • ERROR/FAILEDFAILED
  • Resolução da mensagem: por messageId, depois evolutionKeyId + instanceId, depois fallback evolutionKeyId.
  • Aplica progressão de status (não regredir READ → SENT).
  • Emite webhooks do cliente para FAILED, DELIVERED, READ.

Fase 4 — Sistemas de Garantia e Monitoramento

Reconciliador de Fila (message-reconciler.ts)

  • Frequência: a cada 1 hora (configurável via MESSAGE_RECONCILER_INTERVAL_MS).
  • Lock: Redis ps:message-reconciler:lock, TTL 55s — evita execução concorrente.
  • Ações sobre mensagens QUEUED:
    • Expira mensagens com deliverUntil ultrapassado → FAILED.
    • Falha mensagens com mais de 7 dias em QUEUED → FAILED ("timeout de entrega").
    • Valida dados mínimos: whatsappInstanceName, templateVersionId, destinationNumber, payload.
    • Reenfileira mensagens órfãs (job sumiu da fila — ex: Redis reiniciou).
    • Promove jobs "delayed" que já deveriam ter sido processados.
    • Reprocessa jobs completed ou failed que ainda estejam QUEUED no banco.

Healthcheck de Instâncias (all-instances-healthcheck.ts)

  • Frequência: a cada 10 minutos via cron no BullMQ.
  • Fase 1 – Verificação: para cada instância OPEN, checa connectionState + presence.
    • Se not_found (404): marca CLOSE no banco.
    • Se desconectada: adiciona à lista "disconnected".
  • Fase 2 – Restart: chama restartWhatsappInstance() para todas em paralelo.
  • Aguarda: HEALTHCHECK_SLEEP_MS (padrão 2 min).
  • Fase 3 – Recheck: se ainda offline → alerta Pushover (dev) + WhatsApp (admin do tenant).
  • Deduplicação de alertas: Redis ps:instance:disconnect-alert-sent:{nome}.

Validação Individual (whatsapp-instance-validate-sweep)

  • Frequência: a cada 5 minutos.
  • Para cada instância OPEN, verifica connectionState + presence. Se inválida → alerta admin.

Sincronização de Workers

  • Frequência: a cada 60s (WHATSAPP_QUEUE_WORKER_REFRESH_MS).
  • Sincroniza lista de filas ativas com instâncias OPEN no banco.

3. Agendamento e Expiração

deliverAt (Agendamento)

  • Data/hora alvo (ISO 8601) a partir da qual a mensagem pode ser enviada.
  • Salvo no banco como Message.deliverBy.
  • Job enfileirado com delay = max(0, deliverBy - now).
  • Mesmo após estar "pronta", o delay aleatório padrão (30–45s) ainda se aplica.

deliverUntil (Expiração)

  • Data/hora limite para entrega.
  • Se omitido, aplica regras padrão por categoria: OTP (2min), UTILITY (1h), MARKETING (4h).
  • Worker e reconciliador verificam antes de processar/reenfileirar.

Reconciliação no Startup (SLA de 10 minutos)

  • Se now > deliverBy + 10 min: marca FAILED.
  • Se deliverBy <= now <= deliverBy + 10 min: reenfileira/promove.
  • Configurável via MESSAGE_RECONCILER_INTERVAL_MS.

4. Diagrama de Estados da Mensagem

                     ┌─────────────┐
                     │   QUEUED    │ ◄─── Reenfileirado pelo reconciliador
                     └──────┬──────┘
                            │
              ┌─────────────┼──────────────┐
              │             │              │
        Expirou       Enviou com       Erro de envio
       deliverUntil    sucesso        (não-fatal)
              │             │              │
              ▼             ▼              │ (retry BullMQ, até 10x)
           FAILED    (aguarda webhook)     │
                           │              │
                    Evolution envia        │
                    SERVER_ACK webhook     │
                           │              │
                           ▼              │
                         SENT  ◄──────────┘
                           │
                    DELIVERY_ACK
                           │
                           ▼
                       DELIVERED
                           │
                    Usuário lê (READ/PLAYED)
                           │
                           ▼
                          READ

Garantia prática: sistema é at-least-once (pelo menos uma tentativa). Duplicidade é possível se não houver idempotência ponta a ponta.


5. Regras de Negócio Consolidadas

| Regra | Detalhe | |---|---| | Um job por mensagem | jobId = messageId — sem duplicatas no BullMQ | | Idempotência no worker | Verifica status = QUEUED antes de processar | | Lock por mensagem | Redis SET NX 60s impede processamento concorrente | | Concorrência = 1 | Cada fila de instância tem concurrency = 1 | | Delay aleatório | Após processar: sleep(30–46s) para simular comportamento humano | | Prioridade | Jobs urgentes: priority = 1, normais: priority = 5 | | 10 tentativas | attempts = 10, backoff = fixed/30s (~5 min de retry total) | | Instâncias separadas | Cada instância WhatsApp tem fila e worker próprios | | MARKETING exige número próprio | Não pode usar instância default/Pilot Status | | Roteamento por instância | Fila messages--{instance} — todos os tenants da mesma instância compartilham fila | | Confirmação por webhook | Status SENT só é definido pelo webhook SERVER_ACK da Evolution |


6. Mecanismos Atuais de Resiliência

| Mecanismo | Como funciona | |---|---| | Persistência antes do envio | Registro no banco com QUEUED antes de enfileirar | | Desacoplamento com filas | BullMQ: API não aguarda envio, responde imediatamente | | Retry automático | 10 tentativas, backoff fixo 30s via BullMQ | | Smart Wait | Se instância desconectada, não "queima" tentativa — volta para QUEUED | | Reconciliador | Varredura periódica de mensagens presas em QUEUED | | Lock distribuído | Redis SET NX evita processamento concorrente | | Deduplicação Pilot Status | Hash do texto para evitar mesma mensagem para mesmo número | | Healthcheck de instâncias | Verificação + restart automático a cada 10 min | | Validação individual | Sweep a cada 5 min para instâncias OPEN | | Sincronização de workers | A cada 60s, novos workers para novas instâncias | | Alertas | Pushover (dev) + WhatsApp (admin) para instâncias offline |


7. Pontos de Falha e Gargalos (Inventário Completo)

🔴 Críticos

| ID | Ponto de Falha | Impacto | Cenário | |---|---|---|---| | PF-01 | Redis como SPOF | Sistema para completamente | Se Redis cair: nenhum job enfileirado/processado, API retorna 500 | | PF-02 | Worker crash → duplicata | Mensagem enviada 2x | Job executado na Evolution, mas banco ainda QUEUED → lock expira → reconciliador reenfileira | | PF-03 | Status SENT depende de webhook assíncrono | Mensagem enviada 2x | Evolution envia mas webhook SERVER_ACK não chega → mensagem fica QUEUED → reconciliador reenvia | | PF-04 | Sem timeout nas chamadas HTTP | Fila trava, lock expira, duplicidade | fetch() sem AbortController → job preso indefinidamente → lock Redis 60s expira |

🟠 Importantes

| ID | Ponto de Falha | Impacto | Cenário | |---|---|---|---| | PF-05 | Webhook do cliente sem retry | Notificações de status perdidas | dispatchCustomerWebhook() falha → apenas loga e segue | | PF-06 | OTP expira antes do reconciliador | Mensagem OTP nunca chega | TTL 2min, reconciliador roda 1x/hora | | PF-07 | Instância offline acumula QUEUED por 7 dias | Volume cresce, mensagens expiram | Healthcheck tenta reconectar, mas se falhar, mensagens ficam paradas | | PF-08 | Sem isolamento de projetos na fila | Starvation entre projetos | Burst de um projeto atrasa mensagens de outro na mesma instância | | PF-09 | bodyOverride descartado pelo reconciliador | Mensagens com body customizado incorretamente rejeitadas | coercePayload retorna null para payloads com estrutura diferente | | PF-10 | Lock do reconciliador com TTL curto (55s) | Dupla reconciliação | Reconciliação lenta → lock expira → segundo worker inicia concorrente | | PF-11 | Contabilização de rate limit depende de sentAt (ACK) | Envio acima do limite | Se webhook atrasar, mensagens não entram na contagem | | PF-12 | Retenção 0 apaga dados para reconciliação | Mensagem não pode ser reenviada | PII redigida, se job perdido, reconciliador não consegue reconstruir | | PF-13 | Heurística frágil para "disconnected" | Retry infinito ou falha prematura | Classificação por includes() em texto do erro — falso positivo/negativo | | PF-14 | Retry fixo 30s (Thundering Herd) | DDoS acidental na Evolution | Se Evolution cai e volta, todas as mensagens retentam ao mesmo tempo |

🟡 Menores

| ID | Ponto de Falha | Impacto | |---|---|---| | PF-15 | DLQ silenciosa (sem alarme) | Falhas passam despercebidas | | PF-16 | Delay aleatório em jobs urgentes do reconciliador | Latência extra desnecessária | | PF-17 | Concorrência=1 gargalo em alto volume | Fila cresce indefinidamente | | PF-18 | Perda de webhook durante deploy/restart | Status "fantasma" (QUEUED/SENT para sempre) |


8. Capacidade: Cálculo para 50k/mês

Números

  • 50.000 mensagens / mês = ~1.667/dia = ~70/hora = ~1,16/minuto (média).
  • 3 instâncias (Ash, Brock, Misty) = ~556/dia/instância = ~23/hora/instância.

Throughput Atual (concurrency=1 + delay 30-46s)

  • Pior caso: 1 mensagem a cada 46s por instância = ~78 mensagens/hora/instância = ~234/hora total.
  • Melhor caso: 1 mensagem a cada 30s por instância = ~120/hora/instância = ~360/hora total.
  • Por dia (24h): 5.616 a 8.640 mensagens total.
  • Por mês (30 dias): 168.480 a 259.200 mensagens total.

Conclusão de Capacidade

Com 3 instâncias e o throughput atual, o sistema suporta 50k/mês COM FOLGA, mesmo considerando que os envios se concentram em horários comerciais (8h–20h = 12h/dia).

Em 12h/dia: 2.808 a 4.320 mensagens/dia × 30 = 84.240 a 129.600/mês → ainda suficiente.

⚠️ Risco: se os envios forem concentrados em picos (ex: 10.000 mensagens de marketing de uma vez), pode levar de 2,7h a 4,2h para processar com 3 instâncias. Mensagens OTP nesse período podem sofrer atraso crítico.

Recomendação para Picos

  • Separar filas por categoria (OTP/UTILITY em fila prioritária, MARKETING em fila separada).
  • Reduzir delay para instâncias próprias: 15-20s em vez de 30-46s.
  • Distribuir marketing entre as 3 instâncias de forma balanceada.

9. Plano de Resiliência Máxima (Melhorias)

🚀 Prioridade 1 — Anti-Duplicidade e Confiabilidade (Semanas 1-2)

MR-01: Estado intermediário DISPATCHED (anti-duplicidade)

Resolve: PF-02, PF-03, PF-18

Após receber HTTP 2xx da Evolution, atualizar imediatamente:

await prisma.message.update({
  where: { id: msg.id },
  data: {
    status: 'DISPATCHED', // novo estado intermediário
    dispatchedAt: new Date(),
    evolutionKeyId: response.key?.id,
    evolutionInstanceId: response.instanceId,
  },
});
  • Webhook SERVER_ACK promove DISPATCHED → SENT.
  • Reconciliador respeita grace period (ex: 60 min) antes de reenfileirar DISPATCHED.
  • Se grace period expirar sem ACK, consulta Evolution API pelo evolutionKeyId antes de reenfileirar.

MR-02: Timeout em todas as chamadas HTTP

Resolve: PF-04

const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), TIMEOUT_MS);

try {
  const response = await fetch(url, { signal: controller.signal, ... });
} finally {
  clearTimeout(timeout);
}

Timeouts recomendados:

  • Typing/presence: 3s
  • Envio Evolution: 15s
  • Webhook do cliente: 8s
  • DeepSeek API (reescrita): 10s

MR-03: Verificar status na Evolution antes de reenfileirar

Resolve: PF-02, PF-03

// No reconciliador, antes de reenfileirar:
if (msg.evolutionKeyId) {
  const status = await checkEvolutionMessageStatus(msg.evolutionKeyId);
  if (status === 'delivered' || status === 'sent') {
    await prisma.message.update({
      where: { id: msg.id },
      data: { status: 'SENT', sentAt: new Date() },
    });
    continue; // NÃO reenfileira
  }
}

MR-04: Backoff exponencial com jitter

Resolve: PF-14

// De:
{ backoff: { type: 'fixed', delay: 30_000 } }
// Para:
{ backoff: { type: 'exponential', delay: 5_000 } }
// Resultado: 5s → 10s → 20s → 40s → 80s → 160s → 320s → 640s → 1280s → 2560s

Adicionar jitter (aleatoriedade ±20%) para evitar thundering herd.

MR-05: Retry inteligente por status code

Resolve: PF-13

function classifyError(error: any): 'retry' | 'fatal' | 'disconnect' {
  const status = error.response?.status;
  if (status === 404) return 'fatal';         // instância não existe
  if (status === 400 || status === 422) return 'fatal'; // payload inválido
  if (status === 401 || status === 403) return 'fatal'; // auth
  if (status === 429) return 'retry';         // rate limit
  if (status >= 500) return 'retry';          // erro servidor
  if (error.code === 'ECONNREFUSED' || error.code === 'ECONNRESET') return 'disconnect';
  return 'retry';
}

⚡ Prioridade 2 — Infraestrutura e Filas (Semanas 3-4)

MR-06: Transactional Outbox Pattern (Redis SPOF)

Resolve: PF-01

Em vez de escrever no Redis diretamente ao receber a requisição:

  1. API salva mensagem no Postgres (transação) — já faz isso.
  2. Relay process (novo): lê mensagens novas do Postgres e empurra para Redis.
  3. Se Redis cair, API continua aceitando. Quando Redis voltar, relay preenche a fila.
// Relay (executar via cron ou loop com polling)
async function relayOutbox() {
  const pending = await prisma.message.findMany({
    where: { status: 'QUEUED', enqueuedAt: null },
    take: 100,
    orderBy: { createdAt: 'asc' },
  });
  for (const msg of pending) {
    await queue.add('send-message', buildJobData(msg), { jobId: msg.id, ... });
    await prisma.message.update({ where: { id: msg.id }, data: { enqueuedAt: new Date() } });
  }
}

MR-07: Separação de filas por categoria (Lane Isolation)

Resolve: PF-08, PF-06

Fila: messages--ash--otp       (concurrency=1, delay=5s,  priority=máxima)
Fila: messages--ash--utility   (concurrency=1, delay=15s, priority=alta)
Fila: messages--ash--marketing (concurrency=1, delay=30s, priority=normal)
  • Workers independentes com pesos diferentes.
  • OTP nunca é atrasado por burst de marketing.
  • Reconciliador de alta frequência (30-60s) para filas OTP.

MR-08: Fila de webhook com retry

Resolve: PF-05

// Fila separada para webhooks do cliente
await webhookQueue.add('deliver', { webhookId, event, payload }, {
  attempts: 5,
  backoff: { type: 'exponential', delay: 5_000 },
});

MR-09: Redis com persistência AOF + Sentinel

Resolve: PF-01

# docker-compose.yml - Redis com AOF
redis:
  image: redis:7-alpine
  command: >
    redis-server
    --appendonly yes
    --appendfsync everysec
    --maxmemory 512mb
    --maxmemory-policy noeviction
  volumes:
    - redis-data:/data
  restart: always
  healthcheck:
    test: ["CMD", "redis-cli", "ping"]
    interval: 10s
    timeout: 5s
    retries: 3

Para alta disponibilidade futura: Redis Sentinel ou migrar para AWS ElastiCache.

🔧 Prioridade 3 — Robustez Operacional (Mês 2)

MR-10: Renovação de lock do reconciliador (Watchdog)

Resolve: PF-10

export async function withMessageReconcileLock(params) {
  // acquire lock...
  const extender = setInterval(() => connection.pexpire(key, ttlMs), ttlMs / 2);
  try {
    return await params.fn();
  } finally {
    clearInterval(extender);
    // release lock...
  }
}

MR-11: Corrigir reconciliador para bodyOverride

Resolve: PF-09

Verificar se payload contém bodyOverride: true e usar fluxo sendRaw ao reenfileirar.

MR-12: Alertas automáticos para DLQ

Resolve: PF-15

worker.on('failed', (job, err) => {
  if (job && job.attemptsMade >= job.opts.attempts) {
    pushoverService.sendDeveloperAlert({
      title: '🚨 Mensagem em DLQ',
      message: `Job ${job.id} falhou ${job.attemptsMade}x: ${err.message}`,
    });
  }
});

MR-13: Circuit Breaker para Evolution API

Resolve: PF-14

class EvolutionCircuitBreaker {
  private failures = 0;
  private openUntil: Date | null = null;
  private readonly threshold = 5;       // 5 falhas consecutivas
  private readonly cooldownMs = 120_000; // 2 min

  async call<T>(fn: () => Promise<T>): Promise<T> {
    if (this.openUntil && new Date() < this.openUntil) {
      throw new Error('Circuit breaker OPEN — Evolution API indisponível');
    }
    try {
      const result = await fn();
      this.failures = 0;
      this.openUntil = null;
      return result;
    } catch (err) {
      this.failures++;
      if (this.failures >= this.threshold) {
        this.openUntil = new Date(Date.now() + this.cooldownMs);
      }
      throw err;
    }
  }
}

MR-14: Fallback de Instâncias (Roteamento Inteligente)

Se o cliente possui múltiplas instâncias (ex: Ash como primária, Brock como fallback) e a primária desconectar, o sistema re-roteia para uma instância ativa em vez de esperar deliverUntil expirar.

MR-15: Sincronização Ativa de Status (Anti-Perda de Webhooks)

O reconciliador consulta mensagens DISPATCHED/SENT há mais de X minutos sem DELIVERY_ACK. Faz polling ativo na Evolution API para checar status real e atualizar banco.

MR-16: Delay configurável por prioridade

Jobs do reconciliador ou com urgent = truedelayMs = 0.

MR-17: Concorrência aumentada com rate-limit

Para instâncias próprias do cliente: concurrency = 3-5 com rate-limiter (token bucket) baseado nos limites da Evolution API.


10. Integração com IA (DeepSeek) para Marketing

Objetivo

Cada mensagem de Marketing é reescrita pela IA para ser única, reduzindo risco de detecção como spam pelo WhatsApp e aumentando engajamento.

Arquitetura Proposta

Worker recebe job de Marketing
    │
    ▼
Template renderizado com variáveis
    │
    ▼
┌──────────────────────────────────┐
│ DeepSeek API (reescrita)         │
│ POST /chat/completions           │
│ Prompt: "Reescreva mantendo      │
│  sentido e tom. Máx 160 chars."  │
│ Timeout: 10s                     │
│ Fallback: texto original         │
└───────────────┬──────────────────┘
    │
    ▼ (texto reescrito ou original se IA falhar)
    │
    ▼
Envio via Evolution API

Regras Importantes

  1. Fallback obrigatório: se DeepSeek falhar (timeout, erro, rate limit), enviar texto original. Nunca bloquear envio por falha de IA.
  2. Cache de variações: armazenar últimas N variações geradas para evitar repetição.
  3. Timeout agressivo: 10s máximo para chamada DeepSeek.
  4. Rate limit de IA: limitar chamadas ao DeepSeek (ex: 100/min) para controlar custos.
  5. Validação do resultado: verificar se o texto reescrito mantém variáveis obrigatórias e não excede limite de caracteres.
  6. Salvar no banco: gravar texto original e texto reescrito para auditoria.
  7. Flag por template: permitir ativar/desativar reescrita por template, não globalmente.

Modelo de Dados

model Message {
  // ... campos existentes ...
  originalBody     String?   // corpo antes da reescrita
  aiRewrittenBody  String?   // corpo após reescrita
  aiRewriteModel   String?   // "deepseek-chat"
  aiRewriteLatency Int?      // ms
}

Consideração de Volume

  • 50k mensagens/mês, supondo 60% marketing = 30k reescritas/mês.
  • DeepSeek é muito mais barato que OpenAI — custos estimados baixos.
  • Fazer batching se possível (enviar 5-10 textos por chamada).

11. Arquitetura de Infraestrutura

Estado Atual (Servidor Único)

┌─────────────────────────────────────────────┐
│  Servidor Único                              │
│  ┌───────────┐ ┌──────┐ ┌────────────────┐  │
│  │ PostgreSQL│ │Redis │ │ Monolito       │  │
│  │           │ │ +AOF │ │ (Next.js)      │  │
│  │           │ │      │ │ Front+Back+    │  │
│  │           │ │      │ │    Worker       │  │
│  └───────────┘ └──────┘ └────────────────┘  │
│                                              │
│  ┌────────────────────────────────────────┐  │
│  │ Evolution API (Docker)                 │  │
│  │ 3 instâncias: Ash, Brock, Misty       │  │
│  └────────────────────────────────────────┘  │
└─────────────────────────────────────────────┘

Docker Compose Recomendado (Otimizado para Resiliência)

version: '3.8'

services:
  postgres:
    image: postgres:16-alpine
    restart: always
    environment:
      POSTGRES_DB: pilot_status
      POSTGRES_USER: ${POSTGRES_USER}
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
    volumes:
      - postgres-data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER}"]
      interval: 10s
      timeout: 5s
      retries: 5
    deploy:
      resources:
        limits:
          memory: 1G
          cpus: '1.0'

  redis:
    image: redis:7-alpine
    restart: always
    command: >
      redis-server
      --appendonly yes
      --appendfsync everysec
      --maxmemory 256mb
      --maxmemory-policy noeviction
      --save 60 1000
      --save 300 100
    volumes:
      - redis-data:/data
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 10s
      timeout: 5s
      retries: 5
    deploy:
      resources:
        limits:
          memory: 512M
          cpus: '0.5'

  app:
    image: ghcr.io/${GHCR_USER}/pilot-status:${VERSION}
    restart: always
    depends_on:
      postgres:
        condition: service_healthy
      redis:
        condition: service_healthy
    environment:
      DATABASE_URL: postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@postgres:5432/pilot_status
      REDIS_URL: redis://redis:6379
      EVOLUTION_API_URL: ${EVOLUTION_API_URL}
      EVOLUTION_API_KEY: ${EVOLUTION_API_KEY}
      DEEPSEEK_API_KEY: ${DEEPSEEK_API_KEY}
      MESSAGE_RECONCILER_INTERVAL_MS: "300000"  # 5 min (mais frequente)
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:3000/api/health"]
      interval: 30s
      timeout: 10s
      retries: 3
    deploy:
      resources:
        limits:
          memory: 2G
          cpus: '2.0'

volumes:
  postgres-data:
  redis-data:

Evolução para AWS (Quando Escalar)

┌──────────────────────────────────────────────────────────┐
│  AWS                                                       │
│                                                            │
│  ┌─────────────┐  ┌──────────────────┐  ┌──────────────┐ │
│  │ RDS          │  │ ElastiCache      │  │ ECS/Fargate  │ │
│  │ PostgreSQL   │  │ Redis (Cluster)  │  │ App + Worker │ │
│  │ Multi-AZ     │  │ Multi-AZ         │  │ Auto-scaling │ │
│  │ Backups auto │  │ Failover auto    │  │              │ │
│  └─────────────┘  └──────────────────┘  └──────────────┘ │
│                                                            │
│  ┌────────────────────────────────────────────────────────┐│
│  │ CloudWatch: Alertas + Métricas + Logs                  ││
│  └────────────────────────────────────────────────────────┘│
└──────────────────────────────────────────────────────────┘

Vantagens AWS:

  • RDS PostgreSQL Multi-AZ: failover automático, backups, point-in-time recovery.
  • ElastiCache Redis: elimina SPOF Redis, failover automático.
  • ECS Fargate: escalar workers horizontalmente sob demanda.
  • CloudWatch: métricas, logs centralizados, alertas.

12. Observabilidade e Operação

Métricas Essenciais

| Métrica | Como medir | Alerta | |---|---|---| | Latência QUEUED → DISPATCHED | Diferença dispatchedAt - createdAt | > 5 min média | | Latência DISPATCHED → SENT | Diferença sentAt - dispatchedAt | > 15 min | | Taxa de retry | attemptsMade > 1 / total | > 20% | | Profundidade de fila | BullMQ getWaitingCount() por instância | > 500 | | Taxa de FAILED | FAILED / total nos últimos 60 min | > 5% | | Instâncias offline | Contagem de instâncias com state != OPEN | > 0 | | DLQ | Jobs com attemptsMade >= maxAttempts | > 0 | | Volume de QUEUED > 30 min | Mensagens QUEUED há + de 30 min | > 50 |

Alertas Automáticos

  1. Pushover (desenvolvedor): instância offline > 5 min, DLQ, taxa de FAILED > 5%.
  2. WhatsApp (admin do tenant): instância desconectada, falha de envio crítica.
  3. E-mail/Slack (futuro): relatórios diários de volume, taxa de entrega, SLA.

Dashboard Recomendado

  • BullMQ Board ou Bull Monitor para visualização de filas.
  • Endpoint interno /api/internal/queue-health com métricas JSON.
  • Gráficos: volume/hora, taxa de sucesso, latência P50/P95/P99.

Ferramentas Operacionais

  • Reprocessar manualmente: botão no dashboard para reenfileirar mensagem específica.
  • Pause sending: pausar processamento por instância (útil durante manutenção Evolution).
  • DLQ viewer: listar mensagens em DLQ com motivo da falha e opção "Forçar Reenvio".

13. Roadmap de Implementação

Semana 1-2 (Crítico — Anti-Duplicidade):
  □ MR-01: Estado intermediário DISPATCHED
  □ MR-02: Timeout em todas as chamadas HTTP (AbortController)
  □ MR-03: Verificar status na Evolution antes de reenfileirar
  □ MR-04: Backoff exponencial com jitter
  □ MR-05: Retry inteligente por status code

Semana 3-4 (Infraestrutura de Filas):
  □ MR-06: Transactional Outbox Pattern (Redis SPOF)
  □ MR-07: Separação de filas por categoria (OTP/UTILITY/MARKETING)
  □ MR-08: Fila de webhook com retry
  □ MR-09: Redis com persistência AOF

Mês 2 (Robustez Operacional):
  □ MR-10: Renovação de lock do reconciliador (Watchdog)
  □ MR-11: Corrigir reconciliador para bodyOverride
  □ MR-12: Alertas automáticos para DLQ
  □ MR-13: Circuit Breaker para Evolution API
  □ Integração DeepSeek para reescrita de Marketing

Mês 3+ (Otimização e Escala):
  □ MR-14: Fallback de Instâncias (Roteamento Inteligente)
  □ MR-15: Sincronização Ativa de Status
  □ MR-16: Delay configurável por prioridade
  □ MR-17: Concorrência aumentada com rate-limit
  □ Dashboard de saúde das filas (BullMQ Board)
  □ Migração para AWS (RDS + ElastiCache + ECS) se necessário

Resumo Executivo

| Aspecto | Atual | Com Melhorias | |---|---|---| | Capacidade | ~168k-259k/mês (3 instâncias) | ✅ Suporta 50k/mês com folga | | Duplicidade | Possível (sem estado intermediário) | Eliminada (DISPATCHED + verificação Evolution) | | Redis SPOF | Sim (parada total) | Mitigado (Outbox + AOF) → Eliminado (AWS ElastiCache) | | Webhook perdido | Mensagem fantasma para sempre | Sincronização ativa + grace period | | OTP latência | Até 2min + delay fila | Fila prioritária dedicada | | Marketing spam detection | Textos idênticos | IA (DeepSeek) reescreve cada mensagem | | Observabilidade | Logs básicos | Métricas, alertas, dashboard, DLQ viewer | | Retry | Fixo 30s (thundering herd) | Exponencial com jitter | | HTTP timeout | Nenhum | 3-15s conforme operação |


Documento gerado em: 2026-03-21 Consolidação de: agendamento-mensagens.md, analise-fluxo-envio-mensagens.md, analise-fluxo-mensagens.md, fluxo-envio-mensagens.md Contexto: 3 números (Ash, Brock, Misty) · Evolution API · Monolito Docker · PostgreSQL + Redis · DeepSeek IA · Meta 50k/mês