Confiabilidade
Com store durável e conforme, trabalho aceito pode sobreviver ao processo e ser retomado em modelo at-least-once.
Estudo de caso arquitetural · biblioteca open source
Uma plataforma de comércio eletrônico precisa comunicar eventos de pedidos mesmo quando destinatários apresentam timeout, erros HTTP, indisponibilidade ou recebem uma mesma entrega mais de uma vez.
O cenário de e-commerce é ilustrativo. As garantias descritas correspondem à API e aos testes da versão 1.0.0; não representam adoção, benchmark ou operação real em produção.
Contexto e problema
Depois de registrar um pedido, enviar HTTP uma única vez mistura a decisão de negócio com uma dependência remota. Timeout não revela se o destinatário processou a requisição; respostas 5xx podem ser temporárias; um 4xx normalmente exige correção, não repetição infinita.
Se o processo cair após o destinatário aceitar a mensagem e antes de persistir o sucesso, uma tentativa posterior pode duplicar o evento. A solução precisa tornar fila, tentativas, propriedade temporária do trabalho e estados finais observáveis, sem transformar incerteza distribuída em promessa de exactly-once.
Requisitos arquiteturais
Com store durável e conforme, trabalho aceito pode sobreviver ao processo e ser retomado em modelo at-least-once.
IDs estáveis tornam o enqueue idempotente; o destinatário ainda precisa evitar efeitos duplicados.
Concorrência limitada evita claims além da capacidade; leases coordenam workers que compartilham o store.
HTTPS é o padrão; HMAC-SHA256 autentica o envelope quando signer e secret provider são registrados.
Logs estruturados, ActivitySource e Meter expõem fila, tentativas, desfechos e duração.
Lease expirada permite reclaim; retry persiste o próximo instante; estados terminais deixam de ser claimable.
Arquitetura da solução
IWebhookDeliveryStore fornecido pela aplicação, e idempotência de efeitos depende do destinatário.IWebhookEnqueueService valida limites e usa o ID estável.IWebhookDeliveryStore guarda mensagem, estado e agendamento.WebhookDispatcher reclama somente trabalho vencido dentro da capacidade.IWebhookDeliveryTransport representa uma tentativa HTTP.O serviço público valida os limites configurados antes de persistir. O ID identifica o par evento–subscriber; enfileirá-lo novamente deve retornar o registro existente sem substituir payload, destino ou estado.
Implementação do enqueue ↗IWebhookDeliveryStore isola tecnologia de persistência, mas exige claim atômico, renovação, rejeição de owner obsoleto e transições terminais. O pacote não inclui adapter durável.
O dispatcher preenche apenas slots disponíveis, renova a lease durante a tentativa e cancela o trabalho quando perde ownership. No shutdown, para novos claims e aguarda o grace period.
Código do dispatcher ↗Uma chamada a SendAsync é uma tentativa. O transporte HTTP classifica 2xx como sucesso, 408, 425, 429 e 5xx como retryable; demais status são permanentes.
A política padrão usa backoff exponencial limitado e jitter positivo. Um Retry-After válido pode postergar além do limite local; ao atingir o máximo de tentativas, o item vira dead letter.
O signer HMAC-SHA256 cobre timestamp, ID, tipo de evento, content type e bytes exatos do payload. Instrumentação publica nomes estáveis de traces e métricas sem incluir payload, assinatura ou destino.
Signer HMAC ↗API pública
// Produção: a aplicação fornece um store durável e conforme.
services.AddSingleton<IWebhookDeliveryStore, MyDurableWebhookStore>();
ReliableWebhooksBuilder webhooks = services.AddReliableWebhooks(options =>
{
options.Dispatcher.MaxConcurrency = 8;
options.Dispatcher.LeaseDuration = TimeSpan.FromMinutes(2);
options.Retry = new WebhookRetryPolicyOptions { MaxAttempts = 5 };
});
webhooks.AddHostedDispatcher();
WebhookMessage message = new(
id: $"{orderCreated.EventId}:{subscriber.Id}",
eventType: "order.created",
destination: subscriber.WebhookUri,
payload: JsonSerializer.SerializeToUtf8Bytes(orderCreated),
contentType: "application/json");
await webhookEnqueueService.EnqueueAsync(message, cancellationToken);
AddHostedDispatcher() é opt-in. A aplicação também pode resolver WebhookDispatcher e controlar seu ciclo de vida diretamente.
Cenários de execução
O dispatcher obtém uma lease, o transporte recebe 2xx e o store executa MarkSucceededAsync. Succeeded é terminal e não volta aos claims normais.
Garantia: uma transição de sucesso para o owner atual; duplicatas ainda são possíveis se o endpoint aceitou e a persistência do sucesso falhou.
O transporte retorna falha retryable. A política calcula o próximo instante com backoff/jitter e o store executa ScheduleRetryAsync, libera a lease e preserva o contador.
Garantia: nova tentativa após o agendamento quando o store é conforme; timeout não prova que o destinatário deixou de processar.
Um status como 400 é classificado como permanente por padrão. O dispatcher chama MarkPermanentlyFailedAsync; o item não é repetido automaticamente.
Garantia: estado terminal rastreável. Falhas retryable esgotadas terminam em DeadLettered, também sem replay automático.
Com store durável, trabalho pendente permanece disponível e uma lease abandonada pode ser reclamada após expirar. O token anterior não pode gravar uma transição atrasada.
Limite: com InMemoryWebhookDeliveryStore, o restart perde a fila; não existe recuperação durável nesse modo.
Decisões e trade-offs
Reenvio depois de resultado remoto incerto preserva a chance de entrega, mas aceita duplicidade. Exactly-once sobre HTTP exigiria coordenação transacional que a biblioteca e o destinatário não compartilham.
Ownership temporário permite escalar workers e recuperar abandono. Em troca, o store precisa de claim atômico, relógio autoritativo e compare-and-set do token.
Espaçar tentativas reduz pressão sobre um destinatário indisponível; jitter reduz sincronização entre workers. A latência de recuperação cresce e cada política precisa refletir o contrato remoto.
Não acoplar o core a banco ou ORM mantém a biblioteca reutilizável. O custo é explícito: a aplicação implementa e opera um adapter durável conforme para produção.
O ID estável ajuda a reconhecer duplicatas, mas a biblioteca não controla o efeito remoto. O receiver escolhe uma chave autenticada adequada e persiste sua própria decisão idempotente.
Store, transporte, classificador, retry, signer e providers podem evoluir isoladamente. A composição ganha flexibilidade, mas implementações customizadas devem preservar os contratos documentados.
| Responsabilidade da biblioteca | Responsabilidade da aplicação |
|---|---|
| Protocolo de enqueue, claim, lease, tentativa e transição. | Fornecer persistência durável e atômica adequada ao ambiente de produção. |
| Transporte HTTP padrão, classificação e retry configurável. | Escolher política, limites, quotas, operação de dead letters e eventual replay. |
| Signer HMAC-SHA256 e hooks para secret/header providers. | Gerenciar secrets, rotação, autorização de destinos e verificação no receiver. |
| Logs, traces e métricas com nomes públicos estáveis. | Coletar, exportar, alertar, definir SLOs e proteger dados persistidos. |
| ID estável e enqueue idempotente no contrato do store. | Garantir idempotência dos efeitos de negócio no destinatário. |
Limitações e evolução
A biblioteca não garante exactly-once, transação distribuída com o receiver, idempotência do lado remoto, replay automático de dead letters nem política de rotação de secrets. O destinatário deve tolerar mensagens duplicadas.
InMemoryWebhookDeliveryStore é local ao processo e não é durável. O pacote não entrega store de produção: a aplicação deve implementar um IWebhookDeliveryStore conforme, incluindo atomicidade, lease, relógio de coordenação, persistência de estado e testes específicos do backend.
O PublicNetworkWebhookDestinationPolicy reduz risco de SSRF para URLs não confiáveis, mas não substitui autorização de alterações, allow-lists, controles de egress ou proteção contra DNS rebinding no momento da conexão. HTTPS protege confidencialidade; HMAC autentica conteúdo, não cifra payload.
Resultados e aprendizados
Em vez de esconder falhas dentro de um loop HTTP, o desenho registra quando o trabalho existe, quem pode alterá-lo, quando uma nova tentativa é elegível e quais estados encerram o fluxo. Isso permite raciocinar sobre concorrência e recuperação com evidência testável.
A principal lição é de fronteira: retries não criam exactly-once, leases não criam durabilidade e HMAC não cria confidencialidade. O resultado arquitetural depende da combinação entre protocolo da biblioteca, adapter persistente, operação observável e receiver idempotente.
Evidências técnicas