mirror of
https://github.com/coder/coder.git
synced 2025-07-03 16:13:58 +00:00
feat: implement scheduling mechanism for prebuilds (#18126)
Closes https://github.com/coder/internal/issues/312 Depends on https://github.com/coder/terraform-provider-coder/pull/408 This PR adds support for defining an **autoscaling block** for prebuilds, allowing number of desired instances to scale dynamically based on a schedule. Example usage: ``` data "coder_workspace_preset" "us-nix" { ... prebuilds = { instances = 0 # default to 0 instances scheduling = { timezone = "UTC" # a single timezone is used for simplicity # Scale to 3 instances during the work week schedule { cron = "* 8-18 * * 1-5" # from 8AM–6:59PM, Mon–Fri, UTC instances = 3 # scale to 3 instances } # Scale to 1 instance on Saturdays for urgent support queries schedule { cron = "* 8-14 * * 6" # from 8AM–2:59PM, Sat, UTC instances = 1 # scale to 1 instance } } } } ``` ### Behavior - Multiple `schedule` blocks per `prebuilds` block are supported. - If the current time matches any defined autoscaling schedule, the corresponding number of instances is used. - If no schedule matches, the **default instance count** (`prebuilds.instances`) is used as a fallback. ### Why This feature allows prebuild instance capacity to adapt to predictable usage patterns, such as: - Scaling up during business hours or high-demand periods - Reducing capacity during off-hours to save resources ### Cron specification The cron specification is interpreted as a **continuous time range.** For example, the expression: ``` * 9-18 * * 1-5 ``` is intended to represent a continuous range from **09:00 to 18:59**, Monday through Friday. However, due to minor implementation imprecision, it is currently interpreted as a range from **08:59:00 to 18:58:59**, Monday through Friday. This slight discrepancy arises because the evaluation is based on whether a specific **point in time** falls within the range, using the `github.com/coder/coder/v2/coderd/schedule/cron` library, which performs per-minute matching rather than strict range evaluation. --------- Co-authored-by: Danny Kopping <danny@coder.com>
This commit is contained in:
committed by
GitHub
parent
511fd09582
commit
0f6ca55238
@ -6511,7 +6511,8 @@ SELECT
|
||||
tvp.id,
|
||||
tvp.name,
|
||||
tvp.desired_instances AS desired_instances,
|
||||
tvp.invalidate_after_secs AS ttl,
|
||||
tvp.scheduling_timezone,
|
||||
tvp.invalidate_after_secs AS ttl,
|
||||
tvp.prebuild_status,
|
||||
t.deleted,
|
||||
t.deprecated != '' AS deprecated
|
||||
@ -6535,6 +6536,7 @@ type GetTemplatePresetsWithPrebuildsRow struct {
|
||||
ID uuid.UUID `db:"id" json:"id"`
|
||||
Name string `db:"name" json:"name"`
|
||||
DesiredInstances sql.NullInt32 `db:"desired_instances" json:"desired_instances"`
|
||||
SchedulingTimezone string `db:"scheduling_timezone" json:"scheduling_timezone"`
|
||||
Ttl sql.NullInt32 `db:"ttl" json:"ttl"`
|
||||
PrebuildStatus PrebuildStatus `db:"prebuild_status" json:"prebuild_status"`
|
||||
Deleted bool `db:"deleted" json:"deleted"`
|
||||
@ -6564,6 +6566,7 @@ func (q *sqlQuerier) GetTemplatePresetsWithPrebuilds(ctx context.Context, templa
|
||||
&i.ID,
|
||||
&i.Name,
|
||||
&i.DesiredInstances,
|
||||
&i.SchedulingTimezone,
|
||||
&i.Ttl,
|
||||
&i.PrebuildStatus,
|
||||
&i.Deleted,
|
||||
@ -6582,8 +6585,51 @@ func (q *sqlQuerier) GetTemplatePresetsWithPrebuilds(ctx context.Context, templa
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const getActivePresetPrebuildSchedules = `-- name: GetActivePresetPrebuildSchedules :many
|
||||
SELECT
|
||||
tvpps.id, tvpps.preset_id, tvpps.cron_expression, tvpps.desired_instances
|
||||
FROM
|
||||
template_version_preset_prebuild_schedules tvpps
|
||||
INNER JOIN template_version_presets tvp ON tvp.id = tvpps.preset_id
|
||||
INNER JOIN template_versions tv ON tv.id = tvp.template_version_id
|
||||
INNER JOIN templates t ON t.id = tv.template_id
|
||||
WHERE
|
||||
-- Template version is active, and template is not deleted or deprecated
|
||||
tv.id = t.active_version_id
|
||||
AND NOT t.deleted
|
||||
AND t.deprecated = ''
|
||||
`
|
||||
|
||||
func (q *sqlQuerier) GetActivePresetPrebuildSchedules(ctx context.Context) ([]TemplateVersionPresetPrebuildSchedule, error) {
|
||||
rows, err := q.db.QueryContext(ctx, getActivePresetPrebuildSchedules)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []TemplateVersionPresetPrebuildSchedule
|
||||
for rows.Next() {
|
||||
var i TemplateVersionPresetPrebuildSchedule
|
||||
if err := rows.Scan(
|
||||
&i.ID,
|
||||
&i.PresetID,
|
||||
&i.CronExpression,
|
||||
&i.DesiredInstances,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Close(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const getPresetByID = `-- name: GetPresetByID :one
|
||||
SELECT tvp.id, tvp.template_version_id, tvp.name, tvp.created_at, tvp.desired_instances, tvp.invalidate_after_secs, tvp.prebuild_status, tv.template_id, tv.organization_id FROM
|
||||
SELECT tvp.id, tvp.template_version_id, tvp.name, tvp.created_at, tvp.desired_instances, tvp.invalidate_after_secs, tvp.prebuild_status, tvp.scheduling_timezone, tv.template_id, tv.organization_id FROM
|
||||
template_version_presets tvp
|
||||
INNER JOIN template_versions tv ON tvp.template_version_id = tv.id
|
||||
WHERE tvp.id = $1
|
||||
@ -6597,6 +6643,7 @@ type GetPresetByIDRow struct {
|
||||
DesiredInstances sql.NullInt32 `db:"desired_instances" json:"desired_instances"`
|
||||
InvalidateAfterSecs sql.NullInt32 `db:"invalidate_after_secs" json:"invalidate_after_secs"`
|
||||
PrebuildStatus PrebuildStatus `db:"prebuild_status" json:"prebuild_status"`
|
||||
SchedulingTimezone string `db:"scheduling_timezone" json:"scheduling_timezone"`
|
||||
TemplateID uuid.NullUUID `db:"template_id" json:"template_id"`
|
||||
OrganizationID uuid.UUID `db:"organization_id" json:"organization_id"`
|
||||
}
|
||||
@ -6612,6 +6659,7 @@ func (q *sqlQuerier) GetPresetByID(ctx context.Context, presetID uuid.UUID) (Get
|
||||
&i.DesiredInstances,
|
||||
&i.InvalidateAfterSecs,
|
||||
&i.PrebuildStatus,
|
||||
&i.SchedulingTimezone,
|
||||
&i.TemplateID,
|
||||
&i.OrganizationID,
|
||||
)
|
||||
@ -6620,7 +6668,7 @@ func (q *sqlQuerier) GetPresetByID(ctx context.Context, presetID uuid.UUID) (Get
|
||||
|
||||
const getPresetByWorkspaceBuildID = `-- name: GetPresetByWorkspaceBuildID :one
|
||||
SELECT
|
||||
template_version_presets.id, template_version_presets.template_version_id, template_version_presets.name, template_version_presets.created_at, template_version_presets.desired_instances, template_version_presets.invalidate_after_secs, template_version_presets.prebuild_status
|
||||
template_version_presets.id, template_version_presets.template_version_id, template_version_presets.name, template_version_presets.created_at, template_version_presets.desired_instances, template_version_presets.invalidate_after_secs, template_version_presets.prebuild_status, template_version_presets.scheduling_timezone
|
||||
FROM
|
||||
template_version_presets
|
||||
INNER JOIN workspace_builds ON workspace_builds.template_version_preset_id = template_version_presets.id
|
||||
@ -6639,6 +6687,7 @@ func (q *sqlQuerier) GetPresetByWorkspaceBuildID(ctx context.Context, workspaceB
|
||||
&i.DesiredInstances,
|
||||
&i.InvalidateAfterSecs,
|
||||
&i.PrebuildStatus,
|
||||
&i.SchedulingTimezone,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
@ -6720,7 +6769,7 @@ func (q *sqlQuerier) GetPresetParametersByTemplateVersionID(ctx context.Context,
|
||||
|
||||
const getPresetsByTemplateVersionID = `-- name: GetPresetsByTemplateVersionID :many
|
||||
SELECT
|
||||
id, template_version_id, name, created_at, desired_instances, invalidate_after_secs, prebuild_status
|
||||
id, template_version_id, name, created_at, desired_instances, invalidate_after_secs, prebuild_status, scheduling_timezone
|
||||
FROM
|
||||
template_version_presets
|
||||
WHERE
|
||||
@ -6744,6 +6793,7 @@ func (q *sqlQuerier) GetPresetsByTemplateVersionID(ctx context.Context, template
|
||||
&i.DesiredInstances,
|
||||
&i.InvalidateAfterSecs,
|
||||
&i.PrebuildStatus,
|
||||
&i.SchedulingTimezone,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -6765,7 +6815,8 @@ INSERT INTO template_version_presets (
|
||||
name,
|
||||
created_at,
|
||||
desired_instances,
|
||||
invalidate_after_secs
|
||||
invalidate_after_secs,
|
||||
scheduling_timezone
|
||||
)
|
||||
VALUES (
|
||||
$1,
|
||||
@ -6773,8 +6824,9 @@ VALUES (
|
||||
$3,
|
||||
$4,
|
||||
$5,
|
||||
$6
|
||||
) RETURNING id, template_version_id, name, created_at, desired_instances, invalidate_after_secs, prebuild_status
|
||||
$6,
|
||||
$7
|
||||
) RETURNING id, template_version_id, name, created_at, desired_instances, invalidate_after_secs, prebuild_status, scheduling_timezone
|
||||
`
|
||||
|
||||
type InsertPresetParams struct {
|
||||
@ -6784,6 +6836,7 @@ type InsertPresetParams struct {
|
||||
CreatedAt time.Time `db:"created_at" json:"created_at"`
|
||||
DesiredInstances sql.NullInt32 `db:"desired_instances" json:"desired_instances"`
|
||||
InvalidateAfterSecs sql.NullInt32 `db:"invalidate_after_secs" json:"invalidate_after_secs"`
|
||||
SchedulingTimezone string `db:"scheduling_timezone" json:"scheduling_timezone"`
|
||||
}
|
||||
|
||||
func (q *sqlQuerier) InsertPreset(ctx context.Context, arg InsertPresetParams) (TemplateVersionPreset, error) {
|
||||
@ -6794,6 +6847,7 @@ func (q *sqlQuerier) InsertPreset(ctx context.Context, arg InsertPresetParams) (
|
||||
arg.CreatedAt,
|
||||
arg.DesiredInstances,
|
||||
arg.InvalidateAfterSecs,
|
||||
arg.SchedulingTimezone,
|
||||
)
|
||||
var i TemplateVersionPreset
|
||||
err := row.Scan(
|
||||
@ -6804,6 +6858,7 @@ func (q *sqlQuerier) InsertPreset(ctx context.Context, arg InsertPresetParams) (
|
||||
&i.DesiredInstances,
|
||||
&i.InvalidateAfterSecs,
|
||||
&i.PrebuildStatus,
|
||||
&i.SchedulingTimezone,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
@ -6852,6 +6907,37 @@ func (q *sqlQuerier) InsertPresetParameters(ctx context.Context, arg InsertPrese
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const insertPresetPrebuildSchedule = `-- name: InsertPresetPrebuildSchedule :one
|
||||
INSERT INTO template_version_preset_prebuild_schedules (
|
||||
preset_id,
|
||||
cron_expression,
|
||||
desired_instances
|
||||
)
|
||||
VALUES (
|
||||
$1,
|
||||
$2,
|
||||
$3
|
||||
) RETURNING id, preset_id, cron_expression, desired_instances
|
||||
`
|
||||
|
||||
type InsertPresetPrebuildScheduleParams struct {
|
||||
PresetID uuid.UUID `db:"preset_id" json:"preset_id"`
|
||||
CronExpression string `db:"cron_expression" json:"cron_expression"`
|
||||
DesiredInstances int32 `db:"desired_instances" json:"desired_instances"`
|
||||
}
|
||||
|
||||
func (q *sqlQuerier) InsertPresetPrebuildSchedule(ctx context.Context, arg InsertPresetPrebuildScheduleParams) (TemplateVersionPresetPrebuildSchedule, error) {
|
||||
row := q.db.QueryRowContext(ctx, insertPresetPrebuildSchedule, arg.PresetID, arg.CronExpression, arg.DesiredInstances)
|
||||
var i TemplateVersionPresetPrebuildSchedule
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.PresetID,
|
||||
&i.CronExpression,
|
||||
&i.DesiredInstances,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const updatePresetPrebuildStatus = `-- name: UpdatePresetPrebuildStatus :exec
|
||||
UPDATE template_version_presets
|
||||
SET prebuild_status = $1
|
||||
|
Reference in New Issue
Block a user