Files
nexarch/dms/internal/jobqueue/worker.go
T
sysopsandClaude Sonnet 5 bfea94b032 FDN-04: job-queue & worker-runtime
Postgres-Jobqueue (processing_jobs, FOR UPDATE SKIP LOCKED), In-Prozess-
Worker-Goroutinen, kein Redis/AMQP. Enqueue mit Idempotency-Key-Dedup,
Dequeue mit Stale-Lock-Wiedervorlage (Absturzsicherheit), Fail mit
arithmetischem Backoff und Dead-Letter-Queue nach erschoepften Versuchen,
RequeueDeadLetter fuer manuelle Wiederholung.

Auf 192.168.1.131 verifiziert: Absturz-Wiedervorlage (Job von einem
"abgestuerzten" Worker nie completed/failed, zweiter Worker holt ihn nach
Ablauf der Sperre erneut), Idempotenz bei Doppelzustellung (gleicher
idempotency_key erzeugt nur 1 Zeile), DLQ-Eintrag manuell wiederholbar.

Vier reale Fehler beim Testen gefunden und behoben: zwei pgx-Typinferenz-
Bugs im SQL-Parameterhandling (toter workerID-Parameter ohne Referenz in
der Query; untypisiertes any statt []string fuer den ::text[]-Cast), sowie
zwei Testinfrastruktur-Bugs (dms_tenant_test sammelte schema_migrations-
Zustand ueber Sitzungen hinweg an, jobqueue-Testfixture raeumte
processing_jobs nicht auf) - neues scripts/reset-test-env.sh + make check
(-p 1) analog Core behoben.

Siehe dms/docs/FDN-04-PRUEFPROTOKOLL.md fuer alle Pruefungsergebnisse.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HhgFcLS8tYMhDJpP74C6AQ
2026-08-29 20:58:26 +02:00

76 lines
2.4 KiB
Go

package jobqueue
import (
"context"
"errors"
"log"
"time"
)
// Handler verarbeitet einen einzelnen Job. Ein zurueckgegebener Fehler
// fuehrt zu Queue.Fail (Backoff/DLQ), nil zu Queue.Complete.
type Handler func(ctx context.Context, job *Job) error
// Worker pollt die Queue in einer In-Prozess-Goroutine und ruft Handler je
// abgeholtem Job auf — die "In-Prozess-Worker-Goroutinen" aus der
// Ticket-Vorgabe, kein separater Prozess/Redis noetig.
type Worker struct {
queue *Queue
workerID string
jobTypes []string
pollInterval time.Duration
handler Handler
}
func NewWorker(queue *Queue, workerID string, jobTypes []string, pollInterval time.Duration, handler Handler) *Worker {
return &Worker{queue: queue, workerID: workerID, jobTypes: jobTypes, pollInterval: pollInterval, handler: handler}
}
// Run blockiert, bis ctx beendet wird, und verarbeitet dabei fortlaufend
// Jobs. Absturzsicherheit (Pruefung 1) entsteht NICHT durch Run selbst,
// sondern dadurch, dass ein abgestuerzter Prozess (der Run gar nicht mehr
// ausfuehrt) seine "processing"-Sperren nie verlaengert — ein ANDERER
// Worker-Prozess holt den Job nach Ablauf von staleLockAfter erneut ab
// (siehe Queue.Dequeue).
func (w *Worker) Run(ctx context.Context) {
ticker := time.NewTicker(w.pollInterval)
defer ticker.Stop()
for {
select {
case <-ctx.Done():
return
case <-ticker.C:
w.processOne(ctx)
}
}
}
// processOne holt und verarbeitet EINEN Job, falls verfuegbar. Oeffentlich
// über RunOnce fuer Tests, die deterministisch (ohne Polling-Timing) einen
// einzelnen Verarbeitungsschritt auslösen wollen.
func (w *Worker) processOne(ctx context.Context) {
job, err := w.queue.Dequeue(ctx, w.workerID, w.jobTypes)
if err != nil {
if !errors.Is(err, ErrNoJobAvailable) {
log.Printf("jobqueue: dequeue fehlgeschlagen: %v", err)
}
return
}
if handlerErr := w.handler(ctx, job); handlerErr != nil {
if err := w.queue.Fail(ctx, job.ID, handlerErr); err != nil {
log.Printf("jobqueue: fehlschlag fuer job %q nicht erfassbar: %v", job.ID, err)
}
return
}
if err := w.queue.Complete(ctx, job.ID); err != nil {
log.Printf("jobqueue: abschluss fuer job %q fehlgeschlagen: %v", job.ID, err)
}
}
// RunOnce verarbeitet synchron genau einen Job (falls verfuegbar) und
// kehrt zurueck — fuer Tests, die ohne Polling-Intervall arbeiten wollen.
func (w *Worker) RunOnce(ctx context.Context) {
w.processOne(ctx)
}