Matrix logo

Schedule Engine

The schedule package computes alarm fire times for the two kinds: once (relative delay or absolute instant) and cron (5-field expression evaluated in the alarm's IANA timezone).

The schedule package computes alarm fire times for the two alarm kinds. It is a pure computation layer with no database or network dependencies.

Source file: internal/schedule/schedule.go.


Overview

The schedule engine handles two distinct time-resolution tasks:

  1. Once alarms: resolve a single fire instant from either a relative delay (delay_seconds) or an absolute RFC3339 timestamp (fire_at).
  2. Cron alarms: compute the next fire time strictly after a given instant from a standard cron expression, evaluated in the alarm's IANA timezone.

The engine uses robfig/cron/v3 for cron parsing and evaluation.


Cron parser

The parser accepts the standard 5-field syntax plus @ descriptors and @every:

var cronParser = cron.NewParser(
    cron.Minute | cron.Hour | cron.Dom | cron.Month | cron.Dow | cron.Descriptor,
)

Supported cron surfaces:

SurfaceExampleMeaning
5-field0 9 * * 1-5At 09:00 Monday through Friday
@yearly / @annually@yearlyOnce a year, midnight Jan 1
@monthly@monthlyOnce a month, midnight 1st
@weekly@weeklyOnce a week, midnight Sunday
@daily / @midnight@dailyOnce a day, midnight
@hourly@hourlyOnce an hour, at :00
@every Nm@every 45mEvery N minutes/hours/etc.

NextCron

NextCron returns the next fire time strictly after after, evaluated in the alarm's timezone:

func NextCron(expr, tz string, after time.Time) (time.Time, error)

The timezone defaults to UTC when empty. The function:

  1. Parses the cron expression via the cronParser.
  2. Loads the IANA timezone via LoadLocation (defaults to UTC when empty).
  3. Converts after to the alarm's timezone.
  4. Calls sched.Next(after.In(loc)) to get the next fire time.
  5. Returns the result in UTC.

If the cron expression yields no future time, an error is returned.


NextOnce

NextOnce resolves the single fire instant for a once alarm from exactly one of delaySeconds (relative to now) or fireAt (RFC3339 absolute):

func NextOnce(delaySeconds int64, fireAt string, now time.Time) (time.Time, error)

Rules:

InputBehavior
Both delaySeconds > 0 and fireAt != ""Error: "once alarm takes either delay_seconds or fire_at, not both"
Only delaySeconds > 0Returns now + delaySeconds in UTC
Only fireAt != ""Parses as RFC3339; errors if not in the future; returns in UTC
NeitherError: "once alarm requires delay_seconds or fire_at"

The resolved time must be in the future relative to now.


Timezone handling

LoadLocation resolves an IANA timezone string, defaulting to UTC when empty:

func LoadLocation(tz string) (*time.Location, error)

For once alarms, the timezone is always UTC (the server sets alarm.Timezone = "UTC" explicitly). For cron alarms, the timezone comes from the CreateAlarmRequest.Timezone field and defaults to UTC when omitted.

All fire times are stored and returned in UTC. The timezone is only used during cron evaluation to ensure expressions like "every day at 9am" fire at 9am in the user's local time.


Cron alarm rescheduling

After a successful cron fire, the dispatch worker calls NextCron to compute the next fire time and stores it via store.Reschedule. The next_fire_at column is the dispatch worker's claim key: the next tick finds it, claims it, and fires again.

When NextCron yields no future time (the cron has expired), the alarm is retired by marking it fired.

When a cron fire fails and retries are exhausted, the dispatch worker calls store.RescheduleAfterFailure to advance next_fire_at to the next occurrence, so one bad fire does not wedge the entire series.