feat(jobs): Module wiring + WorkerOptions.OnDeadLetter hook

This commit is contained in:
2026-05-06 08:31:24 +08:00
parent 2d0c45a52b
commit 895a816221
4 changed files with 279 additions and 0 deletions
+22
View File
@@ -2,6 +2,7 @@ package jobs
import (
"context"
"errors"
"fmt"
"log/slog"
"sync"
@@ -19,6 +20,12 @@ type WorkerOptions struct {
Backoff []time.Duration
Clock clock.Clock
Log *slog.Logger
// OnDeadLetter, if non-nil, is invoked once after a job is moved to dead-letter
// (either because IsPermanent(err) or attempts exhausted). The hook runs in
// the worker goroutine; panics are recovered and logged but do not stop the
// worker. Use this for fallback admin notifications when external delivery
// fails permanently.
OnDeadLetter func(ctx context.Context, job Job, err error)
}
// Worker pulls leased jobs from the repository, dispatches them to handlers,
@@ -88,6 +95,7 @@ func (w *Worker) execute(ctx context.Context, job Job) {
w.opts.Log.Error("job.dead_letter_write_failed", "id", job.ID, "err", derr.Error())
}
w.opts.Log.Error("job.dead_letter", "id", job.ID, "type", job.Type, "reason", "unknown_type")
w.fireOnDeadLetter(ctx, job, errors.New("unknown job type: "+string(job.Type)))
return
}
@@ -110,6 +118,7 @@ func (w *Worker) execute(ctx context.Context, job Job) {
}
w.opts.Log.Error("job.dead_letter",
"id", job.ID, "type", job.Type, "attempts", job.Attempts, "err", msg)
w.fireOnDeadLetter(ctx, job, err)
return
}
@@ -148,6 +157,19 @@ func backoffFor(seq []time.Duration, attempt int) time.Duration {
return seq[idx]
}
// fireOnDeadLetter invokes opts.OnDeadLetter, if set, recovering any panics.
func (w *Worker) fireOnDeadLetter(ctx context.Context, job Job, err error) {
if w.opts.OnDeadLetter == nil {
return
}
defer func() {
if r := recover(); r != nil {
w.opts.Log.Error("job.dead_letter_hook_panic", "id", job.ID, "panic", r)
}
}()
w.opts.OnDeadLetter(ctx, job, err)
}
func truncate(s string, n int) string {
if len(s) <= n {
return s