Skip to content

Commit 6e41cd1

Browse files
authored
feat: add activity bumping to template scheduling (#9040)
1 parent 6214117 commit 6e41cd1

File tree

30 files changed

+669
-190
lines changed

30 files changed

+669
-190
lines changed

coderd/database/dbauthz/dbauthz.go

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2385,6 +2385,14 @@ func (q *querier) UpdateTemplateVersionGitAuthProvidersByJobID(ctx context.Conte
23852385
return q.db.UpdateTemplateVersionGitAuthProvidersByJobID(ctx, arg)
23862386
}
23872387

2388+
func (q *querier) UpdateTemplateWorkspacesLastUsedAt(ctx context.Context, arg database.UpdateTemplateWorkspacesLastUsedAtParams) error {
2389+
fetch := func(ctx context.Context, arg database.UpdateTemplateWorkspacesLastUsedAtParams) (database.Template, error) {
2390+
return q.db.GetTemplateByID(ctx, arg.TemplateID)
2391+
}
2392+
2393+
return fetchAndExec(q.log, q.auth, rbac.ActionUpdate, fetch, q.db.UpdateTemplateWorkspacesLastUsedAt)(ctx, arg)
2394+
}
2395+
23882396
// UpdateUserDeletedByID
23892397
// Deprecated: Delete this function in favor of 'SoftDeleteUserByID'. Deletes are
23902398
// irreversible.
@@ -2663,12 +2671,12 @@ func (q *querier) UpdateWorkspaceTTL(ctx context.Context, arg database.UpdateWor
26632671
return update(q.log, q.auth, fetch, q.db.UpdateWorkspaceTTL)(ctx, arg)
26642672
}
26652673

2666-
func (q *querier) UpdateWorkspacesDeletingAtByTemplateID(ctx context.Context, arg database.UpdateWorkspacesDeletingAtByTemplateIDParams) error {
2667-
fetch := func(ctx context.Context, arg database.UpdateWorkspacesDeletingAtByTemplateIDParams) (database.Template, error) {
2674+
func (q *querier) UpdateWorkspacesLockedDeletingAtByTemplateID(ctx context.Context, arg database.UpdateWorkspacesLockedDeletingAtByTemplateIDParams) error {
2675+
fetch := func(ctx context.Context, arg database.UpdateWorkspacesLockedDeletingAtByTemplateIDParams) (database.Template, error) {
26682676
return q.db.GetTemplateByID(ctx, arg.TemplateID)
26692677
}
26702678

2671-
return fetchAndExec(q.log, q.auth, rbac.ActionUpdate, fetch, q.db.UpdateWorkspacesDeletingAtByTemplateID)(ctx, arg)
2679+
return fetchAndExec(q.log, q.auth, rbac.ActionUpdate, fetch, q.db.UpdateWorkspacesLockedDeletingAtByTemplateID)(ctx, arg)
26722680
}
26732681

26742682
func (q *querier) UpsertAppSecurityKey(ctx context.Context, data string) error {

coderd/database/dbfake/dbfake.go

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5199,6 +5199,26 @@ func (q *FakeQuerier) UpdateTemplateVersionGitAuthProvidersByJobID(_ context.Con
51995199
return sql.ErrNoRows
52005200
}
52015201

5202+
func (q *FakeQuerier) UpdateTemplateWorkspacesLastUsedAt(_ context.Context, arg database.UpdateTemplateWorkspacesLastUsedAtParams) error {
5203+
err := validateDatabaseType(arg)
5204+
if err != nil {
5205+
return err
5206+
}
5207+
5208+
q.mutex.Lock()
5209+
defer q.mutex.Unlock()
5210+
5211+
for i, ws := range q.workspaces {
5212+
if ws.TemplateID != arg.TemplateID {
5213+
continue
5214+
}
5215+
ws.LastUsedAt = arg.LastUsedAt
5216+
q.workspaces[i] = ws
5217+
}
5218+
5219+
return nil
5220+
}
5221+
52025222
func (q *FakeQuerier) UpdateUserDeletedByID(_ context.Context, params database.UpdateUserDeletedByIDParams) error {
52035223
if err := validateDatabaseType(params); err != nil {
52045224
return err
@@ -5796,7 +5816,7 @@ func (q *FakeQuerier) UpdateWorkspaceTTL(_ context.Context, arg database.UpdateW
57965816
return sql.ErrNoRows
57975817
}
57985818

5799-
func (q *FakeQuerier) UpdateWorkspacesDeletingAtByTemplateID(_ context.Context, arg database.UpdateWorkspacesDeletingAtByTemplateIDParams) error {
5819+
func (q *FakeQuerier) UpdateWorkspacesLockedDeletingAtByTemplateID(_ context.Context, arg database.UpdateWorkspacesLockedDeletingAtByTemplateIDParams) error {
58005820
q.mutex.Lock()
58015821
defer q.mutex.Unlock()
58025822

@@ -5806,9 +5826,21 @@ func (q *FakeQuerier) UpdateWorkspacesDeletingAtByTemplateID(_ context.Context,
58065826
}
58075827

58085828
for i, ws := range q.workspaces {
5829+
if ws.TemplateID != arg.TemplateID {
5830+
continue
5831+
}
5832+
58095833
if ws.LockedAt.Time.IsZero() {
58105834
continue
58115835
}
5836+
5837+
if !arg.LockedAt.IsZero() {
5838+
ws.LockedAt = sql.NullTime{
5839+
Valid: true,
5840+
Time: arg.LockedAt,
5841+
}
5842+
}
5843+
58125844
deletingAt := sql.NullTime{
58135845
Valid: arg.LockedTtlMs > 0,
58145846
}

coderd/database/dbmetrics/dbmetrics.go

Lines changed: 10 additions & 3 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: 20 additions & 6 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

coderd/database/querier.go

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

coderd/database/queries.sql.go

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

coderd/database/queries/workspaces.sql

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -512,12 +512,23 @@ AND
512512
workspaces.id = $1
513513
RETURNING workspaces.*;
514514

515-
-- name: UpdateWorkspacesDeletingAtByTemplateID :exec
516-
UPDATE
517-
workspaces
515+
-- name: UpdateWorkspacesLockedDeletingAtByTemplateID :exec
516+
UPDATE workspaces
518517
SET
519-
deleting_at = CASE WHEN @locked_ttl_ms::bigint = 0 THEN NULL ELSE locked_at + interval '1 milliseconds' * @locked_ttl_ms::bigint END
518+
deleting_at = CASE
519+
WHEN @locked_ttl_ms::bigint = 0 THEN NULL
520+
WHEN @locked_at::timestamptz > '0001-01-01 00:00:00+00'::timestamptz THEN (@locked_at::timestamptz) + interval '1 milliseconds' * @locked_ttl_ms::bigint
521+
ELSE locked_at + interval '1 milliseconds' * @locked_ttl_ms::bigint
522+
END,
523+
locked_at = CASE WHEN @locked_at::timestamptz > '0001-01-01 00:00:00+00'::timestamptz THEN @locked_at::timestamptz ELSE locked_at END
520524
WHERE
521-
template_id = @template_id
525+
template_id = @template_id
522526
AND
523-
locked_at IS NOT NULL;
527+
locked_at IS NOT NULL;
528+
529+
-- name: UpdateTemplateWorkspacesLastUsedAt :exec
530+
UPDATE workspaces
531+
SET
532+
last_used_at = @last_used_at::timestamptz
533+
WHERE
534+
template_id = @template_id;

coderd/schedule/template.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,18 @@ type TemplateScheduleOptions struct {
105105
// LockedTTL dictates the duration after which locked workspaces will be
106106
// permanently deleted.
107107
LockedTTL time.Duration `json:"locked_ttl"`
108+
// UpdateWorkspaceLastUsedAt updates the template's workspaces'
109+
// last_used_at field. This is useful for preventing updates to the
110+
// templates inactivity_ttl immediately triggering a lock action against
111+
// workspaces whose last_used_at field violates the new template
112+
// inactivity_ttl threshold.
113+
UpdateWorkspaceLastUsedAt bool `json:"update_workspace_last_used_at"`
114+
// UpdateWorkspaceLockedAt updates the template's workspaces'
115+
// locked_at field. This is useful for preventing updates to the
116+
// templates locked_ttl immediately triggering a delete action against
117+
// workspaces whose locked_at field violates the new template locked_ttl
118+
// threshold.
119+
UpdateWorkspaceLockedAt bool `json:"update_workspace_locked_at"`
108120
}
109121

110122
// TemplateScheduleStore provides an interface for retrieving template

coderd/templates.go

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -622,9 +622,11 @@ func (api *API) patchTemplateMeta(rw http.ResponseWriter, r *http.Request) {
622622
DaysOfWeek: restartRequirementDaysOfWeekParsed,
623623
Weeks: req.RestartRequirement.Weeks,
624624
},
625-
FailureTTL: failureTTL,
626-
InactivityTTL: inactivityTTL,
627-
LockedTTL: lockedTTL,
625+
FailureTTL: failureTTL,
626+
InactivityTTL: inactivityTTL,
627+
LockedTTL: lockedTTL,
628+
UpdateWorkspaceLastUsedAt: req.UpdateWorkspaceLastUsedAt,
629+
UpdateWorkspaceLockedAt: req.UpdateWorkspaceLockedAt,
628630
})
629631
if err != nil {
630632
return xerrors.Errorf("set template schedule options: %w", err)

codersdk/templates.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,15 @@ type UpdateTemplateMeta struct {
192192
FailureTTLMillis int64 `json:"failure_ttl_ms,omitempty"`
193193
InactivityTTLMillis int64 `json:"inactivity_ttl_ms,omitempty"`
194194
LockedTTLMillis int64 `json:"locked_ttl_ms,omitempty"`
195+
// UpdateWorkspaceLastUsedAt updates the last_used_at field of workspaces
196+
// spawned from the template. This is useful for preventing workspaces being
197+
// immediately locked when updating the inactivity_ttl field to a new, shorter
198+
// value.
199+
UpdateWorkspaceLastUsedAt bool `json:"update_workspace_last_used_at"`
200+
// UpdateWorkspaceLockedAt updates the locked_at field of workspaces spawned
201+
// from the template. This is useful for preventing locked workspaces being immediately
202+
// deleted when updating the locked_ttl field to a new, shorter value.
203+
UpdateWorkspaceLockedAt bool `json:"update_workspace_locked_at"`
195204
}
196205

197206
type TemplateExample struct {

enterprise/coderd/schedule/template.go

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -113,11 +113,11 @@ func (s *EnterpriseTemplateScheduleStore) Set(ctx context.Context, db database.S
113113
}
114114

115115
var template database.Template
116-
err = db.InTx(func(db database.Store) error {
116+
err = db.InTx(func(tx database.Store) error {
117117
ctx, span := tracing.StartSpanWithName(ctx, "(*schedule.EnterpriseTemplateScheduleStore).Set()-InTx()")
118118
defer span.End()
119119

120-
err := db.UpdateTemplateScheduleByID(ctx, database.UpdateTemplateScheduleByIDParams{
120+
err := tx.UpdateTemplateScheduleByID(ctx, database.UpdateTemplateScheduleByIDParams{
121121
ID: tpl.ID,
122122
UpdatedAt: s.now(),
123123
AllowUserAutostart: opts.UserAutostartEnabled,
@@ -134,27 +134,44 @@ func (s *EnterpriseTemplateScheduleStore) Set(ctx context.Context, db database.S
134134
return xerrors.Errorf("update template schedule: %w", err)
135135
}
136136

137+
var lockedAt time.Time
138+
if opts.UpdateWorkspaceLockedAt {
139+
lockedAt = database.Now()
140+
}
141+
137142
// If we updated the locked_ttl we need to update all the workspaces deleting_at
138143
// to ensure workspaces are being cleaned up correctly. Similarly if we are
139144
// disabling it (by passing 0), then we want to delete nullify the deleting_at
140145
// fields of all the template workspaces.
141-
err = db.UpdateWorkspacesDeletingAtByTemplateID(ctx, database.UpdateWorkspacesDeletingAtByTemplateIDParams{
146+
err = tx.UpdateWorkspacesLockedDeletingAtByTemplateID(ctx, database.UpdateWorkspacesLockedDeletingAtByTemplateIDParams{
142147
TemplateID: tpl.ID,
143148
LockedTtlMs: opts.LockedTTL.Milliseconds(),
149+
LockedAt: lockedAt,
144150
})
145151
if err != nil {
146152
return xerrors.Errorf("update deleting_at of all workspaces for new locked_ttl %q: %w", opts.LockedTTL, err)
147153
}
148154

149-
template, err = db.GetTemplateByID(ctx, tpl.ID)
155+
if opts.UpdateWorkspaceLastUsedAt {
156+
err = tx.UpdateTemplateWorkspacesLastUsedAt(ctx, database.UpdateTemplateWorkspacesLastUsedAtParams{
157+
TemplateID: tpl.ID,
158+
LastUsedAt: database.Now(),
159+
})
160+
if err != nil {
161+
return xerrors.Errorf("update template workspaces last_used_at: %w", err)
162+
}
163+
}
164+
165+
// TODO: update all workspace max_deadlines to be within new bounds
166+
template, err = tx.GetTemplateByID(ctx, tpl.ID)
150167
if err != nil {
151168
return xerrors.Errorf("get updated template schedule: %w", err)
152169
}
153170

154171
// Recalculate max_deadline and deadline for all running workspace
155172
// builds on this template.
156173
if s.UseRestartRequirement.Load() {
157-
err = s.updateWorkspaceBuilds(ctx, db, template)
174+
err = s.updateWorkspaceBuilds(ctx, tx, template)
158175
if err != nil {
159176
return xerrors.Errorf("update workspace builds: %w", err)
160177
}

0 commit comments

Comments
 (0)