Durable Delays : gérer le temps comme un état distribué

SchemaBridge Team · 2026-01-10 · Time, Scheduling, Orchestration

Gérer des attentes de 30 jours sans fuite mémoire. Pourquoi `sleep()` est un anti-pattern des systèmes distribués.

Le mensonge de sleep() : pourquoi le temps est la primitive distribuée la plus difficile

Tous les langages de programmation disposent d'une commande pour attendre. En Python, c'est time.sleep(). En Node.js, c'est setTimeout(). En Java, c'est Thread.sleep(). Ces commandes sont simples, intuitives et, pour tout ce qui dépasse quelques secondes, totalement inutiles dans un système distribué en production.

L'humble sleep() est un mensonge parce qu'il suppose un environnement stable. Il suppose que la machine qui exécute le code restera en vie, que le processus ne sera pas tué par un load balancer et que la mémoire ne sera pas récupérée par l'OS. Dans un environnement cloud moderne, ces hypothèses sont fausses. Si vous faites sleep(24 60 60) (une journée) dans un pod Kubernetes standard, ce pod sera très probablement remplacé, replanifié ou redéployé avant la fin de la journée.

Quand le processus meurt, votre « sleep » meurt avec lui. Votre logique métier se perd dans le vide. C'est ainsi que naît le « logiciel amnésique » : des systèmes qui perdent la trace des périodes d'essai des clients, ratent des renouvellements d'abonnement et n'envoient pas des e-mails de relance critiques.

La hiérarchie du temps : des secondes aux mois

Pour gérer le temps correctement, il faut d'abord le catégoriser. Tous les délais ne se valent pas :

1. Délais transitoires (millisecondes à secondes) : Il s'agit généralement de backoffs réseau ou d'attentes sur un verrou de base de données rapide. sleep() est parfois acceptable ici, car le risque de crash dans une fenêtre de 50 ms est faible.

2. Délais courts (minutes) : C'est là que sleep() commence à échouer. Vous immobilisez un thread de worker ou les ressources d'un conteneur pendant des minutes sans rien faire. C'est du gaspillage et un risque pour le pool de connexions.

3. Durable delays (heures à mois) : C'est le domaine des cycles de vie métier. Un essai gratuit de 14 jours, un délai de paiement de 30 jours ou un calendrier de maintenance semestriel. Ils ne peuvent pas vivre dans le code ; ils doivent vivre dans l'infrastructure.

L'architecture des workflows endormis

Chez SchemaBridge, nous traitons le temps comme un état durable. Lorsque votre workflow atteint un vertex DELAY, il ne bloque aucun thread. L'étape est mise en attente avec le statut SUSPENDED, l'intention de réveil est écrite en base de données, et le worker est immédiatement libre pour d'autres tâches.

Le modèle de polling en base (faible précision, haute durabilité)

L'approche artisanale classique stocke l'« heure de réveil » dans une table, et un worker en arrière-plan (le « poller ») interroge la table toutes les quelques secondes : SELECT * FROM timers WHERE wake_up < NOW(). C'est durable, car un enregistrement en base survit aux redémarrages — mais vous le payez par un flux constant de requêtes vides et un plancher de précision égal à votre intervalle de polling.

L'approche SchemaBridge

SchemaBridge conserve la durabilité de l'enregistrement en base et abandonne le polling à intervalle fixe. Chaque réveil est persisté sous la forme d'une ligne avec un execute_at absolu dans une table PostgreSQL scheduled_tasks (derrière notre ScheduledTaskRepository). Le clock worker dort jusqu'au plus proche execute_at en attente, et il est réveillé plus tôt par un push PostgreSQL LISTEN/NOTIFY chaque fois qu'une tâche devient exécutable. Quand des tâches sont dues, il les réclame en une seule instruction avec FOR UPDATE SKIP LOCKED, de sorte que plusieurs réplicas du clock worker peuvent se répartir le backlog sans entrer en collision. Un plafond d'inactivité large n'existe qu'en filet de sécurité, au cas où une notification serait manquée.

-- Forme simplifiée du claim : lignes échues, les plus anciennes d'abord, réparties entre réplicas
UPDATE scheduled_tasks SET status = 'PROCESSING'
WHERE task_id IN (
  SELECT task_id FROM scheduled_tasks
  WHERE status = 'PENDING' AND execute_at <= :now
  ORDER BY execute_at
  FOR UPDATE SKIP LOCKED
  LIMIT 100
)
RETURNING task_id;

Annulation et sorties anticipées : changer le futur

Un durable delay ne raconte que la moitié de l'histoire. Si un utilisateur se trouve dans une étape « Attendre 3 jours le paiement » et qu'il paie au bout de 2 heures, que doit-il se passer ?

D'abord, l'annulation. Arrêter un run dans SchemaBridge annule l'exécution, puis désarme les timers de délai et d'approbation en attente de ce run, de sorte qu'un parcours arrêté ne se réveille pas quelques jours plus tard pour continuer. L'ordre est délibéré — annuler d'abord, désarmer ensuite — si bien que le pire cas est un timer qui se déclenche sur un run déjà annulé, où les gardes d'état terminal en font un no-op inoffensif.

Ensuite, le réveil anticipé. SchemaBridge ne dispose pas d'une primitive intégrée « réveiller ce délai à l'arrivée d'un événement externe ». Un vertex DELAY attend la durée configurée. Il existe deux façons honnêtes de modéliser une sortie anticipée :

Aucun de ces deux patterns n'oblige le moteur à faire courir un timer contre un signal, ce qui est précisément le genre de course que l'on veut éliminer par conception plutôt que de s'y fier.

Dérive d'horloge et précision

Dans tout système distribué, les horloges ne sont jamais parfaitement synchronisées. C'est le phénomène de dérive d'horloge (clock drift). Si l'horloge d'une machine avance de 50 ms sur celle d'une autre, un « Attendre 1 seconde » peut se comporter différemment selon l'endroit où la tâche est prise en charge.

SchemaBridge limite le problème en faisant du réveil un horodatage absolu stocké en un seul endroit : la colonne execute_at dans PostgreSQL. L'armement d'un délai calcule now + delaySeconds une seule fois et le stocke ; dès lors, aucun worker ne tient de compte à rebours en mémoire, il n'y a donc pas de timer par worker susceptible de dériver ou d'être perdu au redémarrage. Le décalage d'horloge ordinaire entre machines existe toujours, mais pour des délais métier mesurés en heures ou en jours, il est sans incidence sur le résultat.

Scénario illustratif : automatiser une campagne drip de 30 jours

Prenons une plateforme de marketing automation aux prises avec la logique de son « Welcome Journey ».

Le défi

Un parcours comprend :

Bâtis sur une solution maison faite de cron jobs et d'une table Postgres, de tels parcours sont fragiles : pendant une fenêtre de maintenance de la base de données ou un déploiement, une partie des utilisateurs peut rester « bloquée » dans un état d'attente et ne jamais recevoir l'e-mail suivant.

La méthode SchemaBridge

Imaginez les mêmes parcours reconstruits sous forme de workflows SchemaBridge.

1. Délais visuels : Vous glissez un vertex DELAY dans le graphe et réglez delaySeconds sur 259200 (3 jours) et 604800 (7 jours). L'éditeur les affiche sous forme de durées compactes comme « 3d ».

2. Reprise durable : Pendant les déploiements, les runs en attente restent simplement stationnés en base de données. Quand le clock worker revient, il réclame les timers arrivés à échéance pendant l'interruption, les plus anciens d'abord, et les reprend.

3. Revérifier au réveil : Après chaque délai, une étape Gateway demande au fournisseur d'e-mail ou de facturation si l'utilisateur a cliqué ou est passé à l'offre supérieure, et une arête conditionnelle choisit l'étape suivante.

Le bénéfice attendu

Comparaison : stratégies de planification existantes

| Fonctionnalité | setTimeout() | Cron jobs / Quartz | Durable Delays SchemaBridge |

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

| Persistance | Aucune (volatile) | Manuelle (adossée à une base) | Native (PostgreSQL scheduled_tasks) |

| Scalabilité | Faible (liée à la RAM) | Moyenne (goulot d'étranglement en base) | Élevée (SKIP LOCKED entre réplicas) |

| Annulation | Complexe (handle manuel) | Nettoyage manuel en base | Arrêter un run désarme ses timers |

| Visibilité | Aucune | Requêtes SQL | Étapes en attente (suspendues) visibles dans la vue de l'exécution |

| Résolution | Millisecondes | Secondes/minutes | Réveil par push (LISTEN/NOTIFY), sans polling fixe |

Checklist d'expert pour la logique métier de longue durée

Si vous concevez un système qui attend plus de 5 minutes, suivez ces heuristiques :

1. Libérez le thread : Ne bloquez jamais un worker pendant l'attente. Votre worker doit être sans état et prêt à être tué à tout moment.

2. Externalisez l'horloge : Utilisez un stockage central et durable pour conserver l'heure de réveil, et non le timer en mémoire de la machine locale.

3. Prévoyez les issues anticipées : Partez toujours du principe que l'utilisateur peut effectuer l'action que vous attendez avant l'expiration du délai. Revérifiez l'état à la fin de l'attente au lieu de supposer que rien n'a changé.

4. Auditez la salle d'attente : Surveillez le nombre d'exécutions dont des étapes attendent un minuteur. C'est un indicateur important de la santé de l'activité.

5. Préférez les délais relatifs : Exprimez une attente comme une durée à partir de l'étape précédente (delaySeconds) plutôt que comme une date calendaire fixe, afin que l'écart relatif entre les étapes reste constant si une étape antérieure prend du retard.

Conclusion : le temps est un problème d'infrastructure

Un logiciel qui oublie est un logiciel qui échoue. En traitant le temps comme un état durable de premier plan, nous comblons le fossé entre les événements temps réel et le comportement humain à forte latence. Avec SchemaBridge, un parcours qui s'étend sur des jours ou des semaines survit aux redémarrages et aux déploiements qui tueraient un thread endormi — à condition de prévoir les sorties anticipées et de transporter les données dont les étapes ultérieures ont besoin.

La capacité à maîtriser de manière fiable le timing de vos interactions est un véritable avantage concurrentiel. Nous fournissons l'horloge durable ; vous fournissez le parcours.

Fait partie de notre série Building the Bridge. Voir aussi : Le fossé de l'observabilité pour déboguer des runs longs et multi-étapes sans vous perdre dans les logs.

Explorer