lucid.page lucid.page
Text size
Read time9 min

Un flux d’événements peut sembler sain tout en racontant une histoire fausse. Une API répond, les messages continuent d’arriver et le tableau de bord reste vert ; pourtant, une reconnexion peut renvoyer les mêmes observations, un relais peut changer leur ordre, ou une réponse peut se perdre après que le serveur a déjà validé l’écriture. Le problème n’est donc pas seulement de recevoir des données. Il faut pouvoir distinguer ce qui est nouveau, répété, tardif, corrigé ou contradictoire.

Contents

L’idempotence est souvent résumée par une formule : traiter deux fois le même message doit produire le même résultat que le traiter une fois. Cette définition est utile, mais incomplète pour des données de marché ou des événements blockchain. Si le système élimine un doublon sans conserver la raison de sa décision, il évite peut-être une double écriture, mais il perd une partie de la preuve opérationnelle. Mon objectif est plus précis : empêcher les effets répétés tout en gardant visible l’incertitude qui accompagne chaque événement.

# Commencer par un contrat d’événement

Avant de choisir une file, une base ou un framework, je définis l’enveloppe minimale qui accompagne le contenu métier :

Ces champs ne sont pas interchangeables. source_event_id décrit l’identité ; source_sequence peut décrire un ordre local ; occurred_at exprime le temps métier ; received_at mesure le trajet jusqu’à nous ; ingestion_id distingue les tentatives techniques. Les fusionner dans une seule clé fabrique des ambiguïtés difficiles à corriger plus tard.

# L’identité n’est pas l’ordre

Une clé d’idempotence fiable commence généralement par le couple (source, source_event_id). La source fait partie de la clé parce que deux producteurs peuvent employer le même identifiant sans parler du même événement. Si l’identifiant n’existe pas, une empreinte composée peut servir de solution de repli, mais elle doit être décrite comme une heuristique, pas comme une certitude.

Par exemple, dédupliquer uniquement par symbole et timestamp est dangereux. Deux transactions légitimes peuvent concerner le même marché pendant la même milliseconde. À l’inverse, une même observation retransmise peut recevoir un nouvel horaire de livraison. L’empreinte doit donc porter sur les champs qui définissent réellement l’événement selon le contrat de la source, avec une version explicite de la règle de calcul.

Le numéro de séquence ne doit pas devenir une identité par défaut. Une séquence peut repartir à zéro après un redémarrage, être indépendante par partition ou contenir des trous autorisés. Avant de l’utiliser, il faut connaître son périmètre : connexion, instrument, compte, partition ou producteur entier.

# Classer les répétitions au lieu de les effacer silencieusement

À la première réception, le système enregistre la clé, l’empreinte et le contenu brut. Une nouvelle réception avec la même clé peut ensuite appartenir à deux catégories très différentes :

  1. Doublon exact : la clé et l’empreinte sont identiques. Aucun nouvel effet métier n’est produit, mais un compteur de répétition et le dernier horaire de réception peuvent être mis à jour.
  2. Doublon conflictuel : la clé est identique, mais l’empreinte diffère. Il ne faut ni remplacer silencieusement l’ancienne valeur ni accepter les deux comme si elles étaient indépendantes. L’événement doit être placé en quarantaine avec les deux versions et la règle qui a détecté le conflit.

Cette séparation évite qu’un mécanisme de déduplication transforme une correction, une corruption ou un changement de contrat en disparition invisible. Elle aide aussi à répondre à une question simple lors d’un incident : avons-nous reçu deux fois la même chose, ou deux choses différentes portant le même nom ?

# Rendre l’écriture et l’effet cohérents

Le cas classique de double traitement apparaît lorsque le service écrit l’événement, produit un effet, puis tombe avant d’accuser réception. Le producteur réessaie et le même effet est déclenché une seconde fois. Une contrainte d’unicité protège la table d’événements, mais pas automatiquement une notification, une mise à jour d’index ou un appel vers un autre service.

Un modèle simple consiste à enregistrer l’événement accepté et un message d’outbox dans la même transaction locale :

text
begin transaction
  insert event if idempotency_key is new
  if inserted:
    insert outbox_message(event_id, effect_type, status='pending')
commit

worker:
  claim pending outbox_message
  deliver using event_id as downstream idempotency key
  mark delivered

Ce pseudocode n’impose pas une technologie particulière. L’invariant important est que l’événement et l’intention de produire l’effet apparaissent ensemble, ou n’apparaissent pas du tout. Le consommateur suivant doit appliquer sa propre clé d’idempotence ; une outbox réduit une fenêtre de panne, elle ne rend pas magiquement tout le réseau « exactement une fois ».

# Séparer retard, obsolescence et désordre

Un événement tardif n’est pas nécessairement invalide. Il peut compléter un historique, corriger une agrégation passée ou expliquer une rupture. En revanche, il ne doit pas toujours remplacer l’état courant affiché à l’utilisateur. Il faut donc séparer au moins trois décisions :

Une borne d’avancement (watermark) peut aider : pour une partition donnée, le système suit le plus grand occurred_at observé et lui retranche un budget de retard documenté. Tout événement plus ancien que cette limite est classé comme tardif. Ce budget n’est pas universel. Il dépend du comportement réel de la source et du coût d’une correction. La valeur doit être configurable, versionnée et visible dans les journaux de décision.

Le temps futur mérite aussi un état propre. Un occurred_at supérieur à received_at au-delà d’une tolérance ne prouve pas une fraude ; il peut signaler une horloge mal synchronisée ou une unité incorrecte. Le bon comportement est de marquer l’écart et de conserver la donnée pour examen, pas de réécrire l’heure pour la rendre plausible.

# Les corrections doivent conserver leur filiation

Modifier sur place un événement déjà accepté rend les relectures impossibles à expliquer. Une correction plus sûre est un nouvel événement contenant une relation comme supersedes, l’identifiant de la version remplacée et un motif. La projection courante peut alors choisir la version active, tandis que l’historique conserve la chaîne de décisions.

Ce modèle est particulièrement utile quand une source publie d’abord une valeur provisoire puis une valeur finale. Les deux messages ne sont pas des doublons ; ils représentent deux états du même fait. La clé métier, la version et la relation de remplacement doivent permettre de les distinguer sans inventer une chronologie après coup.

# Rejouer sans multiplier les effets

Un replay est inévitable : reconstruction d’un index, correction d’un bug, migration d’un schéma ou reprise après incident. Pour qu’il soit sûr, il faut distinguer l’événement brut de ses projections. J’associe à chaque projection une projection_version et, pour chaque exécution, un replay_id. Le traitement peut alors répondre séparément à trois questions : l’événement brut existe-t-il déjà, cette version de projection l’a-t-elle déjà consommé, et les effets externes sont-ils autorisés pendant ce replay ?

Par défaut, un replay de calcul ne devrait pas renvoyer des notifications, ordres ou webhooks historiques. Ces effets demandent une autorisation explicite et une clé propre. Cette séparation rend possible la reconstruction d’un état sans simuler accidentellement le passé sur des systèmes actifs.

# Observer les décisions, pas seulement le débit

Le nombre de messages par seconde dit peu de chose sur leur qualité. Un tableau opérationnel utile suit plutôt :

Les identifiants d’événement ne doivent pas devenir des labels de métriques à forte cardinalité. Ils appartiennent aux traces ou aux journaux structurés. Les métriques agrègent par source, catégorie de décision et version de contrat ; les traces permettent ensuite d’examiner un cas précis.

# Tester les frontières de panne

Les tests les plus utiles ne se limitent pas à envoyer deux fois le même JSON. Je construis une petite matrice de scénarios synthétiques :

Pour chaque scénario, le test doit vérifier quatre sorties : le registre brut, la projection courante, les effets externes et la raison de la décision. Vérifier uniquement la valeur finale peut masquer un double effet ou une correction perdue.

# Un socle raisonnable pour une petite équipe

Il n’est pas nécessaire de commencer par une architecture distribuée complexe. Une base transactionnelle peut contenir le registre d’événements, une contrainte unique, l’outbox et les états de projection. Une tâche de réconciliation compare régulièrement les événements acceptés, les messages d’outbox et les accusés de traitement. La complexité supplémentaire n’est justifiée que lorsque les limites mesurées de ce modèle sont atteintes.

Le principe reste le même quelle que soit l’infrastructure : ne pas confondre répétition et conflit, ne pas confondre retard et invalidité, ne pas faire disparaître une correction, et rendre chaque effet rejouable ou explicitement non rejouable. L’idempotence utile n’est pas le silence obtenu en supprimant des lignes ; c’est la capacité de reproduire une décision et d’expliquer pourquoi aucun effet supplémentaire n’a eu lieu.

# Contexte de l’auteur

Je suis Mushegh Manukyan, Founder & CEO d’ARMCP à Erevan, en Arménie. Je travaille sur des sujets liés aux données de marché, aux outils Web3 et à la manière de rendre l’état d’un système plus compréhensible. Cette note présente un modèle de conception et des exemples synthétiques ; elle ne décrit pas une mesure de production ni un résultat de performance, et ne constitue pas un conseil financier.

Note éditoriale : une IA a contribué à la structuration du plan et à la révision linguistique de ce texte. Les choix d’architecture restent des propositions à adapter et à vérifier dans le contexte réel de chaque système.

End