Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
22b9be8
feat: add user maintenance schedule for max_ttl autostop
deansheather Jun 20, 2023
92c05b3
fixup! feat: add user maintenance schedule for max_ttl autostop
deansheather Jun 20, 2023
54d939a
fixup! feat: add user maintenance schedule for max_ttl autostop
deansheather Jun 21, 2023
b274c67
rename maintenance schedule to quiet hours schedule
deansheather Jun 28, 2023
5bf53eb
Merge branch 'main' into dean/user-maintenance-window
deansheather Jun 28, 2023
ede278e
stuff
deansheather Jun 28, 2023
06272e2
progress
deansheather Jun 28, 2023
a1ebbdb
progress
deansheather Jul 6, 2023
780812c
working
deansheather Jul 6, 2023
00e4a0f
tests mostly fixed
deansheather Jul 7, 2023
6cfd270
working!
deansheather Jul 7, 2023
53f5d62
move autostop algorithm to schedule package
deansheather Jul 10, 2023
eb1c1f6
more tests
deansheather Jul 10, 2023
3dbd077
Merge branch 'main' into dean/user-maintenance-window
deansheather Jul 10, 2023
0e9437e
Merge branch 'main' into dean/user-maintenance-window
deansheather Jul 12, 2023
fd26e69
add back max_ttl and put restart_requirement behind feature flag
deansheather Jul 12, 2023
cb9428e
fixup! add back max_ttl and put restart_requirement behind feature flag
deansheather Jul 12, 2023
024233a
fixup! add back max_ttl and put restart_requirement behind feature flag
deansheather Jul 12, 2023
c7ef9cb
fixup! add back max_ttl and put restart_requirement behind feature flag
deansheather Jul 12, 2023
4c70ade
fixup! add back max_ttl and put restart_requirement behind feature flag
deansheather Jul 13, 2023
2fb3053
Merge branch 'main' into dean/user-maintenance-window
deansheather Jul 13, 2023
554e837
Disable quiet hours endpoint if not entitled
deansheather Jul 13, 2023
eb46ae2
add DST and week calculation tests
deansheather Jul 13, 2023
6af3e33
fixup! add DST and week calculation tests
deansheather Jul 13, 2023
159d107
Merge branch 'main' into dean/user-maintenance-window
deansheather Jul 16, 2023
96f5e2e
rename interface methods, merge migrations
deansheather Jul 16, 2023
cd77138
steven comments and test fix
deansheather Jul 16, 2023
87b065b
fixup! steven comments and test fix
deansheather Jul 17, 2023
8658112
remove duration
deansheather Jul 19, 2023
fe26e3f
Merge branch 'main' into dean/user-maintenance-window
deansheather Jul 19, 2023
8cebc67
Merge branch 'main' into dean/user-maintenance-window
deansheather Jul 20, 2023
85142a6
fixup! Merge branch 'main' into dean/user-maintenance-window
deansheather Jul 20, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
progress
  • Loading branch information
deansheather committed Jun 28, 2023
commit 06272e26fc419a20d8ef682bbba17cd560d89d67
7 changes: 0 additions & 7 deletions coderd/database/dbauthz/dbauthz.go
Original file line number Diff line number Diff line change
Expand Up @@ -2558,13 +2558,6 @@ func (q *querier) UpdateWorkspaceTTL(ctx context.Context, arg database.UpdateWor
return update(q.log, q.auth, fetch, q.db.UpdateWorkspaceTTL)(ctx, arg)
}

func (q *querier) UpdateWorkspaceTTLToBeWithinTemplateMax(ctx context.Context, arg database.UpdateWorkspaceTTLToBeWithinTemplateMaxParams) error {
fetch := func(ctx context.Context, arg database.UpdateWorkspaceTTLToBeWithinTemplateMaxParams) (database.Template, error) {
return q.db.GetTemplateByID(ctx, arg.TemplateID)
}
return fetchAndExec(q.log, q.auth, rbac.ActionUpdate, fetch, q.db.UpdateWorkspaceTTLToBeWithinTemplateMax)(ctx, arg)
}

func (q *querier) UpsertAppSecurityKey(ctx context.Context, data string) error {
// No authz checks as this is done during startup
return q.db.UpsertAppSecurityKey(ctx, data)
Expand Down
20 changes: 0 additions & 20 deletions coderd/database/dbfake/dbfake.go
Original file line number Diff line number Diff line change
Expand Up @@ -5276,26 +5276,6 @@ func (q *fakeQuerier) UpdateWorkspaceTTL(_ context.Context, arg database.UpdateW
return sql.ErrNoRows
}

func (q *fakeQuerier) UpdateWorkspaceTTLToBeWithinTemplateMax(_ context.Context, arg database.UpdateWorkspaceTTLToBeWithinTemplateMaxParams) error {
if err := validateDatabaseType(arg); err != nil {
return err
}

q.mutex.Lock()
defer q.mutex.Unlock()

for index, workspace := range q.workspaces {
if workspace.TemplateID != arg.TemplateID || !workspace.Ttl.Valid || workspace.Ttl.Int64 < arg.TemplateMaxTTL {
continue
}

workspace.Ttl = sql.NullInt64{Int64: arg.TemplateMaxTTL, Valid: true}
q.workspaces[index] = workspace
}

return nil
}

func (q *fakeQuerier) UpsertAppSecurityKey(_ context.Context, data string) error {
q.mutex.Lock()
defer q.mutex.Unlock()
Expand Down
7 changes: 0 additions & 7 deletions coderd/database/dbmetrics/dbmetrics.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

14 changes: 0 additions & 14 deletions coderd/database/dbmock/dbmock.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 0 additions & 1 deletion coderd/database/querier.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

25 changes: 0 additions & 25 deletions coderd/database/queries.sql.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

14 changes: 0 additions & 14 deletions coderd/database/queries/workspaces.sql
Original file line number Diff line number Diff line change
Expand Up @@ -329,20 +329,6 @@ SET
WHERE
id = $1;

-- name: UpdateWorkspaceTTLToBeWithinTemplateMax :exec
UPDATE
workspaces
SET
ttl = LEAST(ttl, @template_max_ttl::bigint)
WHERE
template_id = @template_id
-- LEAST() does not pick NULL, so filter it out as we don't want to set a
-- TTL on the workspace if it's unset.
--
-- During build time, the template max TTL will still be used if the
-- workspace TTL is NULL.
AND ttl IS NOT NULL;

-- name: GetDeploymentWorkspaceStats :one
WITH workspaces_with_jobs AS (
SELECT
Expand Down
87 changes: 57 additions & 30 deletions coderd/provisionerdserver/provisionerdserver.go
Original file line number Diff line number Diff line change
Expand Up @@ -927,41 +927,68 @@ func (server *Server) CompleteJob(ctx context.Context, completed *proto.Complete
deadline = now.Add(templateSchedule.DefaultTTL)
}
}
if templateSchedule.MaxTTL > 0 {
maxDeadline = now.Add(templateSchedule.MaxTTL)
if templateSchedule.RestartRequirement.DaysOfWeek != 0 {
// The template has a restart requirement, so determine the max
// deadline of this workspace build.

if deadline.IsZero() || maxDeadline.Before(deadline) {
// If the workspace doesn't have a deadline or the max
// deadline is sooner than the workspace deadline, use the
// max deadline as the actual deadline.
// First, get the user's quiet hours schedule (this will return
// the default if the user has not set their own schedule).
userQuietHoursSchedule, err := (*server.UserQuietHoursScheduleStore.Load()).GetUserQuietHoursScheduleOptions(ctx, db, workspace.OwnerID)
if err != nil {
return xerrors.Errorf("get user quiet hours schedule options: %w", err)
}

// If the schedule is nil, that means the deployment isn't
// entitled to use quiet hours or the default schedule has not
// been set. In this case, do not set a max deadline on the
// workspace.
if userQuietHoursSchedule.Schedule != nil {
loc := userQuietHoursSchedule.Schedule.Location()
now := time.Now().In(loc)
startOfDay := now.Truncate(24 * time.Hour)

// First, determine if we can restart today or if the
// schedule is too near/already passed.
//
// Notably, this isn't affected by the user's quiet hours
// schedule below because we'd still like to use the max TTL
// as the TTL for the workspace if it's not set.
deadline = maxDeadline
// Allow an hour of leeway (i.e. any workspaces started
// within an hour of the scheduled stop time will always
// bounce to the next stop window).
todaySchedule := userQuietHoursSchedule.Schedule.Next(startOfDay)
if todaySchedule.Before(now.Add(-time.Hour)) {
// Set the first stop day we try to tomorrow because
// today's schedule is too close to now or has already
// passed.
startOfDay = startOfDay.Add(24 * time.Hour)
}

// Get the current day of week and iterate through the days
// of week until we wrap or find a day present in the
// restart requirement.
requirementDays := templateSchedule.RestartRequirement.DaysMap()
for i := 0; i < len(schedule.DaysOfWeek)+1; i++ {
if i == len(schedule.DaysOfWeek) {
// We've wrapped, so somehow we couldn't find a day
// in the restart requirement. This shouldn't happen
// because the restart requirement has a day set.
return xerrors.Errorf("could not find suitable day for template restart requirement in the next 7 days")
}
if requirementDays[startOfDay.Weekday()] {
break
}
startOfDay = startOfDay.Add(24 * time.Hour)
}

// Get the next occurrence of the restart schedule after
// the start of today.
maxDeadline = userQuietHoursSchedule.Schedule.Next(startOfDay)
}
}

userQuietHoursSchedule, err := (*server.UserQuietHoursScheduleStore.Load()).GetUserQuietHoursScheduleOptions(ctx, db, workspace.OwnerID)
if err != nil {
return xerrors.Errorf("get user quiet hours schedule options: %w", err)
}
if userQuietHoursSchedule.Schedule != nil {
// Round the max deadline up to the nearest occurrence of the
// user's quiet hours schedule. This ensures that workspaces
// can't be force-stopped due to max TTL during business hours.

// Get the schedule occurrence that happens right before, during
// or after the max deadline.
// TODO: change to the quiet hours window BEFORE max TTL
scheduleDur := userQuietHoursSchedule.Duration
if scheduleDur > 1*time.Hour {
// Allow a 15 minute buffer when possible so we're not too
// constrained with the autostop time.
scheduleDur -= 15 * time.Minute
// If the workspace doesn't have a deadline or the max deadline
// is sooner than the workspace deadline, use the max deadline
// as the actual deadline.
if deadline.IsZero() || maxDeadline.Before(deadline) {
deadline = maxDeadline
}
windowStart := userQuietHoursSchedule.Schedule.Next(maxDeadline.Add(scheduleDur))
maxDeadline = windowStart
}

err = db.UpdateProvisionerJobWithCompleteByID(ctx, database.UpdateProvisionerJobWithCompleteByIDParams{
Expand Down
6 changes: 3 additions & 3 deletions coderd/provisionerdserver/provisionerdserver_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ func testTemplateScheduleStore() *atomic.Pointer[schedule.TemplateScheduleStore]
return ptr
}

func testUserMaintenanceScheduleStore() *atomic.Pointer[schedule.UserQuietHoursScheduleStore] {
func testUserQuietHoursScheduleStore() *atomic.Pointer[schedule.UserQuietHoursScheduleStore] {
ptr := &atomic.Pointer[schedule.UserQuietHoursScheduleStore]{}
store := schedule.NewAGPLUserQuietHoursScheduleStore()
ptr.Store(&store)
Expand All @@ -71,7 +71,7 @@ func TestAcquireJob(t *testing.T) {
AcquireJobDebounce: time.Hour,
Auditor: mockAuditor(),
TemplateScheduleStore: testTemplateScheduleStore(),
UserQuietHoursScheduleStore: testUserMaintenanceScheduleStore(),
UserQuietHoursScheduleStore: testUserQuietHoursScheduleStore(),
Tracer: trace.NewNoopTracerProvider().Tracer("noop"),
DeploymentValues: &codersdk.DeploymentValues{},
}
Expand Down Expand Up @@ -1278,7 +1278,7 @@ func setup(t *testing.T, ignoreLogErrors bool) *provisionerdserver.Server {
Telemetry: telemetry.NewNoop(),
Auditor: mockAuditor(),
TemplateScheduleStore: testTemplateScheduleStore(),
UserQuietHoursScheduleStore: testUserMaintenanceScheduleStore(),
UserQuietHoursScheduleStore: testUserQuietHoursScheduleStore(),
Tracer: trace.NewNoopTracerProvider().Tracer("noop"),
DeploymentValues: &codersdk.DeploymentValues{},
}
Expand Down
58 changes: 50 additions & 8 deletions coderd/schedule/template.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,54 @@ import (
"github.com/coder/coder/coderd/database"
)

var DaysOfWeek = []time.Weekday{
time.Sunday,
time.Monday,
time.Tuesday,
time.Wednesday,
time.Thursday,
time.Friday,
time.Saturday,
}

type TemplateRestartRequirement struct {
// DaysOfWeek is a bitmap of which days of the week the workspace must be
// restarted. If fully zero, the workspace is not required to be restarted
// ever.
//
// First bit is Sunday, second bit is Monday, ..., seventh bit is Saturday,
// eighth bit is unused.
DaysOfWeek uint8
}

// Days returns the days of the week that the workspace must be restarted.
func (r TemplateRestartRequirement) Days() []time.Weekday {
days := make([]time.Weekday, 0, 7)
for i, day := range DaysOfWeek {
if r.DaysOfWeek&(1<<uint(i)) != 0 {
days = append(days, day)
}
}
return days
}

// DaysMap returns a map of the days of the week that the workspace must be
// restarted.
func (r TemplateRestartRequirement) DaysMap() map[time.Weekday]bool {
days := make(map[time.Weekday]bool)
for i, day := range DaysOfWeek {
days[day] = r.DaysOfWeek&(1<<uint(i)) != 0
}
return days
}

type TemplateScheduleOptions struct {
UserAutostartEnabled bool `json:"user_autostart_enabled"`
UserAutostopEnabled bool `json:"user_autostop_enabled"`
DefaultTTL time.Duration `json:"default_ttl"`
// If MaxTTL is set, the workspace must be stopped before this time or it
// will be stopped automatically.
//
// If set, users cannot disable automatic workspace shutdown.
MaxTTL time.Duration `json:"max_ttl"`
// RestartRequirement dictates when the workspace must be restarted. This
// used to be handled by MaxTTL.
RestartRequirement TemplateRestartRequirement `json:"restart_requirement"`
// FailureTTL dictates the duration after which failed workspaces will be
// stopped automatically.
FailureTTL time.Duration `json:"failure_ttl"`
Expand Down Expand Up @@ -56,9 +95,11 @@ func (*agplTemplateScheduleStore) GetTemplateScheduleOptions(ctx context.Context
UserAutostartEnabled: true,
UserAutostopEnabled: true,
DefaultTTL: time.Duration(tpl.DefaultTTL),
// Disregard the values in the database, since MaxTTL, FailureTTL,
// InactivityTTL, and LockedTTL are enterprise features.
MaxTTL: 0,
// Disregard the values in the database, since RestartRequirement,
// FailureTTL, InactivityTTL, and LockedTTL are enterprise features.
RestartRequirement: TemplateRestartRequirement{
DaysOfWeek: 0,
},
FailureTTL: 0,
InactivityTTL: 0,
LockedTTL: 0,
Expand All @@ -71,6 +112,7 @@ func (*agplTemplateScheduleStore) SetTemplateScheduleOptions(ctx context.Context
return tpl, nil
}

// TODO: fix storage to use new restart requirement
return db.UpdateTemplateScheduleByID(ctx, database.UpdateTemplateScheduleByIDParams{
ID: tpl.ID,
UpdatedAt: database.Now(),
Expand Down
Loading