-
Notifications
You must be signed in to change notification settings - Fork 887
chore: cover deadline crossing autostart border #13115
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
package schedule | ||
|
||
import ( | ||
"time" | ||
|
||
"github.com/coder/coder/v2/coderd/schedule/cron" | ||
) | ||
|
||
// NextAutostart takes the workspace and template schedule and returns the next autostart schedule | ||
// after "at". The boolean returned is if the autostart should be allowed to start based on the template | ||
// schedule. | ||
func NextAutostart(at time.Time, wsSchedule string, templateSchedule TemplateScheduleOptions) (time.Time, bool) { | ||
sched, err := cron.Weekly(wsSchedule) | ||
if err != nil { | ||
return time.Time{}, false | ||
} | ||
|
||
// Round down to the nearest minute, as this is the finest granularity cron supports. | ||
// Truncate is probably not necessary here, but doing it anyway to be sure. | ||
nextTransition := sched.Next(at).Truncate(time.Minute) | ||
|
||
// The nextTransition is when the auto start should kick off. If it lands on a | ||
// forbidden day, do not allow the auto start. We use the time location of the | ||
// schedule to determine the weekday. So if "Saturday" is disallowed, the | ||
// definition of "Saturday" depends on the location of the schedule. | ||
zonedTransition := nextTransition.In(sched.Location()) | ||
allowed := templateSchedule.AutostartRequirement.DaysMap()[zonedTransition.Weekday()] | ||
|
||
return zonedTransition, allowed | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -25,6 +25,12 @@ func TestCalculateAutoStop(t *testing.T) { | |
|
||
now := time.Now() | ||
|
||
chicago, err := time.LoadLocation("America/Chicago") | ||
require.NoError(t, err, "loading chicago time location") | ||
|
||
// pastDateNight is 9:45pm on a wednesday | ||
pastDateNight := time.Date(2024, 2, 14, 21, 45, 0, 0, chicago) | ||
|
||
// Wednesday the 8th of February 2023 at midnight. This date was | ||
// specifically chosen as it doesn't fall on a applicable week for both | ||
// fortnightly and triweekly autostop requirements. | ||
|
@@ -70,8 +76,12 @@ func TestCalculateAutoStop(t *testing.T) { | |
t.Log("saturdayMidnightAfterDstOut", saturdayMidnightAfterDstOut) | ||
|
||
cases := []struct { | ||
name string | ||
now time.Time | ||
name string | ||
now time.Time | ||
|
||
wsAutostart string | ||
templateAutoStart schedule.TemplateAutostartRequirement | ||
|
||
templateAllowAutostop bool | ||
templateDefaultTTL time.Duration | ||
templateAutostopRequirement schedule.TemplateAutostopRequirement | ||
|
@@ -364,6 +374,115 @@ func TestCalculateAutoStop(t *testing.T) { | |
// expectedDeadline is copied from expectedMaxDeadline. | ||
expectedMaxDeadline: dstOutQuietHoursExpectedTime, | ||
}, | ||
{ | ||
// A user expects this workspace to be online from 9am -> 9pm. | ||
// So if a deadline is going to land in the middle of this range, | ||
// we should bump it to the end. | ||
// This is already done on `ActivityBumpWorkspace`, but that requires | ||
// activity on the workspace. | ||
name: "AutostopCrossAutostartBorder", | ||
// Starting at 9:45pm, with the autostart at 9am. | ||
now: pastDateNight, | ||
templateAllowAutostop: false, | ||
templateDefaultTTL: time.Hour * 12, | ||
workspaceTTL: time.Hour * 12, | ||
// At 9am every morning | ||
wsAutostart: "CRON_TZ=America/Chicago 0 9 * * *", | ||
|
||
// No quiet hours | ||
templateAutoStart: schedule.TemplateAutostartRequirement{ | ||
// Just allow all days of the week | ||
DaysOfWeek: 0b01111111, | ||
}, | ||
templateAutostopRequirement: schedule.TemplateAutostopRequirement{}, | ||
userQuietHoursSchedule: "", | ||
|
||
expectedDeadline: time.Date(pastDateNight.Year(), pastDateNight.Month(), pastDateNight.Day()+1, 21, 0, 0, 0, chicago), | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. could you use There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
It was easier for me to reason because the offset being 23.25 hours is not really the assertion I am making. I want to assert it's 12hrs after autostart. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fair! |
||
expectedMaxDeadline: time.Time{}, | ||
errContains: "", | ||
}, | ||
{ | ||
// Same as AutostopCrossAutostartBorder, but just misses the autostart. | ||
name: "AutostopCrossMissAutostartBorder", | ||
// Starting at 8:45pm, with the autostart at 9am. | ||
now: time.Date(pastDateNight.Year(), pastDateNight.Month(), pastDateNight.Day(), 20, 30, 0, 0, chicago), | ||
templateAllowAutostop: false, | ||
templateDefaultTTL: time.Hour * 12, | ||
workspaceTTL: time.Hour * 12, | ||
// At 9am every morning | ||
wsAutostart: "CRON_TZ=America/Chicago 0 9 * * *", | ||
|
||
// No quiet hours | ||
templateAutoStart: schedule.TemplateAutostartRequirement{ | ||
// Just allow all days of the week | ||
DaysOfWeek: 0b01111111, | ||
}, | ||
templateAutostopRequirement: schedule.TemplateAutostopRequirement{}, | ||
userQuietHoursSchedule: "", | ||
|
||
expectedDeadline: time.Date(pastDateNight.Year(), pastDateNight.Month(), pastDateNight.Day()+1, 8, 30, 0, 0, chicago), | ||
expectedMaxDeadline: time.Time{}, | ||
errContains: "", | ||
}, | ||
{ | ||
// Same as AutostopCrossAutostartBorderMaxEarlyDeadline with max deadline to limit it. | ||
// The autostop deadline is before the autostart threshold. | ||
name: "AutostopCrossAutostartBorderMaxEarlyDeadline", | ||
// Starting at 9:45pm, with the autostart at 9am. | ||
now: pastDateNight, | ||
templateAllowAutostop: false, | ||
templateDefaultTTL: time.Hour * 12, | ||
workspaceTTL: time.Hour * 12, | ||
// At 9am every morning | ||
wsAutostart: "CRON_TZ=America/Chicago 0 9 * * *", | ||
|
||
// No quiet hours | ||
templateAutoStart: schedule.TemplateAutostartRequirement{ | ||
// Just allow all days of the week | ||
DaysOfWeek: 0b01111111, | ||
}, | ||
templateAutostopRequirement: schedule.TemplateAutostopRequirement{ | ||
// Autostop every day | ||
DaysOfWeek: 0b01111111, | ||
Weeks: 0, | ||
}, | ||
// 6am quiet hours | ||
userQuietHoursSchedule: "CRON_TZ=America/Chicago 0 6 * * *", | ||
|
||
expectedDeadline: time.Date(pastDateNight.Year(), pastDateNight.Month(), pastDateNight.Day()+1, 6, 0, 0, 0, chicago), | ||
expectedMaxDeadline: time.Date(pastDateNight.Year(), pastDateNight.Month(), pastDateNight.Day()+1, 6, 0, 0, 0, chicago), | ||
errContains: "", | ||
}, | ||
{ | ||
// Same as AutostopCrossAutostartBorder with max deadline to limit it. | ||
// The autostop deadline is after autostart threshold. | ||
// So the deadline is > 12 hours, but stops at the max deadline. | ||
name: "AutostopCrossAutostartBorderMaxDeadline", | ||
// Starting at 9:45pm, with the autostart at 9am. | ||
now: pastDateNight, | ||
templateAllowAutostop: false, | ||
templateDefaultTTL: time.Hour * 12, | ||
workspaceTTL: time.Hour * 12, | ||
// At 9am every morning | ||
wsAutostart: "CRON_TZ=America/Chicago 0 9 * * *", | ||
|
||
// No quiet hours | ||
templateAutoStart: schedule.TemplateAutostartRequirement{ | ||
// Just allow all days of the week | ||
DaysOfWeek: 0b01111111, | ||
}, | ||
templateAutostopRequirement: schedule.TemplateAutostopRequirement{ | ||
// Autostop every day | ||
DaysOfWeek: 0b01111111, | ||
Weeks: 0, | ||
}, | ||
// 11am quiet hours, yea this is werid case. | ||
userQuietHoursSchedule: "CRON_TZ=America/Chicago 0 11 * * *", | ||
|
||
expectedDeadline: time.Date(pastDateNight.Year(), pastDateNight.Month(), pastDateNight.Day()+1, 11, 0, 0, 0, chicago), | ||
expectedMaxDeadline: time.Date(pastDateNight.Year(), pastDateNight.Month(), pastDateNight.Day()+1, 11, 0, 0, 0, chicago), | ||
errContains: "", | ||
}, | ||
} | ||
|
||
for _, c := range cases { | ||
|
@@ -382,6 +501,7 @@ func TestCalculateAutoStop(t *testing.T) { | |
UserAutostopEnabled: c.templateAllowAutostop, | ||
DefaultTTL: c.templateDefaultTTL, | ||
AutostopRequirement: c.templateAutostopRequirement, | ||
AutostartRequirement: c.templateAutoStart, | ||
}, nil | ||
}, | ||
} | ||
|
@@ -433,11 +553,20 @@ func TestCalculateAutoStop(t *testing.T) { | |
Valid: true, | ||
} | ||
} | ||
|
||
autostart := sql.NullString{} | ||
if c.wsAutostart != "" { | ||
autostart = sql.NullString{ | ||
String: c.wsAutostart, | ||
Valid: true, | ||
} | ||
} | ||
workspace := dbgen.Workspace(t, db, database.Workspace{ | ||
TemplateID: template.ID, | ||
OrganizationID: org.ID, | ||
OwnerID: user.ID, | ||
Ttl: workspaceTTL, | ||
TemplateID: template.ID, | ||
OrganizationID: org.ID, | ||
OwnerID: user.ID, | ||
Ttl: workspaceTTL, | ||
AutostartSchedule: autostart, | ||
}) | ||
|
||
autostop, err := schedule.CalculateAutostop(ctx, schedule.CalculateAutostopParams{ | ||
|
@@ -446,6 +575,7 @@ func TestCalculateAutoStop(t *testing.T) { | |
UserQuietHoursScheduleStore: userQuietHoursScheduleStore, | ||
Now: c.now, | ||
Workspace: workspace, | ||
WorkspaceAutostart: c.wsAutostart, | ||
}) | ||
if c.errContains != "" { | ||
require.Error(t, err) | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we need to handle the
min(deadline, max_deadline)
here too?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No, that is handled down below.
coder/coderd/schedule/autostop.go
Line 238 in 94872aa
I'd rather not duplicate that logic since it also handles the case where MaxDeadline is not set.