internal/notify: Dispatcher.Enqueue ist die EINE schmale Schnittstelle, ueber
die Module Benachrichtigungen ausloesen (Akzeptanzkriterium 1) — kein Modul
baut eigenen Versandcode. Warteschlange ist die Postgres-Tabelle
notification_jobs (Projekt-Konvention statt Redis/AMQP), existiert
ausschliesslich in der Datenbank, nicht im Prozessspeicher.
Dispatcher.ProcessDue holt faellige Jobs per FOR UPDATE SKIP LOCKED
(dieselbe Konvention wie internal/tenant.Lifecycle.ProcessDueDeletions) —
serialisiert konkurrierende Worker/Module, verhindert doppelte Zustellung.
Fehlschlag erhoeht attempts und plant next_attempt_at mit linearem Backoff;
nach max_attempts wird der Job kontrolliert auf status=failed gesetzt statt
endlos wiederholt zu werden (Akzeptanzkriterium 2).
Sender ist eine schmale Schnittstelle fuer die eigentlichen Kanaele
(E-Mail/In-App = CFG-03, nicht Teil dieser Kachel) — der Dispatcher kennt
nur "zustellen oder nicht", keine Kanal-Details.
Pruefungen (ausgefuehrt auf root@192.168.1.131, go build/vet/test PASS):
1. Neustart waehrend offener Zustellung verliert keine Nachricht —
TestQueue_SurvivesRestartWithoutMessageLoss: Enqueue durch eine
Dispatcher-Instanz, Verarbeitung durch eine komplett neue (simulierter
Neustart), Nachricht wird trotzdem zugestellt. PASS.
2. Wiederholungslogik greift bei simuliertem Fehler und bricht kontrolliert
ab — TestProcessDue_RetriesThenGivesUpAfterMaxAttempts: 3 Versuche bei
max_attempts=3, danach status=failed, keine weitere Verarbeitung. PASS.
3. Zwei Module loesen gleichzeitig aus, beide korrekt zugestellt —
TestProcessDue_ConcurrentDispatchBothDelivered: zwei parallele
ProcessDue-Aufrufe, beide Nachrichten je genau einmal zugestellt, keine
Doppelzustellung. PASS.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>