package runners import ( "context" "log/slog" "time" "github.com/google/uuid" ) // JobsReapRepo is the minimal jobs repository surface for the reaper. type JobsReapRepo interface { ReapStuckRunningJobs(ctx context.Context, leasedBefore time.Time) ([]uuid.UUID, error) } // JobReaper resets jobs.running rows whose lease has exceeded the timeout // (worker probably crashed). Periodic; idempotent; no retry needed. type JobReaper struct { repo JobsReapRepo leaseTimeout time.Duration log *slog.Logger } // NewJobReaper builds the reaper. func NewJobReaper(repo JobsReapRepo, leaseTimeout time.Duration, log *slog.Logger) *JobReaper { if log == nil { log = slog.Default() } return &JobReaper{repo: repo, leaseTimeout: leaseTimeout, log: log} } // Run reaps stuck rows once. func (r *JobReaper) Run(ctx context.Context) { cutoff := time.Now().Add(-r.leaseTimeout) ids, err := r.repo.ReapStuckRunningJobs(ctx, cutoff) if err != nil { r.log.Error("job_reaper.reap", "err", err.Error()) return } if len(ids) > 0 { r.log.Warn("job_reaper.reaped", "count", len(ids), "ids", ids) } }