Skip to content

Commit 868e553

Browse files
committed
chore: move autobuild/executor into autobuild
1 parent c8e6783 commit 868e553

File tree

6 files changed

+96
-89
lines changed

6 files changed

+96
-89
lines changed

cli/server.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ import (
6262
"github.com/coder/coder/cli/cliui"
6363
"github.com/coder/coder/cli/config"
6464
"github.com/coder/coder/coderd"
65-
"github.com/coder/coder/coderd/autobuild/executor"
65+
"github.com/coder/coder/coderd/autobuild"
6666
"github.com/coder/coder/coderd/database"
6767
"github.com/coder/coder/coderd/database/dbfake"
6868
"github.com/coder/coder/coderd/database/dbmetrics"
@@ -899,7 +899,7 @@ func (r *RootCmd) Server(newAPI func(context.Context, *coderd.Options) (*coderd.
899899

900900
autobuildPoller := time.NewTicker(cfg.AutobuildPollInterval.Value())
901901
defer autobuildPoller.Stop()
902-
autobuildExecutor := executor.New(ctx, options.Database, coderAPI.TemplateScheduleStore, logger, autobuildPoller.C)
902+
autobuildExecutor := autobuild.NewExecutor(ctx, options.Database, coderAPI.TemplateScheduleStore, logger, autobuildPoller.C)
903903
autobuildExecutor.Run()
904904

905905
// Currently there is no way to ask the server to shut

coderd/autobuild/doc.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
// Package autobuild contains logic for scheduling workspace
2+
// builds in the background.
3+
package autobuild

coderd/autobuild/executor/lifecycle_executor.go renamed to coderd/autobuild/lifecycle_executor.go

Lines changed: 69 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
package executor
1+
package autobuild
22

33
import (
44
"context"
@@ -35,8 +35,8 @@ type Stats struct {
3535
Error error
3636
}
3737

38-
// New returns a new autobuild executor.
39-
func New(ctx context.Context, db database.Store, tss *atomic.Pointer[schedule.TemplateScheduleStore], log slog.Logger, tick <-chan time.Time) *Executor {
38+
// New returns a new wsactions executor.
39+
func NewExecutor(ctx context.Context, db database.Store, tss *atomic.Pointer[schedule.TemplateScheduleStore], log slog.Logger, tick <-chan time.Time) *Executor {
4040
le := &Executor{
4141
//nolint:gocritic // Autostart has a limited set of permissions.
4242
ctx: dbauthz.AsAutostart(ctx),
@@ -125,77 +125,57 @@ func (e *Executor) runOnce(t time.Time) Stats {
125125
log := e.log.With(slog.F("workspace_id", wsID))
126126

127127
eg.Go(func() error {
128-
err := e.db.InTx(func(db database.Store) error {
128+
err := e.db.InTx(func(tx database.Store) error {
129129
// Re-check eligibility since the first check was outside the
130130
// transaction and the workspace settings may have changed.
131-
ws, err := db.GetWorkspaceByID(e.ctx, wsID)
131+
ws, err := tx.GetWorkspaceByID(e.ctx, wsID)
132132
if err != nil {
133133
log.Error(e.ctx, "get workspace autostart failed", slog.Error(err))
134134
return nil
135135
}
136136

137137
// Determine the workspace state based on its latest build.
138-
priorHistory, err := db.GetLatestWorkspaceBuildByWorkspaceID(e.ctx, ws.ID)
138+
latestBuild, err := tx.GetLatestWorkspaceBuildByWorkspaceID(e.ctx, ws.ID)
139139
if err != nil {
140140
log.Warn(e.ctx, "get latest workspace build", slog.Error(err))
141141
return nil
142142
}
143143

144-
templateSchedule, err := (*(e.templateScheduleStore.Load())).GetTemplateScheduleOptions(e.ctx, db, ws.TemplateID)
144+
templateSchedule, err := (*(e.templateScheduleStore.Load())).GetTemplateScheduleOptions(e.ctx, tx, ws.TemplateID)
145145
if err != nil {
146146
log.Warn(e.ctx, "get template schedule options", slog.Error(err))
147147
return nil
148148
}
149149

150-
if !isEligibleForAutoStartStop(ws, priorHistory, templateSchedule) {
151-
return nil
152-
}
153-
154-
priorJob, err := db.GetProvisionerJobByID(e.ctx, priorHistory.JobID)
150+
latestJob, err := tx.GetProvisionerJobByID(e.ctx, latestBuild.JobID)
155151
if err != nil {
156152
log.Warn(e.ctx, "get last provisioner job for workspace %q: %w", slog.Error(err))
157153
return nil
158154
}
159155

160-
validTransition, nextTransition, err := getNextTransition(ws, priorHistory, priorJob)
156+
nextTransition, reason, err := getNextTransition(ws, latestBuild, latestJob, templateSchedule, currentTick)
161157
if err != nil {
162158
log.Debug(e.ctx, "skipping workspace", slog.Error(err))
163159
return nil
164160
}
165161

166-
if currentTick.Before(nextTransition) {
167-
log.Debug(e.ctx, "skipping workspace: too early",
168-
slog.F("next_transition_at", nextTransition),
169-
slog.F("transition", validTransition),
170-
slog.F("current_tick", currentTick),
171-
)
172-
return nil
173-
}
174-
builder := wsbuilder.New(ws, validTransition).
175-
SetLastWorkspaceBuildInTx(&priorHistory).
176-
SetLastWorkspaceBuildJobInTx(&priorJob)
177-
178-
switch validTransition {
179-
case database.WorkspaceTransitionStart:
180-
builder = builder.Reason(database.BuildReasonAutostart)
181-
case database.WorkspaceTransitionStop:
182-
builder = builder.Reason(database.BuildReasonAutostop)
183-
default:
184-
log.Error(e.ctx, "unsupported transition", slog.F("transition", validTransition))
185-
return nil
186-
}
187-
if _, _, err := builder.Build(e.ctx, db, nil); err != nil {
162+
builder := wsbuilder.New(ws, nextTransition).
163+
SetLastWorkspaceBuildInTx(&latestBuild).
164+
SetLastWorkspaceBuildJobInTx(&latestJob).
165+
Reason(reason)
166+
167+
if _, _, err := builder.Build(e.ctx, tx, nil); err != nil {
188168
log.Error(e.ctx, "unable to transition workspace",
189-
slog.F("transition", validTransition),
169+
slog.F("transition", nextTransition),
190170
slog.Error(err),
191171
)
192172
return nil
193173
}
194174
statsMu.Lock()
195-
stats.Transitions[ws.ID] = validTransition
175+
stats.Transitions[ws.ID] = nextTransition
196176
statsMu.Unlock()
197177

198-
log.Info(e.ctx, "scheduling workspace transition", slog.F("transition", validTransition))
178+
log.Info(e.ctx, "scheduling workspace transition", slog.F("transition", nextTransition))
199179

200180
return nil
201181

@@ -218,7 +198,9 @@ func (e *Executor) runOnce(t time.Time) Stats {
218198
return stats
219199
}
220200

221-
func isEligibleForAutoStartStop(ws database.Workspace, priorHistory database.WorkspaceBuild, templateSchedule schedule.TemplateScheduleOptions) bool {
201+
// isEligibleForTransition returns true if the workspace meets basic criteria
202+
// for transitioning to a new state.
203+
func isEligibleForTransition(ws database.Workspace, latestBuild database.WorkspaceBuild, templateSchedule schedule.TemplateScheduleOptions) bool {
222204
if ws.Deleted {
223205
return false
224206
}
@@ -227,7 +209,7 @@ func isEligibleForAutoStartStop(ws database.Workspace, priorHistory database.Wor
227209
}
228210
// Don't check the template schedule to see whether it allows autostop, this
229211
// is done during the build when determining the deadline.
230-
if priorHistory.Transition == database.WorkspaceTransitionStart && !priorHistory.Deadline.IsZero() {
212+
if latestBuild.Transition == database.WorkspaceTransitionStart && !latestBuild.Deadline.IsZero() {
231213
return true
232214
}
233215

@@ -236,35 +218,57 @@ func isEligibleForAutoStartStop(ws database.Workspace, priorHistory database.Wor
236218

237219
func getNextTransition(
238220
ws database.Workspace,
239-
priorHistory database.WorkspaceBuild,
240-
priorJob database.ProvisionerJob,
221+
latestBuild database.WorkspaceBuild,
222+
latestJob database.ProvisionerJob,
223+
templateSchedule schedule.TemplateScheduleOptions,
224+
currentTick time.Time,
241225
) (
242-
validTransition database.WorkspaceTransition,
243-
nextTransition time.Time,
244-
err error,
226+
database.WorkspaceTransition,
227+
database.BuildReason,
228+
error,
245229
) {
246-
if !priorJob.CompletedAt.Valid || priorJob.Error.String != "" {
247-
return "", time.Time{}, xerrors.Errorf("last workspace build did not complete successfully")
230+
if !isEligibleForTransition(ws, latestBuild, templateSchedule) {
231+
return "", "", xerrors.Errorf("workspace ineligible for transition")
248232
}
249233

250-
switch priorHistory.Transition {
251-
case database.WorkspaceTransitionStart:
252-
if priorHistory.Deadline.IsZero() {
253-
return "", time.Time{}, xerrors.Errorf("latest workspace build has zero deadline")
254-
}
255-
// For stopping, do not truncate. This is inconsistent with autostart, but
256-
// it ensures we will not stop too early.
257-
return database.WorkspaceTransitionStop, priorHistory.Deadline, nil
258-
case database.WorkspaceTransitionStop:
259-
sched, err := schedule.Weekly(ws.AutostartSchedule.String)
260-
if err != nil {
261-
return "", time.Time{}, xerrors.Errorf("workspace has invalid autostart schedule: %w", err)
262-
}
263-
// Round down to the nearest minute, as this is the finest granularity cron supports.
264-
// Truncate is probably not necessary here, but doing it anyway to be sure.
265-
nextTransition = sched.Next(priorHistory.CreatedAt).Truncate(time.Minute)
266-
return database.WorkspaceTransitionStart, nextTransition, nil
234+
if !latestJob.CompletedAt.Valid || latestJob.Error.String != "" {
235+
return "", "", xerrors.Errorf("last workspace build did not complete successfully")
236+
}
237+
238+
switch {
239+
case isEligibleForAutostop(latestBuild, currentTick):
240+
return database.WorkspaceTransitionStop, database.BuildReasonAutostop, nil
241+
case isEligibleForAutostart(ws, latestBuild, currentTick):
242+
return database.WorkspaceTransitionStart, database.BuildReasonAutostart, nil
267243
default:
268-
return "", time.Time{}, xerrors.Errorf("last transition not valid for autostart or autostop")
244+
return "", "", xerrors.Errorf("last transition not valid for autostart or autostop")
245+
}
246+
}
247+
248+
// isEligibleForAutostart returns true if the workspace should be autostarted.
249+
func isEligibleForAutostart(ws database.Workspace, build database.WorkspaceBuild, currentTick time.Time) bool {
250+
// If the last transition for the workspace was not 'stop' then the workspace
251+
// cannot be started.
252+
if build.Transition != database.WorkspaceTransitionStop {
253+
return false
254+
}
255+
256+
sched, err := schedule.Weekly(ws.AutostartSchedule.String)
257+
if err != nil {
258+
return false
269259
}
260+
// Round down to the nearest minute, as this is the finest granularity cron supports.
261+
// Truncate is probably not necessary here, but doing it anyway to be sure.
262+
nextTransition := sched.Next(build.CreatedAt).Truncate(time.Minute)
263+
264+
return !currentTick.Before(nextTransition)
265+
}
266+
267+
// isEligibleForAutostart returns true if the workspace should be autostopped.
268+
func isEligibleForAutostop(build database.WorkspaceBuild, currentTick time.Time) bool {
269+
// A workspace must be started in order for it to be auto-stopped.
270+
return build.Transition == database.WorkspaceTransitionStart &&
271+
!build.Deadline.IsZero() &&
272+
// We do not want to stop a workspace prior to it breaching its deadline.
273+
!currentTick.Before(build.Deadline)
270274
}

coderd/autobuild/executor/lifecycle_executor_test.go renamed to coderd/autobuild/lifecycle_executor_test.go

Lines changed: 18 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
package executor_test
1+
package autobuild_test
22

33
import (
44
"context"
@@ -11,7 +11,7 @@ import (
1111
"github.com/stretchr/testify/require"
1212
"go.uber.org/goleak"
1313

14-
"github.com/coder/coder/coderd/autobuild/executor"
14+
"github.com/coder/coder/coderd/autobuild"
1515
"github.com/coder/coder/coderd/coderdtest"
1616
"github.com/coder/coder/coderd/database"
1717
"github.com/coder/coder/coderd/schedule"
@@ -27,7 +27,7 @@ func TestExecutorAutostartOK(t *testing.T) {
2727
var (
2828
sched = mustSchedule(t, "CRON_TZ=UTC 0 * * * *")
2929
tickCh = make(chan time.Time)
30-
statsCh = make(chan executor.Stats)
30+
statsCh = make(chan autobuild.Stats)
3131
client = coderdtest.New(t, &coderdtest.Options{
3232
AutobuildTicker: tickCh,
3333
IncludeProvisionerDaemon: true,
@@ -66,7 +66,7 @@ func TestExecutorAutostartTemplateUpdated(t *testing.T) {
6666
ctx = context.Background()
6767
err error
6868
tickCh = make(chan time.Time)
69-
statsCh = make(chan executor.Stats)
69+
statsCh = make(chan autobuild.Stats)
7070
client = coderdtest.New(t, &coderdtest.Options{
7171
AutobuildTicker: tickCh,
7272
IncludeProvisionerDaemon: true,
@@ -113,7 +113,7 @@ func TestExecutorAutostartAlreadyRunning(t *testing.T) {
113113
var (
114114
sched = mustSchedule(t, "CRON_TZ=UTC 0 * * * *")
115115
tickCh = make(chan time.Time)
116-
statsCh = make(chan executor.Stats)
116+
statsCh = make(chan autobuild.Stats)
117117
client = coderdtest.New(t, &coderdtest.Options{
118118
AutobuildTicker: tickCh,
119119
IncludeProvisionerDaemon: true,
@@ -145,7 +145,7 @@ func TestExecutorAutostartNotEnabled(t *testing.T) {
145145

146146
var (
147147
tickCh = make(chan time.Time)
148-
statsCh = make(chan executor.Stats)
148+
statsCh = make(chan autobuild.Stats)
149149
client = coderdtest.New(t, &coderdtest.Options{
150150
AutobuildTicker: tickCh,
151151
IncludeProvisionerDaemon: true,
@@ -180,7 +180,7 @@ func TestExecutorAutostopOK(t *testing.T) {
180180

181181
var (
182182
tickCh = make(chan time.Time)
183-
statsCh = make(chan executor.Stats)
183+
statsCh = make(chan autobuild.Stats)
184184
client = coderdtest.New(t, &coderdtest.Options{
185185
AutobuildTicker: tickCh,
186186
IncludeProvisionerDaemon: true,
@@ -216,7 +216,7 @@ func TestExecutorAutostopExtend(t *testing.T) {
216216
var (
217217
ctx = context.Background()
218218
tickCh = make(chan time.Time)
219-
statsCh = make(chan executor.Stats)
219+
statsCh = make(chan autobuild.Stats)
220220
client = coderdtest.New(t, &coderdtest.Options{
221221
AutobuildTicker: tickCh,
222222
IncludeProvisionerDaemon: true,
@@ -266,7 +266,7 @@ func TestExecutorAutostopAlreadyStopped(t *testing.T) {
266266

267267
var (
268268
tickCh = make(chan time.Time)
269-
statsCh = make(chan executor.Stats)
269+
statsCh = make(chan autobuild.Stats)
270270
client = coderdtest.New(t, &coderdtest.Options{
271271
AutobuildTicker: tickCh,
272272
IncludeProvisionerDaemon: true,
@@ -299,7 +299,7 @@ func TestExecutorAutostopNotEnabled(t *testing.T) {
299299
var (
300300
ctx = context.Background()
301301
tickCh = make(chan time.Time)
302-
statsCh = make(chan executor.Stats)
302+
statsCh = make(chan autobuild.Stats)
303303
client = coderdtest.New(t, &coderdtest.Options{
304304
AutobuildTicker: tickCh,
305305
IncludeProvisionerDaemon: true,
@@ -341,7 +341,7 @@ func TestExecutorWorkspaceDeleted(t *testing.T) {
341341
var (
342342
sched = mustSchedule(t, "CRON_TZ=UTC 0 * * * *")
343343
tickCh = make(chan time.Time)
344-
statsCh = make(chan executor.Stats)
344+
statsCh = make(chan autobuild.Stats)
345345
client = coderdtest.New(t, &coderdtest.Options{
346346
AutobuildTicker: tickCh,
347347
IncludeProvisionerDaemon: true,
@@ -374,7 +374,7 @@ func TestExecutorWorkspaceAutostartTooEarly(t *testing.T) {
374374
var (
375375
sched = mustSchedule(t, "CRON_TZ=UTC 0 * * * *")
376376
tickCh = make(chan time.Time)
377-
statsCh = make(chan executor.Stats)
377+
statsCh = make(chan autobuild.Stats)
378378
client = coderdtest.New(t, &coderdtest.Options{
379379
AutobuildTicker: tickCh,
380380
IncludeProvisionerDaemon: true,
@@ -405,7 +405,7 @@ func TestExecutorWorkspaceAutostopBeforeDeadline(t *testing.T) {
405405

406406
var (
407407
tickCh = make(chan time.Time)
408-
statsCh = make(chan executor.Stats)
408+
statsCh = make(chan autobuild.Stats)
409409
client = coderdtest.New(t, &coderdtest.Options{
410410
AutobuildTicker: tickCh,
411411
IncludeProvisionerDaemon: true,
@@ -433,7 +433,7 @@ func TestExecutorWorkspaceAutostopNoWaitChangedMyMind(t *testing.T) {
433433
var (
434434
ctx = context.Background()
435435
tickCh = make(chan time.Time)
436-
statsCh = make(chan executor.Stats)
436+
statsCh = make(chan autobuild.Stats)
437437
client = coderdtest.New(t, &coderdtest.Options{
438438
AutobuildTicker: tickCh,
439439
IncludeProvisionerDaemon: true,
@@ -501,8 +501,8 @@ func TestExecutorAutostartMultipleOK(t *testing.T) {
501501
sched = mustSchedule(t, "CRON_TZ=UTC 0 * * * *")
502502
tickCh = make(chan time.Time)
503503
tickCh2 = make(chan time.Time)
504-
statsCh1 = make(chan executor.Stats)
505-
statsCh2 = make(chan executor.Stats)
504+
statsCh1 = make(chan autobuild.Stats)
505+
statsCh2 = make(chan autobuild.Stats)
506506
client = coderdtest.New(t, &coderdtest.Options{
507507
AutobuildTicker: tickCh,
508508
IncludeProvisionerDaemon: true,
@@ -556,7 +556,7 @@ func TestExecutorAutostartWithParameters(t *testing.T) {
556556
var (
557557
sched = mustSchedule(t, "CRON_TZ=UTC 0 * * * *")
558558
tickCh = make(chan time.Time)
559-
statsCh = make(chan executor.Stats)
559+
statsCh = make(chan autobuild.Stats)
560560
client = coderdtest.New(t, &coderdtest.Options{
561561
AutobuildTicker: tickCh,
562562
IncludeProvisionerDaemon: true,
@@ -609,7 +609,7 @@ func TestExecutorAutostartTemplateDisabled(t *testing.T) {
609609
var (
610610
sched = mustSchedule(t, "CRON_TZ=UTC 0 * * * *")
611611
tickCh = make(chan time.Time)
612-
statsCh = make(chan executor.Stats)
612+
statsCh = make(chan autobuild.Stats)
613613

614614
client = coderdtest.New(t, &coderdtest.Options{
615615
AutobuildTicker: tickCh,

coderd/autobuild/notify/notifier_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import (
99
"go.uber.org/atomic"
1010
"go.uber.org/goleak"
1111

12-
"github.com/coder/coder/coderd/autobuild/notify"
12+
"github.com/coder/coder/coderd/wsactions/notify"
1313
)
1414

1515
func TestNotifier(t *testing.T) {

0 commit comments

Comments
 (0)