Skip to content

Schedule Package

The internal/application/schedule/ package owns the full lifecycle from task declaration to execution.

Pipeline

Domain specs (TaskSpec, GlobalTaskSpec)

Registry DSL — NewTasks()
    ↓  repos.Pack()     → multiply × partitions, wrap closures
    ↓  Preflight()      → mark phase
    ↓  globalTasks()    → wrap without multiplication

[]spec.Task (flat list, ready to schedule)

Planner — split by Phase

Scheduler
    ├── runPreflights() → longrun.NewOneShotTask
    └── runWorkers()    → longrun.NewIntervalTask

Key Types

TypeFileRole
RepositoryTasksrepository_tasks.goMultiplies specs × configured repos
Preflight()repository_tasks.goMarks a spec as preflight phase
globalTasks()global.goWraps global specs without multiplication
join()global.goConcatenates task slices
Plannerplanner.goSplits tasks by Phase, validates duplicates
Schedulerschedule.goExecutes preflights → workers
infraClassifier()schedule.goClassifies infra errors for retry

Registry DSL

The registry reads as a manifest:

go
func NewTasks(...) []spec.Task {
    return join(
        repos.Pack(
            Preflight(repoValidator.TaskSpec()),
            issuePoller.TaskSpec(),
            outboxRelay.TaskSpec(),
        ),
        globalTasks(
            issueExplainer.TaskSpec(),
        ),
    )
}

Adding a task = one line in the appropriate section. Adding a partition dimension = one new provider + one new section.

Pack Internals

Pack(entries ...any) accepts two types via type switch:

  • repository.TaskSpec → default PhaseWork, lazy repoID resolution
  • repoTask (from Preflight()) → PhasePreflight, repoID = 0

Panics on zero Interval (must use spec.OneShot) or unknown entry type. This is a conscious trade-off: any loses compile-time safety but enables clean DSL syntax. See REVIEW.md "DSL internals may use pragmatic shortcuts."

Partition Multiplication

For each spec × each configured repository, Pack creates a closure that:

  1. Lazily resolves RepositoryID on first call (work-phase only)
  2. Constructs repository.Partition{Owner, Name, RepositoryID}
  3. Calls the domain's Work(ctx, partition) with honest arguments

Domain code never sees closures, config, or lazy resolution. It receives a typed Partition and works.

Phase Model

PhaseWhenDegraded policyrepoID
PhasePreflightBefore all workersnil — unknown errors crash0 (row may not exist)
PhaseWorkAfter preflights completeSet — survive and retryLazy-resolved

Planner splits []spec.Task by Phase. Scheduler runs each phase with its own longrun.Runner and Baseline config.

Extension Points

WhatWherePlanner/Scheduler touched?
New per-repo taskrepos.Pack(...) +1 lineNo
New global taskglobalTasks(...) +1 lineNo
New partitionNew XxxTasks provider + registry sectionNo
New phase+1 Phase const, +1 Scheduler runnerYes

Error Classification

infraClassifier() checks apierr interfaces on errors from infrastructure clients:

  • WaitHinted with positive duration → Service + explicit wait
  • ServicePressure → Service
  • Retryable → Service
  • Not classified → nil (baseline handles as Unknown/Degraded)

Apache 2.0 · Built in public · Contributions welcome