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

52 lines
1.4 KiB
Python

"""The scheduler thread: one per process, one tick every 30 seconds (AD-12)."""
from __future__ import annotations
import logging
import threading
from collections.abc import Callable
from django.db import close_old_connections
from . import services
logger = logging.getLogger(__name__)
TICK_SECONDS = 30
class Runner:
def __init__(self, tick: Callable[[], None] = services.tick, interval: float = TICK_SECONDS):
self._tick = tick
self._interval = interval
self._stop = threading.Event()
self._thread: threading.Thread | None = None
def start(self) -> None:
"""Start the thread; calling it again while it runs does nothing."""
if self._thread is not None and self._thread.is_alive():
return
self._stop.clear()
self._thread = threading.Thread(target=self._loop, name="scheduler", daemon=True)
self._thread.start()
def stop(self, timeout: float = 5) -> None:
self._stop.set()
if self._thread is not None:
self._thread.join(timeout)
def _loop(self) -> None:
logger.info("Scheduler started")
while not self._stop.is_set():
try:
self._tick()
except Exception:
logger.exception("Scheduler tick failed")
finally:
close_old_connections()
self._stop.wait(self._interval)
logger.info("Scheduler stopped")
runner = Runner()