Skip to content

feat: add API/SDK support for autostop extension #1778

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 our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 11 commits into from
May 26, 2022
25 changes: 17 additions & 8 deletions coderd/autobuild/executor/lifecycle_executor.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,18 @@ func (e *Executor) Run() {
func (e *Executor) runOnce(t time.Time) error {
currentTick := t.Truncate(time.Minute)
return e.db.InTx(func(db database.Store) error {
// TTL is set at the workspace level, and deadline at the workspace build level.
// When a workspace build is created, its deadline initially starts at zero.
// When provisionerd successfully completes a provision job, the deadline is
// set to now + TTL if the associated workspace has a TTL set. This deadline
// is what we compare against when performing autostop operations, rounded down
// to the minute.
//
// NOTE: Currently, if a workspace build is created with a given TTL and then
// the user either changes or unsets the TTL, the deadline for the workspace
// build will not have changed. So, autostop will still happen at the
// original TTL value from when the workspace build was created.
// Whether this is expected behavior from a user's perspective is not yet known.
eligibleWorkspaces, err := db.GetWorkspacesAutostart(e.ctx)
if err != nil {
return xerrors.Errorf("get eligible workspaces for autostart or autostop: %w", err)
Expand Down Expand Up @@ -88,18 +100,15 @@ func (e *Executor) runOnce(t time.Time) error {
switch priorHistory.Transition {
case database.WorkspaceTransitionStart:
validTransition = database.WorkspaceTransitionStop
if !ws.Ttl.Valid || ws.Ttl.Int64 == 0 {
e.log.Debug(e.ctx, "invalid or zero ws ttl, skipping",
if priorHistory.Deadline.IsZero() {
e.log.Debug(e.ctx, "latest workspace build has zero deadline, skipping",
slog.F("workspace_id", ws.ID),
slog.F("ttl", time.Duration(ws.Ttl.Int64)),
slog.F("workspace_build_id", priorHistory.ID),
)
continue
}
ttl := time.Duration(ws.Ttl.Int64)
// Measure TTL from the time the workspace finished building.
// Truncate to nearest minute for consistency with autostart
// behavior, and add one minute for padding.
nextTransition = priorHistory.UpdatedAt.Truncate(time.Minute).Add(ttl + time.Minute)
// Truncate to nearest minute for consistency with autostart behavior
nextTransition = priorHistory.Deadline.Truncate(time.Minute)
case database.WorkspaceTransitionStop:
validTransition = database.WorkspaceTransitionStart
sched, err := schedule.Weekly(ws.AutostartSchedule.String)
Expand Down
96 changes: 88 additions & 8 deletions coderd/autobuild/executor/lifecycle_executor_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -190,14 +190,14 @@ func TestExecutorAutostopOK(t *testing.T) {
})
// Given: we have a user with a workspace
workspace = mustProvisionWorkspace(t, client)
ttl = *workspace.TTL
)
// Given: workspace is running
require.Equal(t, codersdk.WorkspaceTransitionStart, workspace.LatestBuild.Transition)
require.NotZero(t, workspace.LatestBuild.Deadline)

// When: the autobuild executor ticks *after* the TTL:
// When: the autobuild executor ticks *after* the deadline:
go func() {
tickCh <- time.Now().UTC().Add(ttl + time.Minute)
tickCh <- workspace.LatestBuild.Deadline.Add(time.Minute)
close(tickCh)
}()

Expand All @@ -209,6 +209,55 @@ func TestExecutorAutostopOK(t *testing.T) {
require.Equal(t, codersdk.WorkspaceTransitionStop, ws.LatestBuild.Transition, "expected workspace not to be running")
}

func TestExecutorAutostopExtend(t *testing.T) {
t.Parallel()

var (
ctx = context.Background()
tickCh = make(chan time.Time)
client = coderdtest.New(t, &coderdtest.Options{
AutobuildTicker: tickCh,
IncludeProvisionerD: true,
})
// Given: we have a user with a workspace
workspace = mustProvisionWorkspace(t, client)
originalDeadline = workspace.LatestBuild.Deadline
)
// Given: workspace is running
require.Equal(t, codersdk.WorkspaceTransitionStart, workspace.LatestBuild.Transition)
require.NotZero(t, originalDeadline)

// Given: we extend the workspace deadline
err := client.PutExtendWorkspace(ctx, workspace.ID, codersdk.PutExtendWorkspaceRequest{
Deadline: originalDeadline.Add(30 * time.Minute),
})
require.NoError(t, err, "extend workspace deadline")

// When: the autobuild executor ticks *after* the original deadline:
go func() {
tickCh <- originalDeadline.Add(time.Minute)
}()

// Then: nothing should happen
<-time.After(5 * time.Second)
ws := mustWorkspace(t, client, workspace.ID)
require.Equal(t, workspace.LatestBuild.ID, ws.LatestBuild.ID, "expected no further workspace builds to occur")
require.Equal(t, codersdk.WorkspaceTransitionStart, ws.LatestBuild.Transition, "expected workspace to be running")

// When: the autobuild executor ticks after the *new* deadline:
go func() {
tickCh <- ws.LatestBuild.Deadline.Add(time.Minute)
close(tickCh)
}()

// Then: the workspace should be stopped
<-time.After(5 * time.Second)
ws = mustWorkspace(t, client, workspace.ID)
require.NotEqual(t, workspace.LatestBuild.ID, ws.LatestBuild.ID, "expected a workspace build to occur")
require.Equal(t, codersdk.ProvisionerJobSucceeded, ws.LatestBuild.Job.Status, "expected provisioner job to have succeeded")
require.Equal(t, codersdk.WorkspaceTransitionStop, ws.LatestBuild.Transition, "expected workspace not to be running")
}

func TestExecutorAutostopAlreadyStopped(t *testing.T) {
t.Parallel()

Expand All @@ -222,15 +271,14 @@ func TestExecutorAutostopAlreadyStopped(t *testing.T) {
workspace = mustProvisionWorkspace(t, client, func(cwr *codersdk.CreateWorkspaceRequest) {
cwr.AutostartSchedule = nil
})
ttl = *workspace.TTL
)

// Given: workspace is stopped
workspace = mustTransitionWorkspace(t, client, workspace.ID, database.WorkspaceTransitionStart, database.WorkspaceTransitionStop)

// When: the autobuild executor ticks past the TTL
go func() {
tickCh <- time.Now().UTC().Add(ttl + time.Minute)
tickCh <- workspace.LatestBuild.Deadline.Add(time.Minute)
close(tickCh)
}()

Expand Down Expand Up @@ -264,7 +312,7 @@ func TestExecutorAutostopNotEnabled(t *testing.T) {

// When: the autobuild executor ticks past the TTL
go func() {
tickCh <- time.Now().UTC().Add(time.Minute)
tickCh <- workspace.LatestBuild.Deadline.Add(time.Minute)
close(tickCh)
}()

Expand Down Expand Up @@ -352,7 +400,7 @@ func TestExecutorWorkspaceAutostartTooEarly(t *testing.T) {
require.Equal(t, codersdk.WorkspaceTransitionStart, ws.LatestBuild.Transition, "expected workspace to be running")
}

func TestExecutorWorkspaceTTLTooEarly(t *testing.T) {
func TestExecutorWorkspaceAutostopBeforeDeadline(t *testing.T) {
t.Parallel()

var (
Expand All @@ -367,7 +415,7 @@ func TestExecutorWorkspaceTTLTooEarly(t *testing.T) {

// When: the autobuild executor ticks before the TTL
go func() {
tickCh <- time.Now().UTC()
tickCh <- workspace.LatestBuild.Deadline.Add(-1 * time.Minute)
close(tickCh)
}()

Expand All @@ -378,6 +426,38 @@ func TestExecutorWorkspaceTTLTooEarly(t *testing.T) {
require.Equal(t, codersdk.WorkspaceTransitionStart, ws.LatestBuild.Transition, "expected workspace to be running")
}

func TestExecutorWorkspaceAutostopNoWaitChangedMyMind(t *testing.T) {
t.Parallel()

var (
ctx = context.Background()
tickCh = make(chan time.Time)
client = coderdtest.New(t, &coderdtest.Options{
AutobuildTicker: tickCh,
IncludeProvisionerD: true,
})
// Given: we have a user with a workspace
workspace = mustProvisionWorkspace(t, client)
)

// Given: the user changes their mind and decides their workspace should not auto-stop
err := client.UpdateWorkspaceTTL(ctx, workspace.ID, codersdk.UpdateWorkspaceTTLRequest{TTL: nil})
require.NoError(t, err)

// When: the autobuild executor ticks after the deadline
go func() {
tickCh <- workspace.LatestBuild.Deadline.Add(time.Minute)
close(tickCh)
}()

// Then: the workspace should still stop - sorry!
<-time.After(5 * time.Second)
ws := mustWorkspace(t, client, workspace.ID)
require.NotEqual(t, workspace.LatestBuild.ID, ws.LatestBuild.ID, "expected a workspace build to occur")
require.Equal(t, codersdk.ProvisionerJobSucceeded, ws.LatestBuild.Job.Status, "expected provisioner job to have succeeded")
require.Equal(t, codersdk.WorkspaceTransitionStop, ws.LatestBuild.Transition, "expected workspace not to be running")
}

func TestExecutorAutostartMultipleOK(t *testing.T) {
if os.Getenv("DB") == "" {
t.Skip(`This test only really works when using a "real" database, similar to a HA setup`)
Expand Down
1 change: 1 addition & 0 deletions coderd/coderd.go
Original file line number Diff line number Diff line change
Expand Up @@ -315,6 +315,7 @@ func New(options *Options) *API {
r.Put("/", api.putWorkspaceTTL)
})
r.Get("/watch", api.watchWorkspace)
r.Put("/extend", api.putExtendWorkspace)
})
})
r.Route("/workspacebuilds/{workspacebuild}", func(r chi.Router) {
Expand Down
2 changes: 2 additions & 0 deletions coderd/database/databasefake/databasefake.go
Original file line number Diff line number Diff line change
Expand Up @@ -1485,6 +1485,7 @@ func (q *fakeQuerier) InsertWorkspaceBuild(_ context.Context, arg database.Inser
InitiatorID: arg.InitiatorID,
JobID: arg.JobID,
ProvisionerState: arg.ProvisionerState,
Deadline: arg.Deadline,
}
q.workspaceBuilds = append(q.workspaceBuilds, workspaceBuild)
return workspaceBuild, nil
Expand Down Expand Up @@ -1693,6 +1694,7 @@ func (q *fakeQuerier) UpdateWorkspaceBuildByID(_ context.Context, arg database.U
}
workspaceBuild.UpdatedAt = arg.UpdatedAt
workspaceBuild.ProvisionerState = arg.ProvisionerState
workspaceBuild.Deadline = arg.Deadline
q.workspaceBuilds[index] = workspaceBuild
return nil
}
Expand Down
3 changes: 2 additions & 1 deletion coderd/database/dump.sql

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ALTER TABLE ONLY workspace_builds DROP COLUMN deadline;
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ALTER TABLE ONLY workspace_builds ADD COLUMN deadline TIMESTAMPTZ NOT NULL DEFAULT TIMESTAMPTZ '0001-01-01 00:00:00+00:00';
1 change: 1 addition & 0 deletions coderd/database/models.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading