scheduler.py 3.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102
  1. from apscheduler.schedulers.background import BackgroundScheduler
  2. from apscheduler.triggers.cron import CronTrigger
  3. _flask_app = None
  4. scheduler = BackgroundScheduler(
  5. job_defaults={"coalesce": True, "max_instances": 1},
  6. timezone="UTC",
  7. )
  8. def init_scheduler(flask_app):
  9. global _flask_app
  10. _flask_app = flask_app
  11. # Nettoyage immédiat au démarrage : tout run "running" en DB est forcément
  12. # un vestige d'un process précédent (app redémarrée pendant un backup).
  13. with flask_app.app_context():
  14. from db import db, Run
  15. import datetime as _dt
  16. stale = Run.query.filter_by(status="running").all()
  17. now = _dt.datetime.utcnow()
  18. for run in stale:
  19. run.status = "error"
  20. run.log_text = (run.log_text or "") + "\n[interrompu] Run interrompu par un redémarrage de l'application."
  21. run.finished_at = now
  22. if stale:
  23. db.session.commit()
  24. if not scheduler.running:
  25. scheduler.start()
  26. # Filet de sécurité : marque en erreur tout run resté bloqué > 6h
  27. scheduler.add_job(
  28. func=_cleanup_stuck_runs,
  29. trigger="interval",
  30. hours=1,
  31. id="cleanup_stuck_runs",
  32. replace_existing=True,
  33. )
  34. def _cleanup_stuck_runs():
  35. import datetime as _dt
  36. with _flask_app.app_context():
  37. from db import db, Run
  38. cutoff = _dt.datetime.utcnow() - _dt.timedelta(hours=6)
  39. stuck = Run.query.filter(
  40. Run.status == "running",
  41. Run.started_at < cutoff,
  42. ).all()
  43. now = _dt.datetime.utcnow()
  44. for run in stuck:
  45. duration = now - run.started_at
  46. hours = int(duration.total_seconds() // 3600)
  47. minutes = int((duration.total_seconds() % 3600) // 60)
  48. run.status = "error"
  49. run.log_text = (run.log_text or "") + (
  50. f"\n[timeout] Run bloqué depuis {hours}h{minutes:02d} — marqué en erreur par le nettoyage automatique."
  51. )
  52. run.finished_at = now
  53. if stuck:
  54. db.session.commit()
  55. def _execute_job(job_id):
  56. with _flask_app.app_context():
  57. from jobs.ynh_backup import execute_job
  58. execute_job(job_id)
  59. def schedule_job(job):
  60. import logging
  61. job_key = f"job_{job.id}"
  62. if not job.cron_expr:
  63. return # job manuel uniquement, pas de planification APScheduler
  64. try:
  65. trigger = CronTrigger.from_crontab(job.cron_expr)
  66. except Exception:
  67. logging.warning(f"Job #{job.id} « {job.name} » : expression cron invalide « {job.cron_expr} » — job non planifié.")
  68. return
  69. if scheduler.get_job(job_key):
  70. scheduler.reschedule_job(job_key, trigger=trigger)
  71. else:
  72. scheduler.add_job(
  73. func=_execute_job,
  74. trigger=trigger,
  75. id=job_key,
  76. kwargs={"job_id": job.id},
  77. replace_existing=True,
  78. )
  79. def remove_job(job_id):
  80. job_key = f"job_{job_id}"
  81. if scheduler.get_job(job_key):
  82. scheduler.remove_job(job_key)
  83. def get_next_run(job_id):
  84. job_key = f"job_{job_id}"
  85. apsjob = scheduler.get_job(job_key)
  86. if apsjob and apsjob.next_run_time:
  87. return apsjob.next_run_time
  88. return None