Add watchdog for missed launchd runs, add backup retention
Mirrors the same fix on the Lotto sibling project. watchdog.py (com.eurojackpot.watchdog.plist, daily 09:00) checks whether the latest draw or trained models are older than the launchd schedule allows and alerts via EurojackpotNotifier.send_error() - catches a silently-missed StartCalendarInterval run (e.g. Mac asleep at the scheduled time) that launchd itself never retries. EurojackpotAPIUpdater.create_backup() didn't have Lotto's path bug, but had the same unbounded growth (85 backups, 2.4MB accumulated). Adds retention (keep last 30) and prunes the existing backlog. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
c39babade4
commit
416c646fd9
12 files changed
+144
-7306
No files matched your search
@@ -0,0 +1,46 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>Label</key>
|
||||
<string>com.eurojackpot.watchdog</string>
|
||||
|
||||
<key>ProgramArguments</key>
|
||||
<array>
|
||||
<string>/Users/sebastianfrohlich/Projekte/Eurojackpot/run_watchdog.sh</string>
|
||||
</array>
|
||||
|
||||
<key>StartCalendarInterval</key>
|
||||
<dict>
|
||||
<key>Hour</key>
|
||||
<integer>9</integer>
|
||||
<key>Minute</key>
|
||||
<integer>0</integer>
|
||||
</dict>
|
||||
|
||||
<key>WorkingDirectory</key>
|
||||
<string>/Users/sebastianfrohlich/Projekte/Eurojackpot</string>
|
||||
|
||||
<key>StandardOutPath</key>
|
||||
<string>/Users/sebastianfrohlich/Projekte/Eurojackpot/logs/watchdog_stdout.log</string>
|
||||
<key>StandardErrorPath</key>
|
||||
<string>/Users/sebastianfrohlich/Projekte/Eurojackpot/logs/watchdog_stderr.log</string>
|
||||
|
||||
<key>RunAtLoad</key>
|
||||
<false/>
|
||||
|
||||
<key>EnvironmentVariables</key>
|
||||
<dict>
|
||||
<key>PATH</key>
|
||||
<string>/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin</string>
|
||||
<key>LANG</key>
|
||||
<string>de_DE.UTF-8</string>
|
||||
</dict>
|
||||
|
||||
<key>LaunchOnlyOnce</key>
|
||||
<false/>
|
||||
|
||||
<key>Nice</key>
|
||||
<integer>10</integer>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -91,7 +91,7 @@ class EurojackpotAPIUpdater:
|
||||
return False
|
||||
|
||||
def create_backup(self) -> bool:
|
||||
"""Erstellt Backup."""
|
||||
"""Erstellt Backup und behält nur die letzten KEEP_BACKUPS Stück."""
|
||||
if not os.path.exists(self.data_file):
|
||||
return True
|
||||
|
||||
@@ -108,12 +108,28 @@ class EurojackpotAPIUpdater:
|
||||
shutil.copy2(self.data_file, self.backup_file)
|
||||
print(f" ✅ Backup: {os.path.basename(self.backup_file)}")
|
||||
|
||||
self._prune_old_backups(backup_dir, filename)
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f" ⚠️ Backup-Fehler: {e}")
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def _prune_old_backups(backup_dir: str, filename: str, keep: int = 30) -> None:
|
||||
"""Behält nur die KEEP neuesten Backups, löscht den Rest."""
|
||||
pattern = f"{filename}.backup_"
|
||||
backups = sorted(
|
||||
(f for f in os.listdir(backup_dir) if f.startswith(pattern)),
|
||||
reverse=True,
|
||||
)
|
||||
for old in backups[keep:]:
|
||||
try:
|
||||
os.remove(os.path.join(backup_dir, old))
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
def fetch_from_api(self, api_name: str) -> List[Dict]:
|
||||
"""
|
||||
Holt Daten von spezifischer API.
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Watchdog für die Eurojackpot-Automatisierung.
|
||||
|
||||
Prüft die Aktualität von Ziehungsdaten und trainierten Modellen anhand des
|
||||
Ziehungskalenders (Di + Fr) und meldet sich per Telegram, wenn etwas länger
|
||||
stillsteht als der launchd-Zeitplan erlaubt. Das fängt genau den Fall ab, in
|
||||
dem ein StartCalendarInterval-Job verpasst wurde (z.B. weil der Mac im Schlaf
|
||||
war) - launchd holt das selbst nicht nach, und ohne diesen Check fällt es
|
||||
sonst erst bei der nächsten manuellen Nachfrage auf. Analog zum gleichnamigen
|
||||
Watchdog im Schwesterprojekt Lotto.
|
||||
|
||||
Aufruf: python3 scripts/utils/watchdog.py
|
||||
Gedacht für einen täglichen launchd-Job (siehe com.eurojackpot.watchdog.plist).
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
from datetime import datetime
|
||||
|
||||
import pandas as pd
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
from notifier import EurojackpotNotifier
|
||||
|
||||
PROJECT_ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
DATA_FILE = os.path.join(PROJECT_ROOT, "data", "AlleEurojackpotzahlen.csv")
|
||||
MODEL_FILE = os.path.join(PROJECT_ROOT, "data", "eurojackpot_ml_models", "trained_models_main.pkl")
|
||||
|
||||
# Eurojackpot zieht Di + Fr -> maximal 4 Tage zwischen zwei Ziehungen. 5 Tage
|
||||
# Toleranz lassen dem Update-Job (nächster Morgen, 08 Uhr) Luft, ohne echte
|
||||
# Ausfälle zu verpassen.
|
||||
MAX_DAYS_SINCE_DRAW = 5
|
||||
|
||||
|
||||
def check() -> list[str]:
|
||||
"""Prüft Daten- und Modell-Aktualität, gibt gefundene Probleme zurück."""
|
||||
problems = []
|
||||
|
||||
df = pd.read_csv(DATA_FILE, sep=';')
|
||||
last_draw = pd.to_datetime(df['datum'], format='%Y-%m-%d').max().date()
|
||||
days_since_draw = (datetime.now().date() - last_draw).days
|
||||
|
||||
if days_since_draw > MAX_DAYS_SINCE_DRAW:
|
||||
problems.append(
|
||||
f"Letzte Ziehung in den Daten ist {last_draw.isoformat()} "
|
||||
f"({days_since_draw} Tage her) - der automatische Update-Lauf "
|
||||
f"scheint ausgefallen zu sein."
|
||||
)
|
||||
|
||||
if os.path.exists(MODEL_FILE):
|
||||
model_date = datetime.fromtimestamp(os.path.getmtime(MODEL_FILE)).date()
|
||||
if model_date < last_draw and (datetime.now().date() - model_date).days > 1:
|
||||
problems.append(
|
||||
f"Modelle sind vom {model_date.isoformat()}, aber die neueste "
|
||||
f"Ziehung ({last_draw.isoformat()}) wurde noch nicht eingelernt."
|
||||
)
|
||||
else:
|
||||
problems.append("Modell-Datei trained_models_main.pkl fehlt komplett.")
|
||||
|
||||
return problems
|
||||
|
||||
|
||||
def main():
|
||||
problems = check()
|
||||
if not problems:
|
||||
print("✅ Watchdog: alles aktuell.")
|
||||
return
|
||||
|
||||
print("⚠️ Watchdog hat Probleme gefunden:")
|
||||
for p in problems:
|
||||
print(f" - {p}")
|
||||
|
||||
EurojackpotNotifier().send_error("\n".join(problems), context="Watchdog")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in new issue
Block a user