Pular para o conteúdo principal

Resiliência

Mecanismos de proteção e recuperação do sistema contra falhas.

Timeout HTTP

Todas as chamadas HTTP do runner e API gateway para integrações usam fetchWithTimeout() com AbortSignal.timeout(). Três tiers de timeout:

FaseDefaultEnv varJustificativa
Collect60sTIMEOUT_COLLECT_MSPode retornar datasets grandes
Process30sTIMEOUT_PROCESS_MSItem único, operação pontual
Gateway15sTIMEOUT_GATEWAY_MSSíncrono, cliente HTTP esperando

Se o timeout estourar, o erro é tratado como falha normal (retry no BullMQ, circuit breaker registra falha). No gateway, retorna HTTP 504 ao invés de 502.

Implementação: packages/core/src/http/fetch-with-timeout.ts

Circuit Breaker

Duas camadas (layered), ambas Redis-backed com três estados:

closed ──(N falhas)──▸ open ──(Xs)──▸ half-open ──(M sucessos)──▸ closed

Camadas

CamadaEscopoKey Redis
TenantPor integração + tenantcb:tenant:{integrationKey}:{tenantId}:*
GlobalPor integração (todos tenants)cb:global:{integrationKey}:*

Um request é bloqueado se qualquer camada estiver open. Falhas e sucessos são registrados em ambas as camadas.

Quando cada camada atua:

  • Tenant: credenciais inválidas, rate limit por conta na API externa, problemas isolados de um tenant
  • Global: API externa fora do ar, indisponibilidade generalizada

Configuração

CamadathresholdresetTimeouthalfOpenSuccesses
Tenant530s2
Global1560s3

Transição lazy (sem setTimeout)

A transição open → half-open é feita por checagem lazy a cada request, não por timer:

  1. Ao abrir: grava opened-at no Redis com TTL
  2. Em cada allowRequest(): se estado é open e resetTimeout já passou desde opened-at, muda para half-open

Isso garante que a transição funciona mesmo após restart do processo.

Implementação: packages/core/src/resilience/layered-circuit-breaker.ts (composição) e circuit-breaker.ts (base)

Rate Limiting

Sliding window por tenant via Redis sorted set:

  • Limite: 1.000 items/minuto por tenant (configurable)
  • Comportamento no limite: job é atrasado 5s (moveToDelayed), não falha
  • Objetivo: evitar que tenant massivo sature a fila e bloqueie outros

Implementação: packages/core/src/resilience/rate-limiter.ts

Backpressure

Antes do fan-out (criação de child jobs), o run-worker verifica a profundidade da fila de items:

  • Threshold: 10.000 jobs waiting (env BACKPRESSURE_MAX_DEPTH)
  • Comportamento: se exceder, o run é atrasado 10s e retentado
  • Objetivo: evitar crescimento descontrolado da fila quando o processamento está lento

Retry + DLQ

BullMQ exponential backoff em todos os item jobs:

ConfigValor
Tentativas3
BackoffExponencial
Delay base5s
Progressão5s → 10s → 20s

Após a última tentativa falhada, o item é marcado com dlq_at no banco. Items em DLQ podem ser reprocessados via API (/runs/:id/reprocess).

Lock Duration / Stalled Interval

Workers BullMQ configurados para acomodar os timeouts HTTP:

WorkerlockDurationstalledIntervalMotivo
Run Worker90s120sCollect pode levar até 60s
Item Worker45s60sProcess pode levar até 30s
Log Writer30s (default)30s (default)Escrita rápida, sem HTTP

lockDuration deve ser maior que o timeout HTTP + margem para updates no banco.

Graceful Shutdown

Todos os serviços implementam handlers para SIGINT e SIGTERM:

  1. Remove health marker file (Docker healthcheck)
  2. Para o heartbeat
  3. Fecha workers (worker.close() — drena jobs in-flight)
  4. Desconecta Redis e PostgreSQL
  5. process.exit(0)

Jobs que estavam in-flight são completados antes do shutdown. Jobs na fila permanecem no Redis e são retomados no próximo startup.

Heartbeat

Cada serviço publica liveness no Redis:

  • Key: heartbeat:<service-name>
  • TTL: 30s (intervalo de 10s × 3)
  • Payload: { pid, uptime, timestamp }

Se o serviço parar de publicar, a key expira e o Monitor detecta o serviço como down.

Implementação: packages/core/src/heartbeat/heartbeat.ts

Health Checks

  • Endpoint: todos os serviços expõem GET /health{ status: 'ok' }
  • Docker: arquivo .healthy/<service> criado no startup, removido no shutdown. Healthcheck via test -f
  • Monitor: coleta status de todos os serviços via heartbeat Redis a cada 15s (env POLL_INTERVAL_MS)