Skip to content

Commit d56f49f

Browse files
fix(coderd): make activitybump aware of default template ttl (#10253)
The refactored ActivityBump query did not take into account the template-level TTL, resulting in potentially incorrect bump amounts for workspaces that have both a user-defined and template- defined TTL that differ. This change is ported over from PR#10035 to reduce the overall size of that PR. Also includes a drive-by unit test in autobuild for checking template autostop/TTL. Co-authored-by: Dean Sheather <dean@deansheather.com>
1 parent 2a4ac2a commit d56f49f

File tree

6 files changed

+120
-15
lines changed

6 files changed

+120
-15
lines changed

coderd/activitybump_internal_test.go

Lines changed: 33 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import (
1515
"github.com/coder/coder/v2/coderd/util/ptr"
1616
"github.com/coder/coder/v2/testutil"
1717

18+
"github.com/stretchr/testify/assert"
1819
"github.com/stretchr/testify/require"
1920
)
2021

@@ -32,13 +33,15 @@ func Test_ActivityBumpWorkspace(t *testing.T) {
3233
}
3334

3435
for _, tt := range []struct {
35-
name string
36-
transition database.WorkspaceTransition
37-
jobCompletedAt sql.NullTime
38-
buildDeadlineOffset *time.Duration
39-
maxDeadlineOffset *time.Duration
40-
workspaceTTL time.Duration
41-
expectedBump time.Duration
36+
name string
37+
transition database.WorkspaceTransition
38+
jobCompletedAt sql.NullTime
39+
buildDeadlineOffset *time.Duration
40+
maxDeadlineOffset *time.Duration
41+
workspaceTTL time.Duration
42+
templateTTL time.Duration
43+
templateDisallowsUserAutostop bool
44+
expectedBump time.Duration
4245
}{
4346
{
4447
name: "NotFinishedYet",
@@ -97,7 +100,18 @@ func Test_ActivityBumpWorkspace(t *testing.T) {
97100
jobCompletedAt: sql.NullTime{Valid: true, Time: dbtime.Now().Add(-time.Minute)},
98101
buildDeadlineOffset: ptr.Ref(-time.Minute),
99102
workspaceTTL: 8 * time.Hour,
100-
expectedBump: 0,
103+
},
104+
{
105+
// A workspace built from a template that disallows user autostop should bump
106+
// by the template TTL instead.
107+
name: "TemplateDisallowsUserAutostop",
108+
transition: database.WorkspaceTransitionStart,
109+
jobCompletedAt: sql.NullTime{Valid: true, Time: dbtime.Now().Add(-24 * time.Minute)},
110+
buildDeadlineOffset: ptr.Ref(8*time.Hour - 24*time.Minute),
111+
workspaceTTL: 6 * time.Hour,
112+
templateTTL: 8 * time.Hour,
113+
templateDisallowsUserAutostop: true,
114+
expectedBump: 8 * time.Hour,
101115
},
102116
} {
103117
tt := tt
@@ -144,6 +158,13 @@ func Test_ActivityBumpWorkspace(t *testing.T) {
144158
buildID = uuid.New()
145159
)
146160

161+
require.NoError(t, db.UpdateTemplateScheduleByID(ctx, database.UpdateTemplateScheduleByIDParams{
162+
ID: template.ID,
163+
UpdatedAt: dbtime.Now(),
164+
AllowUserAutostop: !tt.templateDisallowsUserAutostop,
165+
DefaultTTL: int64(tt.templateTTL),
166+
}), "unexpected error updating template schedule")
167+
147168
var buildNumber int32 = 1
148169
// Insert a number of previous workspace builds.
149170
for i := 0; i < 5; i++ {
@@ -202,13 +223,13 @@ func Test_ActivityBumpWorkspace(t *testing.T) {
202223
require.NoError(t, err, "unexpected error getting latest workspace build")
203224
require.Equal(t, bld.MaxDeadline.UTC(), updatedBuild.MaxDeadline.UTC(), "max_deadline should not have changed")
204225
if tt.expectedBump == 0 {
205-
require.Equal(t, bld.UpdatedAt.UTC(), updatedBuild.UpdatedAt.UTC(), "should not have bumped updated_at")
206-
require.Equal(t, bld.Deadline.UTC(), updatedBuild.Deadline.UTC(), "should not have bumped deadline")
226+
assert.Equal(t, bld.UpdatedAt.UTC(), updatedBuild.UpdatedAt.UTC(), "should not have bumped updated_at")
227+
assert.Equal(t, bld.Deadline.UTC(), updatedBuild.Deadline.UTC(), "should not have bumped deadline")
207228
return
208229
}
209-
require.NotEqual(t, bld.UpdatedAt.UTC(), updatedBuild.UpdatedAt.UTC(), "should have bumped updated_at")
230+
assert.NotEqual(t, bld.UpdatedAt.UTC(), updatedBuild.UpdatedAt.UTC(), "should have bumped updated_at")
210231
if tt.maxDeadlineOffset != nil {
211-
require.Equal(t, bld.MaxDeadline.UTC(), updatedBuild.MaxDeadline.UTC(), "new deadline must equal original max deadline")
232+
assert.Equal(t, bld.MaxDeadline.UTC(), updatedBuild.MaxDeadline.UTC(), "new deadline must equal original max deadline")
212233
return
213234
}
214235

coderd/autobuild/lifecycle_executor_test.go

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -731,6 +731,51 @@ func TestExecutorAutostartTemplateDisabled(t *testing.T) {
731731
assert.Len(t, stats.Transitions, 0)
732732
}
733733

734+
func TestExecutorAutostopTemplateDisabled(t *testing.T) {
735+
t.Parallel()
736+
737+
// Given: we have a workspace built from a template that disallows user autostop
738+
var (
739+
sched = mustSchedule(t, "CRON_TZ=UTC 0 * * * *")
740+
tickCh = make(chan time.Time)
741+
statsCh = make(chan autobuild.Stats)
742+
743+
client = coderdtest.New(t, &coderdtest.Options{
744+
AutobuildTicker: tickCh,
745+
IncludeProvisionerDaemon: true,
746+
AutobuildStats: statsCh,
747+
// We are using a mock store here as the AGPL store does not implement this.
748+
TemplateScheduleStore: schedule.MockTemplateScheduleStore{
749+
GetFn: func(_ context.Context, _ database.Store, _ uuid.UUID) (schedule.TemplateScheduleOptions, error) {
750+
return schedule.TemplateScheduleOptions{
751+
UserAutostopEnabled: false,
752+
DefaultTTL: time.Hour,
753+
}, nil
754+
},
755+
},
756+
})
757+
// Given: we have a user with a workspace configured to autostart some time in the future
758+
workspace = mustProvisionWorkspace(t, client, func(cwr *codersdk.CreateWorkspaceRequest) {
759+
cwr.TTLMillis = ptr.Ref(8 * time.Hour.Milliseconds())
760+
})
761+
)
762+
763+
// When: we create the workspace
764+
// Then: the deadline should be set to the template default TTL
765+
assert.WithinDuration(t, workspace.LatestBuild.CreatedAt.Add(time.Hour), workspace.LatestBuild.Deadline.Time, time.Minute)
766+
767+
// When: the autobuild executor ticks before the next scheduled time
768+
go func() {
769+
tickCh <- sched.Next(workspace.LatestBuild.CreatedAt).Add(time.Minute)
770+
close(tickCh)
771+
}()
772+
773+
// Then: nothing should happen
774+
stats := <-statsCh
775+
assert.NoError(t, stats.Error)
776+
assert.Len(t, stats.Transitions, 0)
777+
}
778+
734779
// TestExecutorFailedWorkspace test AGPL functionality which mainly
735780
// ensures that autostop actions as a result of a failed workspace
736781
// build do not trigger.

coderd/database/dbfake/dbfake.go

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -814,8 +814,26 @@ func (q *FakeQuerier) ActivityBumpWorkspace(ctx context.Context, workspaceID uui
814814
if q.workspaceBuilds[i].Deadline.IsZero() {
815815
return nil
816816
}
817+
818+
// Check the template default TTL.
819+
template, err := q.getTemplateByIDNoLock(ctx, workspace.TemplateID)
820+
if err != nil {
821+
return err
822+
}
823+
824+
var ttlDur time.Duration
825+
if workspace.Ttl.Valid {
826+
ttlDur = time.Duration(workspace.Ttl.Int64)
827+
}
828+
if !template.AllowUserAutostop {
829+
ttlDur = time.Duration(template.DefaultTTL)
830+
}
831+
if ttlDur <= 0 {
832+
// There's no TTL set anymore, so we don't know the bump duration.
833+
return nil
834+
}
835+
817836
// Only bump if 5% of the deadline has passed.
818-
ttlDur := time.Duration(workspace.Ttl.Int64)
819837
ttlDur95 := ttlDur - (ttlDur / 20)
820838
minBumpDeadline := q.workspaceBuilds[i].Deadline.Add(-ttlDur95)
821839
if now.Before(minBumpDeadline) {

coderd/database/querier.go

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

coderd/database/queries.sql.go

Lines changed: 11 additions & 1 deletion
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: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,20 @@ WITH latest AS (
1010
workspace_builds.max_deadline::timestamp with time zone AS build_max_deadline,
1111
workspace_builds.transition AS build_transition,
1212
provisioner_jobs.completed_at::timestamp with time zone AS job_completed_at,
13-
(workspaces.ttl / 1000 / 1000 / 1000 || ' seconds')::interval AS ttl_interval
13+
(
14+
CASE
15+
WHEN templates.allow_user_autostop
16+
THEN (workspaces.ttl / 1000 / 1000 / 1000 || ' seconds')::interval
17+
ELSE (templates.default_ttl / 1000 / 1000 / 1000 || ' seconds')::interval
18+
END
19+
) AS ttl_interval
1420
FROM workspace_builds
1521
JOIN provisioner_jobs
1622
ON provisioner_jobs.id = workspace_builds.job_id
1723
JOIN workspaces
1824
ON workspaces.id = workspace_builds.workspace_id
25+
JOIN templates
26+
ON templates.id = workspaces.template_id
1927
WHERE workspace_builds.workspace_id = @workspace_id::uuid
2028
ORDER BY workspace_builds.build_number DESC
2129
LIMIT 1
@@ -33,6 +41,8 @@ FROM latest l
3341
WHERE wb.id = l.build_id
3442
AND l.job_completed_at IS NOT NULL
3543
AND l.build_transition = 'start'
44+
-- We only bump if the raw interval is positive and non-zero.
45+
AND l.ttl_interval > '0 seconds'::interval
3646
-- We only bump if workspace shutdown is manual.
3747
AND l.build_deadline != '0001-01-01 00:00:00+00'
3848
-- We only bump when 5% of the deadline has elapsed.

0 commit comments

Comments
 (0)