mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08: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:
co-authored by
Danny Kopping
parent
511fd09582
commit
0f6ca55238
@@ -907,6 +907,7 @@ func ConvertState(ctx context.Context, modules []*tfjson.StateModule, rawGraph s
|
||||
}
|
||||
var prebuildInstances int32
|
||||
var expirationPolicy *proto.ExpirationPolicy
|
||||
var scheduling *proto.Scheduling
|
||||
if len(preset.Prebuilds) > 0 {
|
||||
prebuildInstances = int32(math.Min(math.MaxInt32, float64(preset.Prebuilds[0].Instances)))
|
||||
if len(preset.Prebuilds[0].ExpirationPolicy) > 0 {
|
||||
@@ -914,6 +915,9 @@ func ConvertState(ctx context.Context, modules []*tfjson.StateModule, rawGraph s
|
||||
Ttl: int32(math.Min(math.MaxInt32, float64(preset.Prebuilds[0].ExpirationPolicy[0].TTL))),
|
||||
}
|
||||
}
|
||||
if len(preset.Prebuilds[0].Scheduling) > 0 {
|
||||
scheduling = convertScheduling(preset.Prebuilds[0].Scheduling[0])
|
||||
}
|
||||
}
|
||||
protoPreset := &proto.Preset{
|
||||
Name: preset.Name,
|
||||
@@ -921,6 +925,7 @@ func ConvertState(ctx context.Context, modules []*tfjson.StateModule, rawGraph s
|
||||
Prebuild: &proto.Prebuild{
|
||||
Instances: prebuildInstances,
|
||||
ExpirationPolicy: expirationPolicy,
|
||||
Scheduling: scheduling,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -978,6 +983,37 @@ func ConvertState(ctx context.Context, modules []*tfjson.StateModule, rawGraph s
|
||||
}, nil
|
||||
}
|
||||
|
||||
func convertScheduling(scheduling provider.Scheduling) *proto.Scheduling {
|
||||
return &proto.Scheduling{
|
||||
Timezone: scheduling.Timezone,
|
||||
Schedule: convertSchedules(scheduling.Schedule),
|
||||
}
|
||||
}
|
||||
|
||||
func convertSchedules(schedules []provider.Schedule) []*proto.Schedule {
|
||||
protoSchedules := make([]*proto.Schedule, len(schedules))
|
||||
for i, schedule := range schedules {
|
||||
protoSchedules[i] = convertSchedule(schedule)
|
||||
}
|
||||
|
||||
return protoSchedules
|
||||
}
|
||||
|
||||
func convertSchedule(schedule provider.Schedule) *proto.Schedule {
|
||||
return &proto.Schedule{
|
||||
Cron: schedule.Cron,
|
||||
Instances: safeInt32Conversion(schedule.Instances),
|
||||
}
|
||||
}
|
||||
|
||||
func safeInt32Conversion(n int) int32 {
|
||||
if n > math.MaxInt32 {
|
||||
return math.MaxInt32
|
||||
}
|
||||
// #nosec G115 - Safe conversion, as we have explicitly checked that the number does not exceed math.MaxInt32.
|
||||
return int32(n)
|
||||
}
|
||||
|
||||
func PtrInt32(number int) *int32 {
|
||||
// #nosec G115 - Safe conversion as the number is expected to be within int32 range
|
||||
n := int32(number)
|
||||
|
||||
@@ -882,6 +882,19 @@ func TestConvertResources(t *testing.T) {
|
||||
ExpirationPolicy: &proto.ExpirationPolicy{
|
||||
Ttl: 86400,
|
||||
},
|
||||
Scheduling: &proto.Scheduling{
|
||||
Timezone: "America/Los_Angeles",
|
||||
Schedule: []*proto.Schedule{
|
||||
{
|
||||
Cron: "* 8-18 * * 1-5",
|
||||
Instances: 3,
|
||||
},
|
||||
{
|
||||
Cron: "* 8-14 * * 6",
|
||||
Instances: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}},
|
||||
},
|
||||
|
||||
@@ -28,6 +28,17 @@ data "coder_workspace_preset" "MyFirstProject" {
|
||||
expiration_policy {
|
||||
ttl = 86400
|
||||
}
|
||||
scheduling {
|
||||
timezone = "America/Los_Angeles"
|
||||
schedule {
|
||||
cron = "* 8-18 * * 1-5"
|
||||
instances = 3
|
||||
}
|
||||
schedule {
|
||||
cron = "* 8-14 * * 6"
|
||||
instances = 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+50
-2
@@ -173,7 +173,22 @@
|
||||
"ttl": 86400
|
||||
}
|
||||
],
|
||||
"instances": 4
|
||||
"instances": 4,
|
||||
"scheduling": [
|
||||
{
|
||||
"schedule": [
|
||||
{
|
||||
"cron": "* 8-18 * * 1-5",
|
||||
"instances": 3
|
||||
},
|
||||
{
|
||||
"cron": "* 8-14 * * 6",
|
||||
"instances": 1
|
||||
}
|
||||
],
|
||||
"timezone": "America/Los_Angeles"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -183,6 +198,14 @@
|
||||
{
|
||||
"expiration_policy": [
|
||||
{}
|
||||
],
|
||||
"scheduling": [
|
||||
{
|
||||
"schedule": [
|
||||
{},
|
||||
{}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
@@ -418,7 +441,32 @@
|
||||
],
|
||||
"instances": {
|
||||
"constant_value": 4
|
||||
}
|
||||
},
|
||||
"scheduling": [
|
||||
{
|
||||
"schedule": [
|
||||
{
|
||||
"cron": {
|
||||
"constant_value": "* 8-18 * * 1-5"
|
||||
},
|
||||
"instances": {
|
||||
"constant_value": 3
|
||||
}
|
||||
},
|
||||
{
|
||||
"cron": {
|
||||
"constant_value": "* 8-14 * * 6"
|
||||
},
|
||||
"instances": {
|
||||
"constant_value": 1
|
||||
}
|
||||
}
|
||||
],
|
||||
"timezone": {
|
||||
"constant_value": "America/Los_Angeles"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
+24
-1
@@ -53,7 +53,22 @@
|
||||
"ttl": 86400
|
||||
}
|
||||
],
|
||||
"instances": 4
|
||||
"instances": 4,
|
||||
"scheduling": [
|
||||
{
|
||||
"schedule": [
|
||||
{
|
||||
"cron": "* 8-18 * * 1-5",
|
||||
"instances": 3
|
||||
},
|
||||
{
|
||||
"cron": "* 8-14 * * 6",
|
||||
"instances": 1
|
||||
}
|
||||
],
|
||||
"timezone": "America/Los_Angeles"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -63,6 +78,14 @@
|
||||
{
|
||||
"expiration_policy": [
|
||||
{}
|
||||
],
|
||||
"scheduling": [
|
||||
{
|
||||
"schedule": [
|
||||
{},
|
||||
{}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
1.11.4
|
||||
Reference in New Issue
Block a user