Aller au contenu

Supervision des crons graphile

Vocabulaire

Quatre objets distincts, souvent confondus — y compris dans nos propres commentaires de revue.

Mot Ce que c'est Où il vit
task le code, identifié par task_identifier (sync-lw-wires) le taskList du worker
cron une entrée de planning qui crée des jobs pour une task cronItems dans le code, suivi dans _private_known_crontabs
job une unité de travail en file : une task, un payload, une heure, un compteur de tentatives graphile_worker.jobs — supprimée au succès
run une tentative d'exécution d'un job nulle part : graphile n'en garde rien

La chaîne : un cron crée un job, qui exécute une task, en une ou plusieurs runs.

Conséquence pour l'UI : « abandonné » est un état de job (la machine à retenter a renoncé), « échec » est l'issue d'un run. Les deux ne se comparent pas ligne à ligne — un job abandonné a produit 25 runs en échec.

Piège : l'identifiant d'un cron n'est pas celui de sa task. _private_known_crontabs.identifier vaut sync-lw-wires-task là où jobs.task_identifier vaut sync-lw-wires. Le catalogue du dashboard s'appuie sur cette différence — les « corriger » pour qu'ils coïncident casse silencieusement l'affichage du dernier déclenchement.

La décision

Le dashboard back-office (/administration/crons, /administration/queues, /administration/jobs) ne lit que ce que graphile expose. Aucune table maison, aucun code posé sur les tâches de production.

Une couche d'audit a été écrite puis retirée avant le merge (PR #5953). Le pourquoi et la façon de la reprendre sont en bas de page.

Ce que graphile donne, et ce qu'il ne donne pas

Besoin Source Limite
Catalogue des crons cron-registry.ts (le code) —
Catalogue des queues queue-registry.ts (le code) pas de crontab : une task queue n'a pas de lastExecutionAt
Dernier déclenchement graphile_worker._private_known_crontabs.last_execution dit que la planification a tourné, pas que la tâche a réussi — CRON only
File en cours vue graphile_worker.jobs un job réussi est supprimé : la file ne contient que l'attente, l'en-cours et le cassé
Message d'erreur jobs.last_error le message seul — la stack part au logger (worker.js:255), jamais en base
Relancer / abandonner reschedule_jobs, permanently_fail_jobs ignorent silencieusement un job verrouillé : 0 ligne renvoyée

Conséquence directe : un cron sain n'apparaît nulle part. Le diagnostic opérationnel se lit en croisant « déclenché récemment » et « rien de coincé dans la file ». Même lecture pour une queue : une task saine a une file vide. Le payload n'est pas sur la liste CRON ; GET /administration/queues/:task/jobs le joint depuis _private_jobs parce qu'un job queue sans projectId est illisible.

Les pièges de graphile-worker (0.16.6)

  • makeWorkerUtils migre le schéma à l'initialisation (dist/lib.js:303), sans option pour l'éviter (issue #475, fermée). Un simple bouton « relancer » aurait fait migrer la base depuis le process web. On appelle les fonctions SQL directement.
  • Aucun statut stocké : il se déduit du verrou et du compteur de tentatives. Cette dérivation doit vivre une seule fois — ici un CASE en SQL, sur lequel la liste filtre via une table dérivée. Deux écritures de la même règle finissent par diverger, et le filtre contredit le libellé.
  • La vue jobs n'expose pas payload : seule colonne lue dans _private_jobs, à revérifier à chaque montée de version.
  • Un job n'est pas une exécution : la ligne survit à ses 25 tentatives, seul le compteur bouge.
  • La planification est figée au boot : changer un horaire demande de redémarrer le runner (issue #318), et backfillPeriod: 0 signifie qu'un cron manqué n'est jamais rattrapé.
  • Les verrous d'un worker mort sont libérés en 8 à 10 minutes (dist/config.js:28), et seulement par un runner en marche.

Si on reprend l'historique des runs (V2)

Le code existe, relu et testé, dans le commit 6b489955e :

git checkout 6b489955e -- projects/api/migration/flyway/V202608141500__create-audit-graphile-task-run.sql
git show 6b489955e:projects/api/src/__new/lib/graphile-worker/create-task-list-with-run-audit.ts

Ce qu'il apporte : statut réel par exécution, durée, erreur complète ({ name, message, stack, causes }), et un statut interrupted pour les process tués.

Quatre contraintes découvertes en route, à ne pas réapprendre :

  1. L'écriture d'audit ne doit jamais faire échouer une tâche déjà exécutée. Elle tourne dans la promesse de la tâche : si l'erreur remonte, graphile marque le job en échec et rejoue un cron dont les effets sont commités — sur un cron de paiement, c'est payer deux fois. Avaler l'échec, après quelques tentatives.
  2. Un run laissé ouvert doit être balayé, mais pas au seul démarrage du worker : un process qui crashe et redémarre voit ses propres jobs encore verrouillés. Balayage périodique, par corrélation avec le verrou graphile, jamais par seuil de durée (six minutes sont normales pour refresh-leaderboard).
  3. interrupted veut dire « issue inconnue », pas « n'a pas tourné » : une panne d'écriture au moment de clôturer produit le même état qu'un process tué. L'UI ne doit pas laisser croire à l'opérateur que la tâche n'a pas eu lieu.
  4. Le wrapper va sur le taskList du runner, pas dans createCronTask : 3 tâches sur 27 l'utilisent, les 24 autres construisent leur Task à la main.