feat(jobs): domain types + Handler interface + Config

This commit is contained in:
2026-05-05 16:52:17 +08:00
parent ba1d657973
commit 215c1bb821
+133
View File
@@ -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
}