@@ -5,14 +5,14 @@ import (
5
5
"encoding/json"
6
6
"time"
7
7
8
- "cdr.dev/slog"
9
-
10
- "github.com/coder/coder/coderd/autobuild/schedule"
11
- "github.com/coder/coder/coderd/database"
12
-
13
8
"github.com/google/uuid"
14
9
"github.com/moby/moby/pkg/namesgenerator"
10
+ "golang.org/x/sync/errgroup"
15
11
"golang.org/x/xerrors"
12
+
13
+ "cdr.dev/slog"
14
+ "github.com/coder/coder/coderd/autobuild/schedule"
15
+ "github.com/coder/coder/coderd/database"
16
16
)
17
17
18
18
// Executor automatically starts or stops workspaces.
@@ -89,80 +89,116 @@ func (e *Executor) runOnce(t time.Time) Stats {
89
89
stats .Error = err
90
90
}()
91
91
currentTick := t .Truncate (time .Minute )
92
- err = e .db .InTx (func (db database.Store ) error {
93
- // TTL is set at the workspace level, and deadline at the workspace build level.
94
- // When a workspace build is created, its deadline initially starts at zero.
95
- // When provisionerd successfully completes a provision job, the deadline is
96
- // set to now + TTL if the associated workspace has a TTL set. This deadline
97
- // is what we compare against when performing autostop operations, rounded down
98
- // to the minute.
99
- //
100
- // NOTE: If a workspace build is created with a given TTL and then the user either
101
- // changes or unsets the TTL, the deadline for the workspace build will not
102
- // have changed. This behavior is as expected per #2229.
103
- eligibleWorkspaces , err := db .GetWorkspacesAutostart (e .ctx )
104
- if err != nil {
105
- return xerrors .Errorf ("get eligible workspaces for autostart or autostop: %w" , err )
92
+
93
+ // TTL is set at the workspace level, and deadline at the workspace build level.
94
+ // When a workspace build is created, its deadline initially starts at zero.
95
+ // When provisionerd successfully completes a provision job, the deadline is
96
+ // set to now + TTL if the associated workspace has a TTL set. This deadline
97
+ // is what we compare against when performing autostop operations, rounded down
98
+ // to the minute.
99
+ //
100
+ // NOTE: If a workspace build is created with a given TTL and then the user either
101
+ // changes or unsets the TTL, the deadline for the workspace build will not
102
+ // have changed. This behavior is as expected per #2229.
103
+ workspaces , err := e .db .GetWorkspaces (e .ctx , database.GetWorkspacesParams {
104
+ Deleted : false ,
105
+ })
106
+ if err != nil {
107
+ e .log .Error (e .ctx , "get workspaces for autostart or autostop" , slog .Error (err ))
108
+ return stats
109
+ }
110
+
111
+ var eligibleWorkspaceIDs []uuid.UUID
112
+ for _ , ws := range workspaces {
113
+ if isEligibleForAutoStartStop (ws ) {
114
+ eligibleWorkspaceIDs = append (eligibleWorkspaceIDs , ws .ID )
106
115
}
116
+ }
107
117
108
- for _ , ws := range eligibleWorkspaces {
109
- // Determine the workspace state based on its latest build.
110
- priorHistory , err := db .GetLatestWorkspaceBuildByWorkspaceID (e .ctx , ws .ID )
111
- if err != nil {
112
- e .log .Warn (e .ctx , "get latest workspace build" ,
113
- slog .F ("workspace_id" , ws .ID ),
114
- slog .Error (err ),
115
- )
116
- continue
117
- }
118
+ // We only use errgroup here for convenience of API, not for early
119
+ // cancellation. This means we only return nil errors in th eg.Go.
120
+ eg := errgroup.Group {}
121
+ // Limit the concurrency to avoid overloading the database.
122
+ eg .SetLimit (10 )
118
123
119
- priorJob , err := db .GetProvisionerJobByID (e .ctx , priorHistory .JobID )
120
- if err != nil {
121
- e .log .Warn (e .ctx , "get last provisioner job for workspace %q: %w" ,
122
- slog .F ("workspace_id" , ws .ID ),
123
- slog .Error (err ),
124
- )
125
- continue
126
- }
124
+ for _ , wsID := range eligibleWorkspaceIDs {
125
+ wsID := wsID
126
+ log := e .log .With (slog .F ("workspace_id" , wsID ))
127
127
128
- validTransition , nextTransition , err := getNextTransition (ws , priorHistory , priorJob )
129
- if err != nil {
130
- e .log .Debug (e .ctx , "skipping workspace" ,
131
- slog .Error (err ),
132
- slog .F ("workspace_id" , ws .ID ),
133
- )
134
- continue
135
- }
128
+ eg .Go (func () error {
129
+ err := e .db .InTx (func (db database.Store ) error {
130
+ // Re-check eligibility since the first check was outside the
131
+ // transaction and the workspace settings may have changed.
132
+ ws , err := db .GetWorkspaceByID (e .ctx , wsID )
133
+ if err != nil {
134
+ log .Error (e .ctx , "get workspace autostart failed" , slog .Error (err ))
135
+ return nil
136
+ }
137
+ if ! isEligibleForAutoStartStop (ws ) {
138
+ return nil
139
+ }
136
140
137
- if currentTick .Before (nextTransition ) {
138
- e .log .Debug (e .ctx , "skipping workspace: too early" ,
139
- slog .F ("workspace_id" , ws .ID ),
140
- slog .F ("next_transition_at" , nextTransition ),
141
- slog .F ("transition" , validTransition ),
142
- slog .F ("current_tick" , currentTick ),
143
- )
144
- continue
145
- }
141
+ // Determine the workspace state based on its latest build.
142
+ priorHistory , err := db .GetLatestWorkspaceBuildByWorkspaceID (e .ctx , ws .ID )
143
+ if err != nil {
144
+ log .Warn (e .ctx , "get latest workspace build" , slog .Error (err ))
145
+ return nil
146
+ }
147
+
148
+ priorJob , err := db .GetProvisionerJobByID (e .ctx , priorHistory .JobID )
149
+ if err != nil {
150
+ log .Warn (e .ctx , "get last provisioner job for workspace %q: %w" , slog .Error (err ))
151
+ return nil
152
+ }
153
+
154
+ validTransition , nextTransition , err := getNextTransition (ws , priorHistory , priorJob )
155
+ if err != nil {
156
+ log .Debug (e .ctx , "skipping workspace" , slog .Error (err ))
157
+ return nil
158
+ }
159
+
160
+ if currentTick .Before (nextTransition ) {
161
+ log .Debug (e .ctx , "skipping workspace: too early" ,
162
+ slog .F ("next_transition_at" , nextTransition ),
163
+ slog .F ("transition" , validTransition ),
164
+ slog .F ("current_tick" , currentTick ),
165
+ )
166
+ return nil
167
+ }
168
+
169
+ log .Info (e .ctx , "scheduling workspace transition" , slog .F ("transition" , validTransition ))
146
170
147
- e .log .Info (e .ctx , "scheduling workspace transition" ,
148
- slog .F ("workspace_id" , ws .ID ),
149
- slog .F ("transition" , validTransition ),
150
- )
171
+ stats .Transitions [ws .ID ] = validTransition
172
+ if err := build (e .ctx , db , ws , validTransition , priorHistory , priorJob ); err != nil {
173
+ log .Error (e .ctx , "unable to transition workspace" ,
174
+ slog .F ("transition" , validTransition ),
175
+ slog .Error (err ),
176
+ )
177
+ return nil
178
+ }
151
179
152
- stats .Transitions [ws .ID ] = validTransition
153
- if err := build (e .ctx , db , ws , validTransition , priorHistory , priorJob ); err != nil {
154
- e .log .Error (e .ctx , "unable to transition workspace" ,
155
- slog .F ("workspace_id" , ws .ID ),
156
- slog .F ("transition" , validTransition ),
157
- slog .Error (err ),
158
- )
180
+ return nil
181
+ })
182
+ if err != nil {
183
+ log .Error (e .ctx , "workspace scheduling failed" , slog .Error (err ))
159
184
}
160
- }
161
- return nil
162
- })
185
+ return nil
186
+ })
187
+ }
188
+
189
+ // This should not happen since we don't want early cancellation.
190
+ err = eg .Wait ()
191
+ if err != nil {
192
+ e .log .Error (e .ctx , "workspace scheduling errgroup failed" , slog .Error (err ))
193
+ }
194
+
163
195
return stats
164
196
}
165
197
198
+ func isEligibleForAutoStartStop (ws database.Workspace ) bool {
199
+ return ! ws .Deleted && (ws .AutostartSchedule .String != "" || ws .Ttl .Int64 > 0 )
200
+ }
201
+
166
202
func getNextTransition (
167
203
ws database.Workspace ,
168
204
priorHistory database.WorkspaceBuild ,
0 commit comments