Skip to main content
Version: 0.1

Scheduler

The scheduler runs named tasks on a pool at the times their schedules say. Each run is submitted to the pool, so it gets the pool's retries, timeouts, middleware, and metrics like any other job.

sched := worker.NewScheduler(pool)

sched.EveryFunc("cleanup", 5*time.Minute, cleanupExpiredSessions)

if err := sched.CronFunc("report", "0 9 * * MON-FRI", sendDailyReport); err != nil {
log.Fatal(err) // invalid cron expression
}

if err := sched.DailyFunc("backup", []string{"02:00", "14:00"}, backup); err != nil {
log.Fatal(err) // invalid time
}

sched.OnceFunc("migrate", time.Now().Add(time.Hour), migrate)

sched.Start()
defer sched.Stop()

Adding tasks​

MethodRuns
Every / EveryFunc(name, interval, ...)every interval
Cron / CronFunc(name, expr, ...)on a cron expression; returns a parse error
Daily / DailyFunc(name, times, ...)at "HH:MM" times each day; returns a parse error
Once / OnceFunc(name, at, ...)once, at at
At(name, start, interval, job, ...)at start, then every interval
Schedule / ScheduleFunc(name, schedule, ...)on any Schedule

Methods taking a Job have a Func variant taking a function. The methods that cannot fail return the scheduler, so they chain:

sched.
EveryFunc("heartbeat", 30*time.Second, heartbeat).
EveryFunc("refresh-cache", 10*time.Minute, refreshCache)

Task names are unique. Adding a task under an existing name replaces it and cancels the old task's in-flight runs.

Tasks can be added before or after Start.

Task options​

OptionEffect
WithOverlapPolicy(p)what to do when a run is due while the previous one is still running — default OverlapSkip
WithJitter(d)delay each run by a random amount up to d
WithMaxRuns(n)stop after n runs; 0 means unlimited
WithRunImmediate()also run as soon as the scheduler starts
sched.EveryFunc("sync", time.Hour, syncInventory,
worker.WithRunImmediate(),
worker.WithJitter(5*time.Minute),
worker.WithOverlapPolicy(worker.OverlapSkip),
)

Overlap policies​

PolicyWhen a run is due and the previous one is still running
OverlapSkipskip this run (the default)
OverlapAllowstart it anyway, so runs overlap
OverlapQueuerun it as soon as the current one finishes; up to 100 queued runs per task

OverlapSkip is the safe default for jobs that should never run twice at once, such as a sync that is not idempotent.

Jitter​

When many processes run the same schedule — every replica of a service refreshing a cache on the hour — jitter spreads them out so they do not all hit the backend in the same second.

Run immediately​

WithRunImmediate fires the task once when the scheduler starts, then follows its schedule. For a Once task, the immediate run replaces the scheduled one.

Controlling tasks at runtime​

sched.Pause("sync") // skip runs until resumed; false if no such task
sched.Resume("sync")
sched.Trigger("sync") // run now, outside the schedule
sched.Remove("sync") // delete the task and cancel its in-flight runs
sched.Tasks() // names of every task

A paused task keeps its due slot, so on Resume it runs once promptly instead of waiting a full period.

Trigger follows the same rules as a scheduled run: it respects the pause state, WithMaxRuns, and the overlap policy. It returns true if a run was started or queued, and false if not — including when no task has that name.

Inspecting tasks​

info := sched.TaskInfo("report") // nil if there is no such task
fmt.Println(info.Next, info.LastRun, info.LastError, info.SuccessRate())
FieldMeaning
Nametask name
Nextwhen it is next due
Runningwhether a run is in progress
Pausedwhether it is paused
RunCount, MaxRunsruns started, and the WithMaxRuns limit
LastRun, LastDuration, LastErrorthe most recent finished run
TotalRuns, TotalErrorsfinished runs, and how many failed

SuccessRate() is the fraction of finished runs that succeeded, or 0 before the first one finishes.

Scheduler options​

sched := worker.NewScheduler(pool,
worker.WithSchedulerLogger(slog.Default()),
worker.WithTickInterval(time.Second),
worker.WithOnTaskStart(func(name string) { /* ... */ }),
worker.WithOnTaskEnd(func(name string, err error, d time.Duration) { /* ... */ }),
)
OptionDefaultEffect
WithSchedulerLogger(l)nonelogs start, stop, and failed runs
WithTickInterval(d)100mshow often due tasks are checked — the scheduler's precision
WithOnTaskStart(fn)nonecalled when a run starts
WithOnTaskEnd(fn)nonecalled when a run finishes, with its error and duration

The task callbacks are recovered if they panic.

How runs execute​

Each run calls SubmitJobWait on the pool. A run therefore:

  • occupies a pool worker for its duration, and waits for queue space if the pool is full — a busy pool delays scheduled work,
  • gets the pool's retries and job timeout; a run "finishes" only after its last attempt,
  • runs with a context that is cancelled when the task is removed or replaced, or the scheduler stops.

A run's time is measured from submission to completion, so LastDuration includes time spent waiting in the queue.

Stopping​

Stop ends the scheduling loop and cancels the context of in-flight runs. It does not wait for them to return; close the pool afterwards to do that.

Stop is permanent: Start after Stop does nothing. Create a new scheduler instead. Start is idempotent, and IsRunning() reports whether the loop is active.