From 215c1bb821a67c5390fa54c84cc394c2b860ad24 Mon Sep 17 00:00:00 2001 From: Jerry Yan <792602257@qq.com> Date: Tue, 5 May 2026 16:52:17 +0800 Subject: [PATCH] feat(jobs): domain types + Handler interface + Config --- internal/jobs/domain.go | 133 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 133 insertions(+) create mode 100644 internal/jobs/domain.go diff --git a/internal/jobs/domain.go b/internal/jobs/domain.go new file mode 100644 index 0000000..193b0ed --- /dev/null +++ b/internal/jobs/domain.go @@ -0,0 +1,133 @@ +// Package jobs implements the background job runner: a hybrid scheduler that +// drives idempotent periodic runners (workspace fetch, worktree prune, +// attachment cleanup, job reaper, job purge) directly via tickers, and routes +// retry-sensitive event handlers (webhook delivery) through a DB-backed jobs +// table with SELECT FOR UPDATE SKIP LOCKED dispatch. +package jobs + +import ( + "context" + "encoding/json" + "errors" + "time" + + "github.com/google/uuid" +) + +// JobType identifies a kind of work that a Handler knows how to execute. +// Runners (periodic) are NOT job types — they tick directly. Handlers +// (event-driven) are. +type JobType string + +const ( + // TypeWebhookDeliver delivers a notify.Message to the configured webhook URL. + TypeWebhookDeliver JobType = "webhook.deliver" +) + +// JobStatus matches the CHECK constraint on jobs.status. +type JobStatus string + +const ( + StatusPending JobStatus = "pending" + StatusRunning JobStatus = "running" + StatusCompleted JobStatus = "completed" + StatusDead JobStatus = "dead" +) + +// Job mirrors a row in the jobs table. +type Job struct { + ID uuid.UUID + Type JobType + Payload json.RawMessage + Status JobStatus + ScheduledAt time.Time + Attempts int32 + MaxAttempts int32 + LeasedAt *time.Time + LeasedBy *string + LastError *string + CompletedAt *time.Time + CreatedAt time.Time + UpdatedAt time.Time +} + +// MaxPayloadBytes caps payload size at 64 KB. Enforced at enqueue time. +const MaxPayloadBytes = 64 * 1024 + +// Handler executes a single Job. Returning a permanentError sends the job +// straight to dead-letter; any other error triggers retry per the worker's +// backoff schedule. Returning nil marks the job completed. +type Handler interface { + Type() JobType + Handle(ctx context.Context, job Job) error +} + +// permanentError marks an error as non-retryable (4xx-equivalent). +type permanentError struct{ err error } + +// Permanent wraps err so the worker routes the job to dead-letter without +// further retries. +func Permanent(err error) error { + if err == nil { + return nil + } + return &permanentError{err: err} +} + +func (e *permanentError) Error() string { return e.err.Error() } +func (e *permanentError) Unwrap() error { return e.err } + +// IsPermanent reports whether err was wrapped by Permanent. +func IsPermanent(err error) bool { + var pe *permanentError + return errors.As(err, &pe) +} + +// RunnerConfig is the per-runner configuration loaded from config.yaml. +// Zero Interval means "use the runner's documented default". +type RunnerConfig struct { + Enabled bool + Interval time.Duration +} + +// WorktreePruneConfig extends RunnerConfig with prune-specific knobs. +type WorktreePruneConfig struct { + Enabled bool + Interval time.Duration + IdleThreshold time.Duration + WarningLead time.Duration +} + +// AttachmentCleanupConfig extends RunnerConfig with retention. +type AttachmentCleanupConfig struct { + Enabled bool + Interval time.Duration + Retention time.Duration +} + +// JobReaperConfig extends RunnerConfig with the lease timeout used to detect +// crashed workers. +type JobReaperConfig struct { + Enabled bool + Interval time.Duration + LeaseTimeout time.Duration +} + +// JobPurgeConfig extends RunnerConfig with completed/dead retention. +type JobPurgeConfig struct { + Enabled bool + Interval time.Duration + CompletedRetention time.Duration +} + +// Config is the full jobs.Module configuration. +type Config struct { + Enabled bool + Workers int + ShutdownGrace time.Duration + WorkspaceFetch RunnerConfig + WorktreePrune WorktreePruneConfig + AttachmentCleanup AttachmentCleanupConfig + JobReaper JobReaperConfig + JobPurge JobPurgeConfig +}