Documentation
¶
Overview ¶
Package scheduledeploy checks, on an interval, which apps have a due cron schedule (store.AppSchedule, migrations/0250_app_schedules.sql) and redeploys the latest commit on their configured branch.
Index ¶
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
This section is empty.
Types ¶
type Executor ¶
type Executor interface {
ExecuteScheduledDeploy(ctx context.Context, d store.ScheduledDeploy) (Outcome, error)
}
Executor starts a due scheduled deploy through the normal deploy path.
type Notifier ¶
type Notifier interface {
NotifyScheduledDeploy(ctx context.Context, d store.ScheduledDeploy, status, reason string)
}
Notifier tells operators a scheduled deploy did not run as planned.
type OneShotRunner ¶
type OneShotRunner struct {
Store OneShotStore
Freeze deploy.FreezeStore
Exec Executor
Notify Notifier
Logger *slog.Logger
Now func() time.Time
Grace time.Duration
StaleRun time.Duration
// contains filtered or unexported fields
}
OneShotRunner is the level-triggered scanner for one-shot scheduled deploys. Each tick re-reads the due rows, so a restart loses nothing: rows within Grace of run_at still run, older ones are marked missed and notified. A row is claimed (pending to running) before executing, so it never fires twice; a claim left behind by a crash is closed as failed, never retried.
type OneShotStore ¶
type OneShotStore interface {
ListDueScheduledDeploys(ctx context.Context, now, staleBefore time.Time) ([]store.ScheduledDeploy, error)
ClaimScheduledDeploy(ctx context.Context, id string, at time.Time) error
FinishScheduledDeploy(ctx context.Context, id, status, reason, approvalID string, at time.Time) error
}
OneShotStore is the store surface the one-shot runner needs.
type Outcome ¶
type Outcome struct {
// ApprovalID is set when the deploy became a pending approval instead of running.
ApprovalID string
Message string
}
Outcome is what an Executor reports for a deploy it started.
type ScheduleStore ¶
type ScheduleStore interface {
ListEnabledAppSchedules(ctx context.Context) ([]store.AppSchedule, error)
ArmAppScheduleNextRun(ctx context.Context, serviceName string, next time.Time) error
RecordAppScheduleHistory(ctx context.Context, e store.AppScheduleHistoryEntry) error
CompleteAppSchedule(ctx context.Context, serviceName string, at time.Time) error
}
ScheduleStore is the narrow store surface Scheduler needs, re-derived fresh every tick, the same shape internal/scheduledtask.ScheduleStore's own doc comment establishes for its sibling. *store.DB satisfies this structurally.
type Scheduler ¶
type Scheduler struct {
Store ScheduleStore
Freeze deploy.FreezeStore
Trigger Trigger
Logger *slog.Logger
// Now returns the current time; nil falls back to time.Now, the same
// testable-clock convention every other scheduler in this codebase
// establishes.
Now func() time.Time
// OneShotCatchUp bounds how late a one-time schedule may still fire
// after its run_at (for example after downtime); later than that it is
// recorded as skipped. Zero or negative means no bound.
OneShotCatchUp time.Duration
// contains filtered or unexported fields
}
Scheduler periodically checks which app schedules are due and fires them through Trigger.
Catch-up semantics: every enabled schedule persists its own next fire time (store.AppSchedule.NextFireAt) rather than keeping it only in an in-memory position. A schedule seen for the first time (NextFireAt nil) is armed to its next real occurrence and does not fire immediately, the same "no fire on sight" rule internal/scheduledtask. Scheduler and internal/backup.Scheduler already apply to a freshly observed schedule. Once armed, Tick re-arms NextFireAt to the next occurrence strictly after now *before* calling Trigger: if the process is killed between that arm and Trigger returning, the missed firing is not retried on the next tick, and it is never fired twice either.
Because the armed time is persisted rather than kept in memory, a schedule that missed one or more occurrences entirely while the control plane was down still fires exactly once the next time it is checked, for the most recent due occurrence, never once per missed occurrence, then resumes its normal cadence from now. This is a deliberate difference from its sibling schedulers (internal/scheduledtask, internal/backup), whose in-memory position is lost on restart and which therefore never catch up a missed occurrence at all: a missed scheduled deploy is worth catching up once, a missed backup or task run is not, matching how this codebase already treats each.
A schedule due during an active deploy freeze window is skipped, not forced: it is recorded in history with a reason and re-armed for its next natural occurrence, never queued to run once the freeze lifts.
func NewScheduler ¶
func NewScheduler(store ScheduleStore, freeze deploy.FreezeStore, trigger Trigger, logger *slog.Logger) *Scheduler
NewScheduler builds a Scheduler ready to Tick or Run. logger defaults to slog.Default() if nil.
func (*Scheduler) Run ¶
Run calls Tick on interval until ctx is done, matching the shape of every other periodic loop in this codebase (backup.Scheduler.Run, scheduledtask.Scheduler.Run, alerting.Engine.Run).
func (*Scheduler) Tick ¶
Tick evaluates every currently enabled app schedule once. One schedule's error (an invalid cron string, Trigger itself failing) is collected and joined, never stopping evaluation of the rest, the same "one broken resource must not block the rest" principle every other scheduler in this codebase already follows.
type Trigger ¶
type Trigger interface {
TriggerScheduledDeploy(ctx context.Context, serviceName, branch string) (result string, err error)
}
Trigger fires a scheduled redeploy of serviceName's latest commit on branch, returning a short human-readable result recorded as the history entry's reason. *api.Router (TriggerScheduledDeploy) satisfies this; redeclared here rather than depended on directly, the same "substitutable without a real Router" reasoning internal/scheduledtask.TaskRunner's own doc comment gives for its sibling.