Pattern qui revient sur n’importe quel système qui prend des décisions sur des mesures bruitées : capteurs IoT, GPS, télémétrie, monitoring d’un service distant. Tu as une state machine simple (ex : inside_zone ↔ outside_zone) et tu veux décider quand transitionner sur la base de mesures qui parfois mentent.
Le piège du gating symétrique
La tentation : « si la mesure est de qualité douteuse, on ignore la transition ». Symétrique, propre.
if (isReliableFix(measurement)) {
if (currentState === A && shouldGoToB(measurement)) transition(A → B);
if (currentState === B && shouldGoToA(measurement)) transition(B → A);
}
Problème : si le système entre dans un mode où il rapporte systématiquement des mesures dégradées (capteur en veille, GPS en mode économie, service distant down qui retourne unknown), tu te retrouves coincé dans un état que tu ne peux plus quitter.
Cas concret vécu : un tracker GPS qui en mode économie envoie des positions sans sat → fail-closed strict bloque les deux transitions → si le device est sorti puis rentré et reste immobile, l’épisode « sorti » ne se ferme jamais.
La bonne approche : asymétrie selon la criticité
Examiner pour chaque transition :
- Quel est le coût d’un faux positif (transition à tort) ?
- Quel est le coût d’un faux négatif (transition manquée) ?
Pour le cas géofence :
inside → outside(ouvrir l’épisode + alerter) : faux positif = SMS de panique pour rien → TRÈS coûteux, fail-closed strictoutside → inside(fermer l’épisode silencieusement) : faux positif = on déclare rentré alors que pas encore → peu coûteux (la prochaine mesure rouvrira un épisode), fail-open
// fail-OPEN, lax : ferme l'épisode même sur fix douteux
if (state === outside && shouldBeInside(m) && isPlausibleInsideFix(m))
transition(outside → inside);
// fail-CLOSED, strict : ouvre l'épisode seulement sur fix fiable
if (state === inside && shouldBeOutside(m) && isReliableFix(m))
transition(inside → outside);
Généralisation
Le pattern s’applique à plein de cas :
- Alerting service down/up : déclarer « down » sur N fails consécutifs (strict, anti faux-positifs de panique), déclarer « up » sur 1 success (lax, on est content d’arrêter d’alerter)
- Auto-scaling : scale-up agressif (lax, on monte vite pour éviter la dégradation), scale-down conservateur (strict, attendre plusieurs périodes basses pour éviter le flapping)
- Health check Kubernetes :
readinessProbeplus tolérant (on garde le pod si en doute),livenessProbeplus strict (on tue si vraiment down, parce que kill+restart est moins coûteux qu’un pod zombie) - Détection de chute IMU : fail-open sur « pas de chute » (on suppose OK), fail-closed sur « chute détectée » (on confirme avec plusieurs frames avant d’alerter pompiers)
Le bon raisonnement n’est pas « fail-closed parce que safe ». C’est fail vers l’état le moins coûteux en cas d’erreur.
Lessons learned
- Le gating symétrique sur la qualité de mesure crée des deadlocks quand le système entre dans un mode « mesures dégradées permanentes »
- Pour chaque transition d’état, expliciter le coût du faux positif vs faux négatif — la conclusion sur fail-open/closed en découle, pas l’inverse
- Documenter l’asymétrie dans le code avec un commentaire explicite — sinon ça ressemble à un bug ou à une incohérence pour un futur lecteur
- Tester chaque cas de figure : mesure manquante, mesure dégradée, mesure normale, et pour chaque direction de transition (matrice 3×2 minimum). Sinon on découvre les deadlocks en prod
- Le « fail-safe » naïf est souvent fail-stuck : bloquer toutes les actions quand on doute = ne plus rien faire, ce qui n’est pas safe pour la plupart des systèmes — c’est juste un freeze
- Prévoir un override manuel (bouton « fermer épisode » dans l’UI, commande admin…) pour les cas où la state machine se coince malgré l’asymétrie — un humain doit toujours pouvoir débloquer
#state-machine #patterns #robustness #design #monitoring #alerting #iot