@@ -9,15 +9,12 @@ import (
9
9
10
10
"github.com/coder/coder/coderd/autostart/schedule"
11
11
"github.com/coder/coder/coderd/database"
12
- "github.com/coder/coder/codersdk"
13
12
14
13
"github.com/google/uuid"
15
14
"github.com/moby/moby/pkg/namesgenerator"
16
15
"golang.org/x/xerrors"
17
16
)
18
17
19
- //var ExecutorUUID = uuid.MustParse("00000000-0000-0000-0000-000000000000")
20
-
21
18
// Executor executes automated workspace lifecycle operations.
22
19
type Executor struct {
23
20
ctx context.Context
@@ -26,6 +23,7 @@ type Executor struct {
26
23
tick <- chan time.Time
27
24
}
28
25
26
+ // NewExecutor returns a new instance of Executor.
29
27
func NewExecutor (ctx context.Context , db database.Store , log slog.Logger , tick <- chan time.Time ) * Executor {
30
28
le := & Executor {
31
29
ctx : ctx ,
@@ -36,15 +34,18 @@ func NewExecutor(ctx context.Context, db database.Store, log slog.Logger, tick <
36
34
return le
37
35
}
38
36
39
- func (e * Executor ) Run () error {
37
+ // Run will cause executor to run workspace lifecycle operations on every
38
+ // tick from its channel. It will stop when its context is Done, or when
39
+ // its channel is closed.
40
+ func (e * Executor ) Run () {
40
41
for {
41
42
select {
42
43
case t := <- e .tick :
43
44
if err := e .runOnce (t ); err != nil {
44
45
e .log .Error (e .ctx , "error running once" , slog .Error (err ))
45
46
}
46
47
case <- e .ctx .Done ():
47
- return nil
48
+ return
48
49
default :
49
50
}
50
51
}
@@ -53,65 +54,89 @@ func (e *Executor) Run() error {
53
54
func (e * Executor ) runOnce (t time.Time ) error {
54
55
currentTick := t .Round (time .Minute )
55
56
return e .db .InTx (func (db database.Store ) error {
56
- autostartWorkspaces , err := db .GetWorkspacesAutostart (e .ctx )
57
+ eligibleWorkspaces , err := db .GetWorkspacesAutostartAutostop (e .ctx )
57
58
if err != nil {
58
- return xerrors .Errorf ("get all workspaces: %w" , err )
59
+ return xerrors .Errorf ("get eligible workspaces for autostart or autostop : %w" , err )
59
60
}
60
61
61
- for _ , ws := range autostartWorkspaces {
62
- sched , err := schedule .Weekly (ws .AutostartSchedule .String )
63
- if err != nil {
64
- e .log .Warn (e .ctx , "workspace has invalid autostart schedule" ,
65
- slog .F ("workspace_id" , ws .ID ),
66
- slog .F ("autostart_schedule" , ws .AutostartSchedule .String ),
67
- )
68
- continue
69
- }
70
-
71
- // Determine the workspace state based on its latest build. We expect it to be stopped.
62
+ for _ , ws := range eligibleWorkspaces {
63
+ // Determine the workspace state based on its latest build.
72
64
latestBuild , err := db .GetWorkspaceBuildByWorkspaceIDWithoutAfter (e .ctx , ws .ID )
73
65
if err != nil {
74
66
return xerrors .Errorf ("get latest build for workspace %q: %w" , ws .ID , err )
75
67
}
76
- if latestBuild .Transition != database .WorkspaceTransitionStop {
77
- e .log .Debug (e .ctx , "autostart: skipping workspace: wrong transition" ,
78
- slog .F ("transition" , latestBuild .Transition ),
68
+
69
+ var validTransition database.WorkspaceTransition
70
+ var sched * schedule.Schedule
71
+ switch latestBuild .Transition {
72
+ case database .WorkspaceTransitionStart :
73
+ validTransition = database .WorkspaceTransitionStop
74
+ sched , err = schedule .Weekly (ws .AutostopSchedule .String )
75
+ if err != nil {
76
+ e .log .Warn (e .ctx , "workspace has invalid autostop schedule, skipping" ,
77
+ slog .F ("workspace_id" , ws .ID ),
78
+ slog .F ("autostart_schedule" , ws .AutostopSchedule .String ),
79
+ )
80
+ continue
81
+ }
82
+ case database .WorkspaceTransitionStop :
83
+ validTransition = database .WorkspaceTransitionStart
84
+ sched , err = schedule .Weekly (ws .AutostartSchedule .String )
85
+ if err != nil {
86
+ e .log .Warn (e .ctx , "workspace has invalid autostart schedule, skipping" ,
87
+ slog .F ("workspace_id" , ws .ID ),
88
+ slog .F ("autostart_schedule" , ws .AutostartSchedule .String ),
89
+ )
90
+ continue
91
+ }
92
+ default :
93
+ e .log .Debug (e .ctx , "last transition not valid for autostart or autostop" ,
79
94
slog .F ("workspace_id" , ws .ID ),
95
+ slog .F ("latest_build_transition" , latestBuild .Transition ),
80
96
)
81
- continue
82
97
}
83
98
84
99
// Round time to the nearest minute, as this is the finest granularity cron supports.
85
- earliestAutostart := sched .Next (latestBuild .CreatedAt ).Round (time .Minute )
86
- if earliestAutostart .After (currentTick ) {
87
- e .log .Debug (e .ctx , "autostart: skipping workspace: too early" ,
100
+ nextTransitionAt := sched .Next (latestBuild .CreatedAt ).Round (time .Minute )
101
+ if nextTransitionAt .After (currentTick ) {
102
+ e .log .Debug (e .ctx , "skipping workspace: too early" ,
88
103
slog .F ("workspace_id" , ws .ID ),
89
- slog .F ("earliest_autostart" , earliestAutostart ),
104
+ slog .F ("next_transition_at" , nextTransitionAt ),
105
+ slog .F ("transition" , validTransition ),
90
106
slog .F ("current_tick" , currentTick ),
91
107
)
92
108
continue
93
109
}
94
110
95
- e .log .Info (e .ctx , "autostart: scheduling workspace start " ,
111
+ e .log .Info (e .ctx , "scheduling workspace transition " ,
96
112
slog .F ("workspace_id" , ws .ID ),
113
+ slog .F ("transition" , validTransition ),
97
114
)
98
115
99
- if err := doBuild (e .ctx , db , ws , currentTick ); err != nil {
100
- e .log .Error (e .ctx , "autostart workspace" , slog .F ("workspace_id" , ws .ID ), slog .Error (err ))
116
+ if err := doBuild (e .ctx , db , ws , validTransition ); err != nil {
117
+ e .log .Error (e .ctx , "transition workspace" ,
118
+ slog .F ("workspace_id" , ws .ID ),
119
+ slog .F ("transition" , validTransition ),
120
+ slog .Error (err ),
121
+ )
101
122
}
102
123
}
103
124
return nil
104
125
})
105
126
}
106
127
107
- // XXX: cian: this function shouldn't really exist . Refactor.
108
- func doBuild (ctx context.Context , store database.Store , workspace database.Workspace , now time. Time ) error {
128
+ // TODO( cian) : this function duplicates most of api.postWorkspaceBuilds . Refactor.
129
+ func doBuild (ctx context.Context , store database.Store , workspace database.Workspace , trans database. WorkspaceTransition ) error {
109
130
template , err := store .GetTemplateByID (ctx , workspace .TemplateID )
110
131
if err != nil {
111
132
return xerrors .Errorf ("get template: %w" , err )
112
133
}
113
134
114
135
priorHistory , err := store .GetWorkspaceBuildByWorkspaceIDWithoutAfter (ctx , workspace .ID )
136
+ if err != nil {
137
+ return xerrors .Errorf ("get prior history: %w" , err )
138
+ }
139
+
115
140
priorJob , err := store .GetProvisionerJobByID (ctx , priorHistory .JobID )
116
141
if err == nil && ! priorJob .CompletedAt .Valid {
117
142
return xerrors .Errorf ("workspace build already active" )
@@ -160,7 +185,7 @@ func doBuild(ctx context.Context, store database.Store, workspace database.Works
160
185
Name : namesgenerator .GetRandomName (1 ),
161
186
ProvisionerState : priorHistory .ProvisionerState ,
162
187
InitiatorID : workspace .OwnerID ,
163
- Transition : database . WorkspaceTransitionStart ,
188
+ Transition : trans ,
164
189
JobID : newProvisionerJob .ID ,
165
190
})
166
191
if err != nil {
@@ -184,24 +209,3 @@ func doBuild(ctx context.Context, store database.Store, workspace database.Works
184
209
}
185
210
return nil
186
211
}
187
-
188
- func provisionerJobStatus (j database.ProvisionerJob , now time.Time ) codersdk.ProvisionerJobStatus {
189
- switch {
190
- case j .CanceledAt .Valid :
191
- if j .CompletedAt .Valid {
192
- return codersdk .ProvisionerJobCanceled
193
- }
194
- return codersdk .ProvisionerJobCanceling
195
- case ! j .StartedAt .Valid :
196
- return codersdk .ProvisionerJobPending
197
- case j .CompletedAt .Valid :
198
- if j .Error .String == "" {
199
- return codersdk .ProvisionerJobSucceeded
200
- }
201
- return codersdk .ProvisionerJobFailed
202
- case now .Sub (j .UpdatedAt ) > 30 * time .Second :
203
- return codersdk .ProvisionerJobFailed
204
- default :
205
- return codersdk .ProvisionerJobRunning
206
- }
207
- }
0 commit comments