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) }