You've already forked agentic-coding-workflow
feat(jobs): Module wiring + WorkerOptions.OnDeadLetter hook
This commit is contained in:
@@ -0,0 +1,129 @@
|
||||
package jobs
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log/slog"
|
||||
"strconv"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/yan1h/agent-coding-workflow/internal/infra/clock"
|
||||
)
|
||||
|
||||
// RunnerFunc is the signature of a periodic runner's tick.
|
||||
type RunnerFunc func(ctx context.Context)
|
||||
|
||||
// ModuleDeps groups the externally-supplied collaborators.
|
||||
type ModuleDeps struct {
|
||||
Repo Repository
|
||||
Handlers map[JobType]Handler
|
||||
RunnerFns map[string]RunnerFunc // name → tick fn (e.g. "workspace_fetch")
|
||||
Clock clock.Clock
|
||||
Log *slog.Logger
|
||||
WorkerBackoff []time.Duration
|
||||
OnDeadLetter func(ctx context.Context, job Job, err error)
|
||||
}
|
||||
|
||||
// Module wires the scheduler, worker pool and runners. Owners construct it via
|
||||
// NewModule + Start, then call Stop on shutdown.
|
||||
type Module struct {
|
||||
cfg Config
|
||||
scheduler *Scheduler
|
||||
workers []*Worker
|
||||
log *slog.Logger
|
||||
wg sync.WaitGroup
|
||||
}
|
||||
|
||||
// NewModule constructs but does not start the module.
|
||||
func NewModule(cfg Config, deps ModuleDeps) *Module {
|
||||
clk := deps.Clock
|
||||
if clk == nil {
|
||||
clk = clock.Real()
|
||||
}
|
||||
log := deps.Log
|
||||
if log == nil {
|
||||
log = slog.Default()
|
||||
}
|
||||
|
||||
sch := NewScheduler(log)
|
||||
for name, fn := range deps.RunnerFns {
|
||||
entry := scheduleEntry(cfg, name)
|
||||
sch.Add(name, entry.interval, entry.enabled, fn)
|
||||
}
|
||||
|
||||
workers := make([]*Worker, cfg.Workers)
|
||||
for i := 0; i < cfg.Workers; i++ {
|
||||
workers[i] = NewWorker(deps.Repo, deps.Handlers, WorkerOptions{
|
||||
ID: workerID(i),
|
||||
PollInterval: 200 * time.Millisecond,
|
||||
Backoff: deps.WorkerBackoff,
|
||||
Clock: clk,
|
||||
Log: log,
|
||||
OnDeadLetter: deps.OnDeadLetter,
|
||||
})
|
||||
}
|
||||
return &Module{cfg: cfg, scheduler: sch, workers: workers, log: log}
|
||||
}
|
||||
|
||||
// Start kicks off the scheduler and worker goroutines. They exit when ctx is
|
||||
// canceled. Caller must call Stop to wait for clean shutdown.
|
||||
func (m *Module) Start(ctx context.Context) {
|
||||
if !m.cfg.Enabled {
|
||||
m.log.Warn("jobs.module.disabled")
|
||||
return
|
||||
}
|
||||
m.scheduler.Start(ctx)
|
||||
for _, w := range m.workers {
|
||||
m.wg.Add(1)
|
||||
go func(w *Worker) {
|
||||
defer m.wg.Done()
|
||||
w.Run(ctx)
|
||||
}(w)
|
||||
}
|
||||
m.log.Info("jobs.module.started", "workers", len(m.workers))
|
||||
}
|
||||
|
||||
// Stop waits up to ShutdownGrace for goroutines to exit.
|
||||
func (m *Module) Stop(ctx context.Context) {
|
||||
done := make(chan struct{})
|
||||
go func() {
|
||||
m.scheduler.Wait()
|
||||
m.wg.Wait()
|
||||
close(done)
|
||||
}()
|
||||
grace := m.cfg.ShutdownGrace
|
||||
if grace <= 0 {
|
||||
grace = 30 * time.Second
|
||||
}
|
||||
select {
|
||||
case <-done:
|
||||
m.log.Info("jobs.module.stopped")
|
||||
case <-time.After(grace):
|
||||
m.log.Warn("jobs.module.shutdown_timeout", "grace_ms", grace.Milliseconds())
|
||||
case <-ctx.Done():
|
||||
m.log.Warn("jobs.module.shutdown_ctx_canceled")
|
||||
}
|
||||
}
|
||||
|
||||
func workerID(i int) string { return "worker#" + strconv.Itoa(i) }
|
||||
|
||||
type schedEntry struct {
|
||||
interval time.Duration
|
||||
enabled bool
|
||||
}
|
||||
|
||||
func scheduleEntry(cfg Config, name string) schedEntry {
|
||||
switch name {
|
||||
case "workspace_fetch":
|
||||
return schedEntry{cfg.WorkspaceFetch.Interval, cfg.WorkspaceFetch.Enabled}
|
||||
case "worktree_prune":
|
||||
return schedEntry{cfg.WorktreePrune.Interval, cfg.WorktreePrune.Enabled}
|
||||
case "attachment_cleanup":
|
||||
return schedEntry{cfg.AttachmentCleanup.Interval, cfg.AttachmentCleanup.Enabled}
|
||||
case "job_reaper":
|
||||
return schedEntry{cfg.JobReaper.Interval, cfg.JobReaper.Enabled}
|
||||
case "job_purge":
|
||||
return schedEntry{cfg.JobPurge.Interval, cfg.JobPurge.Enabled}
|
||||
}
|
||||
return schedEntry{0, false}
|
||||
}
|
||||
Reference in New Issue
Block a user