package worker import ( "context" "fmt" "log/slog" "strings" "time" "moku-backend/internal/jobs" ) type JobStore interface { ClaimNext(ctx context.Context) (*jobs.Job, error) MarkSucceeded(ctx context.Context, jobID string) error MarkFailed(ctx context.Context, jobID, failure string) error } type Handler func(ctx context.Context, job jobs.Job) error type Runner struct { store JobStore logger *slog.Logger pollInterval time.Duration handlers map[string]Handler } func NewRunner(store JobStore, logger *slog.Logger, pollInterval time.Duration) *Runner { interval := pollInterval if interval <= 0 { interval = time.Second } return &Runner{ store: store, logger: logger, pollInterval: interval, handlers: make(map[string]Handler), } } func (runner *Runner) Register(kind string, handler Handler) { runner.handlers[strings.TrimSpace(kind)] = handler } func (runner *Runner) Run(ctx context.Context) error { for { select { case <-ctx.Done(): return nil default: } job, err := runner.store.ClaimNext(ctx) if err != nil { if ctx.Err() != nil { return nil } return err } if job == nil { if err := waitForNextPoll(ctx, runner.pollInterval); err != nil { return nil } continue } handler, ok := runner.handlers[job.Kind] if !ok { failure := fmt.Sprintf("no handler registered for job kind %q", job.Kind) if err := runner.store.MarkFailed(ctx, job.ID, failure); err != nil { return err } runner.logger.Error("worker job failed", "jobID", job.ID, "kind", job.Kind, "error", failure) continue } if err := handler(ctx, *job); err != nil { if ctx.Err() != nil { return nil } failure := strings.TrimSpace(err.Error()) if failure == "" { failure = "job handler returned an empty error" } if markErr := runner.store.MarkFailed(ctx, job.ID, failure); markErr != nil { return markErr } runner.logger.Error("worker job failed", "jobID", job.ID, "kind", job.Kind, "error", failure) continue } if err := runner.store.MarkSucceeded(ctx, job.ID); err != nil { return err } runner.logger.Info("worker job succeeded", "jobID", job.ID, "kind", job.Kind) } } func waitForNextPoll(ctx context.Context, interval time.Duration) error { timer := time.NewTimer(interval) defer timer.Stop() select { case <-ctx.Done(): return ctx.Err() case <-timer.C: return nil } }