Documentation
¶
Overview ¶
Package scheduler runs the tasks modules declare.
A scheduler built on a system cron exists because the runtime has no resident process: cron calls a command every minute and the command decides what to run. That is two artifacts and a dependency on the operating system.
Go has a resident process. The scheduler is a goroutine in the same binary, which is also what keeps the deploy story of doc 17 true: one image, no crontab to configure, nothing to forget when a machine is replaced.
What it does not do is retry. A task that fails is logged and diagnosed, and the next window runs it again; work that needs its own retry budget enqueues a job, and the queue owns the retry. Scheduler fires, queue persists.
Index ¶
- type Module
- func (m *Module) Boot(_ context.Context) error
- func (m *Module) Close(ctx context.Context) error
- func (m *Module) Diagnose(ctx context.Context) []string
- func (*Module) Name() string
- func (*Module) Routes(*httpx.Router)
- func (m *Module) Scheduler() *Scheduler
- func (m *Module) Start(ctx context.Context) error
- type Options
- type Registered
- type Schedule
- type Scheduler
- func (s *Scheduler) Diagnose(ctx context.Context) []string
- func (s *Scheduler) List() []Registered
- func (s *Scheduler) RunNow(ctx context.Context, id, tenant string) error
- func (s *Scheduler) Start(ctx context.Context)
- func (s *Scheduler) Stop(ctx context.Context) error
- func (s *Scheduler) Tick(ctx context.Context, at time.Time)
- type Tenants
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
This section is empty.
Types ¶
type Module ¶
type Module struct {
// contains filtered or unexported fields
}
Module runs the scheduler in the application process.
It is registered like any other module and collects the tasks from the ones registered before it -- which is why it goes last in the Register call. A module never starts its own goroutine; it declares work, and this is what runs it.
func NewModule ¶
NewModule returns the module for a set of tasks.
Use kernel.Tasks() to collect them from the registered modules:
k := kernel.New(cfg).Register(billing.New(...), reports.New(...))
k.Register(scheduler.NewModule(k.Tasks(), scheduler.Options{
Locker: kv.NewLocker(client),
Tenants: tenants.Active,
}))
func (*Module) Boot ¶
Boot parses the tasks. It does not start the loop.
An unparseable spec fails the boot. The application does not start with a task that would silently never run, which is what "config validated at boot, fail fast" means applied to schedules -- and it fails for every command, not just the one that serves, so `aru routes` catches a bad spec too.
func (*Module) Routes ¶
Routes registers nothing. A scheduled task is not reachable over HTTP, and making it reachable would be a way to trigger billing by URL.
func (*Module) Scheduler ¶
Scheduler returns the running scheduler, for `aru schedule:list` and `aru schedule:run` to reach through the application binary.
Nil before Boot.
func (*Module) Start ¶ added in v0.10.0
Start begins the loop, and only the process that serves calls it.
It used to happen in Boot, so every `aru work` replica ran a scheduler and `aru schedule:run` -- the command for running one task by hand -- started the loop that runs all of them. The lock made it harmless; it did not make it right. See kernel.Background.
type Options ¶
type Options struct {
// Locker makes a Singleton task run on exactly one replica. Nil means a
// single replica, and with more than one it means every replica runs
// everything.
Locker kernel.Locker
// Tenants expands PerTenant tasks. Nil means those tasks do not run, which
// is reported rather than silent.
Tenants Tenants
// Now is the clock, for tests. Nil means time.Now.
Now func() time.Time
// Recorder receives each finished run, so the task shows on /_arandu/debug
// with its queries and its timeline -- exactly like a request.
//
// Nil means no instrumentation, and that is what production looks like: no
// Collector is built and every Record method is a no-op on a nil receiver.
// It used to build one on every run and throw it away, so production paid
// for recording and the console the doc promised never showed a task. Found
// by audit. Pass kernel.Recorder() to turn it on.
Recorder *observability.Recorder
}
Options configures the scheduler.
type Registered ¶
type Registered struct {
ID string
Spec string
Scope string
Singleton bool
Timeout time.Duration
Next time.Time
LastRun time.Time
LastError string
}
Registered is one task, as `aru schedule:list` prints it.
type Schedule ¶
type Schedule struct {
// contains filtered or unexported fields
}
Schedule is a parsed five-field cron expression.
Five fields, not six: no seconds. A framework that offers second-level cron offers a way to write a busy loop by accident, and work that has to happen every few seconds is a worker, not a schedule.
Parsed rather than pulled from a library, because the core has two dependencies (ADR 0004) and this is eighty lines. What it supports is the syntax people write: `*`, `5`, `1-5`, `*/15`, `1,15,30`, and the two names that read better than numbers (`@daily`, `@hourly`).
func MustParse ¶
MustParse is Parse for a constant. It panics, which is right for a schedule written in source: a module with an unparseable spec must not boot.
func (Schedule) Matches ¶
Matches reports whether the schedule fires in the minute of t.
Day-of-month and day-of-week are OR when both are restricted, which is the behaviour of every cron since Vixie: "0 0 1 * 1" means the first of the month AND every Monday, not their intersection. It surprises people, and matching the surprise is better than being the one implementation that differs.
func (Schedule) Next ¶
Next returns the first minute at or after t that matches.
It walks minute by minute, bounded to a year. A schedule that matches nothing in a year matches nothing at all -- February 30th, for instance -- and returning the zero time is what lets `aru schedule:list` say so instead of hanging.
type Scheduler ¶
type Scheduler struct {
// contains filtered or unexported fields
}
Scheduler fires tasks on their schedule.
func New ¶
New parses the tasks and returns the scheduler.
An unparseable spec is an error at construction rather than a task that silently never runs -- which is the failure mode of every scheduler that validates lazily.
func (*Scheduler) Diagnose ¶
Diagnose reports tasks that are overdue or that failed.
It feeds the error page through kernel.Diagnostic. A task that stopped firing looks exactly like a task with nothing to do, and the gap between the last run and the schedule is what tells them apart.
func (*Scheduler) List ¶
func (s *Scheduler) List() []Registered
List returns the registered tasks with their next run.
func (*Scheduler) RunNow ¶
RunNow runs one task by id, outside its schedule.
Same lock, same Grant, same instrumentation -- which is what makes the manual run auditable rather than a back door.
func (*Scheduler) Stop ¶
Stop cancels the loop and waits for the run in flight.
Waiting matters: a task killed halfway is a task whose lock is still held and whose work is half done, and the next window will not know either.