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:
| Fase | Default | Env var | Justificativa |
|---|---|---|---|
| Collect | 60s | TIMEOUT_COLLECT_MS | Pode retornar datasets grandes |
| Process | 30s | TIMEOUT_PROCESS_MS | Item único, operação pontual |
| Gateway | 15s | TIMEOUT_GATEWAY_MS | Sí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
| Camada | Escopo | Key Redis |
|---|---|---|
| Tenant | Por integração + tenant | cb:tenant:{integrationKey}:{tenantId}:* |
| Global | Por 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
| Camada | threshold | resetTimeout | halfOpenSuccesses |
|---|---|---|---|
| Tenant | 5 | 30s | 2 |
| Global | 15 | 60s | 3 |
Transição lazy (sem setTimeout)
A transição open → half-open é feita por checagem lazy a cada request, não por timer:
- Ao abrir: grava
opened-atno Redis com TTL - Em cada
allowRequest(): se estado éopeneresetTimeoutjá passou desdeopened-at, muda parahalf-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:
| Config | Valor |
|---|---|
| Tentativas | 3 |
| Backoff | Exponencial |
| Delay base | 5s |
| Progressão | 5s → 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:
| Worker | lockDuration | stalledInterval | Motivo |
|---|---|---|---|
| Run Worker | 90s | 120s | Collect pode levar até 60s |
| Item Worker | 45s | 60s | Process pode levar até 30s |
| Log Writer | 30s (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:
- Remove health marker file (Docker healthcheck)
- Para o heartbeat
- Fecha workers (
worker.close()— drena jobs in-flight) - Desconecta Redis e PostgreSQL
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 viatest -f - Monitor: coleta status de todos os serviços via heartbeat Redis a cada 15s (env
POLL_INTERVAL_MS)