1 Statistik Analyse
Ralf Warmuth edited this page 2026-02-24 11:36:44 +01:00
This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

Analyse: Statistik-Funktion (Mails pro Tag / Gefiltertes)

Was die Funktion soll

  • Zählen, wie viele Mails pro Tag angekommen sind (pro Account/Ordner).
  • Zählen, was gefiltert wurde (Filter-Matches, Aktionen, ggf. fehlgeschlagen).
  • Diese Werte im täglichen Report anzeigen und dafür persistent speichern.

Architektur (Kurzüberblick)

  1. Speicher: Im Scheduler laufen in-memory Zähler (_new_mails_counts, _filter_match_counts, _action_stats, …).
  2. Persistenz: SQLite-Tabelle statistics mit Schlüssel (date_hour, account_name, stat_type, stat_key) also pro Stunde.
  3. Laden: Beim Start wird nur die aktuelle Stunde aus der DB geladen (_load_statistics_from_db() mit now).
  4. Speichern: In festem Intervall (save_interval_hours, z.B. 1h) werden die aktuellen In-Memory-Zähler in die DB geschrieben und zwar nur für die aktuelle Stunde (save_statistics(now, stats)), mit Überschreiben (UPDATE SET count = ?), nicht Inkrement.
  5. Tagesreport: load_statistics_for_day(today) aggregiert alle Zeilen mit date_hour LIKE 'YYYY-MM-DD%' (Summe pro Typ/Account/Key). Anschließend werden diese Werte mit den In-Memory-Zählern zusammengeführt (Maximum), Report versendet, dann alle Tages-Statistiken (Speicher + DB) zurückgesetzt.

Gefundene Probleme

1. Kein Stundenwechsel in den In-Memory-Zählern (Hauptursache)

  • Die Zähler _new_mails_counts, _filter_match_counts, _action_stats usw. werden niemals beim Wechsel der Stunde zurückgesetzt.
  • Es gibt keinen Job oder keine Logik, die zur vollen Stunde die Zähler für die neue Stunde „startet“ und die alte Stunde nur noch in der DB hält.

Folge:

  • Ab 14:00 Uhr laufen z.B. 10 neue Mails und 5 Filter-Matches.
  • Um 14:15 wird gespeichert → Zeile 2025-02-24 14:00 bekommt 10 neue Mails, 5 Matches. Korrekt.
  • Ab 15:00 Uhr kommen 3 weitere Mails, 2 Matches. In-Memory steht jetzt 13 neue Mails, 7 Matches (weil nie zurückgesetzt).
  • Um 15:15 wird gespeichert → Zeile 2025-02-24 15:00 wird mit 13 und 7 überschrieben.
    • Die 15:00-Stunde in der DB ist damit falsch (zeigt 13/7 statt 3/2).
    • Die 14:00-Stunde wird nach 15:00 nie mehr aktualisiert Mails, die zwischen 14:15 und 15:00 ankamen, fehlen in der 14:00-Zeile.

Konsequenz: Tagesaggregat (Summe über alle Stunden des Tages) ist falsch: eine Stunde ist überbewertet, die andere unterbewertet; bei Neustarts oder Report-Zeitpunkt wirkt sich das direkt auf „Mails pro Tag“ und „gefiltert“ aus.


2. Doppelte Schreibpfade für Filter-Matches

  • Bei jedem Filter-Match:
    • In-Memory: _filter_match_counts[filter_key] += 1
    • DB: increment_statistic(now, 'filter_match', rule_name, account_name) (atomares +1 für die aktuelle Stunde).
  • Beim periodischen Speichern: save_statistics(now, stats) überschreibt die Zeile der aktuellen Stunde mit dem In-Memory-Stand.

Damit gilt für die aktuelle Stunde: Zuerst korrekte Inkremente in der DB, dann ein einziges Überschreiben mit dem (stundenübergreifenden) In-Memory-Wert. Das Überschreiben dominiert und führt wieder auf das gleiche Problem wie unter 1: In-Memory enthält mehrere Stunden.


3. „Neue Mails“ nur über In-Memory + Stundenspeicherung

  • _new_mails_counts wird nur im Scheduler erhöht und nicht per increment_statistic() geschrieben.
  • „Neue Mails“ landen also nur in der DB, wenn _save_statistics_to_db() für genau diese Stunde läuft und dann mit dem gemischten Wert (alle Stunden seit Start/Report), siehe 1.

Folge:

  • Wenn der Prozess z.B. um 15:30 startet und bis 16:15 keine Speicherung ausgeführt wird, sind alle in dieser Zeit gezählten neuen Mails nur im Speicher und der 15:00-Slot wurde nie geschrieben, der 16:00-Slot erst beim nächsten Save wieder mit falscher Stundenzuordnung.

4. Race Conditions (bereits in LOGGING_PROBLEME.md)

  • Report-Job und Account-Jobs laufen in verschiedenen Threads.
  • Es gibt zwar _stats_lock beim Lesen/Schreiben der Zähler und beim Zurücksetzen wenn der Report aber genau zwischen Lock-Freigabe und nächster Erhöhung zurücksetzt, können theoretisch noch Verluste entstehen. Das ist gegenüber dem Stunden-Bug eher zweitrangig, aber für „funktioniert nicht richtig“ mitzudenken.

5. Nach Report: komplette Löschung des Tages

  • Nach dem Versand wird clear_statistics_for_day(today) aufgerufen und alle In-Memory-Statistiken gelöscht.
  • Das ist für „Report pro Tag“ sinnvoll. Problematisch ist nur: Weil die gespeicherten Stundendaten schon falsch sind (siehe 13), ist auch das, was vor dem Löschen im Report stand, fehlerbehaftet.

Warum „Mails pro Tag“ und „was gefiltert wurde“ falsch sind

  • Pro Tag: Die Tagesanzahl ist die Summe über date_hour LIKE 'YYYY-MM-DD%'. Da die Stundenzeilen teils zu hoch (gemischte Stunden) und teils zu niedrig (nicht aktualisierte vorherige Stunde) sind, ist die Tages-Summe systematisch verfälscht.
  • Was gefiltert wurde: Filter-Matches und Aktionen hängen an denselben Zählern und derselben Speicherlogik → gleiche Verzerrung.

Vorschläge zur Behebung (ohne Code zu ändern)

Option A: In der bestehenden Architektur bleiben (minimale konzeptionelle Änderung)

  • Stunden-Granularität konsequent umsetzen:
    • Zur vollen Stunde (z.B. per Cron-Job oder Scheduler-Job auf :00) die aktuellen In-Memory-Zähler für die gerade abgelaufene Stunde in die DB schreiben (eine Zeile pro Stunde), danach die In-Memory-Zähler für die Statistik auf 0 setzen (oder einen klaren „Stunden-Bucket“ wechseln).
    • Beim periodischen Save (z.B. alle 1h) nur noch die aktuelle Stunde schreiben/aktualisieren, und zwar mit Zählern, die nur diese Stunde betreffen (also nach Stundenwechsel zurückgesetzt).
  • Konsequenz: Ein einziger „Stundenwechsel“-Punkt (z.B. jede volle Stunde): „Speichere aktuellen Stand unter date_hour = letzte Stunde, setze Zähler für Statistik auf 0.“ Dann ist jede Zeile in der DB genau einer Stunde zugeordnet, und load_statistics_for_day liefert korrekte Tages-Summen.

Option B: Statistik nur noch tagesbasiert

  • Vereinfachung: Keine stündlichen Zeilen mehr; nur noch ein Datum (Tag).
  • Zähler im Speicher bleiben „pro Tag“ (z.B. Key (date, account_name, stat_type, stat_key) oder ein Tages-Datum im StateManager). Beim Speichern immer für heute schreiben (UPSERT pro Tag, nicht pro Stunde). Beim Report: Werte für heute laden, mit In-Memory zusammenführen, Report, dann zurücksetzen.
  • Vorteil: Kein Stundenwechsel nötig; weniger Fehlerquellen; Report braucht keine stündliche Aggregation.
  • Nachteil: Keine stündlichen Auswertungen mehr (falls gewünscht).

Option C: Event-basierte Statistik (Architektur-Anpassung)

  • Jedes relevante Ereignis („neue Mail gesehen“, „Filter gematcht“, „Aktion ausgeführt“) wird einzeln in der DB festgehalten (z.B. eine Zeile pro Event mit Zeitstempel oder zumindest Datum+Stunde).
  • Tages- (und ggf. Stunden-)Statistiken werden nur noch aus diesen Events aggregiert (COUNT/SUM per Tag/Stunde). Keine „laufenden“ In-Memory-Gesamtzähler mehr, die über Stunden hinweg gemischt werden.
  • Vorteil: Keine Race zwischen „Stunde wechseln“ und „Zähler zurücksetzen“; Neustart verliert nur die noch nicht persistierten Events (optional: sofortiges Schreiben oder kurzer Buffer). Korrekte Nachauswertung pro Tag/Stunde möglich.
  • Nachteil: Mehr Schreibzugriffe, evtl. Tabelle wächst; Aggregation für Report muss sauber definiert werden (z.B. Indizes auf Datum/Stunde).

Empfehlung / Umsetzung

  • Umgesetzt: Option C (event-basierte Statistik). Siehe Code:
    • StateManager: Tabelle statistics_events (occurred_at, stat_type, stat_key, account_name), record_stat_event(), aggregate_statistics_for_day(), clear_statistics_events_for_day(), cleanup_old_statistics_events(retention_days).
    • Scheduler: Jedes relevante Ereignis (neue Mail, Filter-Match, Aktion, Aktion fehlgeschlagen) wird per record_stat_event() geschrieben. Täglicher Report liest nur noch load_statistics_for_day() (Aggregation aus Events). Nach Report: clear_statistics_for_day() und Zurücksetzen von _filter_action_counts / _action_log. Kein periodisches „Statistiken speichern“ mehr; stattdessen Job zur Bereinigung alter Events (Retention, z.B. 90 Tage, konfigurierbar unter general.statistics.retention_days).
    • CLI: clear-statistics --day/--hour/--today/--all arbeitet weiterhin; löscht jetzt Events in der statistics_events-Tabelle.