Erfolgreich auf 192.168.1.132 (Test) und 192.168.1.131 (Produktion) deployed, keine Fehler.
11 KiB
PROJ-56: Last-Entzerrung für Hintergrundjobs (OCR, IMAP-Sync)
Status: Deployed
Created: 2026-06-22 Last Updated: 2026-06-22
Dependencies
- PROJ-35 (OCR & Anhang-Volltext-Indexierung)
- PROJ-8 (Automatischer IMAP-Sync)
Hintergrund (Nutzerwunsch)
Vergleich mit Mailpiler: dort laufen Wartungsprozesse zeitlich entzerrt statt komplett parallel. Bei archivmail laufen OCR-Worker (sofort bei Import, 2 parallel, je tesseract-Mehrthread) und IMAP-Sync (alle Postfächer pollen unabhängig, ohne Jitter, exakt zum Intervall-Ablauf) unkoordiniert nebeneinander, was bei begrenztem RAM (z.B. 4GB-LXC) zu Lastspitzen führt.
Manticore-Reindex wurde geprüft, ist aber bereits ein rein manuelles CLI-Kommando (kein Scheduler) — keine automatische Last-Entzerrung nötig/möglich, daher außerhalb des Scopes dieses Tickets.
Entscheidung (Nutzer, 2026-06-22)
Alle drei vorgeschlagenen Maßnahmen gewünscht, soweit technisch sinnvoll:
- OCR zeitlich entzerren — konfigurierbares Zeitfenster, in dem OCR-Verarbeitung pausiert (z.B. Geschäftszeiten ausnehmen).
- IMAP-Sync-Intervalle entzerren — deterministischer Jitter pro Postfach, damit nicht alle Accounts auf derselben Minutengrenze pollen.
- Reindex in Nachtfenster — entfällt, da bereits manuell/CLI-only ohne automatischen Trigger.
Acceptance Criteria
config.ymlerlaubt ein optionales OCR-Zeitfenster (z.B.ocr.paused_hours: [8, 18]= pausiert 08:00–18:00 Uhr); ohne Konfiguration: Verhalten unverändert (sofortige Verarbeitung, wie bisher).- Außerhalb des erlaubten Fensters werden neue OCR-Aufträge weiterhin in die Queue/DB als
pendingaufgenommen, aber nicht abgearbeitet, bis das Fenster sich öffnet (kein Datenverlust, kein Crash bei vollem Queue). - IMAP-Scheduler verteilt den tatsächlichen Sync-Start jedes Accounts über einen deterministischen Jitter (abgeleitet aus Account-ID, kein Zufall bei jedem Tick) innerhalb eines konfigurierbaren Fensters (Default z.B. 0–4 Minuten), sodass bei N Accounts mit gleichem Intervall nicht alle zur gleichen Minute synchronisieren.
- Bestehende Funktionalität (Sync-Intervall pro Account, manuelles "Jetzt synchronisieren") bleibt unverändert nutzbar — Jitter gilt nur für den automatischen Scheduler-Trigger.
- Kein Verhalten ändert sich für Installationen ohne neue Config-Werte (Default = bisheriges Verhalten, kein Opt-in nötig für Jitter da inhärent sinnvoll, aber abschaltbar via
jitter_seconds: 0).
Tech Design
Übersprungen (kleine, klar umrissene Performance-Änderung, kein architektonischer Schnitt nötig) — analog zum Vorgehen bei PROJ-55.
Implementation Notes (2026-06-22)
Geänderte/neue Dateien
config/config.go: neue SektionenOCRConfig(paused_hours *[2]int) undIMAPSchedulerConfig(jitter_seconds *int) inConfig. HelperResolvedJitterSeconds()+ KonstanteDefaultIMAPJitterSeconds = 240.internal/ocr/worker.go:Options.PausedHours, Worker-FeldpausedHours,isPaused(now), Pause-Gate imrun-Loop.internal/imap/scheduler.go: FeldjitterSeconds,SetJitterSeconds(),jitterOffset(), Anpassung der Fälligkeitsbedingung (interval + jitterOffset(acc.ID)).cmd/archivmail/main.go:PausedHoursan OCR-Worker durchgereicht;imapSched.SetJitterSeconds(cfg.IMAPScheduler.ResolvedJitterSeconds()).config/config.docker.yml.example: auskommentierte Beispielsektionenocrundimap_scheduler.
OCR-Pausenmechanismus
Gewählt: Worker konsumiert die Queue gar nicht erst, solange das Pausenfenster aktiv ist. Vor jedem Dequeue prüft jeder Worker isPaused(time.Now()). Bei aktiver Pause wartet er per select (60s-Ticker via pauseCheckInterval, reagiert sofort auf done/ctx.Done()) und liest erst dann wieder aus dem Channel. Vorteile:
- Kein Datenverlust: Jobs bleiben im gepufferten Channel und v.a. als
ocr_status='pending'in PostgreSQL. - Kein Queue-Überlauf: Der Boot-Resume-Refill ist
QueueLen-gesteuert und legt nichts nach, sobald der Channel voll ist; neue Submits werden wie bisher non-blocking verworfen (bleiben aberpendingin der DB und werden beim nächsten Boot-Resume/Fensteröffnen nachgezogen). - Kein Busy-Loop: Pollintervall 60s.
cmd_ocr_reprocess.gosetztPausedHoursbewusst nicht (nil) → manueller Admin-Befehl ignoriert das Fenster und läuft sofort.- Wrap-around-Fenster (z.B.
[22, 6]) werden inisPausedunterstützt (start>end →h>=start || h<end);start==end= No-op (nie pausieren).
IMAP-Jitter
Deterministischer Offset accountID % jitterSeconds Sekunden, nur aus der Account-ID abgeleitet (stabil über alle Ticks, kein rand()). Fälligkeit erst bei now.Sub(lastSync) >= interval + jitterOffset. Default 240s wenn imap_scheduler fehlt (jitter_seconds-Pointer = nil); explizit 0 deaktiviert. TriggerSync (manuell) bleibt unberührt.
Offene Risiken / Edge Cases
- Mitternachts-übergreifendes Fenster
[22,6]ist abgedeckt; reine Stunden-Granularität (kein Minutenanteil) ist bewusst einfach gehalten. - Server-Zeitzone:
time.Now().Hour()nutzt lokale Serverzeit — bei UTC-Servern muss das Fenster entsprechend gesetzt werden. - Jitter verlängert das effektive Intervall um bis zu
jitter_seconds(max +4 Min bei Default); akzeptabel, da nur Spitzenglättung bezweckt ist. - Lokal kein
go buildmöglich (kein Toolchain) — nur statische Konsistenzprüfung erfolgt; Build-Verifikation auf 192.168.1.131.
QA Test Results (Code-Review + Build-Verifikation, 2026-06-22)
Getestet von: QA / Red-Team. Methode: statisches Code-Review der geänderten Dateien
- isolierte Build-Verifikation auf Test-Server 192.168.1.132 (Go 1.24.4), ohne
den produktiven Checkout / Dienst zu berühren (Tarball → /tmp/archivmail-qa56 →
go build→version-Smoke-Test +go vet→ vollständige Bereinigung).
Build-Verifikation (192.168.1.132)
CGO_ENABLED=0 go build -buildvcs=false -o /tmp/archivmail-test ./cmd/archivmail/→ EXIT 0 (20 MB Binary).- Smoke-Test
archivmail-test version→ OK (archivmail 0.9.1, Modul-Liste wird ausgegeben). go vet ./config/... ./internal/ocr/... ./internal/imap/... ./cmd/archivmail/...→ EXIT 0 (keine Befunde).- Kein Eingriff in /opt/archivmail, kein
systemctl restart. Temp-Artefakte auf 132 entfernt.
Acceptance Criteria
- [PASS] AC1 — Optionales OCR-Zeitfenster:
OCRConfig.PausedHours *[2]int(yaml:"paused_hours,omitempty").nil→isPaused()gibtfalse, Verhalten unverändert. - [PASS] AC2 — Außerhalb des Fensters bleiben Jobs
pending: Worker konsumiert die Queue im Pausenfenster nicht (run-Loop: beiisPausednurselectmit 60s-Ticker, kein Dequeue). Jobs bleiben im gepufferten Channel bzw.ocr_status='pending'in der DB; Boot-Resume zieht sie nach. Kein Datenverlust, kein Crash bei vollem Channel (Submitist non-blocking; verworfene Submits bleibenpendingin DB). - [PASS] AC3 — Deterministischer IMAP-Jitter:
jitterOffset(accountID) = (accountID % jitterSeconds) s, nur aus Account-ID abgeleitet, stabil über alle Ticks (keinrand()). Fälligkeit erst beinow.Sub(lastSync) >= interval + jitterOffset. - [PASS] AC4 — Bestehende Funktionalität unverändert:
TriggerSync(manuell) umgehtcheckAccounts/Jitter vollständig und startet sofort (runSyncWithRetrydirekt). Sync-Intervall pro Account bleibt Basis. - [PASS] AC5 — Default ohne Config: OCR
paused_hoursunset → nie Pause.imap_schedulerganz weggelassen →ResolvedJitterSeconds()= 240s Default;jitter_seconds: 0→ Jitter aus (Legacy-Tick exakt zum Intervall).
Prüfpunkte
- Datenverlust bei Pause — PASS. Jobs werden nicht verworfen; Channel-Buffer +
pending-Status + Boot-Resume garantieren Nachzug. - Race Conditions — PASS (in der aktuellen Verdrahtung).
pausedHourswird nur im Konstruktor gesetzt, danach nur gelesen → keine Concurrent-Writes.jitterSecondswird inmain.goperSetJitterSeconds()vorimapSched.Start()gesetzt; der Loop liest erst nachStart(). Kein Daten-Rennen im realen Pfad. Hinweis (LOW, kein Bug):SetJitterSeconds/jitterOffsetgreifen unsynchronisiert aufs.jitterSecondszu — würde ein künftiger Aufrufer Jitter zur Laufzeit ändern, wäre es ein Race. Aktuell nicht der Fall.go vetmeldet nichts; ein-race-Test wurde nicht ausgeführt (kein Test-Harness im Scope). - Edge Cases — PASS.
- Mitternachtsfenster
[22,6]:start>end→h>=start || h<end, korrekt (pausiert 22–05). start==end: explizit No-op (nie pausieren).- Fehlende Config / nil-Pointer:
isPausedprüftpausedHours==nil;ResolvedJitterSecondsprüftJitterSeconds==nil. Keine nil-Derefs. jitter_seconds: 0:jitterOffsetgibt 0 → reines Intervall-Verhalten.- Negativ:
SetJitterSecondsundResolvedJitterSecondsklemmen<0auf 0.
- Mitternachtsfenster
- Backward-Compat — PASS (siehe AC5). Default-Pfad identisch zum Alt-Verhalten, mit einer bewussten Ausnahme: ohne
imap_scheduler-Sektion ist nun 240s Jitter aktiv (laut Spec gewollt, „abschaltbar via jitter_seconds: 0"). Verlängert das effektive Sync-Intervall um bis zu 4 Min — dokumentiert in den Implementation Notes als akzeptiert. cmd_ocr_reprocess.goerbt kein Pausenfenster — PASS. Derocr.Options-Block dort setztPausedHoursnicht (= nil) → manuelle Reprocessing-Läufe ignorieren das Fenster und laufen sofort. Verifiziert (Zeilen 103–107).- Manuelles "Jetzt synchronisieren" ignoriert Jitter — PASS.
TriggerSyncruftrunSyncWithRetrydirekt auf, ohnedueAfter/jitterOffset-Berechnung. Verifiziert.
Findings
- Keine Bugs (Severity Critical/High/Medium) gefunden.
- LOW / INFO 1 (kein Fix nötig):
Scheduler.jitterSecondsist nicht mutex-geschützt. Unkritisch, da set-before-start. Nur relevant, falls künftig eine Laufzeit-Rekonfiguration eingeführt wird → danns.muo.ä. ergänzen. - LOW / INFO 2 (kein Fix nötig): Pausenfenster nutzt lokale Serverzeit (
time.Now().Hour()). Auf UTC-Servern muss das Fenster entsprechend gewählt werden — bereits in den Implementation Notes dokumentiert. - INFO 3 (vorbestehend, nicht Teil von PROJ-56):
config.docker.yml.exampleenthält weiterhinxapian_path/backend: xapian, obwohl derStorageConfigkeinxapian_path-Feld hat und der Default-Backend Manticore ist. Unkritisch (YAML-Unmarshal ignoriert unbekannte Keys), aber irreführend. Außerhalb des Scopes dieses Tickets.
Fazit
Production-Ready: JA. Alle 5 Acceptance Criteria erfüllt, alle 6 Sicherheits-/Korrektheits-Prüfpunkte bestanden, Build + vet + Smoke-Test auf 192.168.1.132 grün. Keine blockierenden Findings; nur 2 LOW-Hinweise (kein Fix nötig) und 1 vorbestehender, ticket-fremder Konfig-Hinweis.
Deployment
- Test (192.168.1.132):
update.sh(Commit4dbf27c), Backend+Frontend aktiv, Health-Checks bestanden, keine Config-Parsing-Fehler trotz fehlender OCRConfig/IMAPSchedulerConfig in der lokalenconfig.yml(Default-Verhalten bestätigt). 2026-06-22. - Produktion (192.168.1.131):
update.sh(Commit12c4dc7), Backend+Frontend aktiv, Health-Checks bestanden, keine Config-Parsing-Fehler. 2026-06-22.