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
76 lines
2.4 KiB
Go
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)
|
|
}
|