Spraxium logoSpraxium

Entrega e confiabilidade

Opere o Signal Client com segurança sob tráfego de produção usando fallback persistente. Configure stores em arquivo ou Redis, backoff exponencial, tratamento de dead-letter e desligamento gracioso.

Modelo de confiabilidade

Signal Client combina rotação de webhooks com persistência opcional de fallback para que tráfego em pico e falhas temporárias não se transformem em atualizações perdidas no lado do bot. O desenho é intencionalmente em camadas: primeiro tenta a entrega ao vivo pelo pool, depois enfileira envelopes que falharam e faz novas tentativas com backoff exponencial. Isso dá às equipes de bot um caminho prático para confiabilidade sem obrigar cada serviço produtor a implementar seu próprio scheduler de retry.

O FallbackWorker é a primitiva central de confiabilidade. Quando o sistema de fallback está habilitado e uma chamada de send falha, o worker persiste o envelope em um FallbackStore e tenta novamente em lotes executados em background. Depois de esgotar o número máximo de tentativas, o envelope é movido para uma partição de dead-letter para inspeção manual.

Referência de configuração de fallback

O objeto FallbackConfig controla todos os aspectos do sistema de retry. Todos os campos, exceto enabled e store, possuem padrões sensatos, então uma configuração mínima só precisa ativar a feature e escolher um backend de persistência.

CampoTipoPadrãoFinalidade
enabledbooleanobrigatório

Ativa o sistema de fallback. Quando verdadeiro, o worker começa no construtor e inicia o processamento imediatamente.

storefile | redisobrigatório

Backend de persistência. file usa um arquivo JSON local; redis usa chaves compartilhadas com o prefixo spraxium:signal:fallback:.

retryIntervalMsnumber5000

Intervalo base entre ciclos de processamento de lotes para retry. Também serve como base da fórmula de backoff exponencial.

maxAttemptsnumber10

Número máximo de tentativas antes de mover um envelope para dead-letter. Depois disso, a recuperação exige intervenção manual.

batchSizenumber20

Quantidade máxima de envelopes reivindicados por ciclo de processamento. Lotes maiores aumentam throughput, mas também a pressão em picos.

eventsstring[]undefined

Filtro opcional de nomes de evento. Quando definido, apenas eventos dessa lista entram na fila ao falhar. Todos os demais continuam falhando pelo fluxo normal.

filePathstring.spraxium-signal-fallback.json

Caminho para o arquivo JSON de fallback. Só é usado quando store é file. O diretório é criado automaticamente se não existir.

redisUrlstringundefined

URL de conexão do Redis. Está aqui apenas para referência documental. Você deve fornecer uma instância de RedisFallbackStore como segundo argumento do construtor.

Escolhendo um fallback store

A interface FallbackStore define um contrato de persistência com oito métodos: enqueue, claimBatch, markProcessed, reschedule, moveToDeadLetter, has, pendingCount e deadLetterCount. Duas implementações embutidas cobrem os cenários de implantação mais comuns. Você também pode implementar a interface por conta própria para backends customizados.

import { SignalClient } from '@spraxium/signal-client';

const client = new SignalClient({
  webhookUrls: [process.env.SIGNAL_WEBHOOK_URL_1!],
  hmacSecret: process.env.SIGNAL_HMAC_SECRET!,
  rateLimitStrategy: 'wait',
  fallback: {
    enabled: true,
    store: 'file',
    filePath: '.spraxium-signal-fallback.json',
    retryIntervalMs: 5_000,
    maxAttempts: 10,
    batchSize: 20,
    events: ['config.update', 'billing.status.changed'],
  },
});

FileFallbackStore mantém entradas pendentes, dead-letter e processadas em um único arquivo JSON. Ele carrega o arquivo sob demanda no primeiro acesso e grava novamente após cada mutação. A lista de entradas processadas é automaticamente reduzida para as últimas 5000 quando ultrapassa 10000, evitando crescimento sem limite. Esse store funciona bem em desenvolvimento local e em serviços pequenos com uma única instância.

RedisFallbackStore persiste entradas como chaves individuais no Redis sob o prefixo spraxium:signal:fallback:pending:, com dead-letters sob spraxium:signal:fallback:deadletter: e IDs processados em um conjunto em spraxium:signal:fallback:processed. O Redis é preferível para emissores com múltiplas instâncias, porque o estado de fallback é compartilhado entre réplicas sem coordenação pelo sistema de arquivos.

Backoff exponencial e jitter

Quando uma tentativa de retry falha, o worker reagenda o envelope usando uma fórmula de backoff exponencial com jitter aditivo. Essa fórmula evita tempestades de retry, espalhando as tentativas por intervalos cada vez maiores, enquanto o componente de jitter impede que vários envelopes tentem de novo exatamente no mesmo instante.

A fórmula exata é: retryIntervalMs * 2^min(attempt, 8) + random() * retryIntervalMs * 0.3. O expoente é limitado a 8 para evitar atrasos absurdamente longos em contagens altas de tentativa. Com retryIntervalMs padrão de 5000 e maxAttempts de 10, o backoff máximo antes do jitter fica em cerca de 1280 segundos, aproximadamente 21 minutos, na oitava tentativa.

TentativaAtraso baseFaixa de jitterFaixa efetiva
110 s0 a 1,5 s10 a 11,5 s
220 s0 a 1,5 s20 a 21,5 s
340 s0 a 1,5 s40 a 41,5 s
5160 s0 a 1,5 s160 a 161,5 s
8+1280 s0 a 1,5 s1280 a 1281,5 s, no teto

Tratamento de dead-letter

Quando um envelope esgota o maxAttempts configurado, o worker o move para a partição de dead-letter via moveToDeadLetter. A entrada original é removida da fila pendente e armazenada com o último motivo de erro anexado. Envelopes em dead-letter nunca recebem retry automático, então exigem investigação manual.

No store em arquivo, entradas de dead-letter se acumulam no array deadLetter do arquivo JSON. No store em Redis, elas viram chaves individuais sob spraxium:signal:fallback:deadletter:. Ambos os stores acompanham essa quantidade via deadLetterCount, que você deve monitorar como parte das suas verificações operacionais de saúde.

O método stats do FallbackWorker retorna as contagens de pending e deadLetter em uma única chamada. Expor esses dados ao seu sistema de monitoramento é a forma mais simples de detectar falhas persistentes de entrega antes que elas afetem o comportamento visível do bot.

fallback-monitoring.ts
const { pending, deadLetter } = await client.fallback!.stats();

if (deadLetter > 0) {
  logger.warn(
    `Signal fallback: ${deadLetter} dead-letter envelopes require investigation`,
  );
}

if (pending > 100) {
  logger.warn(
    `Signal fallback: ${pending} pending envelopes, possible sustained delivery failure`,
  );
}

Ciclo de vida de uma entrada de fallback

Enfileiramento do envelope em caso de falha

Quando uma chamada de send lança erro e o fallback está habilitado, o worker verifica se o evento corresponde ao filtro events ou, se não houver filtro, considera todos os eventos elegíveis. Em seguida, ele faz deduplicação por nonce para evitar enfileiramento em duplicidade e cria uma FallbackEntry com o envelope, nome do evento, guild ID, zero tentativas e um nextAttemptAt ajustado para agora mais retryIntervalMs.

Claim em lote e retry

A cada ciclo de processamento, a cada retryIntervalMs, o worker chama claimBatch para puxar até batchSize entradas cujo nextAttemptAt já passou. Para cada entrada, ele chama send no client com skipFallback: true para evitar loops de reenfileiramento. Se o envio der certo, a entrada é marcada como processada.

Reagendamento exponencial em caso de falha

Quando uma tentativa de retry falha, o worker calcula o próximo instante de tentativa usando a fórmula de backoff exponencial com jitter. A contagem de tentativas da entrada é incrementada, o motivo do erro é gravado em lastError e a entrada é reagendada via reschedule. Isso continua até a entrega funcionar ou o limite de tentativas ser atingido.

Dead-letter ao esgotar

Quando a contagem de tentativas atinge maxAttempts, o worker chama moveToDeadLetter com o motivo final do erro. A entrada é removida em definitivo da fila pendente e colocada na partição de dead-letter. A partir daí, só intervenção manual ou um script customizado pode reprocessá-la.

Perfis de confiabilidade por cenário

CenárioPerfil recomendadoTrade-off
Eventos de baixo volume com tolerância a recuperação manualestratégia skip, fallback desabilitado

Menor complexidade possível. O produtor precisa assumir a lógica de retry ou aceitar perdas ocasionais.

Picos moderados em que throttling temporário é esperado

estratégia 'wait', pool com múltiplos webhooks, fallback desabilitado

Absorve automaticamente picos curtos de rate limit. A latência fica um pouco maior sob pressão.

Eventos críticos de negócio com baixa tolerância a perdaestratégia wait, fallback habilitado com store em arquivo

Retry persistente com monitoramento de dead-letter. Bom para implantações com instância única.

Serviços com múltiplas instâncias e necessidade de estado compartilhado

estratégia 'wait', fallback habilitado com store em Redis

Estado de fallback compartilhado entre réplicas. Exige infraestrutura Redis.

Eventos mistos de alto throughput com confiabilidade seletiva

fallback habilitado com filtro events apenas para nomes críticos

Concentra recursos de retry nos eventos importantes. Eventos não críticos falham rápido, sem sobrecarga de fila.

Desligamento gracioso

Chame destroy no client durante a sequência de encerramento da sua aplicação para parar o FallbackWorker de forma limpa. Isso limpa o timer interno de setInterval e evita loops parciais de retry durante rolling deployments. O worker usa timer.unref() para não manter o processo Node.js vivo, mas destruir explicitamente o client garante uma limpeza determinística.

graceful-shutdown.ts
process.on('SIGTERM', () => {
  client.destroy();
  process.exit(0);
});

Se você pular a chamada de destroy, ciclos pendentes de retry podem ser executados parcialmente durante o shutdown, com potencial de produzir entregas em duplicidade. Em ambientes conteinerizados, onde desligamentos acontecem com frequência, isso é ainda mais importante.

Runbook operacional

Observe o comportamento do pool continuamente

Acompanhe o status do pool com client.pool.status() para entender quais endpoints de webhook estão saudáveis, limitados ou indisponíveis. Correlacione padrões de rate limit com a carga dos webhooks no Discord para decidir se você precisa adicionar mais webhooks ou reduzir a frequência de envio.

Monitore o crescimento da fila de fallback

Quando o fallback está habilitado, contagens crescentes de pending são o primeiro sinal de falhas sustentadas de transporte. Exponha a saída do método stats ao seu sistema de monitoramento e configure alertas com limiares coerentes com o volume dos seus eventos. Quando a contagem de pending permanece acima de zero por vários minutos, normalmente existe um problema sistêmico.

Investigue acúmulo de dead-letter

Crescimento de dead-letter deve disparar investigação imediata. Verifique o lastError armazenado em cada entrada de dead-letter para entender se as falhas são transitórias, como rate limits e erros de rede, ou permanentes, como webhook inválido ou segredo alterado. No store em arquivo, inspecione diretamente o array deadLetter no JSON. No Redis, faça scan das chaves sob spraxium:signal:fallback:deadletter:*.

Ajuste janelas de retry pela criticidade

Eventos de alto impacto, como billing ou escalonamento de moderação, justificam maxAttempts maiores e retryIntervalMs menores para novas tentativas iniciais mais rápidas. Eventos de baixo impacto devem usar ajustes mais leves para evitar que a pressão da fila engula o tráfego crítico. Use o filtro events para aplicar fallback apenas aos nomes de evento que realmente precisam disso.

Destrua emissores durante o shutdown

Sempre chame client.destroy() no handler de SIGTERM ou no fluxo de shutdown gracioso. Isso interrompe o timer do fallback worker e evita processamento em duplicidade durante rolling deployments. Em ambientes serverless, nos quais o processo pode ser congelado em vez de encerrado, considere persistir o estado pendente antes de retornar do handler.