Aller au contenu

Bibliothèque Python : Celery et APScheduler

La bibliothèque Python de SilenceWatch surveille les jobs planifiés de votre application sans que vous déclariez quoi que ce soit : elle lit les jobs de votre planificateur, les déclare avec leur vraie planification, et envoie un signal autour de chaque exécution. Elle est sous licence Apache 2.0, volontairement, pour pouvoir être embarquée dans n’importe quelle application.

Planificateur Installation Ce qu’elle lit
Celery beat pip install "silencewatch[celery]" le beat_schedule (crontab, timedelta, secondes)
APScheduler 3 pip install "silencewatch[apscheduler]" les jobs du planificateur (déclencheurs cron et interval)

Python 3.9 ou plus récent. Aucune autre dépendance.

Créez la clé d’API dans Réglages › Clés d’API de votre projet et donnez-la au processus :

Fenêtre de terminal
export SILENCEWATCH_API_KEY=sw_...
# export SILENCEWATCH_URL=https://watch.example.com # seulement si vous hébergez SilenceWatch vous-même
from celery import Celery
from celery.schedules import crontab
from silencewatch.integrations.celery import instrument
app = Celery("shop")
app.conf.beat_schedule = {
"nightly-backup": {"task": "shop.tasks.backup", "schedule": crontab(hour=2, minute=0)},
"sync-prices": {"task": "shop.tasks.sync_prices", "schedule": 900.0},
}
instrument(app)

Un worker déclare la planification à son démarrage, dans le processus principal avant que son pool ne se divise, si bien que chaque processus enfant a les clés de ping. Les signaux sont envoyés par le worker qui exécute chaque tâche planifiée.

from apscheduler.schedulers.background import BackgroundScheduler
from silencewatch.integrations.apscheduler import instrument
scheduler = BackgroundScheduler()
scheduler.add_job(backup, "cron", hour=2, id="nightly-backup")
scheduler.add_job(sync_prices, "interval", minutes=15, id="sync-prices")
instrument(scheduler)
scheduler.start()

Au démarrage, en un seul appel HTTP, chaque job trouvé est déclaré à POST /api/v1/checks/sync et les clés de ping renvoyées sont gardées en mémoire. Autour de chaque exécution :

POST /p/<clé>/start au début de l’exécution
POST /p/<clé>/0?duration_ms=1234 quand elle se termine
POST /p/<clé>/fail?duration_ms=1234 quand elle lève une exception

Chaque job reçoit une identité stable qui le rattache à son historique malgré les redémarrages, les redéploiements et les renommages :

Intégration Clé
Celery celery:shop.tasks.backup (le nom de la tâche)
APScheduler apscheduler:nightly-backup (l’identifiant du job ; sans lui, apscheduler:app.jobs:backup, la fonction)

Un job qui disparaît du code est marqué orphelin, jamais supprimé. Si plusieurs intégrations tournent dans un même processus, tout ce qu’elles trouvent est déclaré en une seule requête.

Chaque réglage est un argument nommé (instrument(app, api_key="…", environment="staging")) et une variable d’environnement. L’argument l’emporte. Une durée dans l’environnement est un nombre de secondes.

Argument Variable d’environnement Valeur par défaut
api_key SILENCEWATCH_API_KEY aucune : rien n’est envoyé
base_url SILENCEWATCH_URL https://app.silencewatch.com
environment SILENCEWATCH_ENVIRONMENT production
enabled SILENCEWATCH_ENABLED true ; false retire tout
default_grace SILENCEWATCH_DEFAULT_GRACE 5 minutes, pour les jobs qui n’en déclarent pas
auto_register SILENCEWATCH_AUTO_REGISTER true ; false pour gérer les checks à la main
report_start SILENCEWATCH_REPORT_START true ; false pour n’envoyer que le résultat
timeout SILENCEWATCH_TIMEOUT 2 secondes par appel, volontairement court
queue_capacity SILENCEWATCH_QUEUE_CAPACITY 1000 ; les plus anciens sont abandonnés quand elle est pleine
timezone SILENCEWATCH_TIMEZONE UTC, pour les jobs cron dont la planification n’en porte pas
prune SILENCEWATCH_PRUNE true
per_instance_jobs SILENCEWATCH_PER_INSTANCE_JOBS aucune ; jobs surveillés sur chaque instance, voir plus bas
instance_id SILENCEWATCH_INSTANCE_ID aucune ; le nom stable de l’instance

prune marque comme orphelins tous les checks du projet et de l’environnement que la déclaration omet. Utilisez un environnement par application ; si plusieurs services partagent un même projet et un même environnement, mettez prune=False, sinon chacun marquerait les checks des autres comme orphelins.

Quand une application tourne en plusieurs processus ou serveurs, tous déclarent les mêmes jobs dans le même environnement : chaque job est donc un seul check pour tous, en bonne santé tant que l’un d’eux le signale.

  • Celery (et django-celery-beat) : beat confie chaque exécution à un seul worker, quel qu’il soit, et le check l’entend. C’est la bonne règle, et la seule.
  • APScheduler tourne dans chaque processus. Démarré dans chaque worker Gunicorn ou chaque réplique, un job s’exécute une fois par processus, et le check reste en bonne santé tant que l’un d’eux l’exécute : un processus dont le planificateur est bloqué passe inaperçu pendant qu’un autre travaille. Si le job doit tourner une seule fois, démarrez le planificateur dans un seul processus ; s’il doit tourner dans chacun, lisez la suite.

Les jobs qui doivent tourner dans chaque instance (vider un cache local, supprimer des fichiers temporaires) peuvent être surveillés sur chacune :

instrument(scheduler, per_instance_jobs=["apscheduler:evict-cache", "apscheduler:tmp-*"],
instance_id=os.environ["HOSTNAME"])

ou SILENCEWATCH_PER_INSTANCE_JOBS=apscheduler:evict-cache,apscheduler:tmp-* et SILENCEWATCH_INSTANCE_ID. Ces jobs sont déclarés dans un environnement propre à l’instance, production.web-1 : chaque instance a son check, et celle qui se tait est vue pendant que les autres signalent. Un * final désigne un préfixe ; les autres jobs restent partagés. Les tâches Celery ne sont jamais surveillées par instance, puisqu’un seul worker exécute chaque exécution : la bibliothèque le signale et les garde partagées.

instance_id n’a pas de valeur par défaut, et doit rester le même d’un redémarrage et d’un déploiement à l’autre (le nom d’un pod de StatefulSet, le nom d’hôte d’un serveur) : un nom qui change à chaque déploiement laisserait des checks que plus rien ne signale, et qui tomberaient en panne. Sans lui, les jobs listés sont surveillés comme partagés, avec un avertissement. Quand vous retirez une instance pour de bon, supprimez ses checks.

  • Elle ne fait jamais échouer votre job. Toute erreur réseau ou HTTP est avalée et journalisée en WARNING. Votre tâche s’exécute exactement une fois, et son résultat et son exception sont intacts.
  • Elle ne bloque jamais votre job. Envoyer un signal est un ajout à une file traité par un thread démon. Un SilenceWatch injoignable ne coûte rien à votre job.
  • Elle est bornée. La file contient au plus queue_capacity signaux ; au-delà, les plus anciens sont abandonnés, car l’état le plus récent est celui qui compte. Les avertissements sont limités à un par minute.
  • Elle se dégrade en silence. Sans clé, sans réseau, sans serveur : l’application démarre et tourne exactement comme sans la bibliothèque. Un processus démarré pendant que SilenceWatch était injoignable se déclare à son premier signal, au plus une fois par minute.
  • Elle résiste au fork. Le pool prefork de Celery et gunicorn se divisent après le démarrage de la bibliothèque ; chaque enfant démarre sa propre file et son propre thread.
  • enabled=False retire tout le mécanisme : pas de découverte, pas de crochets, pas de threads.
  • Celery. Une tâche planifiée par plusieurs entrées beat est déclarée à partir de celle qui se déclenche le plus souvent. Un .delay() manuel d’une tâche planifiée envoie aussi un signal, car un worker ne peut pas le distinguer d’un déclenchement planifié. Les planifications solaires ne sont pas lues. Les tâches périodiques gardées en base par django-celery-beat sont lues, sans rien configurer, et une modification faite pendant que les workers tournent est déclarée à nouveau en moins d’une minute. Un crontab qui restreint à la fois le jour du mois et le jour de la semaine se déclenche quand les deux correspondent dans Celery et quand l’un des deux correspond dans cron ; le journal le signale.
  • APScheduler. Version 3 uniquement (la version 4 a une autre API). La durée d’un job inclut le temps passé à attendre un exécuteur. Les déclencheurs date ponctuels, les déclencheurs combinés and/or et les champs cron week, year et « troisième vendredi » ne s’expriment pas et sont ignorés avec un avertissement. Les jobs ajoutés ou retirés après le démarrage sont déclarés de nouveau deux secondes plus tard.
  • Les jobs de moins de 30 secondes sont déclarés avec une période de 30 secondes, la plus courte acceptée, et un avertissement les nomme.
  • Les expressions cron que le serveur ne sait pas évaluer sont ignorées avec un avertissement plutôt que de faire échouer toute la déclaration.