fix: set prebuilds lifecycle parameters on creation and claim (#19252)

## Description

This PR ensures that prebuilt workspaces are properly excluded from the
lifecycle executor and treated as a separate class of workspaces, fully
managed by the prebuild reconciliation loop.

It introduces two lifecycle guarantees:
* When a prebuilt workspace is created (i.e., when the workspace build
completes), all lifecycle-related fields are unset, ensuring the
workspace does not participate in TTL, autostop, autostart, dormancy, or
auto-deletion logic.
* When a prebuilt workspace is claimed, it transitions into a regular
user workspace. At this point, all lifecycle fields are correctly
populated according to template-level configurations, allowing the
workspace to be managed by the lifecycle executor as expected.

## Changes

* Prebuilt workspaces now have all lifecycle-relevant fields unset
during creation
* When a prebuild is claimed:
* Lifecycle fields are set based on template and workspace level
configurations. This ensures a clean transition into the standard
workspace lifecycle flow.
* Updated lifecycle-related SQL update queries to explicitly exclude
prebuilt workspaces.

## Relates 

Related issue: https://github.com/coder/coder/issues/18898

To reduce the scope of this PR and make the review process more
manageable, the original implementation has been split into the
following focused PRs:
* https://github.com/coder/coder/pull/19259
* https://github.com/coder/coder/pull/19263
* https://github.com/coder/coder/pull/19264
* https://github.com/coder/coder/pull/19265

These PRs should be considered in conjunction with this one to
understand the complete set of lifecycle separation changes for prebuilt
workspaces.
This commit is contained in:
Susana Ferreira
2025-08-13 12:45:46 +01:00
committed by GitHub
parent f17ab92798
commit 8567ecbe52
14 changed files with 479 additions and 297 deletions
+44 -28
View File
@@ -1183,11 +1183,18 @@ func (s *server) FailJob(ctx context.Context, failJob *proto.FailedJob) (*proto.
if err != nil {
return xerrors.Errorf("update workspace build state: %w", err)
}
deadline := build.Deadline
maxDeadline := build.MaxDeadline
if workspace.IsPrebuild() {
deadline = time.Time{}
maxDeadline = time.Time{}
}
err = db.UpdateWorkspaceBuildDeadlineByID(ctx, database.UpdateWorkspaceBuildDeadlineByIDParams{
ID: input.WorkspaceBuildID,
UpdatedAt: s.timeNow(),
Deadline: build.Deadline,
MaxDeadline: build.MaxDeadline,
Deadline: deadline,
MaxDeadline: maxDeadline,
})
if err != nil {
return xerrors.Errorf("update workspace build deadline: %w", err)
@@ -1860,38 +1867,47 @@ func (s *server) completeWorkspaceBuildJob(ctx context.Context, job database.Pro
return getWorkspaceError
}
templateScheduleStore := *s.TemplateScheduleStore.Load()
// Prebuilt workspaces must not have Deadline or MaxDeadline set,
// as they are managed by the prebuild reconciliation loop, not the lifecycle executor
deadline := time.Time{}
maxDeadline := time.Time{}
autoStop, err := schedule.CalculateAutostop(ctx, schedule.CalculateAutostopParams{
Database: db,
TemplateScheduleStore: templateScheduleStore,
UserQuietHoursScheduleStore: *s.UserQuietHoursScheduleStore.Load(),
// `now` is used below to set the build completion time.
WorkspaceBuildCompletedAt: now,
Workspace: workspace.WorkspaceTable(),
// Allowed to be the empty string.
WorkspaceAutostart: workspace.AutostartSchedule.String,
})
if err != nil {
return xerrors.Errorf("calculate auto stop: %w", err)
}
if !workspace.IsPrebuild() {
templateScheduleStore := *s.TemplateScheduleStore.Load()
if workspace.AutostartSchedule.Valid {
templateScheduleOptions, err := templateScheduleStore.Get(ctx, db, workspace.TemplateID)
autoStop, err := schedule.CalculateAutostop(ctx, schedule.CalculateAutostopParams{
Database: db,
TemplateScheduleStore: templateScheduleStore,
UserQuietHoursScheduleStore: *s.UserQuietHoursScheduleStore.Load(),
// `now` is used below to set the build completion time.
WorkspaceBuildCompletedAt: now,
Workspace: workspace.WorkspaceTable(),
// Allowed to be the empty string.
WorkspaceAutostart: workspace.AutostartSchedule.String,
})
if err != nil {
return xerrors.Errorf("get template schedule options: %w", err)
return xerrors.Errorf("calculate auto stop: %w", err)
}
nextStartAt, err := schedule.NextAllowedAutostart(now, workspace.AutostartSchedule.String, templateScheduleOptions)
if err == nil {
err = db.UpdateWorkspaceNextStartAt(ctx, database.UpdateWorkspaceNextStartAtParams{
ID: workspace.ID,
NextStartAt: sql.NullTime{Valid: true, Time: nextStartAt.UTC()},
})
if workspace.AutostartSchedule.Valid {
templateScheduleOptions, err := templateScheduleStore.Get(ctx, db, workspace.TemplateID)
if err != nil {
return xerrors.Errorf("update workspace next start at: %w", err)
return xerrors.Errorf("get template schedule options: %w", err)
}
nextStartAt, err := schedule.NextAllowedAutostart(now, workspace.AutostartSchedule.String, templateScheduleOptions)
if err == nil {
err = db.UpdateWorkspaceNextStartAt(ctx, database.UpdateWorkspaceNextStartAtParams{
ID: workspace.ID,
NextStartAt: sql.NullTime{Valid: true, Time: nextStartAt.UTC()},
})
if err != nil {
return xerrors.Errorf("update workspace next start at: %w", err)
}
}
}
deadline = autoStop.Deadline
maxDeadline = autoStop.MaxDeadline
}
err = db.UpdateProvisionerJobWithCompleteByID(ctx, database.UpdateProvisionerJobWithCompleteByIDParams{
@@ -1917,8 +1933,8 @@ func (s *server) completeWorkspaceBuildJob(ctx context.Context, job database.Pro
}
err = db.UpdateWorkspaceBuildDeadlineByID(ctx, database.UpdateWorkspaceBuildDeadlineByIDParams{
ID: workspaceBuild.ID,
Deadline: autoStop.Deadline,
MaxDeadline: autoStop.MaxDeadline,
Deadline: deadline,
MaxDeadline: maxDeadline,
UpdatedAt: now,
})
if err != nil {