mirror of https://github.com/coder/coder.git
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:
parent
6085b92fae
commit
290180b104
|
@ -12,13 +12,39 @@ 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.
|
||||
//
|
||||
// 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.
|
||||
// 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.UTC(),
|
||||
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),
|
||||
|
|
|
@ -42,6 +42,7 @@ func Test_ActivityBumpWorkspace(t *testing.T) {
|
|||
templateTTL time.Duration
|
||||
templateDisallowsUserAutostop bool
|
||||
expectedBump time.Duration
|
||||
nextAutostart time.Time
|
||||
}{
|
||||
{
|
||||
name: "NotFinishedYet",
|
||||
|
@ -66,22 +67,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: 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: 1 * time.Hour,
|
||||
expectedBump: time.Minute * 30,
|
||||
},
|
||||
{
|
||||
// A workspace that is still running, has passed its deadline, but has not
|
||||
|
@ -91,7 +111,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,
|
||||
},
|
||||
{
|
||||
// A stopped workspace should never bump.
|
||||
|
@ -106,12 +126,13 @@ func Test_ActivityBumpWorkspace(t *testing.T) {
|
|||
// 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: 8 * time.Hour,
|
||||
expectedBump: 10*time.Hour + (time.Minute * 30),
|
||||
nextAutostart: time.Now().Add(time.Minute * 30),
|
||||
},
|
||||
} {
|
||||
tt := tt
|
||||
|
@ -215,7 +236,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
|
||||
|
@ -233,9 +254,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")
|
||||
})
|
||||
|
|
|
@ -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: dbtime.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,
|
||||
)
|
||||
|
@ -133,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.Equal(firstDeadline) {
|
||||
updatedAfter = time.Now()
|
||||
return false
|
||||
}
|
||||
|
@ -151,6 +152,13 @@ func TestWorkspaceActivityBump(t *testing.T) {
|
|||
require.LessOrEqual(t, workspace.LatestBuild.Deadline.Time, workspace.LatestBuild.MaxDeadline.Time)
|
||||
return
|
||||
}
|
||||
now := dbtime.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)
|
||||
}
|
||||
}
|
||||
|
@ -192,9 +200,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 30min. 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)
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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())
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -30,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"
|
||||
|
@ -1671,7 +1672,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)
|
||||
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, defaulting to bumping by 60min",
|
||||
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()
|
||||
|
|
Loading…
Reference in New Issue