@@ -160,23 +160,65 @@ func (e *Executor) runOnce(t time.Time) Stats {
160
160
return nil
161
161
}
162
162
163
- builder := wsbuilder .New (ws , nextTransition ).
164
- SetLastWorkspaceBuildInTx (& latestBuild ).
165
- SetLastWorkspaceBuildJobInTx (& latestJob ).
166
- Reason (reason )
167
-
168
- if _ , _ , err := builder .Build (e .ctx , tx , nil ); err != nil {
169
- log .Error (e .ctx , "workspace build error" ,
170
- slog .F ("transition" , nextTransition ),
171
- slog .Error (err ),
163
+ if nextTransition != "" {
164
+ builder := wsbuilder .New (ws , nextTransition ).
165
+ SetLastWorkspaceBuildInTx (& latestBuild ).
166
+ SetLastWorkspaceBuildJobInTx (& latestJob ).
167
+ Reason (reason )
168
+
169
+ if _ , _ , err := builder .Build (e .ctx , tx , nil ); err != nil {
170
+ log .Error (e .ctx , "unable to transition workspace" ,
171
+ slog .F ("transition" , nextTransition ),
172
+ slog .Error (err ),
173
+ )
174
+ return nil
175
+ }
176
+ }
177
+
178
+ // Lock the workspace if it has breached the template's
179
+ // threshold for inactivity.
180
+ if reason == database .BuildReasonAutolock {
181
+ err = tx .UpdateWorkspaceLockedAt (e .ctx , database.UpdateWorkspaceLockedAtParams {
182
+ ID : ws .ID ,
183
+ LockedAt : sql.NullTime {
184
+ Time : database .Now (),
185
+ Valid : true ,
186
+ },
187
+ })
188
+ if err != nil {
189
+ log .Error (e .ctx , "unable to lock workspace" ,
190
+ slog .F ("transition" , nextTransition ),
191
+ slog .Error (err ),
192
+ )
193
+ return nil
194
+ }
195
+
196
+ log .Info (e .ctx , "locked workspace" ,
197
+ slog .F ("last_used_at" , ws .LastUsedAt ),
198
+ slog .F ("inactivity_ttl" , templateSchedule .InactivityTTL ),
199
+ slog .F ("since_last_used_at" , time .Since (ws .LastUsedAt )),
200
+ )
201
+ }
202
+
203
+ if reason == database .BuildReasonAutodelete {
204
+ log .Info (e .ctx , "deleted workspace" ,
205
+ slog .F ("locked_at" , ws .LockedAt .Time ),
206
+ slog .F ("locked_ttl" , templateSchedule .LockedTTL ),
172
207
)
208
+ }
209
+
210
+ if nextTransition == "" {
173
211
return nil
174
212
}
213
+
175
214
statsMu .Lock ()
176
215
stats .Transitions [ws .ID ] = nextTransition
177
216
statsMu .Unlock ()
178
217
179
- log .Info (e .ctx , "scheduling workspace transition" , slog .F ("transition" , nextTransition ))
218
+ log .Info (e .ctx , "scheduling workspace transition" ,
219
+ slog .F ("transition" , nextTransition ),
220
+ slog .F ("reason" , reason ),
221
+ )
180
222
181
223
return nil
182
224
@@ -199,6 +241,12 @@ func (e *Executor) runOnce(t time.Time) Stats {
199
241
return stats
200
242
}
201
243
244
+ // getNextTransition returns the next eligible transition for the workspace
245
+ // as well as the reason for why it is transitioning. It is possible
246
+ // for this function to return a nil error as well as an empty transition.
247
+ // In such cases it means no provisioning should occur but the workspace
248
+ // may be "transitioning" to a new state (such as an inactive, stopped
249
+ // workspace transitioning to the locked state).
202
250
func getNextTransition (
203
251
ws database.Workspace ,
204
252
latestBuild database.WorkspaceBuild ,
@@ -211,12 +259,23 @@ func getNextTransition(
211
259
error ,
212
260
) {
213
261
switch {
214
- case isEligibleForAutostop (latestBuild , latestJob , currentTick ):
262
+ case isEligibleForAutostop (ws , latestBuild , latestJob , currentTick ):
215
263
return database .WorkspaceTransitionStop , database .BuildReasonAutostop , nil
216
264
case isEligibleForAutostart (ws , latestBuild , latestJob , templateSchedule , currentTick ):
217
265
return database .WorkspaceTransitionStart , database .BuildReasonAutostart , nil
218
- case isEligibleForFailedStop (latestBuild , latestJob , templateSchedule ):
266
+ case isEligibleForFailedStop (latestBuild , latestJob , templateSchedule , currentTick ):
219
267
return database .WorkspaceTransitionStop , database .BuildReasonAutostop , nil
268
+ case isEligibleForLockedStop (ws , templateSchedule , currentTick ):
269
+ // Only stop started workspaces.
270
+ if latestBuild .Transition == database .WorkspaceTransitionStart {
271
+ return database .WorkspaceTransitionStop , database .BuildReasonAutolock , nil
272
+ }
273
+ // We shouldn't transition the workspace but we should still
274
+ // lock it.
275
+ return "" , database .BuildReasonAutolock , nil
276
+
277
+ case isEligibleForDelete (ws , templateSchedule , currentTick ):
278
+ return database .WorkspaceTransitionDelete , database .BuildReasonAutodelete , nil
220
279
default :
221
280
return "" , "" , xerrors .Errorf ("last transition not valid for autostart or autostop" )
222
281
}
@@ -225,7 +284,12 @@ func getNextTransition(
225
284
// isEligibleForAutostart returns true if the workspace should be autostarted.
226
285
func isEligibleForAutostart (ws database.Workspace , build database.WorkspaceBuild , job database.ProvisionerJob , templateSchedule schedule.TemplateScheduleOptions , currentTick time.Time ) bool {
227
286
// Don't attempt to autostart failed workspaces.
228
- if ! job .CompletedAt .Valid || job .Error .String != "" {
287
+ if db2sdk .ProvisionerJobStatus (job ) == codersdk .ProvisionerJobFailed {
288
+ return false
289
+ }
290
+
291
+ // If the workspace is locked we should not autostart it.
292
+ if ws .LockedAt .Valid {
229
293
return false
230
294
}
231
295
@@ -253,9 +317,13 @@ func isEligibleForAutostart(ws database.Workspace, build database.WorkspaceBuild
253
317
}
254
318
255
319
// isEligibleForAutostart returns true if the workspace should be autostopped.
256
- func isEligibleForAutostop (build database.WorkspaceBuild , job database.ProvisionerJob , currentTick time.Time ) bool {
257
- // Don't attempt to autostop failed workspaces.
258
- if ! job .CompletedAt .Valid || job .Error .String != "" {
320
+ func isEligibleForAutostop (ws database.Workspace , build database.WorkspaceBuild , job database.ProvisionerJob , currentTick time.Time ) bool {
321
+ if db2sdk .ProvisionerJobStatus (job ) == codersdk .ProvisionerJobFailed {
322
+ return false
323
+ }
324
+
325
+ // If the workspace is locked we should not autostop it.
326
+ if ws .LockedAt .Valid {
259
327
return false
260
328
}
261
329
@@ -266,14 +334,35 @@ func isEligibleForAutostop(build database.WorkspaceBuild, job database.Provision
266
334
! currentTick .Before (build .Deadline )
267
335
}
268
336
337
+ // isEligibleForLockedStop returns true if the workspace should be locked
338
+ // for breaching the inactivity threshold of the template.
339
+ func isEligibleForLockedStop (ws database.Workspace , templateSchedule schedule.TemplateScheduleOptions , currentTick time.Time ) bool {
340
+ // Only attempt to lock workspaces not already locked.
341
+ return ! ws .LockedAt .Valid &&
342
+ // The template must specify an inactivity TTL.
343
+ templateSchedule .InactivityTTL > 0 &&
344
+ // The workspace must breach the inactivity TTL.
345
+ currentTick .Sub (ws .LastUsedAt ) > templateSchedule .InactivityTTL
346
+ }
347
+
348
+ func isEligibleForDelete (ws database.Workspace , templateSchedule schedule.TemplateScheduleOptions , currentTick time.Time ) bool {
349
+ // Only attempt to delete locked workspaces.
350
+ return ws .LockedAt .Valid &&
351
+ // Locked workspaces should only be deleted if a locked_ttl is specified.
352
+ templateSchedule .LockedTTL > 0 &&
353
+ // The workspace must breach the locked_ttl.
354
+ currentTick .Sub (ws .LockedAt .Time ) > templateSchedule .LockedTTL
355
+ }
356
+
269
357
// isEligibleForFailedStop returns true if the workspace is eligible to be stopped
270
358
// due to a failed build.
271
- func isEligibleForFailedStop (build database.WorkspaceBuild , job database.ProvisionerJob , templateSchedule schedule.TemplateScheduleOptions ) bool {
359
+ func isEligibleForFailedStop (build database.WorkspaceBuild , job database.ProvisionerJob , templateSchedule schedule.TemplateScheduleOptions , currentTick time. Time ) bool {
272
360
// If the template has specified a failure TLL.
273
361
return templateSchedule .FailureTTL > 0 &&
274
362
// And the job resulted in failure.
275
363
db2sdk .ProvisionerJobStatus (job ) == codersdk .ProvisionerJobFailed &&
276
364
build .Transition == database .WorkspaceTransitionStart &&
277
365
// And sufficient time has elapsed since the job has completed.
278
- job .CompletedAt .Valid && database .Now ().Sub (job .CompletedAt .Time ) > templateSchedule .FailureTTL
366
+ job .CompletedAt .Valid &&
367
+ currentTick .Sub (job .CompletedAt .Time ) > templateSchedule .FailureTTL
279
368
}
0 commit comments