"""Jobs and one tick that runs them (AD-12). Every job reads the database for what is due and is safe to repeat, so a missed or doubled tick does no harm. Each run is recorded for the Admin status page. """ from __future__ import annotations import logging from collections.abc import Callable from dataclasses import dataclass from datetime import datetime, timedelta from django.utils import timezone from portal.mailer import services as mailer from .models import JobRun logger = logging.getLogger(__name__) KEEP_RUNS_DAYS = 7 @dataclass(frozen=True) class Job: name: str run: Callable[[datetime], object] # returns something printable as the run's summary # The order is the order they run in. Later entries add jobs here. JOBS: tuple[Job, ...] = (Job("mail.flush", lambda now: mailer.flush(now)),) def tick(now: datetime | None = None, jobs: tuple[Job, ...] | None = None) -> None: """Run every job once. A failing job is recorded and does not stop the others.""" now = now or timezone.now() for job in JOBS if jobs is None else jobs: run = JobRun.objects.create(job=job.name, started_at=now) try: detail = str(job.run(now)) status = JobRun.Status.OK except Exception as exc: logger.exception("Job %s failed", job.name) detail, status = type(exc).__name__, JobRun.Status.ERROR run.status, run.detail, run.finished_at = status, detail[:200], timezone.now() run.save(update_fields=["status", "detail", "finished_at"]) JobRun.objects.filter(started_at__lt=now - timedelta(days=KEEP_RUNS_DAYS)).delete()