Le moteur d'idempotence : garantir l'intégrité transactionnelle

SchemaBridge Team · 2025-12-22 · Idempotency, Consistency, Transactions

Exécution at-least-once, déduplication en entrée et clés côté destination : où les doublons sont réellement arrêtés dans un workflow distribué, et quelle part vous revient.

Le cauchemar du double prélèvement : pourquoi les systèmes distribués détestent les retries

Dans notre monde de réseaux instables et de ressources cloud éphémères, les pannes ne sont pas seulement fréquentes : elles sont un état permanent. Tout ingénieur senior a vécu le cauchemar de « l'action orpheline », un scénario qui commence par un petit incident réseau et finit en corruption de données ou en responsabilité financière. C'est le scénario de panne classique des systèmes distribués :

1. Requête : votre orchestrateur envoie une requête de « prélèvement » à une API de paiement ou d'« expédition » à un transporteur.

2. Succès : l'API tierce traite la requête avec succès, débite la carte du client ou imprime l'étiquette d'expédition.

3. Partition : un bref incident réseau survient sur le chemin de retour. La réponse de l'API — la confirmation vitale — n'atteint jamais votre machine worker.

4. Retry : votre worker, constatant un timeout, suppose selon la pratique courante que le traitement a échoué. Il suit sa politique de retry et renvoie la requête.

5. Doublon : comme l'API n'a reçu aucune identité unique pour cette intention précise, elle traite la requête à nouveau. Elle la considère comme une nouvelle transaction. Le client est débité deux fois, ou deux étiquettes d'expédition sont générées pour la même commande.

Ce n'est pas un échec de la logique du code ; c'est un échec d'identité. Sans moyen d'identifier de façon unique une intention précise à travers le temps et l'espace, votre système joue avec les données et l'argent de vos clients à chaque nouvelle tentative de connexion.

Les transactions distribuées sont mortes : vive l'idempotence

Dans une architecture monolithique, nous nous appuyons sur le Two-Phase Commit (2PC) ou sur des verrous distribués globaux pour garantir la cohérence. Ces outils nous permettent de traiter plusieurs opérations comme une seule unité de vérité. Mais dans un monde fragmenté d'API SaaS, de workers serverless et de microservices polyglottes, les transactions globales sont une chimère. Elles ne passent pas à l'échelle, introduisent une latence massive, et la plupart des fournisseurs tiers ne les prennent pas en charge. Elles exigent un « verrouillage » des ressources impossible à obtenir au-delà des frontières entre organisations.

La seule voie viable vers la cohérence distribuée est l'idempotence. Mathématiquement, une opération est idempotente si elle peut être appliquée plusieurs fois sans changer le résultat au-delà de la première application. En termes algébriques : f(x) = f(f(x)). En termes d'ingénierie, cela signifie que votre système peut échouer et réessayer autant de fois que nécessaire, et que le résultat final sera toujours le bon.

Le contrat honnête : at-least-once, avec garde-fous

Soyons précis sur ce qu'un orchestrateur peut promettre et ce qu'il ne peut pas. SchemaBridge livre le travail en at-least-once. Un worker peut planter après que l'appel HTTP a réussi mais avant d'avoir enregistré le résultat ; un message peut être relivré ; un lease de claim peut expirer et être repris. Dans chacun de ces cas, le comportement sûr consiste à réexécuter l'étape plutôt qu'à la perdre — ce qui signifie qu'une étape peut, rarement, s'exécuter deux fois.

Ce que fait la plateforme, c'est rendre ce « rarement » aussi rare que possible, à deux niveaux :

Niveau 1 : déduplication des triggers entrants

Chaque déclenchement d'un entry point reçoit un ID d'exécution et, par défaut, cet ID est dérivé du contenu : un SHA-256 déterministe calculé sur le workspace, le vertex d'entry point et le payload de la requête. La même livraison de webhook arrivant deux fois aboutit à la même exécution au lieu d'en démarrer une seconde — exactement ce que vous voulez lorsqu'un émetteur réessaie. Vous contrôlez l'identité avec trois leviers :

| Levier | Où | À utiliser quand |

| :--- | :--- | :--- |

| idempotencyFields | Configuration du vertex d'entry point | Vous voulez dédupliquer sur un identifiant métier (p. ex. order.id) plutôt que sur le corps entier |

| clientIdempotencyId | Paramètre de query de l'appel de trigger | L'appelant possède la clé de déduplication |

| forceNewExecution | Uniquement les exécutions de test et l'appel d'exécution en streaming (activé par défaut) | Indisponible sur les triggers de production — utilisez-y idempotencyFields ou un clientIdempotencyId unique par appel |

{
  "type": "ENTRY_POINT",
  "config": {
    "idempotencyFields": "order.id"
  }
}

Le revers de la médaille est un vrai piège : si deux exécutions légitimes peuvent porter des payloads identiques à l'octet près (« lancer le rapport nocturne avec un corps vide »), elles seront fusionnées en une seule, sauf si vous désactivez ce comportement. Les triggers planifiés (cron) font exception — chaque déclenchement reçoit un nouvel ID aléatoire, car une planification n'a jamais vocation à être dédupliquée. Sachez aussi que la déduplication explicite est une recherche lecture-puis-écriture, et non une contrainte d'unicité en base de données : deux triggers réellement concurrents avec la même clé peuvent donc passer tous les deux.

Niveau 2 : garde-fous au niveau de l'étape

Au sein d'une exécution, une étape déjà COMPLETED n'est pas réexécutée, un worker doit réclamer atomiquement une étape (DISPATCHED → EXECUTING) avant de l'exécuter, les transitions terminales sont protégées, et une époque de claim isole un worker dont le lease a été repris. Ces garde-fous bloquent les chemins de duplication courants — mais aucun d'eux ne peut pénétrer dans une API tierce pour annuler un appel déjà effectué.

Le niveau 3 vous appartient : la clé côté destination

Cette dernière brèche explique pourquoi le contrat de comportement de la plateforme l'énonce clairement : rendez l'effet de bord idempotent côté destination, et ne comptez pas sur l'orchestrateur pour l'exécuter exactement une fois.

La bonne nouvelle, c'est que la plupart des API sérieuses (paiement, expédition, messagerie) acceptent une clé d'idempotence, et qu'une étape Gateway peut en envoyer une dérivée des données de l'exécution. Dans SchemaBridge, la valeur d'un header peut être une expression sur l'entrée de l'étape : la clé est donc calculée à partir d'un identifiant métier stable — jamais à partir d'un horodatage ou d'une valeur aléatoire :

{
  "type": "GATEWAY",
  "config": {
    "method": "POST",
    "url": "https://api.payments.example/v1/charges",
    "headers": {
      "Idempotency-Key": "= 'charge-' & payload.order.id",
      "Authorization": "Bearer {{ secrets.PAYMENTS_KEY }}"
    }
  }
}

Comme la clé ne dépend que de la commande, une réexécution de cette étape — après une relivraison, un Retry déclenché par un opérateur ou un retry automatique en cas de surcharge — envoie la même clé, et la destination renvoie le premier résultat au lieu de débiter à nouveau. Une réserve : Start over (une rejoue du workflow entier) crée une nouvelle exécution, mais si votre clé est dérivée des données métier plutôt que de l'ID d'exécution, elle reste stable même dans ce cas. C'est précisément pour cela que les identifiants métier font de meilleures clés que les identifiants du moteur.

Gérer les API legacy : le pattern « Read-Verify-Write »

Malheureusement, de nombreux systèmes legacy et fournisseurs SaaS de niche ne prennent pas en charge les clés d'idempotence. Ils n'ont pas de header Idempotency-Key. Pour ces endpoints « non idempotents », vous pouvez composer une séquence Read-Verify-Write à partir d'étapes ordinaires :

1. Étape de vérification (Read) : une étape Gateway interroge d'abord le système en aval pour savoir si l'enregistrement existe déjà (p. ex. GET /orders?external_id=123, en utilisant le même identifiant métier que celui que vous auriez utilisé comme clé).

2. Branche conditionnelle : un edge conditionnel examine la réponse. Si la commande existe, l'exécution prend le chemin « déjà fait ». Sinon, elle continue.

3. Étape d'action (Write) : ce n'est que si la vérification ne trouve rien que l'exécution passe à l'écriture proprement dite (POST /orders).

Connaître la fenêtre

Read-Verify-Write réduit la fenêtre de doublons ; il ne la ferme pas. Si un worker plante après l'écriture mais avant d'avoir enregistré le succès, l'étape d'écriture est réexécutée seule — elle ne revient pas en arrière pour revérifier d'abord. Pour une destination sans aucun support de l'idempotence, cherchez donc aussi une contrainte d'unicité naturelle côté destination (un external_id rejeté en cas de conflit), et traitez une réponse de conflit comme « déjà fait ». Par défaut, un 4xx fait échouer une étape Gateway ; régler clientErrorMode sur complete transforme une réponse d'erreur client en données sur lesquelles votre étape suivante peut router.

Le piège de la gestion des clés : pourquoi l'idempotence maison échoue à grande échelle

De nombreuses équipes d'ingénierie tentent de construire une « table d'idempotence » dans leur base de données principale. Cela crée trois problèmes critiques qui finissent par tuer la vélocité et la fiabilité :

1. Le goulot d'étranglement en écriture : chaque appel d'API exige désormais une écriture en base pour enregistrer le jeton. Sous forte charge, votre table d'idempotence devient un point de contention.

2. La complexité du nettoyage, le problème des déchets : les clés d'idempotence ne sont pas nécessaires éternellement. Il vous faut un processus d'arrière-plan ou un TTL (Time-To-Live) pour purger les anciennes clés. Si la purge est trop agressive, vous risquez des doubles prélèvements pour des tâches lentes et réessayées. Si elle est trop lente, la table croît sans limite.

3. L'incohérence d'état distribué : que se passe-t-il si l'écriture en base réussit mais que l'appel d'API échoue ? Ou si votre worker plante après l'appel d'API mais avant que la base puisse être mise à jour avec « Terminé » ? Vous vous retrouvez avec une incohérence qui exige une intervention manuelle.

La solution n'est pas une table plus intelligente de votre côté ; c'est de laisser le système qui réalise l'effet de bord prendre en charge la déduplication. Une destination qui respecte une clé d'idempotence a déjà résolu le stockage, l'expiration et la fenêtre de crash pour sa propre opération — votre rôle est de lui envoyer une clé stable.

Génération du jeton côté client ou côté serveur

Où le jeton doit-il être généré ?

Dans un workflow SchemaBridge, la source de l'intention, ce sont les données propres de l'exécution. Dériver la clé à cet endroit, dans le header de l'étape Gateway, la garde stable sur tous les chemins de retry que le moteur peut emprunter.

Tableau comparatif : où les doublons sont arrêtés

| Source du doublon | Arrêté par | Qui le configure |

| :--- | :--- | :--- |

| L'émetteur renvoie le même webhook | ID d'exécution dérivé du contenu / idempotencyFields / clientIdempotencyId | SchemaBridge, avec la clé de votre choix |

| Étape relivrée au sein d'une exécution | Garde-fous au niveau de l'étape (court-circuit si terminée, claim atomique, époque de claim) | SchemaBridge |

| L'étape est réexécutée alors que l'appel a déjà réussi | Clé d'idempotence côté destination | Vous, sur l'étape Gateway |

| La destination ne prend pas en charge l'idempotence | Read-Verify-Write plus une contrainte d'unicité côté destination | Vous |

Conseils d'expert : la checklist de l'idempotence

1. N'utilisez jamais d'horodatages : votre clé doit reposer sur les données, pas sur le temps.

2. Délimitez vos clés : préfixez les clés par opération (charge-, ship-) afin qu'une clé « Expédition » n'entre jamais en collision avec une clé « Facturation », même si elles partagent un ID de commande.

3. Gérez les conflits 409 : si une API renvoie un 409 (Conflict) pour une clé ou un identifiant qu'elle a déjà vu, votre workflow devrait généralement le traiter comme « déjà fait » si l'entrée correspond.

4. Décidez à l'avance du sort des payloads répétés : si deux exécutions authentiques peuvent porter des corps identiques, définissez idempotencyFields ou passez un clientIdempotencyId unique par appel — sinon la seconde exécution sera éliminée par déduplication. (forceNewExecution n'existe que pour les exécutions de test et l'appel d'exécution en streaming.)

5. Intégrez la clé au modèle : si un développeur doit se souvenir d'ajouter une clé d'idempotence, il finira par l'oublier. Placez-la dans la configuration Gateway que vous réutilisez pour cette API.

Conclusion : l'identité est la colonne vertébrale de la vérité

Dans un système distribué, vous ne pouvez pas faire confiance au réseau, vous ne pouvez pas faire confiance à l'horloge, et vous ne pouvez pas faire confiance à la réponse. La seule chose en laquelle vous pouvez vraiment avoir confiance, c'est l'identité.

SchemaBridge vous donne l'identité aux frontières qu'il contrôle — IDs d'exécution dérivés du contenu, clés de déduplication explicites et garde-fous au niveau de l'étape — et reste honnête sur la frontière qu'il ne contrôle pas : l'effet de bord chez le tiers. Dérivez une clé stable de vos données métier, envoyez-la à chaque appel, et le « cauchemar du double prélèvement » devient une décision de conception prise une fois pour toutes plutôt qu'un incident découvert plus tard.

Fait partie de notre série Building the Bridge. Voir aussi : Maîtriser le merge sur la synchronisation de l'état entre branches parallèles.

Explorer