Skip to content

Commit 67c7919

Browse files
deansheatherEmyrk
authored andcommitted
fix: do not set max deadline for workspaces on template update (#12446)
* fix: do not set max deadline for workspaces on template update When templates are updated and schedule data is changed, we update all running workspaces to have up-to-date scheduling information that sticks to the new policy. When updating the max_deadline for existing running workspaces, if the max_deadline was before now()+2h we would set the max_deadline to now()+2h. Builds that don't/shouldn't have a max_deadline have it set to 0, which is always before now()+2h, and thus would always have the max_deadline updated. * test: add unit test to excercise template schedule bug --------- Co-authored-by: Steven Masley <stevenmasley@gmail.com>
1 parent 840c4e7 commit 67c7919

File tree

2 files changed

+109
-32
lines changed

2 files changed

+109
-32
lines changed

enterprise/coderd/schedule/template.go

+11-2
Original file line numberDiff line numberDiff line change
@@ -277,15 +277,24 @@ func (s *EnterpriseTemplateScheduleStore) updateWorkspaceBuild(ctx context.Conte
277277
}
278278

279279
// If max deadline is before now()+2h, then set it to that.
280+
// This is intended to give ample warning to this workspace about an upcoming auto-stop.
281+
// If we were to omit this "grace" period, then this workspace could be set to be stopped "now".
282+
// The "2 hours" was an arbitrary decision for this window.
280283
now := s.now()
281-
if autostop.MaxDeadline.Before(now.Add(2 * time.Hour)) {
284+
if !autostop.MaxDeadline.IsZero() && autostop.MaxDeadline.Before(now.Add(2*time.Hour)) {
282285
autostop.MaxDeadline = now.Add(time.Hour * 2)
283286
}
284287

285288
// If the current deadline on the build is after the new max_deadline, then
286289
// set it to the max_deadline.
287290
autostop.Deadline = build.Deadline
288-
if autostop.Deadline.After(autostop.MaxDeadline) {
291+
if !autostop.MaxDeadline.IsZero() && autostop.Deadline.After(autostop.MaxDeadline) {
292+
autostop.Deadline = autostop.MaxDeadline
293+
}
294+
295+
// If there's a max_deadline but the deadline is 0, then set the deadline to
296+
// the max_deadline.
297+
if !autostop.MaxDeadline.IsZero() && autostop.Deadline.IsZero() {
289298
autostop.Deadline = autostop.MaxDeadline
290299
}
291300

enterprise/coderd/schedule/template_test.go

+98-30
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import (
1616
"github.com/coder/coder/v2/coderd/database/dbgen"
1717
"github.com/coder/coder/v2/coderd/database/dbtestutil"
1818
agplschedule "github.com/coder/coder/v2/coderd/schedule"
19+
"github.com/coder/coder/v2/coderd/util/ptr"
1920
"github.com/coder/coder/v2/cryptorand"
2021
"github.com/coder/coder/v2/enterprise/coderd/schedule"
2122
"github.com/coder/coder/v2/testutil"
@@ -27,30 +28,35 @@ func TestTemplateUpdateBuildDeadlines(t *testing.T) {
2728
db, _ := dbtestutil.NewDB(t)
2829

2930
var (
30-
org = dbgen.Organization(t, db, database.Organization{})
31-
user = dbgen.User(t, db, database.User{})
31+
org = dbgen.Organization(t, db, database.Organization{})
32+
quietUser = dbgen.User(t, db, database.User{
33+
Username: "quiet",
34+
})
35+
noQuietUser = dbgen.User(t, db, database.User{
36+
Username: "no-quiet",
37+
})
3238
file = dbgen.File(t, db, database.File{
33-
CreatedBy: user.ID,
39+
CreatedBy: quietUser.ID,
3440
})
3541
templateJob = dbgen.ProvisionerJob(t, db, nil, database.ProvisionerJob{
3642
OrganizationID: org.ID,
3743
FileID: file.ID,
38-
InitiatorID: user.ID,
44+
InitiatorID: quietUser.ID,
3945
Tags: database.StringMap{
4046
"foo": "bar",
4147
},
4248
})
4349
templateVersion = dbgen.TemplateVersion(t, db, database.TemplateVersion{
4450
OrganizationID: org.ID,
45-
CreatedBy: user.ID,
51+
CreatedBy: quietUser.ID,
4652
JobID: templateJob.ID,
4753
})
4854
)
4955

5056
const userQuietHoursSchedule = "CRON_TZ=UTC 0 0 * * *" // midnight UTC
5157
ctx := testutil.Context(t, testutil.WaitLong)
52-
user, err := db.UpdateUserQuietHoursSchedule(ctx, database.UpdateUserQuietHoursScheduleParams{
53-
ID: user.ID,
58+
quietUser, err := db.UpdateUserQuietHoursSchedule(ctx, database.UpdateUserQuietHoursScheduleParams{
59+
ID: quietUser.ID,
5460
QuietHoursSchedule: userQuietHoursSchedule,
5561
})
5662
require.NoError(t, err)
@@ -62,20 +68,23 @@ func TestTemplateUpdateBuildDeadlines(t *testing.T) {
6268

6369
// Workspace old max_deadline too soon
6470
cases := []struct {
65-
name string
66-
now time.Time
67-
deadline time.Time
68-
maxDeadline time.Time
69-
newDeadline time.Time // 0 for no change
71+
name string
72+
now time.Time
73+
deadline time.Time
74+
maxDeadline time.Time
75+
// Set to nil for no change.
76+
newDeadline *time.Time
7077
newMaxDeadline time.Time
78+
noQuietHours bool
79+
autostopReq *agplschedule.TemplateAutostopRequirement
7180
}{
7281
{
7382
name: "SkippedWorkspaceMaxDeadlineTooSoon",
7483
now: buildTime,
7584
deadline: buildTime,
7685
maxDeadline: buildTime.Add(1 * time.Hour),
7786
// Unchanged since the max deadline is too soon.
78-
newDeadline: time.Time{},
87+
newDeadline: nil,
7988
newMaxDeadline: buildTime.Add(1 * time.Hour),
8089
},
8190
{
@@ -85,7 +94,7 @@ func TestTemplateUpdateBuildDeadlines(t *testing.T) {
8594
deadline: buildTime,
8695
// Far into the future...
8796
maxDeadline: nextQuietHours.Add(24 * time.Hour),
88-
newDeadline: time.Time{},
97+
newDeadline: nil,
8998
// We will use now() + 2 hours if the newly calculated max deadline
9099
// from the workspace build time is before now.
91100
newMaxDeadline: nextQuietHours.Add(8 * time.Hour),
@@ -97,7 +106,7 @@ func TestTemplateUpdateBuildDeadlines(t *testing.T) {
97106
deadline: buildTime,
98107
// Far into the future...
99108
maxDeadline: nextQuietHours.Add(24 * time.Hour),
100-
newDeadline: time.Time{},
109+
newDeadline: nil,
101110
// We will use now() + 2 hours if the newly calculated max deadline
102111
// from the workspace build time is within the next 2 hours.
103112
newMaxDeadline: nextQuietHours.Add(1 * time.Hour),
@@ -109,7 +118,7 @@ func TestTemplateUpdateBuildDeadlines(t *testing.T) {
109118
deadline: buildTime,
110119
// Far into the future...
111120
maxDeadline: nextQuietHours.Add(24 * time.Hour),
112-
newDeadline: time.Time{},
121+
newDeadline: nil,
113122
newMaxDeadline: nextQuietHours,
114123
},
115124
{
@@ -120,7 +129,56 @@ func TestTemplateUpdateBuildDeadlines(t *testing.T) {
120129
deadline: nextQuietHours.Add(24 * time.Hour),
121130
maxDeadline: nextQuietHours.Add(24 * time.Hour),
122131
// The deadline should match since it is after the new max deadline.
123-
newDeadline: nextQuietHours,
132+
newDeadline: ptr.Ref(nextQuietHours),
133+
newMaxDeadline: nextQuietHours,
134+
},
135+
{
136+
// There was a bug if a user has no quiet hours set, and autostop
137+
// req is not turned on, then the max deadline is set to `time.Time{}`.
138+
// This zero value was "in the past", so the workspace deadline would
139+
// be set to "now" + 2 hours.
140+
// This is a mistake because the max deadline being zero means
141+
// there is no max deadline.
142+
name: "MaxDeadlineShouldBeUnset",
143+
now: buildTime,
144+
deadline: buildTime.Add(time.Hour * 8),
145+
maxDeadline: time.Time{}, // No max set
146+
// Should be unchanged
147+
newDeadline: ptr.Ref(buildTime.Add(time.Hour * 8)),
148+
newMaxDeadline: time.Time{},
149+
noQuietHours: true,
150+
autostopReq: &agplschedule.TemplateAutostopRequirement{
151+
DaysOfWeek: 0,
152+
Weeks: 0,
153+
},
154+
},
155+
{
156+
// A bug existed where MaxDeadline could be set, but deadline was
157+
// `time.Time{}`. This is a logical inconsistency because the "max"
158+
// deadline was ignored.
159+
name: "NoDeadline",
160+
now: buildTime,
161+
deadline: time.Time{},
162+
maxDeadline: time.Time{}, // No max set
163+
// Should be unchanged
164+
newDeadline: ptr.Ref(time.Time{}),
165+
newMaxDeadline: time.Time{},
166+
noQuietHours: true,
167+
autostopReq: &agplschedule.TemplateAutostopRequirement{
168+
DaysOfWeek: 0,
169+
Weeks: 0,
170+
},
171+
},
172+
173+
{
174+
// Similar to 'NoDeadline' test. This has a MaxDeadline set, so
175+
// the deadline of the workspace should now be set.
176+
name: "WorkspaceDeadlineNowSet",
177+
now: nextQuietHours.Add(-6 * time.Hour),
178+
// Start with unset times
179+
deadline: time.Time{},
180+
maxDeadline: time.Time{},
181+
newDeadline: ptr.Ref(nextQuietHours),
124182
newMaxDeadline: nextQuietHours,
125183
},
126184
}
@@ -131,6 +189,11 @@ func TestTemplateUpdateBuildDeadlines(t *testing.T) {
131189
t.Run(c.name, func(t *testing.T) {
132190
t.Parallel()
133191

192+
user := quietUser
193+
if c.noQuietHours {
194+
user = noQuietUser
195+
}
196+
134197
t.Log("buildTime", buildTime)
135198
t.Log("nextQuietHours", nextQuietHours)
136199
t.Log("now", c.now)
@@ -217,17 +280,22 @@ func TestTemplateUpdateBuildDeadlines(t *testing.T) {
217280
templateScheduleStore.TimeNowFn = func() time.Time {
218281
return c.now
219282
}
283+
284+
autostopReq := agplschedule.TemplateAutostopRequirement{
285+
// Every day
286+
DaysOfWeek: 0b01111111,
287+
Weeks: 0,
288+
}
289+
if c.autostopReq != nil {
290+
autostopReq = *c.autostopReq
291+
}
220292
_, err = templateScheduleStore.Set(ctx, db, template, agplschedule.TemplateScheduleOptions{
221-
UserAutostartEnabled: false,
222-
UserAutostopEnabled: false,
223-
DefaultTTL: 0,
224-
MaxTTL: 0,
225-
UseMaxTTL: false,
226-
AutostopRequirement: agplschedule.TemplateAutostopRequirement{
227-
// Every day
228-
DaysOfWeek: 0b01111111,
229-
Weeks: 0,
230-
},
293+
UserAutostartEnabled: false,
294+
UserAutostopEnabled: false,
295+
DefaultTTL: 0,
296+
MaxTTL: 0,
297+
UseMaxTTL: false,
298+
AutostopRequirement: autostopReq,
231299
FailureTTL: 0,
232300
TimeTilDormant: 0,
233301
TimeTilDormantAutoDelete: 0,
@@ -238,10 +306,10 @@ func TestTemplateUpdateBuildDeadlines(t *testing.T) {
238306
newBuild, err := db.GetWorkspaceBuildByID(ctx, wsBuild.ID)
239307
require.NoError(t, err)
240308

241-
if c.newDeadline.IsZero() {
242-
c.newDeadline = wsBuild.Deadline
309+
if c.newDeadline == nil {
310+
c.newDeadline = &wsBuild.Deadline
243311
}
244-
require.WithinDuration(t, c.newDeadline, newBuild.Deadline, time.Second)
312+
require.WithinDuration(t, *c.newDeadline, newBuild.Deadline, time.Second)
245313
require.WithinDuration(t, c.newMaxDeadline, newBuild.MaxDeadline, time.Second)
246314

247315
// Check that the new build has the same state as before.

0 commit comments

Comments
 (0)