feat!: bump workspace activity by 1 hour (#10704)

Marked as a breaking change as the previous activity bump was always the TTL duration of the workspace/template.

This change is more cost conservative, only bumping by 1 hour for workspace activity. To accommodate wrap around, eg bumping a workspace into the next autostart, the deadline is bumped by the TTL if the workspace crosses the autostart threshold.

This is a niche case that is likely caused by an idle terminal making a workspace survive through a night. The next morning, the workspace will get activity bumped the default TTL on the autostart, being similar to as if the workspace was autostarted again.

In practice, a good way to avoid this is to set a max_deadline of <24hrs to avoid wrap around entirely.
This commit is contained in:
Steven Masley
2023-11-15 09:42:27 -06:00
committed by GitHub
parent 6085b92fae
commit 290180b104
12 changed files with 235 additions and 88 deletions
+3 -3
View File
@@ -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)
}
+24 -13
View File
@@ -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
@@ -775,8 +782,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 +791,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
}
@@ -822,15 +829,17 @@ func (q *FakeQuerier) ActivityBumpWorkspace(ctx context.Context, workspaceID uui
}
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 && template.AllowUserAutostop {
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 +851,8 @@ func (q *FakeQuerier) ActivityBumpWorkspace(ctx context.Context, workspaceID uui
// 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)
+1 -1
View File
@@ -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())
+1 -1
View File
@@ -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)
+7 -5
View File
@@ -24,14 +24,16 @@ 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.
// 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.
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.
+40 -12
View File
@@ -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 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 autostart is > 0 and in the past, then
-- that is a mistake by the caller.
AND $1 > NOW()
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
)
@@ -47,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
@@ -59,15 +80,22 @@ AND l.build_deadline != '0001-01-01 00:00:00+00'
AND l.build_deadline - (l.ttl_interval * 0.95) < NOW()
`
// 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.
type ActivityBumpWorkspaceParams struct {
NextAutostart time.Time `db:"next_autostart" json:"next_autostart"`
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.
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
}
+32 -9
View File
@@ -1,7 +1,9 @@
-- 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.
-- 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
@@ -12,9 +14,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 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 autostart is > 0 and in the past, then
-- that is a mistake by the caller.
AND @next_autostart > NOW()
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
@@ -34,8 +56,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