← Journal technique

backend

Réparer l'état bloqué à chaque démarrage

Réparer l'état bloqué à chaque démarrage

Tout processus de longue durée qui gère des workflows avec état peut mourir exactement au mauvais moment et laisser quelque chose à moitié fini. Personne ne le sort de cet état, parce que le processus qui l'aurait fait a simplement cessé d'exister.

Concrètement : le back-end meurt pendant qu'un opérateur est en plein échange avec un client à un comptoir. Le ticket est bloqué dans called pour toujours, et le comptoir croit être occupé. Rien dans le système ne le remarquera jamais tout seul.

Le pattern

Lance une passe de récupération à chaque démarrage, accrochée au cycle de vie de démarrage du framework.

@Injectable()
export class RecoveryService implements OnModuleInit {
  async onModuleInit() {
    if (!recoveryEnabled()) return;
    await this.recoverStuckState();
  }

  async recoverStuckState() {
    const stuck = await this.findRecordsStuckInIntermediateState({
      olderThan: STALE_THRESHOLD,
    });
    for (const record of stuck) {
      await this.forceTransitionToSafeState(record);
    }
  }
}

Quelques détails qui comptent :

  • Un seuil de temps, pas « tout enregistrement dans cet état ». Quelque chose qui est entré dans l'état il y a dix secondes n'est pas bloqué, il est en plein vol. J'utilise 30 minutes pour un ticket called.
  • Idempotent par construction. Ça tourne à chaque démarrage, pas seulement après un crash. La logique n'a pas besoin de savoir si le dernier arrêt était propre — elle identifie juste correctement les enregistrements vraiment bloqués dans les deux cas. Pas de drapeau à mal gérer.
  • Une trappe de sortie par variable d'environnement. En restaurant une sauvegarde connue comme bonne, tu ne veux explicitement pas qu'une réparation automatique remette en question les données restaurées au démarrage suivant.
  • Vérifie ce qui pointe vers l'enregistrement bloqué, pas juste l'enregistrement. Le comptoir garde un pointeur vers son « ticket courant ». Si la récupération repousse ce ticket vers queued mais n'efface pas le pointeur du comptoir, le comptoir a l'air occupé en permanence avec un ticket que personne ne sert. Facile à manquer si tu ne penses qu'à l'entité principale.

Construis-le avant d'en avoir besoin

Cette classe de bug est invisible jusqu'à ce que le crash au mauvais timing précis arrive, puis très visible et pénible à corriger à la main — quelqu'un doit remarquer l'enregistrement bloqué, comprendre pourquoi, et intervenir sans aggraver les choses. Découvrir le besoin en plein incident, avec un vrai client debout à un comptoir qui ne se libère pas, est une pire façon de l'apprendre.