Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commitf4da5d4

Browse files
authored
feat: add lifecycle.Executor to manage autostart and autostop (#1183)
This PR adds a package lifecycle and an Executor implementation that attempts to schedule a build of workspaces with autostart configured.- lifecycle.Executor takes a chan time.Time in its constructor (e.g. time.Tick(time.Minute))- Whenever a value is received from this channel, it executes one iteration of looping through the workspaces and triggering lifecycle operations.- When the context passed to the executor is Done, it exits.- Only workspaces that meet the following criteria will have a lifecycle operation applied to them: - Workspace has a valid and non-empty autostart or autostop schedule (either) - Workspace's last build was successful- The following transitions will be applied depending on the current workspace state: - If the workspace is currently running, it will be stopped. - If the workspace is currently stopped, it will be started. - Otherwise, nothing will be done.- Workspace builds will be created with the same parameters and template version as the last successful build (for example, template version)
1 parente8e6d3c commitf4da5d4

File tree

17 files changed

+765
-6
lines changed

17 files changed

+765
-6
lines changed

‎.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,3 +37,4 @@ site/out/
3737
.terraform/
3838

3939
.vscode/*.log
40+
**/*.swp

‎cli/autostart.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import (
77

88
"github.com/spf13/cobra"
99

10-
"github.com/coder/coder/coderd/autostart/schedule"
10+
"github.com/coder/coder/coderd/autobuild/schedule"
1111
"github.com/coder/coder/codersdk"
1212
)
1313

‎cli/autostop.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import (
77

88
"github.com/spf13/cobra"
99

10-
"github.com/coder/coder/coderd/autostart/schedule"
10+
"github.com/coder/coder/coderd/autobuild/schedule"
1111
"github.com/coder/coder/codersdk"
1212
)
1313

‎cli/server.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ import (
3939
"github.com/coder/coder/cli/cliui"
4040
"github.com/coder/coder/cli/config"
4141
"github.com/coder/coder/coderd"
42+
"github.com/coder/coder/coderd/autobuild/executor"
4243
"github.com/coder/coder/coderd/database"
4344
"github.com/coder/coder/coderd/database/databasefake"
4445
"github.com/coder/coder/coderd/devtunnel"
@@ -343,6 +344,11 @@ func server() *cobra.Command {
343344
returnxerrors.Errorf("notify systemd: %w",err)
344345
}
345346

347+
lifecyclePoller:=time.NewTicker(time.Minute)
348+
deferlifecyclePoller.Stop()
349+
lifecycleExecutor:=executor.New(cmd.Context(),options.Database,logger,lifecyclePoller.C)
350+
lifecycleExecutor.Run()
351+
346352
// Because the graceful shutdown includes cleaning up workspaces in dev mode, we're
347353
// going to make it harder to accidentally skip the graceful shutdown by hitting ctrl+c
348354
// two or more times. So the stopChan is unlimited in size and we don't call
Lines changed: 222 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,222 @@
1+
package executor
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"time"
7+
8+
"cdr.dev/slog"
9+
10+
"github.com/coder/coder/coderd/autobuild/schedule"
11+
"github.com/coder/coder/coderd/database"
12+
13+
"github.com/google/uuid"
14+
"github.com/moby/moby/pkg/namesgenerator"
15+
"golang.org/x/xerrors"
16+
)
17+
18+
// Executor automatically starts or stops workspaces.
19+
typeExecutorstruct {
20+
ctx context.Context
21+
db database.Store
22+
log slog.Logger
23+
tick<-chan time.Time
24+
}
25+
26+
// New returns a new autobuild executor.
27+
funcNew(ctx context.Context,db database.Store,log slog.Logger,tick<-chan time.Time)*Executor {
28+
le:=&Executor{
29+
ctx:ctx,
30+
db:db,
31+
tick:tick,
32+
log:log,
33+
}
34+
returnle
35+
}
36+
37+
// Run will cause executor to start or stop workspaces 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() {
41+
gofunc() {
42+
fort:=rangee.tick {
43+
iferr:=e.runOnce(t);err!=nil {
44+
e.log.Error(e.ctx,"error running once",slog.Error(err))
45+
}
46+
}
47+
}()
48+
}
49+
50+
func (e*Executor)runOnce(t time.Time)error {
51+
currentTick:=t.Truncate(time.Minute)
52+
returne.db.InTx(func(db database.Store)error {
53+
eligibleWorkspaces,err:=db.GetWorkspacesAutostartAutostop(e.ctx)
54+
iferr!=nil {
55+
returnxerrors.Errorf("get eligible workspaces for autostart or autostop: %w",err)
56+
}
57+
58+
for_,ws:=rangeeligibleWorkspaces {
59+
// Determine the workspace state based on its latest build.
60+
priorHistory,err:=db.GetWorkspaceBuildByWorkspaceIDWithoutAfter(e.ctx,ws.ID)
61+
iferr!=nil {
62+
e.log.Warn(e.ctx,"get latest workspace build",
63+
slog.F("workspace_id",ws.ID),
64+
slog.Error(err),
65+
)
66+
continue
67+
}
68+
69+
priorJob,err:=db.GetProvisionerJobByID(e.ctx,priorHistory.JobID)
70+
iferr!=nil {
71+
e.log.Warn(e.ctx,"get last provisioner job for workspace %q: %w",
72+
slog.F("workspace_id",ws.ID),
73+
slog.Error(err),
74+
)
75+
continue
76+
}
77+
78+
if!priorJob.CompletedAt.Valid||priorJob.Error.String!="" {
79+
e.log.Warn(e.ctx,"last workspace build did not complete successfully, skipping",
80+
slog.F("workspace_id",ws.ID),
81+
slog.F("error",priorJob.Error.String),
82+
)
83+
continue
84+
}
85+
86+
varvalidTransition database.WorkspaceTransition
87+
varsched*schedule.Schedule
88+
switchpriorHistory.Transition {
89+
casedatabase.WorkspaceTransitionStart:
90+
validTransition=database.WorkspaceTransitionStop
91+
sched,err=schedule.Weekly(ws.AutostopSchedule.String)
92+
iferr!=nil {
93+
e.log.Warn(e.ctx,"workspace has invalid autostop schedule, skipping",
94+
slog.F("workspace_id",ws.ID),
95+
slog.F("autostart_schedule",ws.AutostopSchedule.String),
96+
)
97+
continue
98+
}
99+
casedatabase.WorkspaceTransitionStop:
100+
validTransition=database.WorkspaceTransitionStart
101+
sched,err=schedule.Weekly(ws.AutostartSchedule.String)
102+
iferr!=nil {
103+
e.log.Warn(e.ctx,"workspace has invalid autostart schedule, skipping",
104+
slog.F("workspace_id",ws.ID),
105+
slog.F("autostart_schedule",ws.AutostartSchedule.String),
106+
)
107+
continue
108+
}
109+
default:
110+
e.log.Debug(e.ctx,"last transition not valid for autostart or autostop",
111+
slog.F("workspace_id",ws.ID),
112+
slog.F("latest_build_transition",priorHistory.Transition),
113+
)
114+
continue
115+
}
116+
117+
// Round time down to the nearest minute, as this is the finest granularity cron supports.
118+
// Truncate is probably not necessary here, but doing it anyway to be sure.
119+
nextTransitionAt:=sched.Next(priorHistory.CreatedAt).Truncate(time.Minute)
120+
ifcurrentTick.Before(nextTransitionAt) {
121+
e.log.Debug(e.ctx,"skipping workspace: too early",
122+
slog.F("workspace_id",ws.ID),
123+
slog.F("next_transition_at",nextTransitionAt),
124+
slog.F("transition",validTransition),
125+
slog.F("current_tick",currentTick),
126+
)
127+
continue
128+
}
129+
130+
e.log.Info(e.ctx,"scheduling workspace transition",
131+
slog.F("workspace_id",ws.ID),
132+
slog.F("transition",validTransition),
133+
)
134+
135+
iferr:=build(e.ctx,db,ws,validTransition,priorHistory,priorJob);err!=nil {
136+
e.log.Error(e.ctx,"unable to transition workspace",
137+
slog.F("workspace_id",ws.ID),
138+
slog.F("transition",validTransition),
139+
slog.Error(err),
140+
)
141+
}
142+
}
143+
returnnil
144+
})
145+
}
146+
147+
// TODO(cian): this function duplicates most of api.postWorkspaceBuilds. Refactor.
148+
// See: https://github.com/coder/coder/issues/1401
149+
funcbuild(ctx context.Context,store database.Store,workspace database.Workspace,trans database.WorkspaceTransition,priorHistory database.WorkspaceBuild,priorJob database.ProvisionerJob)error {
150+
template,err:=store.GetTemplateByID(ctx,workspace.TemplateID)
151+
iferr!=nil {
152+
returnxerrors.Errorf("get workspace template: %w",err)
153+
}
154+
155+
priorHistoryID:= uuid.NullUUID{
156+
UUID:priorHistory.ID,
157+
Valid:true,
158+
}
159+
160+
varnewWorkspaceBuild database.WorkspaceBuild
161+
// This must happen in a transaction to ensure history can be inserted, and
162+
// the prior history can update it's "after" column to point at the new.
163+
workspaceBuildID:=uuid.New()
164+
input,err:=json.Marshal(struct {
165+
WorkspaceBuildIDstring`json:"workspace_build_id"`
166+
}{
167+
WorkspaceBuildID:workspaceBuildID.String(),
168+
})
169+
iferr!=nil {
170+
returnxerrors.Errorf("marshal provision job: %w",err)
171+
}
172+
provisionerJobID:=uuid.New()
173+
now:=database.Now()
174+
newProvisionerJob,err:=store.InsertProvisionerJob(ctx, database.InsertProvisionerJobParams{
175+
ID:provisionerJobID,
176+
CreatedAt:now,
177+
UpdatedAt:now,
178+
InitiatorID:workspace.OwnerID,
179+
OrganizationID:template.OrganizationID,
180+
Provisioner:template.Provisioner,
181+
Type:database.ProvisionerJobTypeWorkspaceBuild,
182+
StorageMethod:priorJob.StorageMethod,
183+
StorageSource:priorJob.StorageSource,
184+
Input:input,
185+
})
186+
iferr!=nil {
187+
returnxerrors.Errorf("insert provisioner job: %w",err)
188+
}
189+
newWorkspaceBuild,err=store.InsertWorkspaceBuild(ctx, database.InsertWorkspaceBuildParams{
190+
ID:workspaceBuildID,
191+
CreatedAt:now,
192+
UpdatedAt:now,
193+
WorkspaceID:workspace.ID,
194+
TemplateVersionID:priorHistory.TemplateVersionID,
195+
BeforeID:priorHistoryID,
196+
Name:namesgenerator.GetRandomName(1),
197+
ProvisionerState:priorHistory.ProvisionerState,
198+
InitiatorID:workspace.OwnerID,
199+
Transition:trans,
200+
JobID:newProvisionerJob.ID,
201+
})
202+
iferr!=nil {
203+
returnxerrors.Errorf("insert workspace build: %w",err)
204+
}
205+
206+
ifpriorHistoryID.Valid {
207+
// Update the prior history entries "after" column.
208+
err=store.UpdateWorkspaceBuildByID(ctx, database.UpdateWorkspaceBuildByIDParams{
209+
ID:priorHistory.ID,
210+
ProvisionerState:priorHistory.ProvisionerState,
211+
UpdatedAt:now,
212+
AfterID: uuid.NullUUID{
213+
UUID:newWorkspaceBuild.ID,
214+
Valid:true,
215+
},
216+
})
217+
iferr!=nil {
218+
returnxerrors.Errorf("update prior workspace build: %w",err)
219+
}
220+
}
221+
returnnil
222+
}

0 commit comments

Comments
 (0)

[8]ページ先頭

©2009-2025 Movatter.jp