Skip to content

Commit 0f6ca55

Browse files
feat: implement scheduling mechanism for prebuilds (#18126)
Closes coder/internal#312 Depends on coder/terraform-provider-coder#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>
1 parent 511fd09 commit 0f6ca55

38 files changed

+2528
-871
lines changed

coderd/database/dbauthz/dbauthz.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1686,6 +1686,13 @@ func (q *querier) GetAPIKeysLastUsedAfter(ctx context.Context, lastUsed time.Tim
16861686
return fetchWithPostFilter(q.auth, policy.ActionRead, q.db.GetAPIKeysLastUsedAfter)(ctx, lastUsed)
16871687
}
16881688

1689+
func (q *querier) GetActivePresetPrebuildSchedules(ctx context.Context) ([]database.TemplateVersionPresetPrebuildSchedule, error) {
1690+
if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceTemplate.All()); err != nil {
1691+
return nil, err
1692+
}
1693+
return q.db.GetActivePresetPrebuildSchedules(ctx)
1694+
}
1695+
16891696
func (q *querier) GetActiveUserCount(ctx context.Context, includeSystem bool) (int64, error) {
16901697
if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceSystem); err != nil {
16911698
return 0, err
@@ -3661,6 +3668,15 @@ func (q *querier) InsertPresetParameters(ctx context.Context, arg database.Inser
36613668
return q.db.InsertPresetParameters(ctx, arg)
36623669
}
36633670

3671+
func (q *querier) InsertPresetPrebuildSchedule(ctx context.Context, arg database.InsertPresetPrebuildScheduleParams) (database.TemplateVersionPresetPrebuildSchedule, error) {
3672+
err := q.authorizeContext(ctx, policy.ActionUpdate, rbac.ResourceTemplate)
3673+
if err != nil {
3674+
return database.TemplateVersionPresetPrebuildSchedule{}, err
3675+
}
3676+
3677+
return q.db.InsertPresetPrebuildSchedule(ctx, arg)
3678+
}
3679+
36643680
func (q *querier) InsertProvisionerJob(ctx context.Context, arg database.InsertProvisionerJobParams) (database.ProvisionerJob, error) {
36653681
// TODO: Remove this once we have a proper rbac check for provisioner jobs.
36663682
// Details in https://github.com/coder/coder/issues/16160

coderd/database/dbauthz/dbauthz_test.go

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -979,6 +979,29 @@ func (s *MethodTestSuite) TestOrganization() {
979979
}
980980
check.Args(insertPresetParametersParams).Asserts(rbac.ResourceTemplate, policy.ActionUpdate)
981981
}))
982+
s.Run("InsertPresetPrebuildSchedule", s.Subtest(func(db database.Store, check *expects) {
983+
org := dbgen.Organization(s.T(), db, database.Organization{})
984+
user := dbgen.User(s.T(), db, database.User{})
985+
template := dbgen.Template(s.T(), db, database.Template{
986+
CreatedBy: user.ID,
987+
OrganizationID: org.ID,
988+
})
989+
templateVersion := dbgen.TemplateVersion(s.T(), db, database.TemplateVersion{
990+
TemplateID: uuid.NullUUID{UUID: template.ID, Valid: true},
991+
OrganizationID: org.ID,
992+
CreatedBy: user.ID,
993+
})
994+
preset := dbgen.Preset(s.T(), db, database.InsertPresetParams{
995+
TemplateVersionID: templateVersion.ID,
996+
Name: "test",
997+
})
998+
arg := database.InsertPresetPrebuildScheduleParams{
999+
PresetID: preset.ID,
1000+
}
1001+
check.Args(arg).
1002+
Asserts(rbac.ResourceTemplate, policy.ActionUpdate).
1003+
ErrorsWithInMemDB(dbmem.ErrUnimplemented)
1004+
}))
9821005
s.Run("DeleteOrganizationMember", s.Subtest(func(db database.Store, check *expects) {
9831006
o := dbgen.Organization(s.T(), db, database.Organization{})
9841007
u := dbgen.User(s.T(), db, database.User{})
@@ -4916,6 +4939,12 @@ func (s *MethodTestSuite) TestPrebuilds() {
49164939
Asserts(template.RBACObject(), policy.ActionRead).
49174940
Returns(insertedParameters)
49184941
}))
4942+
s.Run("GetActivePresetPrebuildSchedules", s.Subtest(func(db database.Store, check *expects) {
4943+
check.Args().
4944+
Asserts(rbac.ResourceTemplate.All(), policy.ActionRead).
4945+
Returns([]database.TemplateVersionPresetPrebuildSchedule{}).
4946+
ErrorsWithInMemDB(dbmem.ErrUnimplemented)
4947+
}))
49194948
s.Run("GetPresetsByTemplateVersionID", s.Subtest(func(db database.Store, check *expects) {
49204949
ctx := context.Background()
49214950
org := dbgen.Organization(s.T(), db, database.Organization{})

coderd/database/dbfake/dbfake.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -415,6 +415,7 @@ func (t TemplateVersionBuilder) Do() TemplateVersionResponse {
415415
CreatedAt: version.CreatedAt,
416416
DesiredInstances: preset.DesiredInstances,
417417
InvalidateAfterSecs: preset.InvalidateAfterSecs,
418+
SchedulingTimezone: preset.SchedulingTimezone,
418419
})
419420
}
420421

coderd/database/dbgen/dbgen.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1302,11 +1302,22 @@ func Preset(t testing.TB, db database.Store, seed database.InsertPresetParams) d
13021302
CreatedAt: takeFirst(seed.CreatedAt, dbtime.Now()),
13031303
DesiredInstances: seed.DesiredInstances,
13041304
InvalidateAfterSecs: seed.InvalidateAfterSecs,
1305+
SchedulingTimezone: seed.SchedulingTimezone,
13051306
})
13061307
require.NoError(t, err, "insert preset")
13071308
return preset
13081309
}
13091310

1311+
func PresetPrebuildSchedule(t testing.TB, db database.Store, seed database.InsertPresetPrebuildScheduleParams) database.TemplateVersionPresetPrebuildSchedule {
1312+
schedule, err := db.InsertPresetPrebuildSchedule(genCtx, database.InsertPresetPrebuildScheduleParams{
1313+
PresetID: takeFirst(seed.PresetID, uuid.New()),
1314+
CronExpression: takeFirst(seed.CronExpression, "* 9-18 * * 1-5"),
1315+
DesiredInstances: takeFirst(seed.DesiredInstances, 1),
1316+
})
1317+
require.NoError(t, err, "insert preset prebuild schedule")
1318+
return schedule
1319+
}
1320+
13101321
func PresetParameter(t testing.TB, db database.Store, seed database.InsertPresetParametersParams) []database.TemplateVersionPresetParameter {
13111322
parameters, err := db.InsertPresetParameters(genCtx, database.InsertPresetParametersParams{
13121323
TemplateVersionPresetID: takeFirst(seed.TemplateVersionPresetID, uuid.New()),

coderd/database/dbmem/dbmem.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2778,6 +2778,10 @@ func (q *FakeQuerier) GetAPIKeysLastUsedAfter(_ context.Context, after time.Time
27782778
return apiKeys, nil
27792779
}
27802780

2781+
func (q *FakeQuerier) GetActivePresetPrebuildSchedules(ctx context.Context) ([]database.TemplateVersionPresetPrebuildSchedule, error) {
2782+
return nil, ErrUnimplemented
2783+
}
2784+
27812785
// nolint:revive // It's not a control flag, it's a filter.
27822786
func (q *FakeQuerier) GetActiveUserCount(_ context.Context, includeSystem bool) (int64, error) {
27832787
q.mutex.RLock()
@@ -9191,6 +9195,15 @@ func (q *FakeQuerier) InsertPresetParameters(_ context.Context, arg database.Ins
91919195
return presetParameters, nil
91929196
}
91939197

9198+
func (q *FakeQuerier) InsertPresetPrebuildSchedule(ctx context.Context, arg database.InsertPresetPrebuildScheduleParams) (database.TemplateVersionPresetPrebuildSchedule, error) {
9199+
err := validateDatabaseType(arg)
9200+
if err != nil {
9201+
return database.TemplateVersionPresetPrebuildSchedule{}, err
9202+
}
9203+
9204+
return database.TemplateVersionPresetPrebuildSchedule{}, ErrUnimplemented
9205+
}
9206+
91949207
func (q *FakeQuerier) InsertProvisionerJob(_ context.Context, arg database.InsertProvisionerJobParams) (database.ProvisionerJob, error) {
91959208
if err := validateDatabaseType(arg); err != nil {
91969209
return database.ProvisionerJob{}, err

coderd/database/dbmetrics/querymetrics.go

Lines changed: 14 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

coderd/database/dbmock/dbmock.go

Lines changed: 30 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

coderd/database/dump.sql

Lines changed: 15 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

coderd/database/foreign_key_constraint.go

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
-- Drop the prebuild schedules table
2+
DROP TABLE template_version_preset_prebuild_schedules;
3+
4+
-- Remove scheduling_timezone column from template_version_presets table
5+
ALTER TABLE template_version_presets
6+
DROP COLUMN scheduling_timezone;

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy