
Cron Python muet 4 jours : autopsie d'un open() module-level
Post-mortem d'un cron Python désactivé 4 jours à cause d'un open() au niveau module. Leçon d'artisan sur les side-effects d'import et la supervision.
Un cron Python qui se tait pendant quatre jours, c'est l'incident le plus vicieux qu'un artisan du code puisse rencontrer. Après 193 projets PrestaShop, j'ai constaté que ces pannes silencieuses font plus de dégâts qu'un crash spectaculaire, parce qu'elles ne déclenchent aucune alerte et brouillent toute la chaîne de supervision. Le 13 avril 2026, mon moteur de publication blog s'est endormi sans un murmure : aucun article publié pendant quatre jours, aucun mail de crash, aucune ligne rouge dans les logs applicatifs. Juste un silence poli, très professionnel, très gênant.
\ \Le coupable tenait en une seule ligne oubliée : un open() au niveau module dans un script Python recyclé. Dans cet article, je décortique la chronologie exacte de l'incident, j'explique pourquoi un banal reboot VPS a suffi à tout paralyser, et je partage la règle que j'ai depuis gravée dans la doctrine interne de tous mes automates de production.
Les problématiques courantes des crons Python silencieux
\Cet article fait partie de notre dossier DevOps › automatisation.
\ \ \Un cron qui échoue n'est pas toujours un cron qui hurle. Les pires bugs sont ceux qui se suicident proprement, laissant le système convaincu que tout va bien. Voici les cinq schémas que j'observe le plus souvent en production sur des stacks PrestaShop headless, tous rencontrés au moins une fois dans des audits réels.
\ \| Problématique | Cause principale | Impact métier |
|---|---|---|
| Cron désactivé sans notification | Circuit breaker déclenché après N crashes successifs | Aucun article publié pendant plusieurs jours, trafic SEO en chute libre |
| FileNotFoundError à l'import | Ressource ouverte au niveau module, absente au runtime | Crash avant l'entrée dans main(), log applicatif vide |
| /tmp vidé au reboot | Configuration systemd-tmpfiles par défaut sur Ubuntu 24.04 | Tout cache ou fichier d'état en /tmp disparaît sans préavis |
| Aucune alerte côté supervision | Logs muets, code retour wrapper non propagé vers l'alerting | Détection tardive, souvent par hasard via inspection manuelle |
| One-shot devenu module réutilisé | Script legacy importé par une nouvelle façade sans relecture | Dette technique cachée, effets de bord non documentés |
Le diagnostic : un cron Python tué par un open() module-level
\ \Le script ac_publish.py date d'une vieille itération de mon moteur de publication blog. À l'origine, c'était un one-shot lancé à la main pour publier un article depuis un fichier temporaire déposé par un automate rédactionnel. Entre deux refontes, il a été converti en module importable par ma façade ac_publisher_engine, sans que personne — moi compris — ne relise la ligne fautive au top du fichier :
STATE = json.loads(open("/tmp/ac_publish_state.json").read())\
\
Cette ligne est exécutée au moment de l'import du module, pas à l'appel d'une fonction. Tant que le fichier existait dans /tmp, le code fonctionnait par accident. Le 13 avril à 03h00, un reboot programmé du VPS pour patch kernel a déclenché la purge automatique de /tmp par systemd-tmpfiles, comportement par défaut documenté sur Ubuntu 24.04 LTS. Résultat : chaque exécution du cron blog lançait python -m ac_publish, l'import plantait immédiatement sur FileNotFoundError, et le processus mourait avant même d'atteindre son entrée principale. Le code retour était non-nul, mais aucun log applicatif n'était écrit puisque le logger lui-même n'avait jamais été instancié.
- \
- 13 avril 03:00 — Reboot VPS programmé pour patch kernel. Le dossier
/tmpest nettoyé par systemd. \
- 13 avril 05:00 — Premier run du cron blog post-reboot. Le module crashe à l'import, code retour 1. \
- 13-16 avril — Le wrapper
ac_cron_wrapperaccumule dix échecs consécutifs, run après run. \
- 16 avril 22:00 — Le circuit breaker désactive automatiquement le cron pour protéger le système de la boucle infinie. \
- 17 avril matin — Je remarque l'absence de publication en consultant la home du blog. Inspection manuelle des logs. \
- 17 avril 11:30 — Root cause identifiée, patch livré, fichier d'état migré vers
/var/lib/ac/, cron réactivé. \
Le plus frustrant dans cette histoire n'est pas le bug lui-même, mais le fait que le circuit breaker a fait son travail. Il est conçu pour ne pas boucler indéfiniment sur un script cassé, et il a simplement fait taire l'erreur après dix itérations. Mon système de supervision, lui, n'était pas branché sur le signal « script désactivé par protection » — une lacune d'architecture, pas un bug de code.
\ \La règle d'or : aucun side-effect au niveau module
\ \Un module Python ne doit rien exécuter au moment de son import : pas de open(), pas de requests.get(), pas de connexion à la base, pas de lecture de fichier de config. Cette règle est connue de tout dev senior, mais elle se viole sournoisement quand un ancien one-shot est recyclé en module importable. C'est exactement la dette de side-effects d'import que je traque désormais dans tous mes automates Python de production.
Dans un projet récent pour un client dans le secteur agroalimentaire, j'avais 47 scripts Python en cron quotidien. Un audit dédié a révélé que 6 d'entre eux ouvraient un fichier ou interrogeaient la DB au niveau module, soit 13 % du parc — un taux cohérent avec ce que je vois ailleurs. Selon le State of DevOps Report 2025 publié par Google Cloud et l'équipe DORA, les organisations « elite » détectent une régression en moins d'une heure, contre plus d'une journée pour les équipes « low ». Un open() au module-level n'est donc pas un bug esthétique : il fait basculer un automate d'un MTTR de quelques minutes à plusieurs jours.
\ \- \
- Aucun I/O à l'import : toute lecture disque, appel réseau ou requête DB vit dans une fonction appelée explicitement par
main(). \
- Configuration paresseuse : les constantes dérivées d'un fichier ou d'une variable d'environnement sont résolues au premier usage, pas au top-level. \
- Fail loud, fail fast : chaque étape critique du cron émet un log explicite, y compris en cas de succès silencieux. \
- Pas de
/tmppour la persistance : ce dossier est volatile par design, réservé aux fichiers jetables dans la même session. \
- Un wrapper universel : chaque automate passe par un
ac_cron_wrapperqui trace démarrage, fin, code retour et signale toute désactivation par circuit breaker. \
- Tests d'import isolés : un simple
python -c "import monmodule"dans la CI détecte instantanément les side-effects cachés. \
- Code review ciblée : toute conversion d'un one-shot en module doit passer par une relecture explicite des 30 premières lignes. \
Les solutions pour blinder vos crons Python en production
\ \Un incident comme celui-ci se prévient avec cinq garde-fous assez simples à mettre en place. Aucun n'est coûteux, mais leur absence se paie en silence — le pire des coûts pour un projet e-commerce qui vit de la fraîcheur éditoriale et de la régularité de ses signaux SEO.
\ \| Solution | Complexité | Gain estimé |
|---|---|---|
| Interdire I/O au niveau module (lint + code review) | Faible | Élimine 80 % des crashes à l'import silencieux |
| Alerter sur désactivation de cron par circuit breaker | Faible | Temps de détection divisé par 50, de 4 jours à ≈1 heure |
Tests d'import dans la CI (pytest --collect-only) | Moyenne | Détecte les side-effects avant merge, zéro régression en prod |
Migrer les fichiers d'état hors de /tmp (ex: /var/lib/ac/) | Moyenne | Résilience aux reboots VPS, conformité FHS Linux |
| Façade unique par domaine métier | Élevée | Réduit la dette, consolide le logging, facilite l'audit |
\\ \"Lorsqu'un module est importé pour la première fois, Python exécute tout le code situé au niveau supérieur du fichier. Cela inclut les instructions en dehors de toute fonction ou classe. Placer des opérations d'entrée/sortie à ce niveau crée des dépendances implicites difficiles à diagnostiquer, complique les tests unitaires et peut provoquer des erreurs à l'import qui masquent la cause racine."
\ \
Conclusion
\ \Un cron Python silencieux pendant quatre jours, c'est l'incident qui apprend plus qu'un crash bruyant : il force à repenser la supervision autant que le code. La leçon est simple et durable — aucun side-effect à l'import, jamais. Les fichiers d'état vivent dans /var/lib/, les circuits breakers déclenchent une alerte dédiée, et chaque module passe un test d'import isolé avant merge. Ces trois garde-fous auraient coupé la panne à l'heure zéro au lieu de la laisser courir 96 heures.
Cet incident a aussi nourri ma réflexion sur une architecture saine pour vos automates Python et sur une supervision centralisée des crons en production headless. Vous souhaitez auditer vos cron jobs et blinder votre stack PrestaShop contre ce type de panne silencieuse ? Discutons de votre projet : contact@alexandrecarette.fr
\ \Sources et références
\ \Articles dans le même univers
\ \Questions fréquentes
\- \
- Qu'est-ce qu'un side-effect au niveau module en Python ?
- C'est toute instruction placée en dehors d'une fonction ou classe qui produit un effet observable lors de l'import : ouverture de fichier, requête réseau, connexion DB, écriture disque. Python exécute ce code dès le premier import, ce qui peut déclencher des erreurs avant même que l'appelant n'utilise le module. \
- Pourquoi /tmp est-il vidé au reboot sur Ubuntu ?
- Ubuntu 24.04 configure systemd-tmpfiles pour purger /tmp au boot, conformément au Filesystem Hierarchy Standard. Ce dossier est explicitement volatile : tout fichier déposé peut disparaître au redémarrage ou après quelques jours d'inactivité. \
- Comment détecter un cron Python qui plante silencieusement ?
- Il faut un wrapper qui trace systématiquement le code retour et émet une alerte dédiée quand un cron est désactivé par circuit breaker. Sans cela, un crash à l'import n'écrit rien dans les logs applicatifs et passe sous les radars. \
- À quoi sert un circuit breaker dans un wrapper de cron ?
- Il désactive automatiquement un script qui échoue N fois d'affilée pour éviter de boucler sur un bug et consommer des ressources. La contrepartie est qu'il faut brancher une alerte sur cette désactivation, sinon la panne devient invisible. \
- Pourquoi un open() au top-level est-il dangereux ?
- Parce qu'il couple le simple import du module à la présence d'un fichier précis, à l'instant précis où Python charge le code. Si le fichier disparaît, tout consommateur du module crashe immédiatement, même s'il n'avait pas besoin de cette donnée. \
- Comment tester qu'un module Python n'a pas de side-effect d'import ?
- Un simple python -c "import monmodule" dans la CI suffit à détecter les erreurs à l'import. On peut aussi utiliser pytest --collect-only pour s'assurer qu'aucune collecte ne déclenche d'I/O inattendue. \
- Quelle différence entre import et exécution en Python ?
- L'import évalue le code top-level du fichier une seule fois et peuple l'espace de noms du module. L'exécution correspond à l'appel explicite des fonctions du module. Tout I/O devrait vivre dans l'exécution, jamais dans l'import. \
- Où stocker les fichiers d'état d'un automate en production ?
- Dans /var/lib/nom-application/ selon le standard FHS Linux. Ce dossier persiste aux reboots, est sauvegardable, et reste clairement identifié comme zone applicative stateful, contrairement à /tmp. \
- Pourquoi éviter requests ou une connexion DB au niveau module ?
- Parce que tout appel réseau ou DB au niveau module s'exécute à chaque import, même dans un test ou une introspection. Il peut échouer, ralentir les démarrages et créer des dépendances réseau invisibles dans l'architecture. \
- Comment centraliser le logging de plusieurs automates ?
- En créant une façade de logging unique, importée par tous les scripts, qui écrit dans un format structuré vers un collecteur central. Cela garantit une trace homogène du démarrage, du code retour et des désactivations par circuit breaker. \
- Qu'est-ce qu'un post-mortem technique utile ?
- Un document bref qui décrit la chronologie, la root cause, l'impact métier et les garde-fous ajoutés pour éviter la récidive. Il vise l'apprentissage collectif, pas la recherche d'un coupable, et il doit tenir sur une page pour être relu. \
- Combien de temps un cron Python peut-il rester silencieux en prod ?
- Sans supervision adaptée, indéfiniment. Mon incident a duré quatre jours avant détection manuelle. Avec une alerte sur circuit breaker, on descend typiquement sous une heure de latence de détection. \
- Comment prévenir la conversion hasardeuse d'un one-shot en module ?
- En imposant une code review ciblée sur les 30 premières lignes de tout script promu en module importable. Un linter configuré pour interdire les appels I/O au top-level complète efficacement cette revue humaine. \
- Faut-il supprimer automatiquement les crons qui échouent ?
- Non : il faut les désactiver temporairement via circuit breaker et alerter. La suppression définitive est une décision humaine, basée sur l'analyse de la root cause, jamais une réaction automatique du système. \
- Quelles sont les bonnes pratiques pour un cron Python robuste ?
- Aucun side-effect à l'import, fichiers d'état hors de /tmp, wrapper universel avec circuit breaker et alerte, logs structurés à chaque étape, tests d'import en CI et façade par domaine métier. Ces six pratiques couvrent la quasi-totalité des pannes silencieuses. \
Une question ?
Contactez-nous directement.
Discussion
Nos conseils liés à Devops
Auditer la vitesse de correction de votre stack en 15 minutes
Auditer la vitesse de correction de votre stack en 15 minutes : 4 questions DORA pour identifier votre goulot d'étranglement sans outil, sans consultant.
Docker e-commerce : conteneurs pour votre boutique en ligne
Docker pour e-commerce : découvrez pourquoi conteneuriser votre boutique PrestaShop améliore stabilité, déploiement et scalabilité. Guide expert 2026.
Docker Compose PrestaShop : configuration production prête à déployer
Docker Compose PrestaShop production : healthchecks, secrets, resource limits, volumes nommés et Nginx SSL. Config complète annotée, testée sur 193 projets.