Garanties de livraison de webhooks
Webhooks - notifications asynchrones « du système à l'abonné » par HTTP (S). Le réseau n'est pas fiable : les réponses sont perdues, les paquets sont dupliqués ou hors d'ordre. C'est pourquoi les garanties de livraison ne sont pas construites « par TCP », mais au niveau du protocole Web et de l'idempotence du domaine.
L'objectif principal est de fournir une livraison at-least-once avec un ordre par clé (le cas échéant), de donner à l'abonné du matériel pour le traitement idempotent et un outil de récupération.
1) Niveaux de garantie
Best-effort est une tentative unique, sans retraits. Seulement acceptable pour les événements « sans importance ».
At-least-once (recommandé) - les doublons et out-of-order sont possibles, mais l'événement sera livré sous réserve de la disponibilité de l'abonné dans un délai raisonnable.
Effectively-exactly-once (au niveau de l'effet) - obtenu par une combinaison d'idempotence et de dedup-store du côté abonné/expéditeur. Dans les transports HTTP, « exactly-once » n'est pas possible.
2) Contrat Webhook : minimum nécessaire
Titres (exemple) :
X-Webhook-Id: 5d1e6a1b-4f7d-4a3d-8b3a-6c2b2f0f3f21 # глобальный ID события
X-Delivery-Attempt: 3 # номер попытки
X-Event-Type: payment.authorized.v1 # тип/версия
X-Event-Time: 2025-10-31T12:34:56Z # ISO8601
X-Partition-Key: psp_tx_987654 # ключ порядка
X-Seq: 418 # монотонный номер по ключу
X-Signature-Alg: HMAC-SHA256
X-Signature: t=1730378096,v1=hex(hmac(secret, t body))
Content-Type: application/json
Corps (exemple) :
json
{
"id": "5d1e6a1b-4f7d-4a3d-8b3a-6c2b2f0f3f21",
"type": "payment.authorized.v1",
"occurred_at": "2025-10-31T12:34:56Z",
"partition_key": "psp_tx_987654",
"sequence": 418,
"data": {
"payment_id": "psp_tx_987654",
"amount": "10.00",
"currency": "EUR",
"status": "AUTHORIZED"
},
"schema_version": 1
}
Exigence pour le destinataire : répondre rapidement '2xx' après la mise en tampon et la validation de la signature, et le traitement de l'entreprise est asynchrone.
3) Ordre et causalité
Ordre par clé : la garantie « ne partira pas » à l'intérieur d'un seul 'partition _ key' (par exemple, 'player _ id', 'wallet _ id', 'psp _ tx _ id').
L'ordre mondial n'est pas garanti.
Du côté de l'expéditeur, il y a une file d'attente avec sérialisation par clé (un consommateur/Charding), du côté du destinataire, inbox avec '(source, event_id)' et l'attente facultative des 'seq' manqués.
Si les omissions sont critiques - fournir pull-API 'GET/events ? after = checkpoint 'pour le statut « rattraper et vérifier ».
4) Idempotence et déduplication
Chaque webhook porte un "X-Webhook-Id'stable.
Le destinataire stocke 'inbox (event_id)' : PK - 'source + event_id' ; répétitions → no-op.
Les effets secondaires (enregistrement dans la base de données/portefeuille) ne se produisent qu'une seule fois lors de la première « vision » de l'événement.
Pour les commandes à effet, utilisez Idempotency-Key et le cache des résultats pendant la fenêtre de retraits.
5) Retrai, backoff et fenêtres
Politique de retraite (référence) :- Rétracter sur '5xx/timeout/connection error/409-Conflict (retryable )/429'.
- Ne pas rétracter sur '4xx' sauf '409/423/429' (et uniquement avec la sémantique convenue).
- Backoff exponentiel + full jitter : 0. 5s, 1s, 2s, 4s, 8s, … jusqu'à « max = 10-15 min » ; Fenêtre de rétraction TTL : par exemple 72 heures.
- Respecter 'Retry-After' chez le destinataire.
- Avoir une date limite commune : « reconnaître un événement non livré » et le traduire en DLQ.
yaml retry:
initial_ms: 500 multiplier: 2.0 jitter: full max_delay_ms: 900000 ttl: 72h retry_on: [TIMEOUT, 5xx, 429]
6) DLQ и redrive
DLQ est un « cimetière » d'événements toxiques ou expirés par TTL avec une métainformation complète (paload, titres, erreurs, tentatives, hachages).
Console Web/API pour redrive (refonte ponctuelle) avec modification en option de endpoint/secret.
Rate-limited redrive et batch-redrive avec priorité.
7) Sécurité
mTLS (si possible) ou TLS 1. 2+.
Signature du corps (HMAC avec secret per tenant/endpoint). Vérification :1. Extraire 't' (timestamp) de l'en-tête, vérifier la fenêtre glissante (par exemple, ± 5 min).
8) Quotas, taux limites et équité
Fair-Queue per tenant/subscriber : qu'un abonné/ténant ne marque pas le pool partagé.
Quotas et limites burst pour le trafic sortant et per-endpoint.
Réaction à '429' : honorer 'Retry-After', noyer le flux ; avec une limite longue - degrade (envoyer uniquement les types d'événements critiques).
9) Cycle de vie de l'abonnement
Register/Verify : POST endpoint → challenge/response ou out-of-band confirmation.
Lease (si désiré) : la signature est valable jusqu'à « valid _ to » ; la prolongation est explicite.
Secret rotation: `current_secret`, `next_secret` с `switch_at`.
Test ping : événement artificiel pour vérifier l'itinéraire avant d'allumer les tops principaux.
Échantillons de santé : HEAD/GET périodiques avec contrôle de latitude et profil TLS.
10) Évolution des schémas (versions des événements)
Versioner le type d'événement : 'payment. authorized. v1` → `…v2`.
Évolution - additive (nouveaux champs → version MINOR de l'API), breaking → nouveau type.
Registre des schémas (JSON-Schema/Avro/Protobuf) + validation automatique avant l'envoi.
L'en-tête « X-Event-Type » et le champ « schema _ version » dans le corps sont obligatoires.
11) Observabilité et SLO
Métriques (par type/tenant/abonné) :- `deliveries_total`, `2xx/4xx/5xx_rate`, `timeout_rate`, `signature_fail_rate`.
- 'attempts _ avg ',' p50/p95/p99 _ delivery _ latency _ ms' (de la publication à 2xx).
- `dedup_rate`, `out_of_order_rate`, `dlq_rate`, `redrive_success_rate`.
- `queue_depth`, `oldest_in_queue_ms`, `throttle_events`.
- La part des livraisons ≤ 60 c (p95) est de 99. 5 % pour les événements critiques.
- DLQ ≤ 0. 1 % en 24 heures
- Signature failures ≤ 0. 05%.
Логи/трейсинг: `event_id`, `partition_key`, `seq`, `attempt`, `endpoint`, `tenant_id`, `schema_version`, `trace_id`.
12) Algorithme de référence de l'expéditeur
1. Enregistrez l'événement dans un outbox transactionnel.
2. Identifier les partition_key et les seq ; mettre en file d'attente.
3. Worker prend par clé, forme une requête, signe, envoie avec des temporisations (connect/read).
4. Avec '2xx' - reconnaître livré, enregistrer la latence et seq-chekpoint.
5. Avec '429/5xx/timeout', c'est une rétrospective selon la politique.
6. Par TTL → DLQ et alert.
13) Processeur de référence (destinataire)
1. Accepter la demande, vérifier TLS/proto.
2. Validation de la signature et de la fenêtre temporelle.
3. ACK 2xx rapide (après écriture synchrone dans l'inbox/file d'attente locale).
4. Le worker asynchrone lit 'inbox', vérifie 'event _ id' (dedup), si nécessaire, ordonne par 'seq' à l'intérieur de 'partition _ key'.
5. Exécute des effets, écrit « offset/seq chekpoint » pour reconcile.
6. En cas d'erreur, les retraits locaux ; les tâches « toxiques » → un DLQ local avec des alertes.
14) Reconcile (circuit pull)
Pour les incidents « non résolus » :- `GET /events? partition_key=...&after_seq=...&limit=...' - donner tous les manquants.
- Token-chekpoint : 'after = opaque _ token' au lieu de seq.
- Idempotent redelivery : même 'event _ id', même signature pour le nouveau 't'.
15) Titres et codes utiles
2xx - accepté (même si le traitement de l'entreprise est plus tard).
410 Gone - endpoint fermé (l'expéditeur arrête la livraison et marque l'abonnement comme « archive »).
409/423 - Blocage temporaire de la ressource → rétrospective raisonnable.
429 - trop souvent → TNT et backoff.
400/401/403/404 - erreur de configuration ; arrêtez les retraits, ouvrez le tiquet.
16) Multi-tenants et régions
Files d'attente séparées et limites per tenant/endpoint.
Data residency : envoi depuis la région de données ; les titres de passage « X-Tenant », « X-Région ».
Isolation des pannes : la chute d'un abonné n'affecte pas les autres (separate pools).
17) Tests
Tests de contrat : exemples fixes de corps/signatures, vérification de validation.
Chaos : drop/doublons, ordre shuffle, retards réseau, « RST », « TLS ».
Load : tempête burst, mesure p95/p99.
Sécurité : anti-relais, timestamp obsolète, secrets erronés, rotation.
DR/Replay : masse redrive de DLQ dans un stand isolé.
18) Pleybooks (runbooks)
1. Croissance de 'signature _ fail _ rate'
Vérifier la dérive de l'horloge qui a expiré 'tolerance', la rotation des secrets ; inclure temporairement « dual secret ».
2. La file d'attente vieillit ('oldest _ in _ queue _ ms' ↑)
Augmenter les workers, activer la priorité des axes critiques, réduire temporairement la fréquence des types « bruyants ».
3. Tempête '429' chez l'abonné
Activer le câblage et les pauses entre les tentatives ; déplacer les types d'événements moins critiques.
4. Masse '5xx'
Ouvrir le circuit-breaker pour un endpoint spécifique, le mettre en mode defer & batch ; signal à l'abonné.
5. Remplissage du DLQ
Arrêter la publication non critique, activer batch-redrive avec un RPS bas, soulever les alertes aux propriétaires d'abonnements.
19) Erreurs typiques
Traitement lourd synchrone jusqu'à la réponse 2xx → retraits et doublons.
Il n'y a pas de signature corps/fenêtre temporelle → de vulnérabilité au remplacement/repliement.
L'absence de 'event _ id' et 'inbox' → ne peut pas être rendue idempotente.
La tentative de « l'ordre mondial » → les blocages éternels des files d'attente.
Retrai sans jitter/limites → amplification de l'incident (thundering herd).
Un pool commun unique pour tous les abonnés → « bruyant » met tout le monde.
20) Chèque-liste avant la vente
- Contrat : 'event _ id', 'partition _ key', 'seq', 'event _ type. vN', signature HMAC et timestamp.
- Expéditeur : outbox, sérialisation par clé, retraits avec backoff + jitter, TTL, DLQ et redrive.
- Destinataire : entrée rapide dans inbox + 2xx ; traitement idempotent ; DLQ local.
- Sécurité : TLS, signatures, anti-repli, dual-secret, rotation.
- Quotas/limites : fair-queue per tenant/endpoint, respect « Retry-After ».
- API reconcile et chekpoints ; documentation pour les abonnés.
- Observabilité : p95/flux/erreurs/DLQ, trace par 'event _ id'.
- La versionation des événements et la politique de l'évolution des schémas.
- Pleybooks d'incidents et « bouton » de pause/décongélation globale.
Conclusion
Les webhooks fiables sont un protocole au-dessus de HTTP, pas seulement « POST with JSON ». Un contrat clair (ID, clé d'ordre, signature), une idempotence, des retraits avec jitter, une file d'attente juste et des playbacks bien réglés transforment le « meilleur cas » en un mécanisme de livraison prévisible et mesurable. Construisez at-least-once + ordre par clé + reconcile, et le système survivra calmement au réseau, aux pics de charge et aux erreurs humaines.