import logging import threading import time from typing import Any from .backup_alert_engine import check_missed_schedules from .settings_store import SettingsStore, get_settings_store logger = logging.getLogger(__name__) class BackupAlertPoller: def __init__(self) -> None: self._thread: threading.Thread | None = None self._stop_event = threading.Event() self._lock = threading.Lock() self._last_run_at: float | None = None self._last_success_at: float | None = None self._last_error: str = "" self._poll_count = 0 self._error_count = 0 def start(self) -> None: with self._lock: if self._thread and self._thread.is_alive(): return self._stop_event.clear() self._thread = threading.Thread(target=self._run, name="backup-alert-poller", daemon=True) self._thread.start() def stop(self, timeout: float = 5.0) -> None: with self._lock: thread = self._thread if not thread: return self._stop_event.set() thread.join(timeout=timeout) @property def status(self) -> dict[str, Any]: with self._lock: return { "worker_running": self._thread is not None and self._thread.is_alive(), "last_run_at": self._last_run_at, "last_success_at": self._last_success_at, "last_error": self._last_error, "poll_count": self._poll_count, "error_count": self._error_count, } def _run(self) -> None: # Wait 30 seconds before first check if self._stop_event.wait(30): return store = get_settings_store() while not self._stop_event.is_set(): try: self._run_cycle(store) except Exception: logger.exception("Backup alert poller cycle failed") # Check every 5 minutes if self._stop_event.wait(300): break def _run_cycle(self, store: SettingsStore) -> None: start = time.perf_counter() jobs = store.list_backup_jobs() existing_alerts = store.list_backup_alerts(acknowledged=False) new_alerts = check_missed_schedules( jobs, lambda job_id: store.get_latest_backup_run(job_id), existing_alerts, ) for alert in new_alerts: store.create_backup_alert(alert) # Prune old resolved alerts (90 days) cutoff = int(time.time()) - (90 * 24 * 60 * 60) store.prune_backup_alerts(cutoff) _duration_ms = int((time.perf_counter() - start) * 1000) with self._lock: self._last_run_at = time.time() self._last_success_at = time.time() self._poll_count += 1 _BACKUP_POLLER = BackupAlertPoller() def get_backup_poller() -> BackupAlertPoller: return _BACKUP_POLLER