1
- package executor
1
+ package autobuild
2
2
3
3
import (
4
4
"context"
@@ -35,8 +35,8 @@ type Stats struct {
35
35
Error error
36
36
}
37
37
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 {
40
40
le := & Executor {
41
41
//nolint:gocritic // Autostart has a limited set of permissions.
42
42
ctx : dbauthz .AsAutostart (ctx ),
@@ -125,77 +125,57 @@ func (e *Executor) runOnce(t time.Time) Stats {
125
125
log := e .log .With (slog .F ("workspace_id" , wsID ))
126
126
127
127
eg .Go (func () error {
128
- err := e .db .InTx (func (db database.Store ) error {
128
+ err := e .db .InTx (func (tx database.Store ) error {
129
129
// Re-check eligibility since the first check was outside the
130
130
// transaction and the workspace settings may have changed.
131
- ws , err := db .GetWorkspaceByID (e .ctx , wsID )
131
+ ws , err := tx .GetWorkspaceByID (e .ctx , wsID )
132
132
if err != nil {
133
133
log .Error (e .ctx , "get workspace autostart failed" , slog .Error (err ))
134
134
return nil
135
135
}
136
136
137
137
// 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 )
139
139
if err != nil {
140
140
log .Warn (e .ctx , "get latest workspace build" , slog .Error (err ))
141
141
return nil
142
142
}
143
143
144
- templateSchedule , err := (* (e .templateScheduleStore .Load ())).GetTemplateScheduleOptions (e .ctx , db , ws .TemplateID )
144
+ templateSchedule , err := (* (e .templateScheduleStore .Load ())).GetTemplateScheduleOptions (e .ctx , tx , ws .TemplateID )
145
145
if err != nil {
146
146
log .Warn (e .ctx , "get template schedule options" , slog .Error (err ))
147
147
return nil
148
148
}
149
149
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 )
155
151
if err != nil {
156
152
log .Warn (e .ctx , "get last provisioner job for workspace %q: %w" , slog .Error (err ))
157
153
return nil
158
154
}
159
155
160
- validTransition , nextTransition , err := getNextTransition (ws , priorHistory , priorJob )
156
+ nextTransition , reason , err := getNextTransition (ws , latestBuild , latestJob , templateSchedule , currentTick )
161
157
if err != nil {
162
158
log .Debug (e .ctx , "skipping workspace" , slog .Error (err ))
163
159
return nil
164
160
}
165
161
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 {
188
168
log .Error (e .ctx , "unable to transition workspace" ,
189
- slog .F ("transition" , validTransition ),
169
+ slog .F ("transition" , nextTransition ),
190
170
slog .Error (err ),
191
171
)
192
172
return nil
193
173
}
194
174
statsMu .Lock ()
195
- stats .Transitions [ws .ID ] = validTransition
175
+ stats .Transitions [ws .ID ] = nextTransition
196
176
statsMu .Unlock ()
197
177
198
- log .Info (e .ctx , "scheduling workspace transition" , slog .F ("transition" , validTransition ))
178
+ log .Info (e .ctx , "scheduling workspace transition" , slog .F ("transition" , nextTransition ))
199
179
200
180
return nil
201
181
@@ -218,7 +198,9 @@ func (e *Executor) runOnce(t time.Time) Stats {
218
198
return stats
219
199
}
220
200
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 {
222
204
if ws .Deleted {
223
205
return false
224
206
}
@@ -227,7 +209,7 @@ func isEligibleForAutoStartStop(ws database.Workspace, priorHistory database.Wor
227
209
}
228
210
// Don't check the template schedule to see whether it allows autostop, this
229
211
// 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 () {
231
213
return true
232
214
}
233
215
@@ -236,35 +218,57 @@ func isEligibleForAutoStartStop(ws database.Workspace, priorHistory database.Wor
236
218
237
219
func getNextTransition (
238
220
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 ,
241
225
) (
242
- validTransition database.WorkspaceTransition ,
243
- nextTransition time. Time ,
244
- err error ,
226
+ database.WorkspaceTransition ,
227
+ database. BuildReason ,
228
+ error ,
245
229
) {
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 " )
248
232
}
249
233
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
267
243
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
269
259
}
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 )
270
274
}
0 commit comments