You've already forked agentic-coding-workflow
175 lines
5.4 KiB
Go
175 lines
5.4 KiB
Go
// 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"
|
|
// TypeSecretReencrypt re-encrypts every encrypted-column row that is still on
|
|
// an old key version onto the current active version (master-key rotation).
|
|
// Enqueued by the rotate-key admin endpoint; idempotent + resumable.
|
|
TypeSecretReencrypt JobType = "secret.reencrypt"
|
|
)
|
|
|
|
// 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)
|
|
}
|
|
|
|
// ErrLeaseLost is returned by the terminal/retry write-back methods
|
|
// (Complete, FailWithRetry, DeadLetter) when the fencing predicate matches no
|
|
// row: the job is no longer leased by this worker (e.g. the reaper re-leased it
|
|
// to another worker after the lease timeout). The caller must treat this as a
|
|
// no-op — the write was correctly dropped to avoid clobbering the new run's
|
|
// state — and must NOT surface it as a failure that triggers further retries.
|
|
var ErrLeaseLost = errors.New("job lease lost; write-back skipped")
|
|
|
|
// 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
|
|
}
|
|
|
|
// AcpEventsPurgeConfig extends RunnerConfig with acp_events retention.
|
|
type AcpEventsPurgeConfig struct {
|
|
Enabled bool
|
|
Interval time.Duration
|
|
Retention time.Duration
|
|
}
|
|
|
|
// MCPTokensPurgeConfig extends RunnerConfig with mcp_tokens retention.
|
|
type MCPTokensPurgeConfig struct {
|
|
Enabled bool
|
|
Interval time.Duration
|
|
Retention time.Duration
|
|
}
|
|
|
|
// AcpSessionReaperConfig controls the ACP session reaper schedule.
|
|
type AcpSessionReaperConfig struct {
|
|
Enabled bool
|
|
Interval 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
|
|
AcpEventsPurge AcpEventsPurgeConfig
|
|
MCPTokensPurge MCPTokensPurgeConfig
|
|
AcpSessionReaper AcpSessionReaperConfig
|
|
// OrchestratorSchedule controls the task-decomposition parallel scheduler tick
|
|
// (autonomy roadmap §10). Registered via RunnerFns key "orchestrator_schedule".
|
|
OrchestratorSchedule RunnerConfig
|
|
// AuditRetention controls the audit-log retention purge tick (autonomy
|
|
// roadmap §11). Registered via RunnerFns key "audit_retention".
|
|
AuditRetention RunnerConfig
|
|
}
|