Replace hand-rolled polling loops, semaphores, and WaitGroups in all 7 background workers with go.gearno.de/kit/worker. Each worker now implements Handler[T] (Claim/Process) and optionally StaleRecoverer, gaining automatic Prometheus metrics and OpenTelemetry tracing. Bumps kit from v0.3.0 to v0.5.0. Signed-off-by: Bryan Frimin <bryan@getprobo.com>
94 lines
2.8 KiB
Markdown
94 lines
2.8 KiB
Markdown
# Go Worker
|
|
|
|
Background workers use `go.gearno.de/kit/worker`. The kit handles the polling loop, semaphore-based concurrency, graceful shutdown, non-cancellable contexts, Prometheus metrics, and OpenTelemetry tracing. You only implement a **handler**.
|
|
|
|
## Handler interface
|
|
|
|
Implement `worker.Handler[T]` with `Claim` and `Process`. Optionally implement `worker.StaleRecoverer` for stale row recovery.
|
|
|
|
```go
|
|
type fooHandler struct {
|
|
pg *pg.Client
|
|
logger *log.Logger
|
|
staleAfter time.Duration
|
|
}
|
|
|
|
func (h *fooHandler) Claim(ctx context.Context) (coredata.FooItem, error) {
|
|
var item coredata.FooItem
|
|
|
|
if err := h.pg.WithTx(
|
|
ctx,
|
|
func(ctx context.Context, tx pg.Tx) error {
|
|
if err := item.LoadNextPendingForUpdateSkipLocked(ctx, tx); err != nil {
|
|
return err
|
|
}
|
|
|
|
now := time.Now()
|
|
item.Status = coredata.FooStatusProcessing
|
|
item.UpdatedAt = now
|
|
return item.Update(ctx, tx, coredata.NewNoScope())
|
|
},
|
|
); err != nil {
|
|
if errors.Is(err, coredata.ErrResourceNotFound) {
|
|
return coredata.FooItem{}, worker.ErrNoTask
|
|
}
|
|
return coredata.FooItem{}, err
|
|
}
|
|
|
|
return item, nil
|
|
}
|
|
|
|
func (h *fooHandler) Process(ctx context.Context, item coredata.FooItem) error {
|
|
if err := h.handle(ctx, &item); err != nil {
|
|
if failErr := h.fail(ctx, &item, err); failErr != nil {
|
|
h.logger.ErrorCtx(ctx, "cannot fail item", log.Error(failErr))
|
|
}
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (h *fooHandler) RecoverStale(ctx context.Context) error {
|
|
return h.pg.WithConn(
|
|
ctx,
|
|
func(ctx context.Context, conn pg.Querier) error {
|
|
return coredata.ResetStaleFooItems(ctx, conn, h.staleAfter)
|
|
},
|
|
)
|
|
}
|
|
```
|
|
|
|
## Constructor
|
|
|
|
The constructor builds the handler and returns `*worker.Worker[T]`. Use `worker.WithInterval` and `worker.WithMaxConcurrency` for tuning. Keep domain-specific options (e.g. timeouts, staleAfter) on the handler struct.
|
|
|
|
```go
|
|
func NewFooWorker(
|
|
pgClient *pg.Client,
|
|
logger *log.Logger,
|
|
opts ...worker.Option,
|
|
) *worker.Worker[coredata.FooItem] {
|
|
h := &fooHandler{
|
|
pg: pgClient,
|
|
logger: logger,
|
|
staleAfter: 5 * time.Minute,
|
|
}
|
|
|
|
return worker.New(
|
|
"foo-worker",
|
|
h,
|
|
logger,
|
|
opts...,
|
|
)
|
|
}
|
|
```
|
|
|
|
## Key principles
|
|
|
|
- **Claim with `FOR UPDATE SKIP LOCKED`** — prevents multiple workers from picking the same row
|
|
- **Return `worker.ErrNoTask`** from `Claim` when no work is available (not the coredata sentinel)
|
|
- **Context is non-cancellable** — the kit provides `context.WithoutCancel` to both `Claim` and `Process`
|
|
- **Process handles its own failures** — update DB status on error, the kit only logs and records metrics
|
|
- **Stale recovery is optional** — implement `worker.StaleRecoverer` if the worker marks rows as "processing"
|
|
- **Kit provides observability** — Prometheus metrics (`worker_tasks_total`, `worker_task_duration_seconds`, etc.) and OTel traces are automatic
|