Skip to content

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

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

Open
wants to merge 15 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
fix: prebuilds lifecycle parameters on creation and claim
  • Loading branch information
ssncferreira committed Aug 8, 2025
commit 641506981d323f034c12aa6aa98cd2bc51e0aaf7
14 changes: 14 additions & 0 deletions cli/schedule.go
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,13 @@ func (r *RootCmd) scheduleStart() *serpent.Command {
return err
}

// Autostart configuration is not supported for prebuilt workspaces.
// Prebuild lifecycle is managed by the reconciliation loop, with scheduling behavior
// defined per preset at the template level, not per workspace.
if workspace.IsPrebuild {
return xerrors.Errorf("autostart configuration is not supported for prebuilt workspaces")
}

var schedStr *string
if inv.Args[1] != "manual" {
sched, err := parseCLISchedule(inv.Args[1:]...)
Expand Down Expand Up @@ -205,6 +212,13 @@ func (r *RootCmd) scheduleStop() *serpent.Command {
return err
}

// Autostop configuration is not supported for prebuilt workspaces.
// Prebuild lifecycle is managed by the reconciliation loop, with scheduling behavior
// defined per preset at the template level, not per workspace.
if workspace.IsPrebuild {
return xerrors.Errorf("autostop configuration is not supported for prebuilt workspaces")
}

var durMillis *int64
if inv.Args[1] != "manual" {
dur, err := parseDuration(inv.Args[1])
Expand Down
17 changes: 17 additions & 0 deletions coderd/database/querier.go

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

44 changes: 38 additions & 6 deletions coderd/database/queries.sql.go

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

2 changes: 2 additions & 0 deletions coderd/database/queries/activitybump.sql
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
--
-- Max deadline is respected, and the deadline will never be bumped past it.
-- The deadline will never decrease.
-- NOTE: This query should only be called for regular user workspaces.
-- Prebuilds are managed by the reconciliation loop and not subject to activity bumping.
-- name: ActivityBumpWorkspace :exec
WITH latest AS (
SELECT
Expand Down
10 changes: 9 additions & 1 deletion coderd/database/queries/prebuilds.sql
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,15 @@
UPDATE workspaces w
SET owner_id = @new_user_id::uuid,
name = @new_name::text,
updated_at = NOW()
updated_at = NOW(),
-- Update last_used_at during claim to ensure the claimed workspace is treated as recently used.
-- This avoids unintended dormancy caused by prebuilds having stale usage timestamps.
last_used_at = NOW(),
-- Clear dormant and deletion timestamps as a safeguard to ensure a clean lifecycle state after claim.
-- These fields should not be set on prebuilds, but we defensively reset them here to prevent
-- accidental dormancy or deletion by the lifecycle executor.
dormant_at = NULL,
deleting_at = NULL
WHERE w.id IN (
SELECT p.id
FROM workspace_prebuilds p
Expand Down
3 changes: 3 additions & 0 deletions coderd/database/queries/workspacebuilds.sql
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,9 @@ WHERE
id = $1;

-- name: UpdateWorkspaceBuildDeadlineByID :exec
-- NOTE: This query should only be called for regular user workspaces.
-- Prebuilds are managed by the reconciliation loop, not the lifecycle
-- executor which handles deadline and max_deadline.
UPDATE
workspace_builds
SET
Expand Down
29 changes: 24 additions & 5 deletions coderd/database/queries/workspaces.sql
Original file line number Diff line number Diff line change
Expand Up @@ -512,6 +512,9 @@ WHERE
RETURNING *;

-- name: UpdateWorkspaceAutostart :exec
-- NOTE: This query should only be called for regular user workspaces.
-- Prebuilds are managed by the reconciliation loop, not the lifecycle
-- executor which handles autostart_schedule and next_start_at.
UPDATE
workspaces
SET
Expand All @@ -521,6 +524,9 @@ WHERE
id = $1;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: make it impossible by adding AND owner_id != ...

Copy link
Contributor Author

@ssncferreira ssncferreira Aug 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I wasn’t sure at first whether to enforce it at the database level or just in the application layer, but I agree it makes more sense to make it impossible by adding a DB-level constraint.

Addressed in 4c847c9 and I’ve updated the other relevant queries as well to align with it.


-- name: UpdateWorkspaceNextStartAt :exec
-- NOTE: This query should only be called for regular user workspaces.
-- Prebuilds are managed by the reconciliation loop, not the lifecycle
-- executor which handles next_start_at.
UPDATE
workspaces
SET
Expand All @@ -545,6 +551,9 @@ WHERE
workspaces.id = batch.id;

-- name: UpdateWorkspaceTTL :exec
-- NOTE: This query should only be called for regular user workspaces.
-- Prebuilds are managed by the reconciliation loop, not the lifecycle
-- executor which handles regular workspace's TTL.
UPDATE
workspaces
SET
Expand All @@ -554,11 +563,15 @@ WHERE

-- name: UpdateWorkspacesTTLByTemplateID :exec
UPDATE
workspaces
workspaces
SET
ttl = $2
ttl = $2
WHERE
template_id = $1;
template_id = $1
-- Prebuilt workspaces (identified by having the prebuilds system user as owner_id)
-- should not have their TTL updated, as they are handled by the prebuilds
-- reconciliation loop.
AND workspaces.owner_id != 'c42fdf75-3097-471c-8c33-fb52454d81c0'::UUID;

-- name: UpdateWorkspaceLastUsedAt :exec
UPDATE
Expand Down Expand Up @@ -768,6 +781,9 @@ WHERE
AND workspaces.owner_id != 'c42fdf75-3097-471c-8c33-fb52454d81c0'::UUID;

-- name: UpdateWorkspaceDormantDeletingAt :one
-- NOTE: This query should only be called for regular user workspaces.
-- Prebuilds are managed by the reconciliation loop, not the lifecycle
-- executor which handles dormant_at and deleting_at.
UPDATE
workspaces
SET
Expand Down Expand Up @@ -805,8 +821,11 @@ SET
dormant_at = CASE WHEN @dormant_at::timestamptz > '0001-01-01 00:00:00+00'::timestamptz THEN @dormant_at::timestamptz ELSE dormant_at END
WHERE
template_id = @template_id
AND
dormant_at IS NOT NULL
AND dormant_at IS NOT NULL
-- Prebuilt workspaces (identified by having the prebuilds system user as owner_id)
-- should not have their dormant or deleting at set, as these are handled by the
-- prebuilds reconciliation loop.
AND workspaces.owner_id != 'c42fdf75-3097-471c-8c33-fb52454d81c0'::UUID
RETURNING *;

-- name: UpdateTemplateWorkspacesLastUsedAt :exec
Expand Down
72 changes: 44 additions & 28 deletions coderd/provisionerdserver/provisionerdserver.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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{
Expand All @@ -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 {
Expand Down
Loading
Loading