- Notifications
You must be signed in to change notification settings - Fork928
feat: add lifecycle.Executor to manage autostart and autostop#1183
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to ourterms of service andprivacy statement. We’ll occasionally send you account related emails.
Already on GitHub?Sign in to your account
Merged
Uh oh!
There was an error while loading.Please reload this page.
Merged
Changes fromall commits
Commits
Show all changes
28 commits Select commitHold shift + click to select a range
a145d6d
feat: add lifecycle.Executor to autostart workspaces.
johnstcn8f401ca
refactor: do not expose Store in coderdtest.Options
johnstcn6d8f5fe
fixup! refactor: do not expose Store in coderdtest.Options
johnstcncfd0d1e
stop accessing db directly, only query workspaces with autostart enabled
johnstcnce63810
refactor unit tests, add tests for autostop
johnstcn579f362
make the new tests pass with some refactoring
johnstcn6e88f67
gitignore *.swp
johnstcn2b1a383
remove unused methods
johnstcnf31588e
fixup! remove unused methods
johnstcn80e0581
fix: range over channel, add continue to default switch case
johnstcnd176478
add test for deleted workspace
johnstcnbd97c1a
workspaces.sql: remove unused methods
johnstcn0931d25
unexport test helper methods
johnstcnfaebe2e
chore: rename package autostart/lifecycle to lifecycle/executor
johnstcnabc0854
add test to ensure workspaces are not autostarted before time
johnstcne53946a
wire up executor to coderd
johnstcn364a27c
fix: executor: skip workspaces whose last build was not successful
johnstcne96414f
address PR comments
johnstcnb5bf50e
add goleak TestMain
johnstcnd37cc2b
fmt
johnstcnd11f5d7
mustTransitionWorkspace should return the updated workspace
johnstcnf6388b4
remove usage of require.Eventually/Never which is flaky on Windows
johnstcnfd0f8a3
make lifecycle executor spawn a new goroutine automatically
johnstcn7b6f2e1
rename unit tests
johnstcna7143bd
s/doBuild/build
johnstcn7d9b696
rename parent package lifecycle to autobuild
johnstcn5cba737
add unit test for behaviour with an updated template
johnstcn7627372
add ticket to reference TODO
johnstcnFile filter
Filter by extension
Conversations
Failed to load comments.
Loading
Uh oh!
There was an error while loading.Please reload this page.
Jump to
Jump to file
Failed to load files.
Loading
Uh oh!
There was an error while loading.Please reload this page.
Diff view
Diff view
There are no files selected for viewing
1 change: 1 addition & 0 deletions.gitignore
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -37,3 +37,4 @@ site/out/ | ||
.terraform/ | ||
.vscode/*.log | ||
**/*.swp |
2 changes: 1 addition & 1 deletioncli/autostart.go
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.Learn more about bidirectional Unicode characters
2 changes: 1 addition & 1 deletioncli/autostop.go
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.Learn more about bidirectional Unicode characters
6 changes: 6 additions & 0 deletionscli/server.go
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.Learn more about bidirectional Unicode characters
222 changes: 222 additions & 0 deletionscoderd/autobuild/executor/lifecycle_executor.go
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,222 @@ | ||
package executor | ||
import ( | ||
"context" | ||
"encoding/json" | ||
"time" | ||
"cdr.dev/slog" | ||
"github.com/coder/coder/coderd/autobuild/schedule" | ||
"github.com/coder/coder/coderd/database" | ||
"github.com/google/uuid" | ||
"github.com/moby/moby/pkg/namesgenerator" | ||
"golang.org/x/xerrors" | ||
) | ||
// Executor automatically starts or stops workspaces. | ||
type Executor struct { | ||
ctx context.Context | ||
db database.Store | ||
log slog.Logger | ||
tick <-chan time.Time | ||
} | ||
// New returns a new autobuild executor. | ||
func New(ctx context.Context, db database.Store, log slog.Logger, tick <-chan time.Time) *Executor { | ||
le := &Executor{ | ||
ctx: ctx, | ||
db: db, | ||
tick: tick, | ||
log: log, | ||
} | ||
return le | ||
} | ||
// Run will cause executor to start or stop workspaces on every | ||
// tick from its channel. It will stop when its context is Done, or when | ||
// its channel is closed. | ||
func (e *Executor) Run() { | ||
go func() { | ||
for t := range e.tick { | ||
if err := e.runOnce(t); err != nil { | ||
e.log.Error(e.ctx, "error running once", slog.Error(err)) | ||
} | ||
} | ||
}() | ||
} | ||
func (e *Executor) runOnce(t time.Time) error { | ||
currentTick := t.Truncate(time.Minute) | ||
return e.db.InTx(func(db database.Store) error { | ||
eligibleWorkspaces, err := db.GetWorkspacesAutostartAutostop(e.ctx) | ||
if err != nil { | ||
return xerrors.Errorf("get eligible workspaces for autostart or autostop: %w", err) | ||
} | ||
for _, ws := range eligibleWorkspaces { | ||
// Determine the workspace state based on its latest build. | ||
priorHistory, err := db.GetWorkspaceBuildByWorkspaceIDWithoutAfter(e.ctx, ws.ID) | ||
if err != nil { | ||
e.log.Warn(e.ctx, "get latest workspace build", | ||
slog.F("workspace_id", ws.ID), | ||
slog.Error(err), | ||
) | ||
continue | ||
} | ||
priorJob, err := db.GetProvisionerJobByID(e.ctx, priorHistory.JobID) | ||
if err != nil { | ||
e.log.Warn(e.ctx, "get last provisioner job for workspace %q: %w", | ||
slog.F("workspace_id", ws.ID), | ||
slog.Error(err), | ||
) | ||
continue | ||
} | ||
if !priorJob.CompletedAt.Valid || priorJob.Error.String != "" { | ||
e.log.Warn(e.ctx, "last workspace build did not complete successfully, skipping", | ||
slog.F("workspace_id", ws.ID), | ||
slog.F("error", priorJob.Error.String), | ||
) | ||
continue | ||
} | ||
var validTransition database.WorkspaceTransition | ||
var sched *schedule.Schedule | ||
switch priorHistory.Transition { | ||
case database.WorkspaceTransitionStart: | ||
validTransition = database.WorkspaceTransitionStop | ||
sched, err = schedule.Weekly(ws.AutostopSchedule.String) | ||
if err != nil { | ||
e.log.Warn(e.ctx, "workspace has invalid autostop schedule, skipping", | ||
slog.F("workspace_id", ws.ID), | ||
slog.F("autostart_schedule", ws.AutostopSchedule.String), | ||
) | ||
continue | ||
} | ||
case database.WorkspaceTransitionStop: | ||
validTransition = database.WorkspaceTransitionStart | ||
sched, err = schedule.Weekly(ws.AutostartSchedule.String) | ||
if err != nil { | ||
e.log.Warn(e.ctx, "workspace has invalid autostart schedule, skipping", | ||
slog.F("workspace_id", ws.ID), | ||
slog.F("autostart_schedule", ws.AutostartSchedule.String), | ||
) | ||
continue | ||
} | ||
default: | ||
e.log.Debug(e.ctx, "last transition not valid for autostart or autostop", | ||
slog.F("workspace_id", ws.ID), | ||
slog.F("latest_build_transition", priorHistory.Transition), | ||
) | ||
continue | ||
} | ||
// Round time down to the nearest minute, as this is the finest granularity cron supports. | ||
// Truncate is probably not necessary here, but doing it anyway to be sure. | ||
nextTransitionAt := sched.Next(priorHistory.CreatedAt).Truncate(time.Minute) | ||
if currentTick.Before(nextTransitionAt) { | ||
e.log.Debug(e.ctx, "skipping workspace: too early", | ||
slog.F("workspace_id", ws.ID), | ||
slog.F("next_transition_at", nextTransitionAt), | ||
slog.F("transition", validTransition), | ||
slog.F("current_tick", currentTick), | ||
) | ||
continue | ||
} | ||
e.log.Info(e.ctx, "scheduling workspace transition", | ||
slog.F("workspace_id", ws.ID), | ||
slog.F("transition", validTransition), | ||
) | ||
if err := build(e.ctx, db, ws, validTransition, priorHistory, priorJob); err != nil { | ||
e.log.Error(e.ctx, "unable to transition workspace", | ||
slog.F("workspace_id", ws.ID), | ||
slog.F("transition", validTransition), | ||
slog.Error(err), | ||
) | ||
} | ||
} | ||
return nil | ||
}) | ||
} | ||
// TODO(cian): this function duplicates most of api.postWorkspaceBuilds. Refactor. | ||
// See: https://github.com/coder/coder/issues/1401 | ||
func build(ctx context.Context, store database.Store, workspace database.Workspace, trans database.WorkspaceTransition, priorHistory database.WorkspaceBuild, priorJob database.ProvisionerJob) error { | ||
template, err := store.GetTemplateByID(ctx, workspace.TemplateID) | ||
if err != nil { | ||
return xerrors.Errorf("get workspace template: %w", err) | ||
} | ||
priorHistoryID := uuid.NullUUID{ | ||
UUID: priorHistory.ID, | ||
Valid: true, | ||
} | ||
var newWorkspaceBuild database.WorkspaceBuild | ||
// This must happen in a transaction to ensure history can be inserted, and | ||
// the prior history can update it's "after" column to point at the new. | ||
workspaceBuildID := uuid.New() | ||
input, err := json.Marshal(struct { | ||
WorkspaceBuildID string `json:"workspace_build_id"` | ||
}{ | ||
WorkspaceBuildID: workspaceBuildID.String(), | ||
}) | ||
if err != nil { | ||
return xerrors.Errorf("marshal provision job: %w", err) | ||
} | ||
provisionerJobID := uuid.New() | ||
now := database.Now() | ||
newProvisionerJob, err := store.InsertProvisionerJob(ctx, database.InsertProvisionerJobParams{ | ||
ID: provisionerJobID, | ||
CreatedAt: now, | ||
UpdatedAt: now, | ||
InitiatorID: workspace.OwnerID, | ||
OrganizationID: template.OrganizationID, | ||
Provisioner: template.Provisioner, | ||
Type: database.ProvisionerJobTypeWorkspaceBuild, | ||
StorageMethod: priorJob.StorageMethod, | ||
StorageSource: priorJob.StorageSource, | ||
Input: input, | ||
}) | ||
if err != nil { | ||
return xerrors.Errorf("insert provisioner job: %w", err) | ||
} | ||
newWorkspaceBuild, err = store.InsertWorkspaceBuild(ctx, database.InsertWorkspaceBuildParams{ | ||
ID: workspaceBuildID, | ||
CreatedAt: now, | ||
UpdatedAt: now, | ||
WorkspaceID: workspace.ID, | ||
TemplateVersionID: priorHistory.TemplateVersionID, | ||
BeforeID: priorHistoryID, | ||
Name: namesgenerator.GetRandomName(1), | ||
ProvisionerState: priorHistory.ProvisionerState, | ||
InitiatorID: workspace.OwnerID, | ||
Transition: trans, | ||
JobID: newProvisionerJob.ID, | ||
}) | ||
if err != nil { | ||
return xerrors.Errorf("insert workspace build: %w", err) | ||
} | ||
if priorHistoryID.Valid { | ||
// Update the prior history entries "after" column. | ||
err = store.UpdateWorkspaceBuildByID(ctx, database.UpdateWorkspaceBuildByIDParams{ | ||
ID: priorHistory.ID, | ||
ProvisionerState: priorHistory.ProvisionerState, | ||
UpdatedAt: now, | ||
AfterID: uuid.NullUUID{ | ||
UUID: newWorkspaceBuild.ID, | ||
Valid: true, | ||
}, | ||
}) | ||
if err != nil { | ||
return xerrors.Errorf("update prior workspace build: %w", err) | ||
} | ||
} | ||
return nil | ||
} |
Oops, something went wrong.
Uh oh!
There was an error while loading.Please reload this page.
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.