scheduling

package
v0.29.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 7, 2026 License: MIT Imports: 16 Imported by: 0

Documentation

Overview

Package scheduling is the events an application runs on a clock.

What moved here

hesape/scheduler used to hold a scheduler of its own, and it is gone: Task is Event, its Schedule is CronExpression, its Scheduler loop is Module and ScheduleWorkCommand, and its Module is Module. Two schedulers is two ways to say the same thing, and only one stayed.

Authorization and tenancy

Every scheduled event carries an auth.Action, and what it hands a callback is the Grant built from that action and the tenant. An event that reads a customer's rows calls PerTenant, and the runner expands it to one Grant per tenant instead of reading a tenant off a flag.

The cron expression

Five fields, not six: no seconds. Sub-minute repetition is EverySecond and its companions, which set RepeatSeconds and leave the expression on every minute, because a schedule that says "@every 90s" is a busy loop with a nicer name.

What makes that true is Runner.repeatEvents: after the due events have run, the ones that repeat are run again every RepeatSeconds until the minute is over. Without it the frequency methods would be a field nobody reads, and EveryFifteenSeconds would run once a minute instead of four times.

Index

Constants

View Source
const (
	// Sunday is 0, which is what cron calls it.
	Sunday = 0
	// Monday is 1.
	Monday = 1
	// Tuesday is 2.
	Tuesday = 2
	// Wednesday is 3.
	Wednesday = 3
	// Thursday is 4.
	Thursday = 4
	// Friday is 5.
	Friday = 5
	// Saturday is 6.
	Saturday = 6
)

The days of the week, as Schedule declares them.

Variables

This section is empty.

Functions

func NormalizeCommand

func NormalizeCommand(command string) string

NormalizeCommand rewrites the running binary's path out of a command line.

It exists so the mutex name of an event does not change when the binary moves: two replicas installed under different paths must resolve to the same lock, or neither ever loses the race.

Types

type CacheAware

type CacheAware interface {
	// UseStore points the mutex at another lock issuer.
	UseStore(locks *cache.Locks)
}

CacheAware is a mutex that can be pointed at another store.

The issuer is passed explicitly rather than resolved from a container.

type CacheEventMutex

type CacheEventMutex struct {
	// contains filtered or unexported fields
}

CacheEventMutex is the EventMutex over the cache locks.

func NewCacheEventMutex

func NewCacheEventMutex(locks *cache.Locks) *CacheEventMutex

NewCacheEventMutex returns the mutex over an issuer.

func (*CacheEventMutex) Create

func (m *CacheEventMutex) Create(ctx context.Context, event *Event) (bool, error)

Create takes the mutex for the event.

A lock another process holds is false and no error: that is the mutex working.

func (*CacheEventMutex) Exists

func (m *CacheEventMutex) Exists(ctx context.Context, event *Event) (bool, error)

Exists reports whether the mutex is held.

It takes the lock to find out and gives it straight back.

func (*CacheEventMutex) Forget

func (m *CacheEventMutex) Forget(ctx context.Context, event *Event) error

Forget releases the mutex this process took.

A lock this process does not hold is left alone: force-releasing it would let the replica that took the lock second delete the mark of the replica that took it first.

func (*CacheEventMutex) UseStore

func (m *CacheEventMutex) UseStore(locks *cache.Locks)

UseStore points the mutex at another lock issuer.

type CacheSchedulingMutex

type CacheSchedulingMutex struct {
	// contains filtered or unexported fields
}

CacheSchedulingMutex is the SchedulingMutex over the cache locks.

func NewCacheSchedulingMutex

func NewCacheSchedulingMutex(locks *cache.Locks) *CacheSchedulingMutex

NewCacheSchedulingMutex returns the mutex over an issuer.

func (*CacheSchedulingMutex) Create

func (m *CacheSchedulingMutex) Create(ctx context.Context, event *Event, at time.Time) (bool, error)

Create claims the window for this replica.

The lock is taken and never released: it marks the window as claimed rather than guarding the work as a mutex would.

func (*CacheSchedulingMutex) Exists

func (m *CacheSchedulingMutex) Exists(ctx context.Context, event *Event, at time.Time) (bool, error)

Exists reports whether the window is already claimed.

func (*CacheSchedulingMutex) UseStore

func (m *CacheSchedulingMutex) UseStore(locks *cache.Locks)

UseStore points the mutex at another lock issuer.

type Callback

type Callback func(ctx context.Context, g auth.Grant) error

Callback is the work a scheduled closure does.

It carries the Grant its event was declared with: a scheduled closure is a path to a repository, so it is handed the Grant directly rather than reaching for one.

type CallbackEvent

type CallbackEvent struct {
	*Event
}

CallbackEvent is a scheduled closure rather than a command line.

It is an Event with the process replaced by a function call, which is why it embeds one: the frequencies, the filters, the mutex and the callbacks are all the same.

Because Go has no virtual dispatch and a Schedule holds *Event, running, executing and summarizing for display all live on Event and decide their behavior by whether there is a callback. What is left here is what a caller reaches on the way in: the two guards that demand a name before a lock can be named after it, and the refusal to background a closure.

func NewCallbackEvent

func NewCallbackEvent(mutex EventMutex, callback Callback, timezone *time.Location) *CallbackEvent

NewCallbackEvent returns the event for a closure.

The mutex name resolver is installed here rather than overridden as a method, for the reason the type says: a mutex holding an *Event would otherwise name the lock after a command line this event does not have.

func (*CallbackEvent) OnOneServer

func (c *CallbackEvent) OnOneServer() *Event

OnOneServer allows the closure to run on exactly one replica per window.

It requires a name for the same reason WithoutOverlapping does.

func (*CallbackEvent) RunInBackground

func (c *CallbackEvent) RunInBackground() *Event

RunInBackground is refused.

A command sent to the background is a program the scheduler waits for beside the schedule; a closure has no program, so there would be nothing to wait for and the call would mean running the closure itself next to everything that reads the event. It panics rather than returning an error because it is a mistake in the schedule, caught the first time the schedule is declared.

func (*CallbackEvent) ShouldSkipDueToOverlapping

func (c *CallbackEvent) ShouldSkipDueToOverlapping(ctx context.Context) (bool, error)

ShouldSkipDueToOverlapping reports whether another copy holds the mutex.

An event with no description has no stable mutex name, so it never skips.

func (*CallbackEvent) WithoutOverlapping

func (c *CallbackEvent) WithoutOverlapping(expiresAt ...int) *Event

WithoutOverlapping stops the closure from running while a copy of it is.

The mutex is named after the description, so without one there is nothing to name the lock and two copies would each take a different key.

type CommandBuilder

type CommandBuilder struct{}

CommandBuilder turns the command line one event carries into the program and arguments it is started with.

It is a type with one method and no state, kept as a name because splitting a line and putting it behind sudo are two separate decisions and each is worth reading on its own.

func (CommandBuilder) BuildCommand

func (b CommandBuilder) BuildCommand(event *Event) ([]string, error)

BuildCommand renders the program and its arguments.

Nothing here builds a line for a shell. The event's command line is split into words, and every character a shell would act on -- a semicolon, an ampersand, a pipe, a redirection, a dollar sign, a backquote, an asterisk -- is an ordinary character of the word it appears in. That is the whole difference between this and a shell, and it is the difference between an argument and a second command.

A command line of nothing but spaces builds nothing, which is what an event with no command means.

type Commands

type Commands struct {
	// Schedule is what the commands read and run.
	Schedule *Schedule

	// Runner runs the due events. Nil means one is built over the schedule.
	Runner *Runner

	// Interrupter backs schedule:interrupt and the worker's check of it.
	Interrupter Interrupter

	// Now is the clock, for tests. Nil means time.Now.
	Now func() time.Time
}

Commands is the set of scheduling commands, ready to register.

It is seven commands as values, built over one schedule. interrupter and cleaner may be nil, and then the two commands that need them say so rather than doing nothing.

func (Commands) All

func (c Commands) All() []console.Command

All returns every scheduling command.

The order is the order they are listed in, which is alphabetical by name because that is how the console help sorts them anyway.

func (Commands) ScheduleClearCacheCommand

func (c Commands) ScheduleClearCacheCommand() console.Command

ScheduleClearCacheCommand is the command that deletes the overlap mutexes left behind.

It is `schedule:clear-cache`. It is for the case a process died holding a lock and the operator does not want to wait out the ttl.

func (Commands) ScheduleFinishCommand

func (c Commands) ScheduleFinishCommand() console.Command

ScheduleFinishCommand is the command that reports the exit code of an event that ran in the background.

It is `schedule:finish {id} {code=0}`, and it is hidden.

Nothing generates it any more: a backgrounded event is waited for and finished by the process that started it, so this is what is left for the case that one ended without finishing -- a replica killed mid-run leaves the mutex held and the after callbacks unrun, and this releases and runs them by name.

It runs in a process that has already lost the run it is reporting on, which shapes two decisions.

A mutex name is a digest of the expression and the command line, so two declarations of the same command share one: every event with that mutex name is finished, not only the first, or the second would keep its lock and never run its after callbacks.

An id that matches nothing succeeds rather than failing: a deploy that changes the schedule while a backgrounded event is still running must not turn into a failed background job. It says which id it could not find, because a silent success is how the same deploy hides a mutex that will now be held until it expires.

func (Commands) ScheduleInterruptCommand

func (c Commands) ScheduleInterruptCommand() console.Command

ScheduleInterruptCommand is the command that stops the running schedule worker at its next tick.

It is `schedule:interrupt`. It leaves a mark that expires, so a worker started afterwards is not stopped by an interrupt meant for the one before it. The mark lives until the end of the current minute.

func (Commands) ScheduleListCommand

func (c Commands) ScheduleListCommand() console.Command

ScheduleListCommand is the command that lists the scheduled events and when they run next.

It is `schedule:list`, with --timezone to read the times somewhere else.

func (Commands) ScheduleRunCommand

func (c Commands) ScheduleRunCommand() console.Command

ScheduleRunCommand is the command that runs the events that are due now.

It is `schedule:run`, with --whisper to keep quiet when there was nothing to do.

func (Commands) ScheduleTestCommand

func (c Commands) ScheduleTestCommand() console.Command

ScheduleTestCommand is the command that runs one scheduled event by hand.

It is `schedule:test {--name=}`. It runs the event through the same path the runner does -- same mutex, same Grant, same callbacks -- which is what makes the manual run auditable rather than a back door.

func (Commands) ScheduleWorkCommand

func (c Commands) ScheduleWorkCommand() console.Command

ScheduleWorkCommand is the command that runs the schedule for as long as the process lives.

It is `schedule:work`. It calls the same runner schedule:run does, in the same process, which is one artifact instead of two and no crontab to configure.

It ticks at the top of each minute rather than every sixty seconds from boot, because an event specified as "0 3 * * *" has to fire at 3:00 and not at 3:00 plus however long after a deploy the process happened to start.

type CronExpression

type CronExpression struct {
	// contains filtered or unexported fields
}

CronExpression is a parsed five-field cron expression.

It is written here rather than pulled in as a dependency, because the module declares only one and this is eighty lines.

Five fields, not six: no seconds. Sub-minute repetition is Event.RepeatSeconds -- the expression stays every-minute and Runner.repeatEvents loops within the minute.

What it supports is the syntax people write: `*`, `5`, `1-5`, `*/15`, `1,15,30`, and the shorthands `@hourly`, `@daily`, `@midnight`, `@weekly` and `@monthly`.

func MustParseCronExpression

func MustParseCronExpression(spec string) CronExpression

MustParseCronExpression is ParseCronExpression for a constant.

It panics, which is right for an expression written in source: a schedule nobody can parse must not reach the runner.

func ParseCronExpression

func ParseCronExpression(spec string) (CronExpression, error)

ParseCronExpression reads a cron expression.

func (CronExpression) GetNextRunDate

func (c CronExpression) GetNextRunDate(t time.Time) time.Time

GetNextRunDate returns the first minute after t that matches.

It walks minute by minute, bounded to a year: an expression that matches nothing in a year matches nothing at all -- February 30th, for instance -- and returning the zero time is what lets schedule:list say so instead of hanging.

func (CronExpression) IsDue

func (c CronExpression) IsDue(t time.Time) bool

IsDue reports whether the expression 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 (CronExpression) String

func (c CronExpression) String() string

String returns the expression it was parsed from, for schedule:list.

type Dispatcher

type Dispatcher interface {
	// Dispatch pushes the job, on the named queue and connection. Either may be
	// empty, and then the job's own is used.
	Dispatch(ctx context.Context, job any, queue, connection string) error
}

Dispatcher is what Job pushes onto the queue.

It is declared here rather than imported: the queue is not a dependency of the scheduler, and what a schedule needs of a dispatcher is one method.

type Event

type Event struct {
	// Command is the command line the event runs. CommandBuilder splits it into
	// the program and the arguments a process is started with; no shell reads
	// it.
	Command string

	// Output is where the command's output is sent. It is a path, and the
	// default is the null device of the system.
	Output string

	// ShouldAppendOutput says whether the output is appended rather than
	// replacing what is there.
	ShouldAppendOutput bool

	// Mutex is what stops the event from overlapping itself.
	Mutex EventMutex

	// MutexNameResolver overrides how the mutex is named. Nil means the default
	// name, which is a digest of the expression and the command.
	MutexNameResolver func(*Event) string

	// ExitCode is what the command ended with, once it has.
	ExitCode int
	// contains filtered or unexported fields
}

Event is one scheduled command.

Its fields are unexported, with setters and getters standing in for them, because Go cannot have a field and a method that share a name -- such as WithoutOverlapping.

Two fields carry the event's authorization: Action, which the event's Grant is issued for, and PerTenant, which says the event is expanded to one run per tenant with a Grant each. Without them a scheduled task would be the one path into the database with no authorization on it.

func NewEvent

func NewEvent(mutex EventMutex, command string, timezone *time.Location) *Event

NewEvent returns an event for a command line.

It sets the mutex, the command and the timezone, and defaults the output for the system.

func (*Event) Action

func (e *Event) Action(action auth.Action) *Event

Action is what the event's Grant is issued for.

A scheduled task reaches a repository the same way a request does, through a Grant that a Policy checked. An event with no action gets a Grant that passes nothing, which is a task that can print and can call an API and cannot read a customer's rows -- and having to say so is the point.

func (*Event) After

func (e *Event) After(callback Hook) *Event

After registers a callback run after the event.

It is one call to Then.

func (*Event) AppendOutputTo

func (e *Event) AppendOutputTo(location string) *Event

AppendOutputTo adds the command's output to what is already at a path.

func (*Event) At

func (e *Event) At(t string) *Event

At runs the event daily at a time.

It is one call to DailyAt.

func (*Event) Before

func (e *Event) Before(callback Hook) *Event

Before registers a callback run before the event.

func (*Event) Between

func (e *Event) Between(startTime, endTime string) *Event

Between runs the event only between two times of day.

The times are "HH:MM", and a range that ends before it starts crosses midnight -- "22:00" to "04:00" is the six hours it reads as, not the eighteen it would be if the comparison were naive.

func (*Event) BuildCommand

func (e *Event) BuildCommand() ([]string, error)

BuildCommand is the program and the arguments the event runs.

It is a list and not a line: nothing joins these back together, and nothing parses them. The error is a command line that ends inside a quoted word.

func (*Event) CallAfterCallbacks

func (e *Event) CallAfterCallbacks(ctx context.Context) error

CallAfterCallbacks runs every callback registered with Then.

Every one of them runs even when an earlier one failed, and the first error is what comes back: a ping that could not be sent must not stop the mail that reports the output.

func (*Event) CallBeforeCallbacks

func (e *Event) CallBeforeCallbacks(ctx context.Context) error

CallBeforeCallbacks runs every callback registered with Before.

func (*Event) CreateMutexNameUsing

func (e *Event) CreateMutexNameUsing(resolver func(*Event) string) *Event

CreateMutexNameUsing overrides how the mutex is named.

func (*Event) Cron

func (e *Event) Cron(expression string) *Event

Cron sets the expression the event fires on.

Every other method in this file ends up here.

func (*Event) Daily

func (e *Event) Daily() *Event

Daily runs the event at midnight.

func (*Event) DailyAt

func (e *Event) DailyAt(t string) *Event

DailyAt runs the event daily at a time: "10:00", "19:30".

func (*Event) Days

func (e *Event) Days(days ...string) *Event

Days limits the event to days of the week.

It takes the field as written, so both Days("1-5") and Days("1", "3") say what they read as.

func (*Event) DaysOfMonth

func (e *Event) DaysOfMonth(days ...int) *Event

DaysOfMonth runs the event on the given days of the month, at midnight.

Naming no day panics. Without it, joinInts would fall back to "0", and "0 0 0 * *" is not a cron expression: there is no zeroth day of the month, so Schedule would refuse it at registration and the application would not start. The mistake is the empty call, and this is the last place it can still be named.

func (*Event) Description

func (e *Event) Description(description string) *Event

Description sets the readable description of the event.

func (*Event) EmailOutputOnFailure

func (e *Event) EmailOutputOnFailure(mailer Mailer, addresses []string) *Event

EmailOutputOnFailure mails the output when the command failed.

It mails even when there was no output: a task that failed silently is the one worth reading.

func (*Event) EmailOutputTo

func (e *Event) EmailOutputTo(mailer Mailer, addresses []string, onlyIfOutputExists ...bool) *Event

EmailOutputTo mails the output of the event.

onlyIfOutputExists defaults to true: a task that printed nothing sends no mail.

func (*Event) EmailWrittenOutputTo

func (e *Event) EmailWrittenOutputTo(mailer Mailer, addresses []string) *Event

EmailWrittenOutputTo mails the output only when there was some.

func (*Event) Environments

func (e *Event) Environments(environments ...string) *Event

Environments limits the environments the event runs in.

Naming none means every one.

func (*Event) EvenInMaintenanceMode

func (e *Event) EvenInMaintenanceMode() *Event

EvenInMaintenanceMode lets the event run while the application is down.

func (*Event) EveryFifteenMinutes

func (e *Event) EveryFifteenMinutes() *Event

EveryFifteenMinutes runs the event every fifteen minutes.

func (*Event) EveryFifteenSeconds

func (e *Event) EveryFifteenSeconds() *Event

EveryFifteenSeconds runs the event every fifteen seconds.

func (*Event) EveryFiveMinutes

func (e *Event) EveryFiveMinutes() *Event

EveryFiveMinutes runs the event every five minutes.

func (*Event) EveryFiveSeconds

func (e *Event) EveryFiveSeconds() *Event

EveryFiveSeconds runs the event every five seconds.

func (*Event) EveryFourHours

func (e *Event) EveryFourHours(offset ...int) *Event

EveryFourHours runs the event every four hours.

func (*Event) EveryFourMinutes

func (e *Event) EveryFourMinutes() *Event

EveryFourMinutes runs the event every four minutes.

func (*Event) EveryMinute

func (e *Event) EveryMinute() *Event

EveryMinute runs the event every minute.

func (*Event) EveryOddHour

func (e *Event) EveryOddHour(offset ...int) *Event

EveryOddHour runs the event on every odd hour.

func (*Event) EverySecond

func (e *Event) EverySecond() *Event

EverySecond runs the event every second.

The expression stays every-minute and the runner loops within the minute, which is what repeatEvery sets up.

func (*Event) EverySixHours

func (e *Event) EverySixHours(offset ...int) *Event

EverySixHours runs the event every six hours.

func (*Event) EveryTenMinutes

func (e *Event) EveryTenMinutes() *Event

EveryTenMinutes runs the event every ten minutes.

func (*Event) EveryTenSeconds

func (e *Event) EveryTenSeconds() *Event

EveryTenSeconds runs the event every ten seconds.

func (*Event) EveryThirtyMinutes

func (e *Event) EveryThirtyMinutes() *Event

EveryThirtyMinutes runs the event every thirty minutes.

func (*Event) EveryThirtySeconds

func (e *Event) EveryThirtySeconds() *Event

EveryThirtySeconds runs the event every thirty seconds.

func (*Event) EveryThreeHours

func (e *Event) EveryThreeHours(offset ...int) *Event

EveryThreeHours runs the event every three hours.

func (*Event) EveryThreeMinutes

func (e *Event) EveryThreeMinutes() *Event

EveryThreeMinutes runs the event every three minutes.

func (*Event) EveryTwentySeconds

func (e *Event) EveryTwentySeconds() *Event

EveryTwentySeconds runs the event every twenty seconds.

func (*Event) EveryTwoHours

func (e *Event) EveryTwoHours(offset ...int) *Event

EveryTwoHours runs the event every two hours.

func (*Event) EveryTwoMinutes

func (e *Event) EveryTwoMinutes() *Event

EveryTwoMinutes runs the event every two minutes.

func (*Event) EveryTwoSeconds

func (e *Event) EveryTwoSeconds() *Event

EveryTwoSeconds runs the event every two seconds.

func (*Event) ExpiresAt

func (e *Event) ExpiresAt() int

ExpiresAt reports how many minutes the overlap lock lives. Go cannot have a field and a method of the same name.

func (*Event) FiltersPass

func (e *Event) FiltersPass(ctx context.Context) bool

FiltersPass reports whether every when passed and no skip matched.

It records the time: a sub-minute repeat counts from the last check.

func (*Event) Finish

func (e *Event) Finish(ctx context.Context, exitCode int) error

Finish records the exit code, runs the after callbacks and releases the mutex.

The mutex is released whatever the callbacks did.

func (*Event) Fridays

func (e *Event) Fridays() *Event

Fridays runs the event only on Fridays.

func (*Event) GetAction

func (e *Event) GetAction() auth.Action

GetAction is what the event is authorized for, and it reads what Action set.

func (*Event) GetDefaultOutput

func (e *Event) GetDefaultOutput() string

GetDefaultOutput is where output goes when nobody said.

func (*Event) GetDescription

func (e *Event) GetDescription() string

GetDescription reports the description, or empty.

Go cannot have a field and a method of the same name, so the setter is Description and the getter is named for what it does instead.

func (*Event) GetExpression

func (e *Event) GetExpression() string

GetExpression is the cron expression the event fires on.

func (*Event) GetSummaryForDisplay

func (e *Event) GetSummaryForDisplay() string

GetSummaryForDisplay is the event as a listing prints it.

It is the description when it has one, the command line when it is a command, and "Callback" when it is a closure that was never named.

The command line, and not what the event is started with: where the output goes and which user it runs as are how the command is run rather than what it is, and a listing that repeated them printed the same redirection on every row.

func (*Event) GetTimezone

func (e *Event) GetTimezone() *time.Location

GetTimezone reports the timezone the expression is evaluated in, or nil for the timezone of the process.

func (*Event) Grant

func (e *Event) Grant() auth.Grant

Grant is the authorization this run carries.

It is auth.SystemGrant of the event's action and tenant, which is the one way a scheduled task reaches a repository.

func (*Event) Hourly

func (e *Event) Hourly() *Event

Hourly runs the event at the top of every hour.

func (*Event) HourlyAt

func (e *Event) HourlyAt(offset ...int) *Event

HourlyAt runs the event every hour, at the given minutes past.

func (*Event) IsDue

func (e *Event) IsDue(now time.Time, environment string, downForMaintenance bool) bool

IsDue reports whether the event should run now.

It checks maintenance mode first, then the expression, then the environment.

func (*Event) IsRepeatable

func (e *Event) IsRepeatable() bool

IsRepeatable reports whether the event was asked to repeat within the minute.

func (*Event) LastDayOfMonth

func (e *Event) LastDayOfMonth(t ...string) *Event

LastDayOfMonth runs the event on the last day of the month, at a time.

The day is resolved when the expression is tested, not when the schedule is declared: it asks for the 28th through the 31st and lets its own When filter drop the days that are not the last one.

func (*Event) Mondays

func (e *Event) Mondays() *Event

Mondays runs the event only on Mondays.

func (*Event) Monthly

func (e *Event) Monthly() *Event

Monthly runs the event at midnight on the first of the month.

func (*Event) MonthlyOn

func (e *Event) MonthlyOn(dayOfMonth int, t ...string) *Event

MonthlyOn runs the event on a day of the month, at a time.

func (*Event) MutexName

func (e *Event) MutexName() string

MutexName is the key the event's overlap lock is stored under.

The name is derived from the expression and the command, so the same event on two replicas resolves to the same key and one of them loses the race.

func (*Event) Name

func (e *Event) Name(description string) *Event

Name sets the readable description of the event.

It is one call to Description. Both exist because WithoutOverlapping on a closure requires one of them to have been called first.

func (*Event) NextRunDate

func (e *Event) NextRunDate(after time.Time) time.Time

NextRunDate is when the event runs next, after the given time.

To find the nth occurrence, call it again with each result: there is no argument for walking further down the list in one call.

func (*Event) OnFailure

func (e *Event) OnFailure(callback Hook) *Event

OnFailure registers a callback run when the event ended badly.

func (*Event) OnFailureWithOutput

func (e *Event) OnFailureWithOutput(callback func(ctx context.Context, output string) error, onlyIfOutputExists ...bool) *Event

OnFailureWithOutput is OnFailure with the output handed to the callback.

func (*Event) OnOneServer

func (e *Event) OnOneServer() *Event

OnOneServer allows the event to run on exactly one replica per window.

It is the scheduling mutex rather than the event mutex: the first replica to claim the window runs it, and the others skip it entirely rather than waiting.

func (*Event) OnSuccess

func (e *Event) OnSuccess(callback Hook) *Event

OnSuccess registers a callback run when the event ended with status zero.

func (*Event) OnSuccessWithOutput

func (e *Event) OnSuccessWithOutput(callback func(ctx context.Context, output string) error, onlyIfOutputExists ...bool) *Event

OnSuccessWithOutput is OnSuccess with the output handed to the callback.

func (*Event) PerTenant

func (e *Event) PerTenant() *Event

PerTenant expands the event to one run per tenant, each with its own Grant and its own lock.

The tenant comes from the Grant and never from a path or a flag, so a task that reads a customer's rows says here that it is per tenant and the runner hands it one Grant at a time.

func (*Event) PingBefore

func (e *Event) PingBefore(pinger Pinger, url string) *Event

PingBefore makes a GET to the URL before the event runs.

func (*Event) PingBeforeIf

func (e *Event) PingBeforeIf(condition bool, pinger Pinger, url string) *Event

PingBeforeIf makes the ping before, when the condition holds.

func (*Event) PingOnFailure

func (e *Event) PingOnFailure(pinger Pinger, url string) *Event

PingOnFailure makes a GET to the URL when the event failed.

func (*Event) PingOnFailureIf

func (e *Event) PingOnFailureIf(condition bool, pinger Pinger, url string) *Event

PingOnFailureIf makes the ping on failure, when the condition holds.

func (*Event) PingOnSuccess

func (e *Event) PingOnSuccess(pinger Pinger, url string) *Event

PingOnSuccess makes a GET to the URL when the event succeeded.

func (*Event) PingOnSuccessIf

func (e *Event) PingOnSuccessIf(condition bool, pinger Pinger, url string) *Event

PingOnSuccessIf makes the ping on success, when the condition holds.

func (*Event) PreventOverlapsUsing

func (e *Event) PreventOverlapsUsing(mutex EventMutex) *Event

PreventOverlapsUsing points the event at another mutex.

func (*Event) Quarterly

func (e *Event) Quarterly() *Event

Quarterly runs the event at midnight on the first day of each quarter.

func (*Event) QuarterlyOn

func (e *Event) QuarterlyOn(dayOfQuarter int, t ...string) *Event

QuarterlyOn runs the event on a day of each quarter, at a time.

func (*Event) RepeatSeconds

func (e *Event) RepeatSeconds() int

RepeatSeconds reports how often the event repeats within a minute, or zero.

func (*Event) Run

func (e *Event) Run(ctx context.Context) error

Run runs the event.

An overlapping event returns without doing anything, and an event that was not sent to the background is finished -- callbacks and mutex release -- as soon as it returns.

func (*Event) RunInBackground

func (e *Event) RunInBackground() *Event

RunInBackground sends the command to the background.

Run returns as soon as the program has started, so the runner goes on to the next event. The command is waited for beside it, and the exit code, the after callbacks and the mutex release happen when it ends rather than when Run returns -- which is why an event sent to the background wants WithoutOverlapping.

func (*Event) RunsInBackground

func (e *Event) RunsInBackground() bool

RunsInBackground reports whether the event was sent to the background.

func (*Event) RunsInEnvironment

func (e *Event) RunsInEnvironment(environment string) bool

RunsInEnvironment reports whether the event runs in the given environment.

An event that named none runs in all.

func (*Event) RunsInMaintenanceMode

func (e *Event) RunsInMaintenanceMode() bool

RunsInMaintenanceMode reports whether the event runs while the application is down.

func (*Event) RunsOnOneServer

func (e *Event) RunsOnOneServer() bool

RunsOnOneServer reports whether the event was limited to one replica.

func (*Event) RunsPerTenant

func (e *Event) RunsPerTenant() bool

RunsPerTenant reports whether the event is expanded to one run per tenant.

func (*Event) Saturdays

func (e *Event) Saturdays() *Event

Saturdays runs the event only on Saturdays.

func (*Event) SendOutputTo

func (e *Event) SendOutputTo(location string, append ...bool) *Event

SendOutputTo sends the command's output to a path.

func (*Event) ShouldRepeatNow

func (e *Event) ShouldRepeatNow() bool

ShouldRepeatNow reports whether enough of the minute has passed for the next repeat.

Runner.repeatEvents is what calls it: without a caller, EveryFifteenSeconds would be a schedule that ran once a minute instead of four times.

func (*Event) ShouldSkipDueToOverlapping

func (e *Event) ShouldSkipDueToOverlapping(ctx context.Context) (bool, error)

ShouldSkipDueToOverlapping reports whether another copy of the event holds the mutex.

func (*Event) Skip

func (e *Event) Skip(reject Filter) *Event

Skip registers a callback that stops the event when it returns true.

func (*Event) SkipTrue

func (e *Event) SkipTrue(condition bool) *Event

SkipTrue is Skip with a value already decided.

func (*Event) StoreOutput

func (e *Event) StoreOutput() *Event

StoreOutput makes sure the output is written to a file rather than discarded.

func (*Event) Sundays

func (e *Event) Sundays() *Event

Sundays runs the event only on Sundays.

func (*Event) Tenant

func (e *Event) Tenant(tenant string) *Event

Tenant fixes the tenant this copy of the event runs for.

The runner calls it while expanding a per-tenant event; a schedule that pins one tenant by hand calls it too.

func (*Event) Then

func (e *Event) Then(callback Hook) *Event

Then registers a callback run after the event.

func (*Event) ThenPing

func (e *Event) ThenPing(pinger Pinger, url string) *Event

ThenPing makes a GET to the URL after the event runs.

func (*Event) ThenPingIf

func (e *Event) ThenPingIf(condition bool, pinger Pinger, url string) *Event

ThenPingIf makes the ping after, when the condition holds.

func (*Event) ThenWithOutput

func (e *Event) ThenWithOutput(callback func(ctx context.Context, output string) error, onlyIfOutputExists ...bool) *Event

ThenWithOutput registers a callback that is handed what the event printed.

func (*Event) Thursdays

func (e *Event) Thursdays() *Event

Thursdays runs the event only on Thursdays.

func (*Event) Timezone

func (e *Event) Timezone(timezone *time.Location) *Event

Timezone sets the timezone the expression is evaluated in.

func (*Event) Tuesdays

func (e *Event) Tuesdays() *Event

Tuesdays runs the event only on Tuesdays.

func (*Event) TwiceDaily

func (e *Event) TwiceDaily(hours ...int) *Event

TwiceDaily runs the event at two hours of the day.

The defaults are 1am and 1pm.

func (*Event) TwiceDailyAt

func (e *Event) TwiceDailyAt(first, second, offset int) *Event

TwiceDailyAt runs the event at two hours of the day, at an offset in the hour.

func (*Event) TwiceMonthly

func (e *Event) TwiceMonthly(first, second int, t ...string) *Event

TwiceMonthly runs the event on two days of the month, at a time.

The defaults are the 1st and the 16th, at midnight.

func (*Event) UnlessBetween

func (e *Event) UnlessBetween(startTime, endTime string) *Event

UnlessBetween stops the event between two times of day.

func (*Event) UseProcessFactory added in v0.18.0

func (e *Event) UseProcessFactory(factory *process.Factory) *Event

UseProcessFactory points the event at another process factory, and a nil one leaves the event on the factory it has.

It is the seam a test uses: a factory with fakes registered answers the event's command line without a program being started.

func (*Event) User

func (e *Event) User(user string) *Event

User sets which user the command runs as.

CommandBuilder is what turns it into a sudo, and it does nothing on Windows.

func (*Event) Wednesdays

func (e *Event) Wednesdays() *Event

Wednesdays runs the event only on Wednesdays.

func (*Event) Weekdays

func (e *Event) Weekdays() *Event

Weekdays runs the event Monday to Friday.

func (*Event) Weekends

func (e *Event) Weekends() *Event

Weekends runs the event on Saturday and Sunday.

func (*Event) Weekly

func (e *Event) Weekly() *Event

Weekly runs the event at midnight on Sunday.

func (*Event) WeeklyOn

func (e *Event) WeeklyOn(dayOfWeek int, t ...string) *Event

WeeklyOn runs the event on a day of the week, at a time.

The default time is midnight.

func (*Event) When

func (e *Event) When(filter Filter) *Event

When registers a callback that has to return true for the event to run.

WhenTrue is the bool form, because a Go method cannot take both a closure and a bool.

func (*Event) WhenTrue

func (e *Event) WhenTrue(condition bool) *Event

WhenTrue is When with a value already decided.

func (*Event) WithoutOverlapping

func (e *Event) WithoutOverlapping(expiresAt ...int) *Event

WithoutOverlapping stops the event from running while a copy of it is still running.

The lock is both the mark and the filter: an event whose previous run is still going is not merely refused, it is reported as skipped.

expiresAt is in minutes, and left out it is a day. It is how long the lock lives when the process holding it dies, so it is sized above the longest run rather than tight against the usual one.

func (*Event) Yearly

func (e *Event) Yearly() *Event

Yearly runs the event at midnight on the first of January.

func (*Event) YearlyOn

func (e *Event) YearlyOn(month, dayOfMonth int, t ...string) *Event

YearlyOn runs the event on a month and day, at a time.

type EventMutex

type EventMutex interface {
	// Create takes the mutex for the event, and reports whether it got it.
	Create(ctx context.Context, event *Event) (bool, error)

	// Exists reports whether the mutex is held.
	Exists(ctx context.Context, event *Event) (bool, error)

	// Forget releases the mutex.
	Forget(ctx context.Context, event *Event) error
}

EventMutex is what stops one event from overlapping itself.

Every method takes a context, because a lock is a round trip to a store, and every method can fail, because that round trip can -- and a mutex that reports "not held" when it could not ask is a mutex that lets both copies run.

type Filter

type Filter func(ctx context.Context) bool

Filter is a callback that decides whether an event may run.

type Hook

type Hook func(ctx context.Context) error

Hook is a callback run before or after an event.

type Interrupter

type Interrupter interface {
	// Interrupt marks the current schedule run as interrupted, for ttl.
	Interrupt(ctx context.Context, ttl time.Duration) error

	// Interrupted reports whether the mark is there, and clears it.
	Interrupted(ctx context.Context) (bool, error)
}

Interrupter is what schedule:interrupt writes to and the worker reads.

It is declared here rather than imported as a cache repository: what the two commands need of a store is a flag that expires, and a flag that expires is a lock with a ttl.

type Listener

type Listener func(event any)

Listener is told about every scheduled event that starts, finishes, fails or is skipped.

It is a function rather than a dispatcher interface because the scheduler fires four events and nothing else, and a listener that wants them all switches on the type.

type Mailer

type Mailer interface {
	// Raw sends a plain text message.
	Raw(ctx context.Context, addresses []string, subject, body string) error
}

Mailer is what EmailOutputTo sends through.

It is declared here rather than imported: the mail package is not a dependency of the scheduler, and what an event needs of a mailer is one method.

type Module

type Module struct {
	// contains filtered or unexported fields
}

Module runs the schedule in the application process.

It is what `schedule:work` is, wired as a module rather than typed as a command: this binary has a resident process, so nothing external has to call `schedule:run` on a timer. That is one artifact instead of two, and nothing to forget when a machine is replaced.

It registers no routes. A scheduled event is not reachable over HTTP, and making it reachable would be a way to trigger billing by URL.

It is the last piece of the hesape/scheduler package, which this one replaced: Task became Event, Schedule became CronExpression, and the loop is here.

func NewModule

func NewModule(schedule *Schedule) *Module

NewModule returns the module for a schedule.

func (*Module) Boot

func (m *Module) Boot(context.Context) error

Boot checks that every declared expression parses.

Waiting for Event.IsDue to parse it would mean a schedule nobody can read is found only at the first tick.

The application does not start with an event that would silently never run, and it fails for every command and not only the one that serves -- so `schedule:list` catches a bad expression too.

func (*Module) Close

func (m *Module) Close(ctx context.Context) error

Close stops the loop and waits for the run in flight.

Waiting matters: an event killed halfway is an event whose lock is still held and whose work is half done, and the next window will not know either.

func (*Module) Name

func (*Module) Name() string

Name is the module identifier.

It belongs to the Arandu module contract.

func (*Module) Runner

func (m *Module) Runner() *Runner

Runner returns the runner, so a listener can be installed before Start.

func (*Module) Start

func (m *Module) Start(ctx context.Context) error

Start begins the loop, and only the process that serves calls it.

It ticks at the top of each minute rather than every sixty seconds from boot, because an event specified as "0 3 * * *" has to fire at 3:00 and not at 3:00 plus however long after a deploy the process happened to start.

type PendingEventAttributes

type PendingEventAttributes struct {
	*Event
	// contains filtered or unexported fields
}

PendingEventAttributes is a set of attributes waiting for an event to be declared.

It is what makes Schedule.Daily().Group(...) work: the frequency is named before there is an event to put it on, so it is held here and merged into every event the group declares.

It embeds an Event rather than repeating the seventy frequency and attribute methods. The event it embeds is never run: it is a bag of attributes with the methods already on it.

func NewPendingEventAttributes

func NewPendingEventAttributes(schedule *Schedule) *PendingEventAttributes

NewPendingEventAttributes returns an empty set of attributes.

func (*PendingEventAttributes) Attributes

func (p *PendingEventAttributes) Attributes() *Event

Attributes returns the event the attributes are collected on, so the frequency methods chain from a group the way they chain from an event.

func (*PendingEventAttributes) MergeAttributes

func (p *PendingEventAttributes) MergeAttributes(event *Event)

MergeAttributes copies what was collected onto an event.

Only what was actually set is copied, so an event that named its own timezone keeps it.

func (*PendingEventAttributes) Schedule

func (p *PendingEventAttributes) Schedule() *Schedule

Schedule returns the schedule the group belongs to.

Go has no fallthrough for a missing method, so a schedule-level call in a chain that started from Attributes reaches it through this instead.

func (*PendingEventAttributes) WithoutOverlapping

func (p *PendingEventAttributes) WithoutOverlapping(expiresAt ...int) *PendingEventAttributes

WithoutOverlapping records that the events of this group must not overlap themselves.

The attributes have no mutex to register a skip against, so it only records the decision and MergeAttributes is what turns it into the skip.

type Pinger

type Pinger interface {
	// Ping makes a GET request to the URL.
	Ping(ctx context.Context, url string) error
}

Pinger is what the ping callbacks call.

It is declared here for the same reason Mailer is: a GET to a URL is the whole of what an event asks of an HTTP client.

type Runner

type Runner struct {

	// Listen receives ScheduledTaskStarting, ScheduledTaskFinished,
	// ScheduledTaskFailed and ScheduledTaskSkipped. Nil means nobody is
	// listening.
	Listen Listener

	// Now is the clock, for tests. Nil means time.Now. It is also the clock the
	// due events read, so a sub-minute repeat is decided against the same time
	// the loop around it is.
	Now func() time.Time

	// Sleep is how the repeat loop waits between passes, for tests. Nil means
	// time.Sleep.
	Sleep func(d time.Duration)
	// contains filtered or unexported fields
}

Runner runs the events of a schedule that are due.

schedule:run and schedule:work both do exactly this, and a second copy in the worker is the copy that drifts.

func NewRunner

func NewRunner(schedule *Schedule) *Runner

NewRunner returns the runner for a schedule.

func (*Runner) Run

func (r *Runner) Run(ctx context.Context, at time.Time) int

Run fires everything due in the minute of at, and reports how many ran.

The order is the filters first, then the one-server claim, then the run. An event that fails does not stop the ones after it -- a schedule where one broken task silences the rest is a schedule that hides its own failures.

type Schedule

type Schedule struct {

	// Tenants expands per-tenant events. Nil means those events do not run,
	// which the runner reports rather than swallowing.
	Tenants Tenants

	// Environment is what an event's Environments list is matched against.
	Environment string

	// DownForMaintenance reports whether the application is down. Nil means it
	// is not.
	DownForMaintenance func() bool
	// contains filtered or unexported fields
}

Schedule is every event an application runs on a clock.

It is built at wiring time and read afterwards: it is not safe to declare an event on one goroutine while another is running the schedule.

func NewSchedule

func NewSchedule(eventMutex EventMutex, schedulingMutex SchedulingMutex, timezone *time.Location) *Schedule

NewSchedule returns an empty schedule.

The two mutexes are passed explicitly rather than resolved implicitly; nil for either means the events that need it are refused rather than run unprotected.

func (*Schedule) Attributes

func (s *Schedule) Attributes() *PendingEventAttributes

Attributes returns the attributes waiting to be merged into the next event, creating them if there are none.

Go has no fallthrough for a missing method, so the caller asks for the attributes explicitly instead of one being created implicitly. Chaining a frequency onto the result is what Group then reads.

func (*Schedule) Call

func (s *Schedule) Call(callback Callback) *CallbackEvent

Call schedules a closure.

The closure receives the Grant the event was declared with, which is required of any path that can reach a repository.

func (*Schedule) Command

func (s *Schedule) Command(command string, parameters ...string) *Event

Command schedules one of the application's own console commands.

func (*Schedule) CompileArrayInput

func (s *Schedule) CompileArrayInput(key string, values []string) string

CompileArrayInput renders a repeated parameter as a command line fragment.

A long flag repeats as --key=value, a short one as -k value, and a positional list is just the values.

func (*Schedule) DueEvents

func (s *Schedule) DueEvents(at time.Time) []*Event

DueEvents returns the events that should run at the given time.

func (*Schedule) Events

func (s *Schedule) Events() []*Event

Events returns every declared event.

func (*Schedule) Exec

func (s *Schedule) Exec(command string, parameters ...string) *Event

Exec schedules a command line.

An expression that does not parse is not possible here -- the event starts on every minute -- but a parameter list that would change the meaning of the line is escaped, which is what compileParameters is for.

func (*Schedule) ForgetMutexCache

func (s *Schedule) ForgetMutexCache()

ForgetMutexCache drops what ServerShouldRun remembered.

The cache is scoped to one tick: the runner calls this between them, and without it a replica that lost one window would lose every window afterwards.

func (*Schedule) Group

func (s *Schedule) Group(events func(*Schedule))

Group declares several events that share the attributes named before it.

Calling it without having named an attribute first panics: a group with nothing to merge is a group that does nothing, and it is a mistake in the schedule rather than a condition to handle.

func (*Schedule) Job

func (s *Schedule) Job(job any, queue, connection string) *CallbackEvent

Job schedules a queued job.

A job that reaches the queue with no dispatcher wired fails the run rather than being dropped, because a scheduled job that silently never enqueues is the failure nobody notices for a month.

func (*Schedule) ServerShouldRun

func (s *Schedule) ServerShouldRun(ctx context.Context, event *Event, at time.Time) (bool, error)

ServerShouldRun reports whether this replica is the one that runs the event.

An event asked about twice in one tick gets the same answer, because the second ask would otherwise find the window already claimed by the first and say no.

func (*Schedule) UseCache

func (s *Schedule) UseCache(locks *cache.Locks) *Schedule

UseCache points both mutexes at another lock issuer.

The issuer is passed here, and a mutex that is not CacheAware is left alone.

func (*Schedule) UseDispatcher

func (s *Schedule) UseDispatcher(dispatcher Dispatcher) *Schedule

UseDispatcher gives the schedule the queue Job pushes onto.

type SchedulingMutex

type SchedulingMutex interface {
	// Create claims the window, and reports whether it got it.
	Create(ctx context.Context, event *Event, at time.Time) (bool, error)

	// Exists reports whether the window is claimed.
	Exists(ctx context.Context, event *Event, at time.Time) (bool, error)
}

SchedulingMutex is what makes an event run on exactly one replica per window.

It is a different mutex from EventMutex and for a different reason: this one marks a window as claimed, and it is never released -- releasing it would let the replica that ticks two hundred milliseconds later claim the same window and run it a second time, which is the duplicate the mark exists to stop.

type Tenants

type Tenants func(ctx context.Context) ([]string, error)

Tenants returns the tenants a per-tenant event expands to.

The tenant comes from the Grant, and the core does not know where the application keeps its tenants -- a table, a config file, a control plane -- so the list is injected. Returning an empty list is valid and means those events simply do not run.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL