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
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
a9ede93176
commit
bfea94b032
@@ -0,0 +1,75 @@
|
||||
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)
|
||||
}
|
||||
Reference in New Issue
Block a user