Skip to content

Commit 560cf84

Browse files
authored
fix: prevent activity bump for prebuilt workspaces (#19263)
## Description This PR ensures that activity-based deadline extensions ("activity bumping") are not applied to prebuilt workspaces. Prebuilds are managed by the reconciliation loop and must not have `deadline` or `max_deadline` values set or extended, as they are not part of the regular lifecycle executor path. ## Changes - Update `ActivityBumpWorkspace` SQL query to discard prebuilt workspaces - Update application layer to avoid calling activity bump logic on prebuilt workspaces Related with: * Issue: #18898 * PR: #19252
1 parent cd1faff commit 560cf84

File tree

4 files changed

+148
-28
lines changed

4 files changed

+148
-28
lines changed

coderd/database/queries.sql.go

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

coderd/database/queries/activitybump.sql

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ WITH latest AS (
2222
-- be as if the workspace auto started at the given time and the
2323
-- original TTL was applied.
2424
--
25-
-- Sadly we can't define `activity_bump_interval` above since
25+
-- Sadly we can't define 'activity_bump_interval' above since
2626
-- it won't be available for this CASE statement, so we have to
2727
-- copy the cast twice.
2828
WHEN NOW() + (templates.activity_bump / 1000 / 1000 / 1000 || ' seconds')::interval > @next_autostart :: timestamptz
@@ -52,7 +52,11 @@ WITH latest AS (
5252
ON workspaces.id = workspace_builds.workspace_id
5353
JOIN templates
5454
ON templates.id = workspaces.template_id
55-
WHERE workspace_builds.workspace_id = @workspace_id::uuid
55+
WHERE
56+
workspace_builds.workspace_id = @workspace_id::uuid
57+
-- Prebuilt workspaces (identified by having the prebuilds system user as owner_id)
58+
-- are managed by the reconciliation loop and not subject to activity bumping
59+
AND workspaces.owner_id != 'c42fdf75-3097-471c-8c33-fb52454d81c0'::UUID
5660
ORDER BY workspace_builds.build_number DESC
5761
LIMIT 1
5862
)

coderd/workspacestats/reporter.go

Lines changed: 27 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -149,33 +149,36 @@ func (r *Reporter) ReportAgentStats(ctx context.Context, now time.Time, workspac
149149
return nil
150150
}
151151

152-
// check next autostart
153-
var nextAutostart time.Time
154-
if workspace.AutostartSchedule.String != "" {
155-
templateSchedule, err := (*(r.opts.TemplateScheduleStore.Load())).Get(ctx, r.opts.Database, workspace.TemplateID)
156-
// If the template schedule fails to load, just default to bumping
157-
// without the next transition and log it.
158-
switch {
159-
case err == nil:
160-
next, allowed := schedule.NextAutostart(now, workspace.AutostartSchedule.String, templateSchedule)
161-
if allowed {
162-
nextAutostart = next
152+
// Prebuilds are not subject to activity-based deadline bumps
153+
if !workspace.IsPrebuild() {
154+
// check next autostart
155+
var nextAutostart time.Time
156+
if workspace.AutostartSchedule.String != "" {
157+
templateSchedule, err := (*(r.opts.TemplateScheduleStore.Load())).Get(ctx, r.opts.Database, workspace.TemplateID)
158+
// If the template schedule fails to load, just default to bumping
159+
// without the next transition and log it.
160+
switch {
161+
case err == nil:
162+
next, allowed := schedule.NextAutostart(now, workspace.AutostartSchedule.String, templateSchedule)
163+
if allowed {
164+
nextAutostart = next
165+
}
166+
case database.IsQueryCanceledError(err):
167+
r.opts.Logger.Debug(ctx, "query canceled while loading template schedule",
168+
slog.F("workspace_id", workspace.ID),
169+
slog.F("template_id", workspace.TemplateID))
170+
default:
171+
r.opts.Logger.Error(ctx, "failed to load template schedule bumping activity, defaulting to bumping by 60min",
172+
slog.F("workspace_id", workspace.ID),
173+
slog.F("template_id", workspace.TemplateID),
174+
slog.Error(err),
175+
)
163176
}
164-
case database.IsQueryCanceledError(err):
165-
r.opts.Logger.Debug(ctx, "query canceled while loading template schedule",
166-
slog.F("workspace_id", workspace.ID),
167-
slog.F("template_id", workspace.TemplateID))
168-
default:
169-
r.opts.Logger.Error(ctx, "failed to load template schedule bumping activity, defaulting to bumping by 60min",
170-
slog.F("workspace_id", workspace.ID),
171-
slog.F("template_id", workspace.TemplateID),
172-
slog.Error(err),
173-
)
174177
}
175-
}
176178

177-
// bump workspace activity
178-
ActivityBumpWorkspace(ctx, r.opts.Logger.Named("activity_bump"), r.opts.Database, workspace.ID, nextAutostart)
179+
// bump workspace activity
180+
ActivityBumpWorkspace(ctx, r.opts.Logger.Named("activity_bump"), r.opts.Database, workspace.ID, nextAutostart)
181+
}
179182

180183
// bump workspace last_used_at
181184
r.opts.UsageTracker.Add(workspace.ID)

enterprise/coderd/workspaces_test.go

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ import (
4242
agplschedule "github.com/coder/coder/v2/coderd/schedule"
4343
"github.com/coder/coder/v2/coderd/schedule/cron"
4444
"github.com/coder/coder/v2/coderd/util/ptr"
45+
"github.com/coder/coder/v2/coderd/workspacestats"
4546
"github.com/coder/coder/v2/codersdk"
4647
entaudit "github.com/coder/coder/v2/enterprise/audit"
4748
"github.com/coder/coder/v2/enterprise/audit/backends"
@@ -2767,6 +2768,114 @@ func TestPrebuildUpdateLifecycleParams(t *testing.T) {
27672768
}
27682769
}
27692770

2771+
func TestPrebuildActivityBump(t *testing.T) {
2772+
t.Parallel()
2773+
2774+
clock := quartz.NewMock(t)
2775+
clock.Set(dbtime.Now())
2776+
2777+
// Setup
2778+
log := testutil.Logger(t)
2779+
client, db, owner := coderdenttest.NewWithDatabase(t, &coderdenttest.Options{
2780+
Options: &coderdtest.Options{
2781+
IncludeProvisionerDaemon: true,
2782+
Clock: clock,
2783+
},
2784+
LicenseOptions: &coderdenttest.LicenseOptions{
2785+
Features: license.Features{
2786+
codersdk.FeatureWorkspacePrebuilds: 1,
2787+
},
2788+
},
2789+
})
2790+
2791+
// Given: a template and a template version with preset and a prebuilt workspace
2792+
presetID := uuid.New()
2793+
version := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, nil)
2794+
_ = coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
2795+
// Configure activity bump on the template
2796+
activityBump := time.Hour
2797+
template := coderdtest.CreateTemplate(t, client, owner.OrganizationID, version.ID, func(ctr *codersdk.CreateTemplateRequest) {
2798+
ctr.ActivityBumpMillis = ptr.Ref[int64](activityBump.Milliseconds())
2799+
})
2800+
dbgen.Preset(t, db, database.InsertPresetParams{
2801+
ID: presetID,
2802+
TemplateVersionID: version.ID,
2803+
DesiredInstances: sql.NullInt32{Int32: 1, Valid: true},
2804+
})
2805+
// Given: a prebuild with an expired Deadline
2806+
deadline := clock.Now().Add(-30 * time.Minute)
2807+
wb := dbfake.WorkspaceBuild(t, db, database.WorkspaceTable{
2808+
OwnerID: database.PrebuildsSystemUserID,
2809+
TemplateID: template.ID,
2810+
}).Seed(database.WorkspaceBuild{
2811+
TemplateVersionID: version.ID,
2812+
TemplateVersionPresetID: uuid.NullUUID{
2813+
UUID: presetID,
2814+
Valid: true,
2815+
},
2816+
Deadline: deadline,
2817+
}).WithAgent(func(agent []*proto.Agent) []*proto.Agent {
2818+
return agent
2819+
}).Do()
2820+
2821+
// Mark the prebuilt workspace's agent as ready so the prebuild can be claimed
2822+
// nolint:gocritic
2823+
ctx := dbauthz.AsSystemRestricted(testutil.Context(t, testutil.WaitLong))
2824+
agent, err := db.GetWorkspaceAgentAndLatestBuildByAuthToken(ctx, uuid.MustParse(wb.AgentToken))
2825+
require.NoError(t, err)
2826+
err = db.UpdateWorkspaceAgentLifecycleStateByID(ctx, database.UpdateWorkspaceAgentLifecycleStateByIDParams{
2827+
ID: agent.WorkspaceAgent.ID,
2828+
LifecycleState: database.WorkspaceAgentLifecycleStateReady,
2829+
})
2830+
require.NoError(t, err)
2831+
2832+
// Given: a prebuilt workspace with a Deadline and an empty MaxDeadline
2833+
prebuild := coderdtest.MustWorkspace(t, client, wb.Workspace.ID)
2834+
require.Equal(t, deadline.UTC(), prebuild.LatestBuild.Deadline.Time.UTC())
2835+
require.Zero(t, prebuild.LatestBuild.MaxDeadline)
2836+
2837+
// When: activity bump is applied to an unclaimed prebuild
2838+
workspacestats.ActivityBumpWorkspace(ctx, log, db, prebuild.ID, clock.Now().Add(10*time.Hour))
2839+
2840+
// Then: prebuild Deadline/MaxDeadline remain unchanged
2841+
prebuild = coderdtest.MustWorkspace(t, client, wb.Workspace.ID)
2842+
require.Equal(t, deadline.UTC(), prebuild.LatestBuild.Deadline.Time.UTC())
2843+
require.Zero(t, prebuild.LatestBuild.MaxDeadline)
2844+
2845+
// Given: the prebuilt workspace is claimed by a user
2846+
user, err := client.User(ctx, "testUser")
2847+
require.NoError(t, err)
2848+
claimedWorkspace, err := client.CreateUserWorkspace(ctx, user.ID.String(), codersdk.CreateWorkspaceRequest{
2849+
TemplateVersionID: version.ID,
2850+
TemplateVersionPresetID: presetID,
2851+
Name: coderdtest.RandomUsername(t),
2852+
})
2853+
require.NoError(t, err)
2854+
coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, claimedWorkspace.LatestBuild.ID)
2855+
workspace := coderdtest.MustWorkspace(t, client, claimedWorkspace.ID)
2856+
require.Equal(t, prebuild.ID, workspace.ID)
2857+
// Claimed workspaces have an empty Deadline and MaxDeadline
2858+
require.Zero(t, workspace.LatestBuild.Deadline)
2859+
require.Zero(t, workspace.LatestBuild.MaxDeadline)
2860+
2861+
// Given: the claimed workspace has an expired Deadline
2862+
err = db.UpdateWorkspaceBuildDeadlineByID(ctx, database.UpdateWorkspaceBuildDeadlineByIDParams{
2863+
ID: workspace.LatestBuild.ID,
2864+
Deadline: deadline,
2865+
UpdatedAt: clock.Now(),
2866+
})
2867+
require.NoError(t, err)
2868+
workspace = coderdtest.MustWorkspace(t, client, claimedWorkspace.ID)
2869+
2870+
// When: activity bump is applied to a claimed prebuild
2871+
workspacestats.ActivityBumpWorkspace(ctx, log, db, workspace.ID, clock.Now().Add(10*time.Hour))
2872+
2873+
// Then: Deadline is extended by the activity bump, MaxDeadline remains unset
2874+
workspace = coderdtest.MustWorkspace(t, client, claimedWorkspace.ID)
2875+
require.WithinDuration(t, clock.Now().Add(activityBump).UTC(), workspace.LatestBuild.Deadline.Time.UTC(), testutil.WaitMedium)
2876+
require.Zero(t, workspace.LatestBuild.MaxDeadline)
2877+
}
2878+
27702879
// TestWorkspaceTemplateParamsChange tests a workspace with a parameter that
27712880
// validation changes on apply. The params used in create workspace are invalid
27722881
// according to the static params on import.

0 commit comments

Comments
 (0)