From 9dd9fedc12e59d91572e813d3214cd28d7fb19a1 Mon Sep 17 00:00:00 2001 From: Danny Kopping Date: Tue, 18 Feb 2025 09:39:04 +0000 Subject: [PATCH] Implement strict prebuilds eligibility See https://github.com/coder/internal/issues/372 Signed-off-by: Danny Kopping --- coderd/database/dump.sql | 134 +++++++++++++----- .../migrations/000294_prebuilds.up.sql | 48 ++++--- coderd/database/models.go | 35 ++--- coderd/database/queries.sql.go | 8 ++ coderd/database/queries/prebuilds.sql | 6 + enterprise/coderd/prebuilds/controller.go | 3 +- 6 files changed, 159 insertions(+), 75 deletions(-) diff --git a/coderd/database/dump.sql b/coderd/database/dump.sql index 12cfd49b9f..91c1e7b6fe 100644 --- a/coderd/database/dump.sql +++ b/coderd/database/dump.sql @@ -1841,46 +1841,27 @@ CREATE VIEW workspace_prebuild_builds AS FROM workspace_builds WHERE (workspace_builds.initiator_id = 'c42fdf75-3097-471c-8c33-fb52454d81c0'::uuid); -CREATE TABLE workspaces ( - id uuid NOT NULL, - created_at timestamp with time zone NOT NULL, - updated_at timestamp with time zone NOT NULL, - owner_id uuid NOT NULL, - organization_id uuid NOT NULL, - template_id uuid NOT NULL, - deleted boolean DEFAULT false NOT NULL, - name character varying(64) NOT NULL, - autostart_schedule text, - ttl bigint, - last_used_at timestamp with time zone DEFAULT '0001-01-01 00:00:00+00'::timestamp with time zone NOT NULL, - dormant_at timestamp with time zone, - deleting_at timestamp with time zone, - automatic_updates automatic_updates DEFAULT 'never'::automatic_updates NOT NULL, - favorite boolean DEFAULT false NOT NULL, - next_start_at timestamp with time zone -); - -COMMENT ON COLUMN workspaces.favorite IS 'Favorite is true if the workspace owner has favorited the workspace.'; - CREATE VIEW workspace_prebuilds AS - SELECT workspaces.id, - workspaces.created_at, - workspaces.updated_at, - workspaces.owner_id, - workspaces.organization_id, - workspaces.template_id, - workspaces.deleted, - workspaces.name, - workspaces.autostart_schedule, - workspaces.ttl, - workspaces.last_used_at, - workspaces.dormant_at, - workspaces.deleting_at, - workspaces.automatic_updates, - workspaces.favorite, - workspaces.next_start_at - FROM workspaces - WHERE (workspaces.owner_id = 'c42fdf75-3097-471c-8c33-fb52454d81c0'::uuid); +SELECT + NULL::uuid AS id, + NULL::timestamp with time zone AS created_at, + NULL::timestamp with time zone AS updated_at, + NULL::uuid AS owner_id, + NULL::uuid AS organization_id, + NULL::uuid AS template_id, + NULL::boolean AS deleted, + NULL::character varying(64) AS name, + NULL::text AS autostart_schedule, + NULL::bigint AS ttl, + NULL::timestamp with time zone AS last_used_at, + NULL::timestamp with time zone AS dormant_at, + NULL::timestamp with time zone AS deleting_at, + NULL::automatic_updates AS automatic_updates, + NULL::boolean AS favorite, + NULL::timestamp with time zone AS next_start_at, + NULL::uuid AS agent_id, + NULL::workspace_agent_lifecycle_state AS lifecycle_state, + NULL::timestamp with time zone AS ready_at; CREATE TABLE workspace_proxies ( id uuid NOT NULL, @@ -1952,6 +1933,27 @@ CREATE TABLE workspace_resources ( module_path text ); +CREATE TABLE workspaces ( + id uuid NOT NULL, + created_at timestamp with time zone NOT NULL, + updated_at timestamp with time zone NOT NULL, + owner_id uuid NOT NULL, + organization_id uuid NOT NULL, + template_id uuid NOT NULL, + deleted boolean DEFAULT false NOT NULL, + name character varying(64) NOT NULL, + autostart_schedule text, + ttl bigint, + last_used_at timestamp with time zone DEFAULT '0001-01-01 00:00:00+00'::timestamp with time zone NOT NULL, + dormant_at timestamp with time zone, + deleting_at timestamp with time zone, + automatic_updates automatic_updates DEFAULT 'never'::automatic_updates NOT NULL, + favorite boolean DEFAULT false NOT NULL, + next_start_at timestamp with time zone +); + +COMMENT ON COLUMN workspaces.favorite IS 'Favorite is true if the workspace owner has favorited the workspace.'; + CREATE VIEW workspaces_expanded AS SELECT workspaces.id, workspaces.created_at, @@ -2422,6 +2424,60 @@ CREATE OR REPLACE VIEW provisioner_job_stats AS LEFT JOIN provisioner_job_timings pjt ON ((pjt.job_id = pj.id))) GROUP BY pj.id, wb.workspace_id; +CREATE OR REPLACE VIEW workspace_prebuilds AS + WITH all_prebuilds AS ( + SELECT w.id, + w.created_at, + w.updated_at, + w.owner_id, + w.organization_id, + w.template_id, + w.deleted, + w.name, + w.autostart_schedule, + w.ttl, + w.last_used_at, + w.dormant_at, + w.deleting_at, + w.automatic_updates, + w.favorite, + w.next_start_at + FROM workspaces w + WHERE (w.owner_id = 'c42fdf75-3097-471c-8c33-fb52454d81c0'::uuid) + ), workspace_agents AS ( + SELECT w.id AS workspace_id, + wa.id AS agent_id, + wa.lifecycle_state, + wa.ready_at + FROM (((workspaces w + JOIN workspace_latest_build wlb ON ((wlb.workspace_id = w.id))) + JOIN workspace_resources wr ON ((wr.job_id = wlb.job_id))) + JOIN workspace_agents wa ON ((wa.resource_id = wr.id))) + WHERE (w.owner_id = 'c42fdf75-3097-471c-8c33-fb52454d81c0'::uuid) + GROUP BY w.id, wa.id + ) + SELECT p.id, + p.created_at, + p.updated_at, + p.owner_id, + p.organization_id, + p.template_id, + p.deleted, + p.name, + p.autostart_schedule, + p.ttl, + p.last_used_at, + p.dormant_at, + p.deleting_at, + p.automatic_updates, + p.favorite, + p.next_start_at, + a.agent_id, + a.lifecycle_state, + a.ready_at + FROM (all_prebuilds p + LEFT JOIN workspace_agents a ON ((a.workspace_id = p.id))); + CREATE TRIGGER inhibit_enqueue_if_disabled BEFORE INSERT ON notification_messages FOR EACH ROW EXECUTE FUNCTION inhibit_enqueue_if_disabled(); CREATE TRIGGER remove_organization_member_custom_role BEFORE DELETE ON custom_roles FOR EACH ROW EXECUTE FUNCTION remove_organization_member_role(); diff --git a/coderd/database/migrations/000294_prebuilds.up.sql b/coderd/database/migrations/000294_prebuilds.up.sql index 93f61104db..5b8ef0c59f 100644 --- a/coderd/database/migrations/000294_prebuilds.up.sql +++ b/coderd/database/migrations/000294_prebuilds.up.sql @@ -4,27 +4,18 @@ VALUES ('c42fdf75-3097-471c-8c33-fb52454d81c0', 'prebuilds@system', 'prebuilds', 'active', '{}', 'none', true); -- TODO: do we *want* to use the default org here? how do we handle multi-org? -WITH default_org AS ( - SELECT id FROM organizations WHERE is_default = true LIMIT 1 -) -INSERT INTO organization_members (organization_id, user_id, created_at, updated_at) -SELECT - default_org.id, - 'c42fdf75-3097-471c-8c33-fb52454d81c0', - NOW(), - NOW() +WITH default_org AS (SELECT id + FROM organizations + WHERE is_default = true + LIMIT 1) +INSERT +INTO organization_members (organization_id, user_id, created_at, updated_at) +SELECT default_org.id, + 'c42fdf75-3097-471c-8c33-fb52454d81c0', + NOW(), + NOW() FROM default_org; -CREATE VIEW workspace_prebuilds AS -SELECT * -FROM workspaces -WHERE owner_id = 'c42fdf75-3097-471c-8c33-fb52454d81c0'; - -CREATE VIEW workspace_prebuild_builds AS -SELECT * -FROM workspace_builds -WHERE initiator_id = 'c42fdf75-3097-471c-8c33-fb52454d81c0'; - CREATE VIEW workspace_latest_build AS SELECT wb.* FROM (SELECT tv.template_id, @@ -38,3 +29,22 @@ FROM (SELECT tv.template_id, AND wb.build_number = wbmax.max_build_number ); +CREATE VIEW workspace_prebuilds AS +WITH all_prebuilds AS (SELECT w.* + FROM workspaces w + WHERE w.owner_id = 'c42fdf75-3097-471c-8c33-fb52454d81c0'), + workspace_agents AS (SELECT w.id AS workspace_id, wa.id AS agent_id, wa.lifecycle_state, wa.ready_at + FROM workspaces w + INNER JOIN workspace_latest_build wlb ON wlb.workspace_id = w.id + INNER JOIN workspace_resources wr ON wr.job_id = wlb.job_id + INNER JOIN workspace_agents wa ON wa.resource_id = wr.id + WHERE w.owner_id = 'c42fdf75-3097-471c-8c33-fb52454d81c0' + GROUP BY w.id, wa.id) +SELECT p.*, a.agent_id, a.lifecycle_state, a.ready_at +FROM all_prebuilds p + LEFT JOIN workspace_agents a ON a.workspace_id = p.id; + +CREATE VIEW workspace_prebuild_builds AS +SELECT * +FROM workspace_builds +WHERE initiator_id = 'c42fdf75-3097-471c-8c33-fb52454d81c0'; diff --git a/coderd/database/models.go b/coderd/database/models.go index 4bd085c75f..e445d7594c 100644 --- a/coderd/database/models.go +++ b/coderd/database/models.go @@ -3388,22 +3388,25 @@ type WorkspaceModule struct { } type WorkspacePrebuild struct { - ID uuid.UUID `db:"id" json:"id"` - CreatedAt time.Time `db:"created_at" json:"created_at"` - UpdatedAt time.Time `db:"updated_at" json:"updated_at"` - OwnerID uuid.UUID `db:"owner_id" json:"owner_id"` - OrganizationID uuid.UUID `db:"organization_id" json:"organization_id"` - TemplateID uuid.UUID `db:"template_id" json:"template_id"` - Deleted bool `db:"deleted" json:"deleted"` - Name string `db:"name" json:"name"` - AutostartSchedule sql.NullString `db:"autostart_schedule" json:"autostart_schedule"` - Ttl sql.NullInt64 `db:"ttl" json:"ttl"` - LastUsedAt time.Time `db:"last_used_at" json:"last_used_at"` - DormantAt sql.NullTime `db:"dormant_at" json:"dormant_at"` - DeletingAt sql.NullTime `db:"deleting_at" json:"deleting_at"` - AutomaticUpdates AutomaticUpdates `db:"automatic_updates" json:"automatic_updates"` - Favorite bool `db:"favorite" json:"favorite"` - NextStartAt sql.NullTime `db:"next_start_at" json:"next_start_at"` + ID uuid.UUID `db:"id" json:"id"` + CreatedAt time.Time `db:"created_at" json:"created_at"` + UpdatedAt time.Time `db:"updated_at" json:"updated_at"` + OwnerID uuid.UUID `db:"owner_id" json:"owner_id"` + OrganizationID uuid.UUID `db:"organization_id" json:"organization_id"` + TemplateID uuid.UUID `db:"template_id" json:"template_id"` + Deleted bool `db:"deleted" json:"deleted"` + Name string `db:"name" json:"name"` + AutostartSchedule sql.NullString `db:"autostart_schedule" json:"autostart_schedule"` + Ttl sql.NullInt64 `db:"ttl" json:"ttl"` + LastUsedAt time.Time `db:"last_used_at" json:"last_used_at"` + DormantAt sql.NullTime `db:"dormant_at" json:"dormant_at"` + DeletingAt sql.NullTime `db:"deleting_at" json:"deleting_at"` + AutomaticUpdates AutomaticUpdates `db:"automatic_updates" json:"automatic_updates"` + Favorite bool `db:"favorite" json:"favorite"` + NextStartAt sql.NullTime `db:"next_start_at" json:"next_start_at"` + AgentID uuid.NullUUID `db:"agent_id" json:"agent_id"` + LifecycleState NullWorkspaceAgentLifecycleState `db:"lifecycle_state" json:"lifecycle_state"` + ReadyAt sql.NullTime `db:"ready_at" json:"ready_at"` } type WorkspacePrebuildBuild struct { diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index 4816e57142..f5e3c95cce 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -5409,6 +5409,7 @@ WHERE w.id IN (SELECT p.id AND pj.job_status IN ('succeeded'::provisioner_job_status)) AND b.template_version_id = t.active_version_id AND b.template_version_preset_id = $3::uuid + AND p.lifecycle_state = 'ready'::workspace_agent_lifecycle_state ORDER BY random() LIMIT 1 FOR UPDATE OF p SKIP LOCKED) RETURNING w.id, w.name @@ -5441,6 +5442,9 @@ WITH tvp_curr.id AS current_preset_id, tvp_desired.id AS desired_preset_id, COUNT(*) AS count, + SUM(CASE + WHEN p.lifecycle_state = 'ready'::workspace_agent_lifecycle_state THEN 1 + ELSE 0 END) AS eligible, STRING_AGG(p.id::text, ',') AS ids FROM workspace_prebuilds p INNER JOIN workspace_latest_build b ON b.workspace_id = p.id @@ -5491,6 +5495,8 @@ SELECT t.template_id, ELSE '' END)::text AS running_prebuild_ids, COALESCE(MAX(CASE WHEN t.using_active_version THEN p.count ELSE 0 END), 0)::int AS actual, -- running prebuilds for active version + COALESCE(MAX(CASE WHEN t.using_active_version THEN p.eligible ELSE 0 END), + 0)::int AS eligible, -- prebuilds which can be claimed MAX(CASE WHEN t.using_active_version THEN t.desired_instances ELSE 0 END)::int AS desired, -- we only care about the active version's desired instances COALESCE(MAX(CASE WHEN p.template_version_id = t.template_version_id AND @@ -5534,6 +5540,7 @@ type GetTemplatePrebuildStateRow struct { IsActive bool `db:"is_active" json:"is_active"` RunningPrebuildIds string `db:"running_prebuild_ids" json:"running_prebuild_ids"` Actual int32 `db:"actual" json:"actual"` + Eligible int32 `db:"eligible" json:"eligible"` Desired int32 `db:"desired" json:"desired"` Outdated int32 `db:"outdated" json:"outdated"` Extraneous int32 `db:"extraneous" json:"extraneous"` @@ -5560,6 +5567,7 @@ func (q *sqlQuerier) GetTemplatePrebuildState(ctx context.Context, templateID uu &i.IsActive, &i.RunningPrebuildIds, &i.Actual, + &i.Eligible, &i.Desired, &i.Outdated, &i.Extraneous, diff --git a/coderd/database/queries/prebuilds.sql b/coderd/database/queries/prebuilds.sql index c1071e7a66..006ebf6d11 100644 --- a/coderd/database/queries/prebuilds.sql +++ b/coderd/database/queries/prebuilds.sql @@ -6,6 +6,9 @@ WITH tvp_curr.id AS current_preset_id, tvp_desired.id AS desired_preset_id, COUNT(*) AS count, + SUM(CASE + WHEN p.lifecycle_state = 'ready'::workspace_agent_lifecycle_state THEN 1 + ELSE 0 END) AS eligible, STRING_AGG(p.id::text, ',') AS ids FROM workspace_prebuilds p INNER JOIN workspace_latest_build b ON b.workspace_id = p.id @@ -56,6 +59,8 @@ SELECT t.template_id, ELSE '' END)::text AS running_prebuild_ids, COALESCE(MAX(CASE WHEN t.using_active_version THEN p.count ELSE 0 END), 0)::int AS actual, -- running prebuilds for active version + COALESCE(MAX(CASE WHEN t.using_active_version THEN p.eligible ELSE 0 END), + 0)::int AS eligible, -- prebuilds which can be claimed MAX(CASE WHEN t.using_active_version THEN t.desired_instances ELSE 0 END)::int AS desired, -- we only care about the active version's desired instances COALESCE(MAX(CASE WHEN p.template_version_id = t.template_version_id AND @@ -106,6 +111,7 @@ WHERE w.id IN (SELECT p.id AND pj.job_status IN ('succeeded'::provisioner_job_status)) AND b.template_version_id = t.active_version_id AND b.template_version_preset_id = @preset_id::uuid + AND p.lifecycle_state = 'ready'::workspace_agent_lifecycle_state ORDER BY random() LIMIT 1 FOR UPDATE OF p SKIP LOCKED) RETURNING w.id, w.name; diff --git a/enterprise/coderd/prebuilds/controller.go b/enterprise/coderd/prebuilds/controller.go index 27ebf03c5f..0674b43e23 100644 --- a/enterprise/coderd/prebuilds/controller.go +++ b/enterprise/coderd/prebuilds/controller.go @@ -328,7 +328,8 @@ func (c *Controller) reconcileTemplate(ctx context.Context, template database.Te slog.F("to_create", len(actions.createIDs)), slog.F("to_delete", len(actions.deleteIDs)), slog.F("desired", actions.meta.Desired), slog.F("actual", actions.meta.Actual), slog.F("outdated", actions.meta.Outdated), slog.F("extraneous", actions.meta.Extraneous), - slog.F("starting", actions.meta.Starting), slog.F("stopping", actions.meta.Stopping), slog.F("deleting", actions.meta.Deleting)) + slog.F("starting", actions.meta.Starting), slog.F("stopping", actions.meta.Stopping), + slog.F("deleting", actions.meta.Deleting), slog.F("eligible", actions.meta.Eligible)) // Provision workspaces within the same tx so we don't get any timing issues here. // i.e. we hold the advisory lock until all reconciliatory actions have been taken.