Cron Python muet 4 jours : autopsie d'un open() module-level
devops

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.

11 min de lecture

\ \

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 DevOpsautomatisation.

\ \ \

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ématiqueCause principaleImpact métier
Cron désactivé sans notificationCircuit breaker déclenché après N crashes successifsAucun article publié pendant plusieurs jours, trafic SEO en chute libre
FileNotFoundError à l'importRessource ouverte au niveau module, absente au runtimeCrash avant l'entrée dans main(), log applicatif vide
/tmp vidé au rebootConfiguration systemd-tmpfiles par défaut sur Ubuntu 24.04Tout cache ou fichier d'état en /tmp disparaît sans préavis
Aucune alerte côté supervisionLogs muets, code retour wrapper non propagé vers l'alertingDétection tardive, souvent par hasard via inspection manuelle
One-shot devenu module réutiliséScript legacy importé par une nouvelle façade sans relectureDette 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é.

\ \
    \
  1. 13 avril 03:00 — Reboot VPS programmé pour patch kernel. Le dossier /tmp est nettoyé par systemd.
  2. \
  3. 13 avril 05:00 — Premier run du cron blog post-reboot. Le module crashe à l'import, code retour 1.
  4. \
  5. 13-16 avril — Le wrapper ac_cron_wrapper accumule dix échecs consécutifs, run après run.
  6. \
  7. 16 avril 22:00 — Le circuit breaker désactive automatiquement le cron pour protéger le système de la boucle infinie.
  8. \
  9. 17 avril matin — Je remarque l'absence de publication en consultant la home du blog. Inspection manuelle des logs.
  10. \
  11. 17 avril 11:30 — Root cause identifiée, patch livré, fichier d'état migré vers /var/lib/ac/, cron réactivé.
  12. \
\ \

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 /tmp pour 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_wrapper qui 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.

\ \ \ \ \ \ \ \ \ \ \ \ \
SolutionComplexité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 breakerFaibleTemps de détection divisé par 50, de 4 jours à ≈1 heure
Tests d'import dans la CI (pytest --collect-only)MoyenneDétecte les side-effects avant merge, zéro régression en prod
Migrer les fichiers d'état hors de /tmp (ex: /var/lib/ac/)MoyenneRésilience aux reboots VPS, conformité FHS Linux
Façade unique par domaine métierÉlevéeRé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

\ \
\ \ \ \ \

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.

Gratuit & sans engagement — réponse sous 24h

Discussion

Votre avis sur cet article

Les commentaires sont modérés et répondus par une intelligence artificielle. Votre email ne sera jamais affiché.

0 / 2000

En publiant, vous acceptez que votre nom et commentaire soient affichés publiquement. Votre email est utilisé uniquement pour la modération (base légale : intérêt légitime, durée : 3 ans). Politique de confidentialité.