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>
21 lines
1001 B
SQL
21 lines
1001 B
SQL
-- Benachrichtigungs-Dispatcher-Warteschlange (CFG-02, siehe
|
|
-- core-kanban/tickets/CFG-02.md). Postgres-basiert statt Redis/AMQP
|
|
-- (Projekt-Konvention, siehe nexarch-state.json techstack.job_queue) —
|
|
-- Zeilen ueberleben einen Neustart des Dispatcher-Prozesses unveraendert
|
|
-- (Akzeptanzkriterium 3).
|
|
CREATE TABLE notification_jobs (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
channel TEXT NOT NULL,
|
|
recipient TEXT NOT NULL,
|
|
payload JSONB NOT NULL DEFAULT '{}'::jsonb,
|
|
status TEXT NOT NULL DEFAULT 'pending' CHECK (status IN ('pending', 'sent', 'failed')),
|
|
attempts INT NOT NULL DEFAULT 0,
|
|
max_attempts INT NOT NULL DEFAULT 5,
|
|
next_attempt_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
last_error TEXT,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
);
|
|
|
|
CREATE INDEX notification_jobs_due_idx ON notification_jobs (status, next_attempt_at);
|