From 7bd21238d0d9f5659bc62c8430c45e9f0aa54276 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Tue, 14 Nov 2023 13:44:32 -0600 Subject: [PATCH 01/17] feat: workspace activity bump by 1 hour Will bump by ttl if crosses an autostart threshold --- coderd/activitybump.go | 31 ++++++++++++++++++-- coderd/activitybump_internal_test.go | 13 +++++---- coderd/database/dbauthz/dbauthz.go | 6 ++-- coderd/database/dbmem/dbmem.go | 8 ++--- coderd/database/dbmetrics/dbmetrics.go | 2 +- coderd/database/dbmock/dbmock.go | 2 +- coderd/database/querier.go | 2 +- coderd/database/queries.sql.go | 37 ++++++++++++++++++++---- coderd/database/queries/activitybump.sql | 26 +++++++++++++++-- coderd/workspaceagents.go | 2 +- 10 files changed, 100 insertions(+), 29 deletions(-) diff --git a/coderd/activitybump.go b/coderd/activitybump.go index 87e9ede552d2e..a2c68769438ff 100644 --- a/coderd/activitybump.go +++ b/coderd/activitybump.go @@ -12,13 +12,38 @@ import ( ) // activityBumpWorkspace automatically bumps the workspace's auto-off timer -// if it is set to expire soon. -func activityBumpWorkspace(ctx context.Context, log slog.Logger, db database.Store, workspaceID uuid.UUID) { +// if it is set to expire soon. The deadline will be bumped by 1 hour*. +// If the bump crosses over an autostart time, the workspace will be +// bumped by the workspace ttl instead. +// +// Autostart is optional if bumping by 1 hour is sufficient. +// It handles the edge case in the example: +// 1. Autostart is set to 9am. +// 2. User works all day, and leaves a terminal open to the workspace overnight. +// 3. The open terminal continually bumps the workspace deadline. +// 4. 9am the next day, the activity bump pushes to 10am. +// 5. If the user goes inactive for 1 hour during the day, the workspace will +// now stop, because it has been extended by 1 hour durations. Despite the TTL +// being set to 8hrs from the autostart time. +// +// So the issue is that when the workspace is bumped across an autostart +// deadline, we should treat the workspace as being "started" again and +// extend the deadline by the autostart time + workspace ttl instead. +// +// The issue still remains with build_max_deadline. We need to respect the original +// maximum deadline, so that will need to be handled separately. +// A way to avoid this is to configure the max deadline to something that will not +// span more than 1 day. This will force the workspace to restart and reset the deadline +// each morning when it autostarts. +func activityBumpWorkspace(ctx context.Context, log slog.Logger, db database.Store, workspaceID uuid.UUID, nextAutostart time.Time) { // We set a short timeout so if the app is under load, these // low priority operations fail first. ctx, cancel := context.WithTimeout(ctx, time.Second*15) defer cancel() - if err := db.ActivityBumpWorkspace(ctx, workspaceID); err != nil { + if err := db.ActivityBumpWorkspace(ctx, database.ActivityBumpWorkspaceParams{ + NextAutostart: nextAutostart, + WorkspaceID: workspaceID, + }); err != nil { if !xerrors.Is(err, context.Canceled) && !database.IsQueryCanceledError(err) { // Bump will fail if the context is canceled, but this is ok. log.Error(ctx, "bump failed", slog.Error(err), diff --git a/coderd/activitybump_internal_test.go b/coderd/activitybump_internal_test.go index 3e5f7c1848db3..d69287735b9b0 100644 --- a/coderd/activitybump_internal_test.go +++ b/coderd/activitybump_internal_test.go @@ -42,6 +42,7 @@ func Test_ActivityBumpWorkspace(t *testing.T) { templateTTL time.Duration templateDisallowsUserAutostop bool expectedBump time.Duration + nextAutostart time.Time }{ { name: "NotFinishedYet", @@ -72,7 +73,7 @@ func Test_ActivityBumpWorkspace(t *testing.T) { jobCompletedAt: sql.NullTime{Valid: true, Time: dbtime.Now().Add(-24 * time.Minute)}, buildDeadlineOffset: ptr.Ref(8*time.Hour - 24*time.Minute), workspaceTTL: 8 * time.Hour, - expectedBump: 8 * time.Hour, + expectedBump: time.Hour, //8 * time.Hour, }, { name: "MaxDeadline", @@ -81,7 +82,7 @@ func Test_ActivityBumpWorkspace(t *testing.T) { buildDeadlineOffset: ptr.Ref(time.Minute), // last chance to bump! maxDeadlineOffset: ptr.Ref(time.Hour), workspaceTTL: 8 * time.Hour, - expectedBump: 1 * time.Hour, + expectedBump: time.Hour, //1 * time.Hour, }, { // A workspace that is still running, has passed its deadline, but has not @@ -91,7 +92,7 @@ func Test_ActivityBumpWorkspace(t *testing.T) { jobCompletedAt: sql.NullTime{Valid: true, Time: dbtime.Now().Add(-24 * time.Minute)}, buildDeadlineOffset: ptr.Ref(-time.Minute), workspaceTTL: 8 * time.Hour, - expectedBump: 8 * time.Hour, + expectedBump: time.Hour, //8 * time.Hour, }, { // A stopped workspace should never bump. @@ -99,7 +100,7 @@ func Test_ActivityBumpWorkspace(t *testing.T) { transition: database.WorkspaceTransitionStop, jobCompletedAt: sql.NullTime{Valid: true, Time: dbtime.Now().Add(-time.Minute)}, buildDeadlineOffset: ptr.Ref(-time.Minute), - workspaceTTL: 8 * time.Hour, + workspaceTTL: time.Hour, //8 * time.Hour, }, { // A workspace built from a template that disallows user autostop should bump @@ -111,7 +112,7 @@ func Test_ActivityBumpWorkspace(t *testing.T) { workspaceTTL: 6 * time.Hour, templateTTL: 8 * time.Hour, templateDisallowsUserAutostop: true, - expectedBump: 8 * time.Hour, + expectedBump: time.Hour, //8 * time.Hour, }, } { tt := tt @@ -215,7 +216,7 @@ func Test_ActivityBumpWorkspace(t *testing.T) { // Bump duration is measured from the time of the bump, so we measure from here. start := dbtime.Now() - activityBumpWorkspace(ctx, log, db, bld.WorkspaceID) + activityBumpWorkspace(ctx, log, db, bld.WorkspaceID, tt.nextAutostart) end := dbtime.Now() // Validate our state after bump diff --git a/coderd/database/dbauthz/dbauthz.go b/coderd/database/dbauthz/dbauthz.go index d26302c54d091..684f6fc75eb07 100644 --- a/coderd/database/dbauthz/dbauthz.go +++ b/coderd/database/dbauthz/dbauthz.go @@ -660,9 +660,9 @@ func (q *querier) AcquireProvisionerJob(ctx context.Context, arg database.Acquir return q.db.AcquireProvisionerJob(ctx, arg) } -func (q *querier) ActivityBumpWorkspace(ctx context.Context, arg uuid.UUID) error { - fetch := func(ctx context.Context, arg uuid.UUID) (database.Workspace, error) { - return q.db.GetWorkspaceByID(ctx, arg) +func (q *querier) ActivityBumpWorkspace(ctx context.Context, arg database.ActivityBumpWorkspaceParams) error { + fetch := func(ctx context.Context, arg database.ActivityBumpWorkspaceParams) (database.Workspace, error) { + return q.db.GetWorkspaceByID(ctx, arg.WorkspaceID) } return update(q.log, q.auth, fetch, q.db.ActivityBumpWorkspace)(ctx, arg) } diff --git a/coderd/database/dbmem/dbmem.go b/coderd/database/dbmem/dbmem.go index 985b2d4b422ee..e4d9af7680426 100644 --- a/coderd/database/dbmem/dbmem.go +++ b/coderd/database/dbmem/dbmem.go @@ -775,8 +775,8 @@ func (q *FakeQuerier) AcquireProvisionerJob(_ context.Context, arg database.Acqu return database.ProvisionerJob{}, sql.ErrNoRows } -func (q *FakeQuerier) ActivityBumpWorkspace(ctx context.Context, workspaceID uuid.UUID) error { - err := validateDatabaseType(workspaceID) +func (q *FakeQuerier) ActivityBumpWorkspace(ctx context.Context, arg database.ActivityBumpWorkspaceParams) error { + err := validateDatabaseType(arg) if err != nil { return err } @@ -784,11 +784,11 @@ func (q *FakeQuerier) ActivityBumpWorkspace(ctx context.Context, workspaceID uui q.mutex.Lock() defer q.mutex.Unlock() - workspace, err := q.getWorkspaceByIDNoLock(ctx, workspaceID) + workspace, err := q.getWorkspaceByIDNoLock(ctx, arg.WorkspaceID) if err != nil { return err } - latestBuild, err := q.getLatestWorkspaceBuildByWorkspaceIDNoLock(ctx, workspaceID) + latestBuild, err := q.getLatestWorkspaceBuildByWorkspaceIDNoLock(ctx, arg.WorkspaceID) if err != nil { return err } diff --git a/coderd/database/dbmetrics/dbmetrics.go b/coderd/database/dbmetrics/dbmetrics.go index 3d04591938b0d..64a2ba7783af3 100644 --- a/coderd/database/dbmetrics/dbmetrics.go +++ b/coderd/database/dbmetrics/dbmetrics.go @@ -93,7 +93,7 @@ func (m metricsStore) AcquireProvisionerJob(ctx context.Context, arg database.Ac return provisionerJob, err } -func (m metricsStore) ActivityBumpWorkspace(ctx context.Context, arg uuid.UUID) error { +func (m metricsStore) ActivityBumpWorkspace(ctx context.Context, arg database.ActivityBumpWorkspaceParams) error { start := time.Now() r0 := m.s.ActivityBumpWorkspace(ctx, arg) m.queryLatencies.WithLabelValues("ActivityBumpWorkspace").Observe(time.Since(start).Seconds()) diff --git a/coderd/database/dbmock/dbmock.go b/coderd/database/dbmock/dbmock.go index bfa9cebc01a13..76229b3742d46 100644 --- a/coderd/database/dbmock/dbmock.go +++ b/coderd/database/dbmock/dbmock.go @@ -69,7 +69,7 @@ func (mr *MockStoreMockRecorder) AcquireProvisionerJob(arg0, arg1 interface{}) * } // ActivityBumpWorkspace mocks base method. -func (m *MockStore) ActivityBumpWorkspace(arg0 context.Context, arg1 uuid.UUID) error { +func (m *MockStore) ActivityBumpWorkspace(arg0 context.Context, arg1 database.ActivityBumpWorkspaceParams) error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "ActivityBumpWorkspace", arg0, arg1) ret0, _ := ret[0].(error) diff --git a/coderd/database/querier.go b/coderd/database/querier.go index 1332644ac4a06..b46a21a44dee5 100644 --- a/coderd/database/querier.go +++ b/coderd/database/querier.go @@ -31,7 +31,7 @@ type sqlcQuerier interface { // We only bump if the raw interval is positive and non-zero. // We only bump if workspace shutdown is manual. // We only bump when 5% of the deadline has elapsed. - ActivityBumpWorkspace(ctx context.Context, workspaceID uuid.UUID) error + ActivityBumpWorkspace(ctx context.Context, arg ActivityBumpWorkspaceParams) error // AllUserIDs returns all UserIDs regardless of user status or deletion. AllUserIDs(ctx context.Context) ([]uuid.UUID, error) // Archiving templates is a soft delete action, so is reversible. diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index 03aff0ea801b2..b3970f0dda511 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -25,9 +25,29 @@ WITH latest AS ( provisioner_jobs.completed_at::timestamp with time zone AS job_completed_at, ( CASE - WHEN templates.allow_user_autostop - THEN (workspaces.ttl / 1000 / 1000 / 1000 || ' seconds')::interval - ELSE (templates.default_ttl / 1000 / 1000 / 1000 || ' seconds')::interval + -- If the extension would push us over the next_autostart + -- interval, then extend the deadline by the full ttl from + -- the autostart time. This will essentially be as if the + -- workspace auto started at the given time and the original + -- TTL was applied. + WHEN NOW() + ('60 minutes')::interval > $1 :: timestamptz + -- If the autostart is behind the created_at, then the + -- autostart schedule is either the 0 time and not provided, + -- or it was the autostart in the past, which is no longer + -- relevant. If a past autostart is being passed in, + -- that is a mistake by the caller. + AND $1 > workspace_builds.created_at + THEN + -- Extend to the autostart, then add the TTL + (($1 :: timestamptz) - NOW()) + CASE + WHEN templates.allow_user_autostop + THEN (workspaces.ttl / 1000 / 1000 / 1000 || ' seconds')::interval + ELSE (templates.default_ttl / 1000 / 1000 / 1000 || ' seconds')::interval + END + + -- Default to 60 minutes. + ELSE + ('60 minutes')::interval END ) AS ttl_interval FROM workspace_builds @@ -37,7 +57,7 @@ WITH latest AS ( ON workspaces.id = workspace_builds.workspace_id JOIN templates ON templates.id = workspaces.template_id - WHERE workspace_builds.workspace_id = $1::uuid + WHERE workspace_builds.workspace_id = $2::uuid ORDER BY workspace_builds.build_number DESC LIMIT 1 ) @@ -59,6 +79,11 @@ AND l.build_deadline != '0001-01-01 00:00:00+00' AND l.build_deadline - (l.ttl_interval * 0.95) < NOW() ` +type ActivityBumpWorkspaceParams struct { + NextAutostart time.Time `db:"next_autostart" json:"next_autostart"` + WorkspaceID uuid.UUID `db:"workspace_id" json:"workspace_id"` +} + // We bump by the original TTL to prevent counter-intuitive behavior // as the TTL wraps. For example, if I set the TTL to 12 hours, sign off // work at midnight, come back at 10am, I would want another full day @@ -66,8 +91,8 @@ AND l.build_deadline - (l.ttl_interval * 0.95) < NOW() // We only bump if the raw interval is positive and non-zero. // We only bump if workspace shutdown is manual. // We only bump when 5% of the deadline has elapsed. -func (q *sqlQuerier) ActivityBumpWorkspace(ctx context.Context, workspaceID uuid.UUID) error { - _, err := q.db.ExecContext(ctx, activityBumpWorkspace, workspaceID) +func (q *sqlQuerier) ActivityBumpWorkspace(ctx context.Context, arg ActivityBumpWorkspaceParams) error { + _, err := q.db.ExecContext(ctx, activityBumpWorkspace, arg.NextAutostart, arg.WorkspaceID) return err } diff --git a/coderd/database/queries/activitybump.sql b/coderd/database/queries/activitybump.sql index fb9ae456501e1..1ca8d0c30c077 100644 --- a/coderd/database/queries/activitybump.sql +++ b/coderd/database/queries/activitybump.sql @@ -12,9 +12,29 @@ WITH latest AS ( provisioner_jobs.completed_at::timestamp with time zone AS job_completed_at, ( CASE - WHEN templates.allow_user_autostop - THEN (workspaces.ttl / 1000 / 1000 / 1000 || ' seconds')::interval - ELSE (templates.default_ttl / 1000 / 1000 / 1000 || ' seconds')::interval + -- If the extension would push us over the next_autostart + -- interval, then extend the deadline by the full ttl from + -- the autostart time. This will essentially be as if the + -- workspace auto started at the given time and the original + -- TTL was applied. + WHEN NOW() + ('60 minutes')::interval > @next_autostart :: timestamptz + -- If the autostart is behind the created_at, then the + -- autostart schedule is either the 0 time and not provided, + -- or it was the autostart in the past, which is no longer + -- relevant. If a past autostart is being passed in, + -- that is a mistake by the caller. + AND @next_autostart > workspace_builds.created_at + THEN + -- Extend to the autostart, then add the TTL + ((@next_autostart :: timestamptz) - NOW()) + CASE + WHEN templates.allow_user_autostop + THEN (workspaces.ttl / 1000 / 1000 / 1000 || ' seconds')::interval + ELSE (templates.default_ttl / 1000 / 1000 / 1000 || ' seconds')::interval + END + + -- Default to 60 minutes. + ELSE + ('60 minutes')::interval END ) AS ttl_interval FROM workspace_builds diff --git a/coderd/workspaceagents.go b/coderd/workspaceagents.go index ff82923036ac3..d097e55b62edc 100644 --- a/coderd/workspaceagents.go +++ b/coderd/workspaceagents.go @@ -1671,7 +1671,7 @@ func (api *API) workspaceAgentReportStats(rw http.ResponseWriter, r *http.Reques ) if req.ConnectionCount > 0 { - activityBumpWorkspace(ctx, api.Logger.Named("activity_bump"), api.Database, workspace.ID) + activityBumpWorkspace(ctx, api.Logger.Named("activity_bump"), api.Database, workspace.ID, time.Time{}) } now := dbtime.Now() From 141cba941ba831fc1a6c77c56c7575dd8527ef92 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Tue, 14 Nov 2023 13:47:28 -0600 Subject: [PATCH 02/17] Never reduce the deadline --- coderd/database/queries/activitybump.sql | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/coderd/database/queries/activitybump.sql b/coderd/database/queries/activitybump.sql index 1ca8d0c30c077..97e43b2223b7d 100644 --- a/coderd/database/queries/activitybump.sql +++ b/coderd/database/queries/activitybump.sql @@ -18,12 +18,12 @@ WITH latest AS ( -- workspace auto started at the given time and the original -- TTL was applied. WHEN NOW() + ('60 minutes')::interval > @next_autostart :: timestamptz - -- If the autostart is behind the created_at, then the + -- If the autostart is behind now(), then the -- autostart schedule is either the 0 time and not provided, -- or it was the autostart in the past, which is no longer -- relevant. If a past autostart is being passed in, -- that is a mistake by the caller. - AND @next_autostart > workspace_builds.created_at + AND @next_autostart > NOW() THEN -- Extend to the autostart, then add the TTL ((@next_autostart :: timestamptz) - NOW()) + CASE @@ -54,8 +54,9 @@ SET updated_at = NOW(), deadline = CASE WHEN l.build_max_deadline = '0001-01-01 00:00:00+00' - THEN NOW() + l.ttl_interval - ELSE LEAST(NOW() + l.ttl_interval, l.build_max_deadline) + -- Never reduce the deadline from activity. + THEN GREATEST(wb.deadline, NOW() + l.ttl_interval) + ELSE LEAST(GREATEST(wb.deadline, NOW() + l.ttl_interval), l.build_max_deadline) END FROM latest l WHERE wb.id = l.build_id From c36b27df9152f5fa1b851c945b05ee332735fd8a Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Tue, 14 Nov 2023 18:51:52 -0600 Subject: [PATCH 03/17] feat: write query to adjust ttl bump --- coderd/autobuild/lifecycle_executor.go | 26 ++++++++++++++++-------- coderd/database/querier.go | 4 ---- coderd/database/queries.sql.go | 13 +++++------- coderd/database/queries/activitybump.sql | 4 ---- coderd/workspaceagents.go | 21 ++++++++++++++++++- 5 files changed, 43 insertions(+), 25 deletions(-) diff --git a/coderd/autobuild/lifecycle_executor.go b/coderd/autobuild/lifecycle_executor.go index 8d3bfb5478dc7..9fa1aaedb0ee4 100644 --- a/coderd/autobuild/lifecycle_executor.go +++ b/coderd/autobuild/lifecycle_executor.go @@ -357,13 +357,27 @@ func isEligibleForAutostart(ws database.Workspace, build database.WorkspaceBuild return false } - sched, err := cron.Weekly(ws.AutostartSchedule.String) - if err != nil { + nextTransition, allowed := NextAutostartSchedule(build.CreatedAt, ws.AutostartSchedule.String, templateSchedule) + if !allowed { return false } + + // Must use '.Before' vs '.After' so equal times are considered "valid for autostart". + return !currentTick.Before(nextTransition) +} + +// NextAutostartSchedule 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 NextAutostartSchedule(at time.Time, wsSchedule string, templateSchedule schedule.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(build.CreatedAt).Truncate(time.Minute) + 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 @@ -371,12 +385,8 @@ func isEligibleForAutostart(ws database.Workspace, build database.WorkspaceBuild // definition of "Saturday" depends on the location of the schedule. zonedTransition := nextTransition.In(sched.Location()) allowed := templateSchedule.AutostartRequirement.DaysMap()[zonedTransition.Weekday()] - if !allowed { - return false - } - // Must used '.Before' vs '.After' so equal times are considered "valid for autostart". - return !currentTick.Before(nextTransition) + return zonedTransition, allowed } // isEligibleForAutostart returns true if the workspace should be autostopped. diff --git a/coderd/database/querier.go b/coderd/database/querier.go index b46a21a44dee5..9a72ae22cfa3a 100644 --- a/coderd/database/querier.go +++ b/coderd/database/querier.go @@ -24,10 +24,6 @@ type sqlcQuerier interface { // multiple provisioners from acquiring the same jobs. See: // https://www.postgresql.org/docs/9.5/sql-select.html#SQL-FOR-UPDATE-SHARE AcquireProvisionerJob(ctx context.Context, arg AcquireProvisionerJobParams) (ProvisionerJob, error) - // We bump by the original TTL to prevent counter-intuitive behavior - // as the TTL wraps. For example, if I set the TTL to 12 hours, sign off - // work at midnight, come back at 10am, I would want another full day - // of uptime. // We only bump if the raw interval is positive and non-zero. // We only bump if workspace shutdown is manual. // We only bump when 5% of the deadline has elapsed. diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index b3970f0dda511..35178dd60b7b0 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -31,12 +31,12 @@ WITH latest AS ( -- workspace auto started at the given time and the original -- TTL was applied. WHEN NOW() + ('60 minutes')::interval > $1 :: timestamptz - -- If the autostart is behind the created_at, then the + -- If the autostart is behind now(), then the -- autostart schedule is either the 0 time and not provided, -- or it was the autostart in the past, which is no longer -- relevant. If a past autostart is being passed in, -- that is a mistake by the caller. - AND $1 > workspace_builds.created_at + AND $1 > NOW() THEN -- Extend to the autostart, then add the TTL (($1 :: timestamptz) - NOW()) + CASE @@ -67,8 +67,9 @@ SET updated_at = NOW(), deadline = CASE WHEN l.build_max_deadline = '0001-01-01 00:00:00+00' - THEN NOW() + l.ttl_interval - ELSE LEAST(NOW() + l.ttl_interval, l.build_max_deadline) + -- Never reduce the deadline from activity. + THEN GREATEST(wb.deadline, NOW() + l.ttl_interval) + ELSE LEAST(GREATEST(wb.deadline, NOW() + l.ttl_interval), l.build_max_deadline) END FROM latest l WHERE wb.id = l.build_id @@ -84,10 +85,6 @@ type ActivityBumpWorkspaceParams struct { WorkspaceID uuid.UUID `db:"workspace_id" json:"workspace_id"` } -// We bump by the original TTL to prevent counter-intuitive behavior -// as the TTL wraps. For example, if I set the TTL to 12 hours, sign off -// work at midnight, come back at 10am, I would want another full day -// of uptime. // We only bump if the raw interval is positive and non-zero. // We only bump if workspace shutdown is manual. // We only bump when 5% of the deadline has elapsed. diff --git a/coderd/database/queries/activitybump.sql b/coderd/database/queries/activitybump.sql index 97e43b2223b7d..377cff7da5dba 100644 --- a/coderd/database/queries/activitybump.sql +++ b/coderd/database/queries/activitybump.sql @@ -1,7 +1,3 @@ --- We bump by the original TTL to prevent counter-intuitive behavior --- as the TTL wraps. For example, if I set the TTL to 12 hours, sign off --- work at midnight, come back at 10am, I would want another full day --- of uptime. -- name: ActivityBumpWorkspace :exec WITH latest AS ( SELECT diff --git a/coderd/workspaceagents.go b/coderd/workspaceagents.go index d097e55b62edc..31a7b49799458 100644 --- a/coderd/workspaceagents.go +++ b/coderd/workspaceagents.go @@ -19,6 +19,8 @@ import ( "sync/atomic" "time" + "github.com/coder/coder/v2/coderd/autobuild" + "github.com/google/uuid" "github.com/sqlc-dev/pqtype" "golang.org/x/exp/maps" @@ -1671,7 +1673,24 @@ func (api *API) workspaceAgentReportStats(rw http.ResponseWriter, r *http.Reques ) if req.ConnectionCount > 0 { - activityBumpWorkspace(ctx, api.Logger.Named("activity_bump"), api.Database, workspace.ID, time.Time{}) + var nextAutostart time.Time + if workspace.AutostartSchedule.String != "" { + templateSchedule, err := (*(api.TemplateScheduleStore.Load())).Get(ctx, api.Database, workspace.TemplateID) + // If the template schedule fails to load, just default to bumping without the next trasition and log it. + if err != nil { + api.Logger.Warn(ctx, "failed to load template schedule bumping activity", + slog.F("workspace_id", workspace.ID), + slog.F("template_id", workspace.TemplateID), + slog.Error(err), + ) + } else { + next, allowed := autobuild.NextAutostartSchedule(time.Now(), workspace.AutostartSchedule.String, templateSchedule) + if allowed { + nextAutostart = next + } + } + } + activityBumpWorkspace(ctx, api.Logger.Named("activity_bump"), api.Database, workspace.ID, nextAutostart) } now := dbtime.Now() From b8f88a73d20aeee787be4e03d5cc4052b86b0838 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Tue, 14 Nov 2023 19:52:51 -0600 Subject: [PATCH 04/17] Implement dbmem --- coderd/activitybump_internal_test.go | 51 ++++++++++++++++++++-------- coderd/database/dbmem/dbmem.go | 31 ++++++++++++----- 2 files changed, 58 insertions(+), 24 deletions(-) diff --git a/coderd/activitybump_internal_test.go b/coderd/activitybump_internal_test.go index d69287735b9b0..55c548fff76d5 100644 --- a/coderd/activitybump_internal_test.go +++ b/coderd/activitybump_internal_test.go @@ -1,6 +1,7 @@ package coderd import ( + "context" "database/sql" "testing" "time" @@ -67,22 +68,41 @@ func Test_ActivityBumpWorkspace(t *testing.T) { workspaceTTL: 8 * time.Hour, expectedBump: 0, }, + { + // Expected bump is 0 because the original deadline is more than 1 hour + // out, so a bump would decrease the deadline. + name: "BumpLessThanDeadline", + transition: database.WorkspaceTransitionStart, + jobCompletedAt: sql.NullTime{Valid: true, Time: dbtime.Now().Add(-30 * time.Minute)}, + buildDeadlineOffset: ptr.Ref(8*time.Hour - 30*time.Minute), + workspaceTTL: 8 * time.Hour, + expectedBump: 0, + }, { name: "TimeToBump", transition: database.WorkspaceTransitionStart, - jobCompletedAt: sql.NullTime{Valid: true, Time: dbtime.Now().Add(-24 * time.Minute)}, - buildDeadlineOffset: ptr.Ref(8*time.Hour - 24*time.Minute), + jobCompletedAt: sql.NullTime{Valid: true, Time: dbtime.Now().Add(-30 * time.Minute)}, + buildDeadlineOffset: ptr.Ref(-30 * time.Minute), workspaceTTL: 8 * time.Hour, - expectedBump: time.Hour, //8 * time.Hour, + expectedBump: time.Hour, + }, + { + name: "TimeToBumpNextAutostart", + transition: database.WorkspaceTransitionStart, + jobCompletedAt: sql.NullTime{Valid: true, Time: dbtime.Now().Add(-30 * time.Minute)}, + buildDeadlineOffset: ptr.Ref(-30 * time.Minute), + workspaceTTL: 8 * time.Hour, + expectedBump: 8*time.Hour + 30*time.Minute, + nextAutostart: time.Now().Add(time.Minute * 30), }, { name: "MaxDeadline", transition: database.WorkspaceTransitionStart, jobCompletedAt: sql.NullTime{Valid: true, Time: dbtime.Now().Add(-24 * time.Minute)}, buildDeadlineOffset: ptr.Ref(time.Minute), // last chance to bump! - maxDeadlineOffset: ptr.Ref(time.Hour), + maxDeadlineOffset: ptr.Ref(time.Minute * 30), workspaceTTL: 8 * time.Hour, - expectedBump: time.Hour, //1 * time.Hour, + expectedBump: time.Minute * 30, }, { // A workspace that is still running, has passed its deadline, but has not @@ -92,7 +112,7 @@ func Test_ActivityBumpWorkspace(t *testing.T) { jobCompletedAt: sql.NullTime{Valid: true, Time: dbtime.Now().Add(-24 * time.Minute)}, buildDeadlineOffset: ptr.Ref(-time.Minute), workspaceTTL: 8 * time.Hour, - expectedBump: time.Hour, //8 * time.Hour, + expectedBump: time.Hour, }, { // A stopped workspace should never bump. @@ -100,19 +120,20 @@ func Test_ActivityBumpWorkspace(t *testing.T) { transition: database.WorkspaceTransitionStop, jobCompletedAt: sql.NullTime{Valid: true, Time: dbtime.Now().Add(-time.Minute)}, buildDeadlineOffset: ptr.Ref(-time.Minute), - workspaceTTL: time.Hour, //8 * time.Hour, + workspaceTTL: 8 * time.Hour, }, { // A workspace built from a template that disallows user autostop should bump // by the template TTL instead. name: "TemplateDisallowsUserAutostop", transition: database.WorkspaceTransitionStart, - jobCompletedAt: sql.NullTime{Valid: true, Time: dbtime.Now().Add(-24 * time.Minute)}, - buildDeadlineOffset: ptr.Ref(8*time.Hour - 24*time.Minute), - workspaceTTL: 6 * time.Hour, - templateTTL: 8 * time.Hour, + jobCompletedAt: sql.NullTime{Valid: true, Time: dbtime.Now().Add(-7 * time.Hour)}, + buildDeadlineOffset: ptr.Ref(-30 * time.Minute), + workspaceTTL: 2 * time.Hour, + templateTTL: 10 * time.Hour, templateDisallowsUserAutostop: true, - expectedBump: time.Hour, //8 * time.Hour, + expectedBump: 10*time.Hour + (time.Minute * 30), + nextAutostart: time.Now().Add(time.Minute * 30), }, } { tt := tt @@ -234,9 +255,9 @@ func Test_ActivityBumpWorkspace(t *testing.T) { return } - // Assert that the bump occurred between start and end. - expectedDeadlineStart := start.Add(tt.expectedBump) - expectedDeadlineEnd := end.Add(tt.expectedBump) + // Assert that the bump occurred between start and end. 1min buffer on either side. + expectedDeadlineStart := start.Add(tt.expectedBump).Add(time.Minute * -1) + expectedDeadlineEnd := end.Add(tt.expectedBump).Add(time.Minute) require.GreaterOrEqual(t, updatedBuild.Deadline, expectedDeadlineStart, "new deadline should be greater than or equal to start") require.LessOrEqual(t, updatedBuild.Deadline, expectedDeadlineEnd, "new deadline should be lesser than or equal to end") }) diff --git a/coderd/database/dbmem/dbmem.go b/coderd/database/dbmem/dbmem.go index e4d9af7680426..3b8d84c142dbc 100644 --- a/coderd/database/dbmem/dbmem.go +++ b/coderd/database/dbmem/dbmem.go @@ -678,6 +678,13 @@ func (q *FakeQuerier) GetActiveDBCryptKeys(_ context.Context) ([]database.DBCryp return ks, nil } +func maxTime(t, u time.Time) time.Time { + if t.After(u) { + return t + } + return u +} + func minTime(t, u time.Time) time.Time { if t.Before(u) { return t @@ -821,16 +828,20 @@ func (q *FakeQuerier) ActivityBumpWorkspace(ctx context.Context, arg database.Ac return err } + var a, b, c = arg.NextAutostart.UTC(), now.Add(time.Hour), arg.NextAutostart.After(now) + fmt.Println(a, b, c) var ttlDur time.Duration - if workspace.Ttl.Valid { - ttlDur = time.Duration(workspace.Ttl.Int64) - } - if !template.AllowUserAutostop { - ttlDur = time.Duration(template.DefaultTTL) - } - if ttlDur <= 0 { - // There's no TTL set anymore, so we don't know the bump duration. - return nil + if now.Add(time.Hour).After(arg.NextAutostart) && arg.NextAutostart.After(now) { + // Extend to TTL + add := arg.NextAutostart.Sub(now) + if workspace.Ttl.Valid { + add += time.Duration(workspace.Ttl.Int64) + } else { + add += time.Duration(template.DefaultTTL) + } + ttlDur = add + } else { + ttlDur = time.Hour } // Only bump if 5% of the deadline has passed. @@ -842,6 +853,8 @@ func (q *FakeQuerier) ActivityBumpWorkspace(ctx context.Context, arg database.Ac // Bump. newDeadline := now.Add(ttlDur) + // Never decrease deadlines from a bump + newDeadline = maxTime(newDeadline, q.workspaceBuilds[i].Deadline) q.workspaceBuilds[i].UpdatedAt = now if !q.workspaceBuilds[i].MaxDeadline.IsZero() { q.workspaceBuilds[i].Deadline = minTime(newDeadline, q.workspaceBuilds[i].MaxDeadline) From dc84fb89d2480664259a559bfe09c87c39c0018a Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Tue, 14 Nov 2023 19:56:36 -0600 Subject: [PATCH 05/17] Add comment --- coderd/activitybump_internal_test.go | 1 - coderd/database/dbmem/dbmem.go | 2 +- coderd/database/querier.go | 6 ++++++ coderd/database/queries.sql.go | 6 ++++++ coderd/database/queries/activitybump.sql | 6 ++++++ 5 files changed, 19 insertions(+), 2 deletions(-) diff --git a/coderd/activitybump_internal_test.go b/coderd/activitybump_internal_test.go index 55c548fff76d5..2ee9fc699ce6f 100644 --- a/coderd/activitybump_internal_test.go +++ b/coderd/activitybump_internal_test.go @@ -1,7 +1,6 @@ package coderd import ( - "context" "database/sql" "testing" "time" diff --git a/coderd/database/dbmem/dbmem.go b/coderd/database/dbmem/dbmem.go index 3b8d84c142dbc..f6558e5ec1e15 100644 --- a/coderd/database/dbmem/dbmem.go +++ b/coderd/database/dbmem/dbmem.go @@ -834,7 +834,7 @@ func (q *FakeQuerier) ActivityBumpWorkspace(ctx context.Context, arg database.Ac if now.Add(time.Hour).After(arg.NextAutostart) && arg.NextAutostart.After(now) { // Extend to TTL add := arg.NextAutostart.Sub(now) - if workspace.Ttl.Valid { + if workspace.Ttl.Valid && template.AllowUserAutostop { add += time.Duration(workspace.Ttl.Int64) } else { add += time.Duration(template.DefaultTTL) diff --git a/coderd/database/querier.go b/coderd/database/querier.go index 9a72ae22cfa3a..98316983aef84 100644 --- a/coderd/database/querier.go +++ b/coderd/database/querier.go @@ -24,6 +24,12 @@ type sqlcQuerier interface { // multiple provisioners from acquiring the same jobs. See: // https://www.postgresql.org/docs/9.5/sql-select.html#SQL-FOR-UPDATE-SHARE AcquireProvisionerJob(ctx context.Context, arg AcquireProvisionerJobParams) (ProvisionerJob, error) + // Bumps the workspace deadline by 1 hour. If the workspace bump will + // cross an autostart threshold, then the bump is autostart + TTL. This + // is the deadline behavior if the workspace was to autostart from a stopped + // state. + // Max deadline is respected, and will never be bumped. + // The deadline will never decrease. // We only bump if the raw interval is positive and non-zero. // We only bump if workspace shutdown is manual. // We only bump when 5% of the deadline has elapsed. diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index 35178dd60b7b0..fc5788f2172e8 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -85,6 +85,12 @@ type ActivityBumpWorkspaceParams struct { WorkspaceID uuid.UUID `db:"workspace_id" json:"workspace_id"` } +// Bumps the workspace deadline by 1 hour. If the workspace bump will +// cross an autostart threshold, then the bump is autostart + TTL. This +// is the deadline behavior if the workspace was to autostart from a stopped +// state. +// Max deadline is respected, and will never be bumped. +// The deadline will never decrease. // We only bump if the raw interval is positive and non-zero. // We only bump if workspace shutdown is manual. // We only bump when 5% of the deadline has elapsed. diff --git a/coderd/database/queries/activitybump.sql b/coderd/database/queries/activitybump.sql index 377cff7da5dba..2595c72bb39f2 100644 --- a/coderd/database/queries/activitybump.sql +++ b/coderd/database/queries/activitybump.sql @@ -1,3 +1,9 @@ +-- Bumps the workspace deadline by 1 hour. If the workspace bump will +-- cross an autostart threshold, then the bump is autostart + TTL. This +-- is the deadline behavior if the workspace was to autostart from a stopped +-- state. +-- Max deadline is respected, and will never be bumped. +-- The deadline will never decrease. -- name: ActivityBumpWorkspace :exec WITH latest AS ( SELECT From f088523c1f9d6f6b3f40cd76409b94dace940aa6 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Tue, 14 Nov 2023 20:01:35 -0600 Subject: [PATCH 06/17] remove debug print --- coderd/database/dbmem/dbmem.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/coderd/database/dbmem/dbmem.go b/coderd/database/dbmem/dbmem.go index f6558e5ec1e15..5bb7a1eb00a88 100644 --- a/coderd/database/dbmem/dbmem.go +++ b/coderd/database/dbmem/dbmem.go @@ -828,8 +828,6 @@ func (q *FakeQuerier) ActivityBumpWorkspace(ctx context.Context, arg database.Ac return err } - var a, b, c = arg.NextAutostart.UTC(), now.Add(time.Hour), arg.NextAutostart.After(now) - fmt.Println(a, b, c) var ttlDur time.Duration if now.Add(time.Hour).After(arg.NextAutostart) && arg.NextAutostart.After(now) { // Extend to TTL From 566dd382cd12d3dd6f21468f22f7d9eb71e5be50 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Tue, 14 Nov 2023 20:21:01 -0600 Subject: [PATCH 07/17] Fix activity bump test --- coderd/activitybump_test.go | 33 +++++++++++++++++---------------- coderd/workspaceagents.go | 3 +-- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/coderd/activitybump_test.go b/coderd/activitybump_test.go index 30c338b37a7c7..0202e2e9ccdad 100644 --- a/coderd/activitybump_test.go +++ b/coderd/activitybump_test.go @@ -30,7 +30,7 @@ func TestWorkspaceActivityBump(t *testing.T) { // max_deadline on the build directly in the database. setupActivityTest := func(t *testing.T, deadline ...time.Duration) (client *codersdk.Client, workspace codersdk.Workspace, assertBumped func(want bool)) { t.Helper() - const ttl = time.Minute + const ttl = time.Hour maxTTL := time.Duration(0) if len(deadline) > 0 { maxTTL = deadline[0] @@ -71,28 +71,29 @@ func TestWorkspaceActivityBump(t *testing.T) { }) coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspace.LatestBuild.ID) + var maxDeadline time.Time // Update the max deadline. if maxTTL != 0 { - dbBuild, err := db.GetWorkspaceBuildByID(ctx, workspace.LatestBuild.ID) - require.NoError(t, err) - - err = db.UpdateWorkspaceBuildDeadlineByID(ctx, database.UpdateWorkspaceBuildDeadlineByIDParams{ - ID: workspace.LatestBuild.ID, - UpdatedAt: dbtime.Now(), - Deadline: dbBuild.Deadline, - MaxDeadline: dbtime.Now().Add(maxTTL), - }) - require.NoError(t, err) + maxDeadline = dbtime.Now().Add(maxTTL) } + err := db.UpdateWorkspaceBuildDeadlineByID(ctx, database.UpdateWorkspaceBuildDeadlineByIDParams{ + ID: workspace.LatestBuild.ID, + UpdatedAt: dbtime.Now(), + // Make the deadline really close so it needs to be bumped immediately. + Deadline: time.Now().Add(time.Minute), + MaxDeadline: maxDeadline, + }) + require.NoError(t, err) + _ = agenttest.New(t, client.URL, agentToken) coderdtest.AwaitWorkspaceAgents(t, client, workspace.ID) - // Sanity-check that deadline is near. - workspace, err := client.Workspace(ctx, workspace.ID) + // Sanity-check that deadline is nearing requiring a bump. + workspace, err = client.Workspace(ctx, workspace.ID) require.NoError(t, err) require.WithinDuration(t, - time.Now().Add(time.Duration(ttlMillis)*time.Millisecond), + time.Now().Add(time.Minute), workspace.LatestBuild.Deadline.Time, testutil.WaitMedium, ) @@ -192,9 +193,9 @@ func TestWorkspaceActivityBump(t *testing.T) { t.Run("NotExceedMaxDeadline", func(t *testing.T) { t.Parallel() - // Set the max deadline to be in 61 seconds. We bump by 1 minute, so we + // Set the max deadline to be in 1 hour. We bump by 1 hour, so we // should expect the deadline to match the max deadline exactly. - client, workspace, assertBumped := setupActivityTest(t, 61*time.Second) + client, workspace, assertBumped := setupActivityTest(t, time.Minute*30) // Bump by dialing the workspace and sending traffic. resources := coderdtest.AwaitWorkspaceAgents(t, client, workspace.ID) diff --git a/coderd/workspaceagents.go b/coderd/workspaceagents.go index 31a7b49799458..cef68646e8281 100644 --- a/coderd/workspaceagents.go +++ b/coderd/workspaceagents.go @@ -19,8 +19,6 @@ import ( "sync/atomic" "time" - "github.com/coder/coder/v2/coderd/autobuild" - "github.com/google/uuid" "github.com/sqlc-dev/pqtype" "golang.org/x/exp/maps" @@ -32,6 +30,7 @@ import ( "tailscale.com/tailcfg" "cdr.dev/slog" + "github.com/coder/coder/v2/coderd/autobuild" "github.com/coder/coder/v2/coderd/database" "github.com/coder/coder/v2/coderd/database/dbauthz" "github.com/coder/coder/v2/coderd/database/dbtime" From 696d92506a6fdba134fbd649e147f89077fcc0bb Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Tue, 14 Nov 2023 20:55:24 -0600 Subject: [PATCH 08/17] add tst logs --- coderd/activitybump_test.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/coderd/activitybump_test.go b/coderd/activitybump_test.go index 0202e2e9ccdad..fdaddd7b92b76 100644 --- a/coderd/activitybump_test.go +++ b/coderd/activitybump_test.go @@ -109,6 +109,7 @@ func TestWorkspaceActivityBump(t *testing.T) { require.True(t, workspace.LatestBuild.MaxDeadline.Time.IsZero()) } + startingDeadline := workspace.LatestBuild.Deadline _ = coderdtest.AwaitWorkspaceAgents(t, client, workspace.ID) return client, workspace, func(want bool) { @@ -152,6 +153,7 @@ func TestWorkspaceActivityBump(t *testing.T) { require.LessOrEqual(t, workspace.LatestBuild.Deadline.Time, workspace.LatestBuild.MaxDeadline.Time) return } + t.Logf("originDeadline: %s, deadline: %s, now %s", startingDeadline, workspace.LatestBuild.Deadline.Time, dbtime.Now()) require.WithinDuration(t, dbtime.Now().Add(ttl), workspace.LatestBuild.Deadline.Time, testutil.WaitShort) } } From f4d6c597e4698af59527e88dfde9acca797038fb Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Tue, 14 Nov 2023 20:57:51 -0600 Subject: [PATCH 09/17] Try pushing to utc --- coderd/activitybump.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/coderd/activitybump.go b/coderd/activitybump.go index a2c68769438ff..52c2d6585e908 100644 --- a/coderd/activitybump.go +++ b/coderd/activitybump.go @@ -41,7 +41,7 @@ func activityBumpWorkspace(ctx context.Context, log slog.Logger, db database.Sto ctx, cancel := context.WithTimeout(ctx, time.Second*15) defer cancel() if err := db.ActivityBumpWorkspace(ctx, database.ActivityBumpWorkspaceParams{ - NextAutostart: nextAutostart, + NextAutostart: nextAutostart.UTC(), WorkspaceID: workspaceID, }); err != nil { if !xerrors.Is(err, context.Canceled) && !database.IsQueryCanceledError(err) { From a8327f0e6a2c36597a6547bcad39f40ebb583f1b Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Tue, 14 Nov 2023 21:29:20 -0600 Subject: [PATCH 10/17] Fix log line --- coderd/activitybump_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/coderd/activitybump_test.go b/coderd/activitybump_test.go index fdaddd7b92b76..d340d5133c3fd 100644 --- a/coderd/activitybump_test.go +++ b/coderd/activitybump_test.go @@ -153,7 +153,7 @@ func TestWorkspaceActivityBump(t *testing.T) { require.LessOrEqual(t, workspace.LatestBuild.Deadline.Time, workspace.LatestBuild.MaxDeadline.Time) return } - t.Logf("originDeadline: %s, deadline: %s, now %s", startingDeadline, workspace.LatestBuild.Deadline.Time, dbtime.Now()) + t.Logf("originDeadline: %s, deadline: %s, now %s", startingDeadline.Time, workspace.LatestBuild.Deadline.Time, dbtime.Now()) require.WithinDuration(t, dbtime.Now().Add(ttl), workspace.LatestBuild.Deadline.Time, testutil.WaitShort) } } From 69c6d8ac5e129ed3e603488c83d356a25fcb22fb Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Tue, 14 Nov 2023 21:45:58 -0600 Subject: [PATCH 11/17] Use db time --- coderd/activitybump_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/coderd/activitybump_test.go b/coderd/activitybump_test.go index d340d5133c3fd..a643a112f46af 100644 --- a/coderd/activitybump_test.go +++ b/coderd/activitybump_test.go @@ -81,7 +81,7 @@ func TestWorkspaceActivityBump(t *testing.T) { ID: workspace.LatestBuild.ID, UpdatedAt: dbtime.Now(), // Make the deadline really close so it needs to be bumped immediately. - Deadline: time.Now().Add(time.Minute), + Deadline: dbtime.Now().Add(time.Minute), MaxDeadline: maxDeadline, }) require.NoError(t, err) From 32175d879af940721fa1b8ec3ec99b270b2c8fe6 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Tue, 14 Nov 2023 21:58:40 -0600 Subject: [PATCH 12/17] make fmt --- coderd/activitybump_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/coderd/activitybump_test.go b/coderd/activitybump_test.go index a643a112f46af..dd13595fd777d 100644 --- a/coderd/activitybump_test.go +++ b/coderd/activitybump_test.go @@ -81,7 +81,7 @@ func TestWorkspaceActivityBump(t *testing.T) { ID: workspace.LatestBuild.ID, UpdatedAt: dbtime.Now(), // Make the deadline really close so it needs to be bumped immediately. - Deadline: dbtime.Now().Add(time.Minute), + Deadline: dbtime.Now().Add(time.Minute), MaxDeadline: maxDeadline, }) require.NoError(t, err) From 05e11c492f1ca95c5db63d9c9ff4d9df4e9e0f53 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Wed, 15 Nov 2023 08:26:16 -0600 Subject: [PATCH 13/17] Fix comments and wording --- coderd/activitybump.go | 3 ++- coderd/activitybump_test.go | 2 +- coderd/database/queries.sql.go | 2 +- coderd/database/queries/activitybump.sql | 2 +- coderd/workspaceagents.go | 2 +- 5 files changed, 6 insertions(+), 5 deletions(-) diff --git a/coderd/activitybump.go b/coderd/activitybump.go index 52c2d6585e908..89474bad7f2aa 100644 --- a/coderd/activitybump.go +++ b/coderd/activitybump.go @@ -16,7 +16,8 @@ import ( // If the bump crosses over an autostart time, the workspace will be // bumped by the workspace ttl instead. // -// Autostart is optional if bumping by 1 hour is sufficient. +// If nextAutostart is the zero value or in the past, the workspace +// will be bumped by 1 hour. // It handles the edge case in the example: // 1. Autostart is set to 9am. // 2. User works all day, and leaves a terminal open to the workspace overnight. diff --git a/coderd/activitybump_test.go b/coderd/activitybump_test.go index dd13595fd777d..5854bb0efc9e8 100644 --- a/coderd/activitybump_test.go +++ b/coderd/activitybump_test.go @@ -195,7 +195,7 @@ func TestWorkspaceActivityBump(t *testing.T) { t.Run("NotExceedMaxDeadline", func(t *testing.T) { t.Parallel() - // Set the max deadline to be in 1 hour. We bump by 1 hour, so we + // Set the max deadline to be in 30min. We bump by 1 hour, so we // should expect the deadline to match the max deadline exactly. client, workspace, assertBumped := setupActivityTest(t, time.Minute*30) diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index fc5788f2172e8..7f4004c89d5cc 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -34,7 +34,7 @@ WITH latest AS ( -- If the autostart is behind now(), then the -- autostart schedule is either the 0 time and not provided, -- or it was the autostart in the past, which is no longer - -- relevant. If a past autostart is being passed in, + -- relevant. If autostart is > 0 and in the past, then -- that is a mistake by the caller. AND $1 > NOW() THEN diff --git a/coderd/database/queries/activitybump.sql b/coderd/database/queries/activitybump.sql index 2595c72bb39f2..f35628c686369 100644 --- a/coderd/database/queries/activitybump.sql +++ b/coderd/database/queries/activitybump.sql @@ -23,7 +23,7 @@ WITH latest AS ( -- If the autostart is behind now(), then the -- autostart schedule is either the 0 time and not provided, -- or it was the autostart in the past, which is no longer - -- relevant. If a past autostart is being passed in, + -- relevant. If autostart is > 0 and in the past, then -- that is a mistake by the caller. AND @next_autostart > NOW() THEN diff --git a/coderd/workspaceagents.go b/coderd/workspaceagents.go index cef68646e8281..82a40f699d077 100644 --- a/coderd/workspaceagents.go +++ b/coderd/workspaceagents.go @@ -1677,7 +1677,7 @@ func (api *API) workspaceAgentReportStats(rw http.ResponseWriter, r *http.Reques templateSchedule, err := (*(api.TemplateScheduleStore.Load())).Get(ctx, api.Database, workspace.TemplateID) // If the template schedule fails to load, just default to bumping without the next trasition and log it. if err != nil { - api.Logger.Warn(ctx, "failed to load template schedule bumping activity", + api.Logger.Warn(ctx, "failed to load template schedule bumping activity, defaulting to bumping by 60min", slog.F("workspace_id", workspace.ID), slog.F("template_id", workspace.TemplateID), slog.Error(err), From 3f1327b6705bfce9b5354c0dd6d14973d90ce2bd Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Wed, 15 Nov 2023 08:59:13 -0600 Subject: [PATCH 14/17] Add logging information to unit test --- coderd/activitybump_test.go | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/coderd/activitybump_test.go b/coderd/activitybump_test.go index 5854bb0efc9e8..39647a3a69447 100644 --- a/coderd/activitybump_test.go +++ b/coderd/activitybump_test.go @@ -153,7 +153,12 @@ func TestWorkspaceActivityBump(t *testing.T) { require.LessOrEqual(t, workspace.LatestBuild.Deadline.Time, workspace.LatestBuild.MaxDeadline.Time) return } - t.Logf("originDeadline: %s, deadline: %s, now %s", startingDeadline.Time, workspace.LatestBuild.Deadline.Time, dbtime.Now()) + now := dbtime.Now() + t.Logf("[Zone=%s] originDeadline: %s, deadline: %s, now %s, (now-deadline)=%s", + time.Local.String(), + startingDeadline.Time, workspace.LatestBuild.Deadline.Time, now, + now.Sub(workspace.LatestBuild.Deadline.Time), + ) require.WithinDuration(t, dbtime.Now().Add(ttl), workspace.LatestBuild.Deadline.Time, testutil.WaitShort) } } From ceb3bd8e31191d9bb57a2c8f0d98272683252959 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Wed, 15 Nov 2023 09:10:57 -0600 Subject: [PATCH 15/17] Remove duplicated variable --- coderd/activitybump_test.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/coderd/activitybump_test.go b/coderd/activitybump_test.go index 39647a3a69447..d27d84cf1158e 100644 --- a/coderd/activitybump_test.go +++ b/coderd/activitybump_test.go @@ -109,7 +109,6 @@ func TestWorkspaceActivityBump(t *testing.T) { require.True(t, workspace.LatestBuild.MaxDeadline.Time.IsZero()) } - startingDeadline := workspace.LatestBuild.Deadline _ = coderdtest.AwaitWorkspaceAgents(t, client, workspace.ID) return client, workspace, func(want bool) { @@ -154,9 +153,10 @@ func TestWorkspaceActivityBump(t *testing.T) { return } now := dbtime.Now() - t.Logf("[Zone=%s] originDeadline: %s, deadline: %s, now %s, (now-deadline)=%s", - time.Local.String(), - startingDeadline.Time, workspace.LatestBuild.Deadline.Time, now, + zone, offset := time.Now().Zone() + t.Logf("[Zone=%s %d] originDeadline: %s, deadline: %s, now %s, (now-deadline)=%s", + zone, offset, + firstDeadline, workspace.LatestBuild.Deadline.Time, now, now.Sub(workspace.LatestBuild.Deadline.Time), ) require.WithinDuration(t, dbtime.Now().Add(ttl), workspace.LatestBuild.Deadline.Time, testutil.WaitShort) From 4d04500636f8b4310c823fad267fcccedfb59384 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Wed, 15 Nov 2023 09:22:37 -0600 Subject: [PATCH 16/17] Compare unix timestamps --- coderd/activitybump_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/coderd/activitybump_test.go b/coderd/activitybump_test.go index d27d84cf1158e..1e39013c6583d 100644 --- a/coderd/activitybump_test.go +++ b/coderd/activitybump_test.go @@ -134,7 +134,7 @@ func TestWorkspaceActivityBump(t *testing.T) { workspace, err = client.Workspace(ctx, workspace.ID) require.NoError(t, err) updatedAfter = dbtime.Now() - if workspace.LatestBuild.Deadline.Time == firstDeadline { + if workspace.LatestBuild.Deadline.Time.Unix() == firstDeadline.Unix() { updatedAfter = time.Now() return false } From 8e3ed21435c23b339942f6927e5372db583bfd70 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Wed, 15 Nov 2023 09:24:08 -0600 Subject: [PATCH 17/17] Use time.Equal --- coderd/activitybump_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/coderd/activitybump_test.go b/coderd/activitybump_test.go index 1e39013c6583d..0b55b40508966 100644 --- a/coderd/activitybump_test.go +++ b/coderd/activitybump_test.go @@ -134,7 +134,7 @@ func TestWorkspaceActivityBump(t *testing.T) { workspace, err = client.Workspace(ctx, workspace.ID) require.NoError(t, err) updatedAfter = dbtime.Now() - if workspace.LatestBuild.Deadline.Time.Unix() == firstDeadline.Unix() { + if workspace.LatestBuild.Deadline.Time.Equal(firstDeadline) { updatedAfter = time.Now() return false }