Confiabilidad
Con un store durable y conforme, el trabajo aceptado puede sobrevivir al proceso y reanudarse con semántica at-least-once.
Estudio de caso arquitectónico · biblioteca de código abierto
Una plataforma de comercio electrónico necesita comunicar eventos de pedidos incluso cuando los destinatarios presentan timeout, errores HTTP, indisponibilidad o reciben la misma entrega más de una vez.
El escenario de comercio electrónico es ilustrativo. Las garantías descritas corresponden a la API y a las pruebas de la versión 1.0.0; no representan adopción, benchmark ni operación real en producción.
Contexto y problema
Después de registrar un pedido, enviar HTTP una sola vez mezcla la decisión de negocio con una dependencia remota. Un timeout no revela si el destinatario procesó la solicitud; las respuestas 5xx pueden ser temporales; un 4xx normalmente exige corrección, no repetición infinita.
Si el proceso se interrumpe después de que el destinatario acepta el mensaje y antes de persistir el éxito, un intento posterior puede duplicar el evento. La solución necesita hacer observables la cola, los intentos, la propiedad temporal del trabajo y los estados finales, sin transformar la incertidumbre distribuida en una promesa de exactly-once.
Requisitos arquitectónicos
Con un store durable y conforme, el trabajo aceptado puede sobrevivir al proceso y reanudarse con semántica at-least-once.
Los ID estables hacen que el enqueue sea idempotente; el destinatario todavía debe evitar efectos duplicados.
La concurrencia limitada evita claims por encima de la capacidad; los leases coordinan workers que comparten el store.
HTTPS es el valor predeterminado; HMAC-SHA256 autentica el envelope cuando se registran el signer y el secret provider.
Logs estructurados, ActivitySource y Meter exponen cola, intentos, resultados y duración.
Un lease vencido permite reclaim; el reintento persiste el próximo instante; los estados terminales dejan de ser claimable.
Arquitectura de la solución
IWebhookDeliveryStore proporcionado por la aplicación, y la idempotencia de los efectos depende del destinatario.IWebhookEnqueueService valida límites y utiliza el ID estable.IWebhookDeliveryStore guarda mensaje, estado y programación.WebhookDispatcher reclama solo trabajo vencido dentro de la capacidad.IWebhookDeliveryTransport representa un intento HTTP.El servicio público valida los límites configurados antes de persistir. El ID identifica el par evento–suscriptor; volver a colocarlo en cola debe devolver el registro existente sin reemplazar payload, destino ni estado.
Implementación del enqueue ↗IWebhookDeliveryStore aísla la tecnología de persistencia, pero exige claim atómico, renovación, rechazo del owner obsoleto y transiciones terminales. El paquete no incluye un adapter durable.
El dispatcher ocupa solo los slots disponibles, renueva el lease durante el intento y cancela el trabajo cuando pierde ownership. Durante el shutdown, detiene los nuevos claims y espera el grace period.
Código del dispatcher ↗Una llamada a SendAsync es un intento. El transporte HTTP clasifica 2xx como éxito; 408, 425, 429 y 5xx como retryable; los demás estados son permanentes.
La política predeterminada utiliza backoff exponencial limitado y jitter positivo. Un Retry-After válido puede posponer más allá del límite local; al alcanzar el máximo de intentos, el elemento pasa a dead letter.
El signer HMAC-SHA256 cubre timestamp, ID, tipo de evento, content type y los bytes exactos del payload. La instrumentación publica nombres estables de traces y métricas sin incluir payload, firma ni destino.
Signer HMAC ↗API pública
// Producción: la aplicación proporciona un store durable y 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() es opt-in. La aplicación también puede resolver WebhookDispatcher y controlar directamente su ciclo de vida.
Escenarios de ejecución
El dispatcher obtiene un lease, el transporte recibe 2xx y el store ejecuta MarkSucceededAsync. Succeeded es terminal y no vuelve a los claims normales.
Garantía: una transición de éxito para el owner actual; todavía son posibles los duplicados si el endpoint aceptó y falló la persistencia del éxito.
El transporte devuelve una falla retryable. La política calcula el próximo instante con backoff/jitter y el store ejecuta ScheduleRetryAsync, libera el lease y preserva el contador.
Garantía: un nuevo intento después de la programación cuando el store es conforme; un timeout no demuestra que el destinatario no haya procesado.
Un estado como 400 se clasifica como permanente de forma predeterminada. El dispatcher llama a MarkPermanentlyFailedAsync; el elemento no se repite automáticamente.
Garantía: estado terminal rastreable. Las fallas retryable agotadas terminan en DeadLettered, también sin replay automático.
Con un store durable, el trabajo pendiente permanece disponible y un lease abandonado puede reclamarse después de vencer. El token anterior no puede escribir una transición tardía.
Límite: con InMemoryWebhookDeliveryStore, el reinicio pierde la cola; no existe recuperación durable en este modo.
Decisiones y trade-offs
Reenviar después de un resultado remoto incierto preserva la posibilidad de entrega, pero acepta duplicados. Exactly-once sobre HTTP requeriría una coordinación transaccional que la biblioteca y el destinatario no comparten.
El ownership temporal permite escalar workers y recuperar trabajo abandonado. A cambio, el store necesita claim atómico, reloj autoritativo y compare-and-set del token.
Espaciar los intentos reduce la presión sobre un destinatario no disponible; el jitter reduce la sincronización entre workers. La latencia de recuperación aumenta y cada política debe reflejar el contrato remoto.
No acoplar el core a una base de datos u ORM mantiene reutilizable la biblioteca. El costo es explícito: la aplicación implementa y opera un adapter durable y conforme para producción.
El ID estable ayuda a reconocer duplicados, pero la biblioteca no controla el efecto remoto. El receiver elige una clave autenticada adecuada y persiste su propia decisión idempotente.
Store, transporte, clasificador, retry, signer y providers pueden evolucionar de forma aislada. La composición gana flexibilidad, pero las implementaciones personalizadas deben preservar los contratos documentados.
| Responsabilidad de la biblioteca | Responsabilidad de la aplicación |
|---|---|
| Protocolo de enqueue, claim, lease, intento y transición. | Proporcionar persistencia durable y atómica adecuada para el entorno de producción. |
| Transporte HTTP predeterminado, clasificación y reintentos configurables. | Elegir política, límites, cuotas, operación de dead letters y eventual replay. |
| Signer HMAC-SHA256 y hooks para secret/header providers. | Administrar secrets, rotación, autorización de destinos y verificación en el receiver. |
| Logs, traces y métricas con nombres públicos estables. | Recolectar, exportar, alertar, definir SLO y proteger los datos persistidos. |
| ID estable y enqueue idempotente en el contrato del store. | Garantizar la idempotencia de los efectos de negocio en el destinatario. |
Limitaciones y evolución
La biblioteca no garantiza exactly-once, una transacción distribuida con el receiver, idempotencia del lado remoto, replay automático de dead letters ni una política de rotación de secrets. El destinatario debe tolerar mensajes duplicados.
InMemoryWebhookDeliveryStore es local al proceso y no es durable. El paquete no ofrece un store de producción: la aplicación debe implementar un IWebhookDeliveryStore conforme, incluidos atomicidad, lease, reloj de coordinación, persistencia de estado y pruebas específicas del backend.
PublicNetworkWebhookDestinationPolicy reduce el riesgo de SSRF para URL no confiables, pero no reemplaza autorización de cambios, allow-lists, controles de egress ni protección contra DNS rebinding en el momento de la conexión. HTTPS protege la confidencialidad; HMAC autentica el contenido, no cifra el payload.
Resultados y aprendizajes
En lugar de ocultar las fallas dentro de un ciclo HTTP, el diseño registra cuándo existe el trabajo, quién puede modificarlo, cuándo es elegible un nuevo intento y qué estados terminan el flujo. Esto permite razonar sobre concurrencia y recuperación con evidencia comprobable.
La principal lección está en los límites: los reintentos no crean exactly-once, los leases no crean durabilidad y HMAC no crea confidencialidad. El resultado arquitectónico depende de la combinación entre el protocolo de la biblioteca, el adapter persistente, la operación observable y el receiver idempotente.
Evidencias técnicas