Le code de sortie 143 fait partie de ces nombres qu'on apprend une fois et qu'on voit ensuite partout. C'est 128 plus 15, et 15 c'est SIGTERM, et SIGTERM veut dire que quelque chose a demandé à ton processus de partir et que ton processus n'avait aucune idée de la façon de répondre. Un article Medium qui circule cette semaine raconte le cas d'une équipe data qui a perdu des jours là-dessus : un batch PHP nocturne sur environ deux millions d'enregistrements, deux à trois heures d'habitude, piloté par une unit systemd, qui se termine soudain à 60 % une nuit et à 50 % la suivante. Aucune exception. Aucune erreur fatale. Les logs s'arrêtent net, comme si on avait débranché la machine. La réponse était dans journalctl depuis le début, dans la ligne qui signale que le délai d'arrêt a été atteint.
<?php
declare(strict_types=1);
pcntl_async_signals(true);
$shouldExit = false;
pcntl_signal(SIGTERM, function () use (&$shouldExit) {
$shouldExit = true;
});
while (!$shouldExit) {
// one atomic chunk of work, then a checkpoint
sleep(1);
}
exit(0);Le correctif proposé par l'article est juste, et je le livrerais aujourd'hui même : pcntl_async_signals(true), un gestionnaire sur SIGTERM qui bascule un booléen, une vérification de ce booléen entre deux éléments de travail, exit 0. Ça tient vraiment en quelques lignes, c'est un comportement vérifié sur PHP 8.3, et ça convertit un mystère en une entrée de log. Mon argument, c'est que ces lignes sont la partie la moins chère du chantier et celle où tout le monde s'arrête. Le gestionnaire t'apprend qu'un arrêt a été demandé. Il ne te dit pas si ton job survit au fait d'être arrêté.
Voilà la forme du contrat. systemd envoie SIGTERM (ou ce que tu as mis dans KillSignal), attend TimeoutStopSec, qui vaut 90 secondes par défaut sur la plupart des installations récentes et cinq minutes sur certaines plus anciennes, puis envoie SIGKILL. Le signal 9 ne se négocie pas. PHP ne te laisse même pas faire semblant : tente un pcntl_signal(SIGKILL, ...) et tu récoltes une erreur fatale sur l'installation d'un gestionnaire pour le signal 9, c'est le noyau qui refuse à ta place. Tout le jeu consiste donc à terminer ton nettoyage à l'intérieur de la fenêtre de grâce. Et cette fenêtre, c'est une course qu'il faut gagner à chaque fenêtre de maintenance, à chaque déploiement qui redémarre l'unit, à chaque expiration de RuntimeMaxSec.
C'est pour ça que le premier réflexe est en général le mauvais. Quelqu'un se fait mordre, ouvre le fichier d'unit, et pousse TimeoutStopSec à dix minutes. Le pire cas est désormais borné par la chose la plus lente que tu aies jamais mesurée, et chaque redémarrage de serveur de la flotte l'attend. Pendant ce temps, le batch qui prenait deux heures en prend trois parce que la table a grossi, et le nombre choisi en mars devient discrètement faux en septembre. Tu n'as pas rendu le job reprenable. Tu t'es acheté une fenêtre plus large pour avoir de la malchance, et tu as ralenti l'extinction de tout ce qui partage cette machine.
Le contre-argument honnête, et il est solide, c'est que la plupart des boîtes PHP n'écrivent jamais une ligne de PCNTL et s'en portent très bien. Le queue:work de Laravel traite SIGTERM en finissant le job courant puis en sortant. Le messenger:consume de Symfony fait pareil. Donne-leur un TimeoutStopSec raisonnable, autour de 60 secondes pour des jobs de taille message, et c'est réglé. Je suis d'accord pour du trafic de file. Le piège, c'est que la gestion des signaux par le framework promet exactement une chose : que l'élément de travail en cours se termine. Si un élément est un message qui touche trois lignes, parfait. Si un élément est ta boucle de rapprochement mensuel qui lit deux millions d'enregistrements dans un seul appel, le gestionnaire attend poliment quelque chose qui ne finira pas, et SIGKILL arrive à l'heure prévue avec le code de sortie 137.
La vraie décision de conception, c'est donc la taille de ton plus petit point d'arrêt sûr. Découpe le batch en unités que tu serais content de rejouer, écris un point de reprise après chacune, rends l'unité assez idempotente pour qu'un nouvel essai soit d'un ennui total. À ce moment-là le délai de grâce cesse d'être un pari, parce que ton worker n'est jamais à plus de quelques secondes d'une sortie légale. C'est aussi ce qui rend RuntimeMaxSec, ou un redémarrage côté applicatif sur temps max, agréable plutôt qu'angoissant : tu obtiens un processus neuf sur un rythme que tu as choisi, mémoire récupérée, rien en vol. Une note en petits caractères à garder : pcntl_async_signals(true) n'est pas une décoration facultative. Sans lui, la livraison n'a lieu qu'aux points de dispatch, donc une boucle de calcul serrée sans I/O peut traverser tranquillement un SIGTERM qu'elle ne verra jamais.
Dernier morceau : rien de tout ça n'est observable si tu ne regardes pas. Les codes de sortie sont de la télémétrie gratuite et presque personne ne les met en graphe. Un 143 en production veut dire qu'on a demandé à un processus de s'arrêter et qu'il n'a pas su répondre. Un 137 en dehors de tes fenêtres de déploiement signale en général que le tueur d'OOM est passé avant, et dmesg te le confirmera. Mets un compteur sur les deux, alerte quand ils bougent, et tu trouves le chemin d'arrêt cassé dès la première semaine au lieu de le découvrir après un trimestre de rapports à moitié faits. Envoie un kill -TERM à ton worker sur la préproduction avant de croire quoi que ce soit de tout ça, parce que le chemin d'arrêt est celui que personne n'exerce tant qu'il n'est pas trop tard.
Je te renvoie donc la question, parce que je soupçonne que les avis sont partagés. Où places-tu la limite entre augmenter TimeoutStopSec et refactorer le job en morceaux ? Si ta plus longue unité de travail fait vingt minutes et que la réécrire implique de rouvrir un pipeline de reporting que personne ne veut toucher, est-ce que tu la réécris vraiment, ou est-ce que tu mets le timeout à trente minutes et tu surveilles les codes de sortie ? Et quelqu'un ici a-t-il le comportement d'arrêt sous test en CI, ou est-ce encore un de ces trucs dont on convient tous que c'est une bonne idée et qu'on ne fait jamais ?
Commentaires
Pas encore de commentaire — écris le premier.
Lance la discussion
Pas de compte ni de mot de passe — saisis simplement ton adresse e-mail et nous t’envoyons un lien de connexion à usage unique. Première visite ? Tout se met en place automatiquement.
Ton évaluation sera appliquée automatiquement après ta connexion.
Vérifie ta boîte mail
Nous avons envoyé un lien de connexion à …. Ouvre-le sur cet appareil — cet onglet te connectera automatiquement.
Rien reçu ? Vérifiez le dossier spam — et marquez le message « Non spam » pour qu'il arrive directement la prochaine fois.