Files
ebbe.bassandClaude Sonnet 5.5 20510a73e5 feat: add scheduler and mail outbox
SMTP adapter with TLS and a fake, outbox with retries, backoff, rate limit and restart safety, scheduler tick with job-run records started by gunicorn, and a send_test_mail command.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N3t4WL7bd9bdHHNjEsxzqC
2026-10-18 09:18:42 +02:00

49 lines
1.6 KiB
Python

"""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()