feat(coderd/database): add task status and status view (#20235)

This change updates the `task_workspace_apps` table structure for
improved linking to workspace builds and adds queries to manage tasks
and a view to expose task status.

Updates coder/internal#948
Supersedes coder/coder#20212
Supersedes coder/coder#19773
This commit is contained in:
Mathias Fredriksson
2025-10-13 12:25:58 +03:00
committed by GitHub
parent 299a54a99b
commit 952c69f412
19 changed files with 1249 additions and 130 deletions
@@ -0,0 +1,33 @@
DROP VIEW IF EXISTS tasks_with_status;
DROP TYPE IF EXISTS task_status;
DROP INDEX IF EXISTS tasks_organization_id_idx;
DROP INDEX IF EXISTS tasks_owner_id_idx;
DROP INDEX IF EXISTS tasks_workspace_id_idx;
ALTER TABLE task_workspace_apps
DROP CONSTRAINT IF EXISTS task_workspace_apps_pkey;
-- Add back workspace_build_id column.
ALTER TABLE task_workspace_apps
ADD COLUMN workspace_build_id UUID;
-- Try to populate workspace_build_id from workspace_builds.
UPDATE task_workspace_apps
SET workspace_build_id = workspace_builds.id
FROM workspace_builds
WHERE workspace_builds.build_number = task_workspace_apps.workspace_build_number
AND workspace_builds.workspace_id IN (
SELECT workspace_id FROM tasks WHERE tasks.id = task_workspace_apps.task_id
);
-- Remove rows that couldn't be restored.
DELETE FROM task_workspace_apps
WHERE workspace_build_id IS NULL;
-- Restore original schema.
ALTER TABLE task_workspace_apps
DROP COLUMN workspace_build_number,
ALTER COLUMN workspace_build_id SET NOT NULL,
ALTER COLUMN workspace_agent_id SET NOT NULL,
ALTER COLUMN workspace_app_id SET NOT NULL;
@@ -0,0 +1,104 @@
-- Replace workspace_build_id with workspace_build_number.
ALTER TABLE task_workspace_apps
ADD COLUMN workspace_build_number INTEGER;
-- Try to populate workspace_build_number from workspace_builds.
UPDATE task_workspace_apps
SET workspace_build_number = workspace_builds.build_number
FROM workspace_builds
WHERE workspace_builds.id = task_workspace_apps.workspace_build_id;
-- Remove rows that couldn't be migrated.
DELETE FROM task_workspace_apps
WHERE workspace_build_number IS NULL;
ALTER TABLE task_workspace_apps
DROP COLUMN workspace_build_id,
ALTER COLUMN workspace_build_number SET NOT NULL,
ALTER COLUMN workspace_agent_id DROP NOT NULL,
ALTER COLUMN workspace_app_id DROP NOT NULL,
ADD CONSTRAINT task_workspace_apps_pkey PRIMARY KEY (task_id, workspace_build_number);
-- Add indexes for common joins or filters.
CREATE INDEX IF NOT EXISTS tasks_workspace_id_idx ON tasks (workspace_id);
CREATE INDEX IF NOT EXISTS tasks_owner_id_idx ON tasks (owner_id);
CREATE INDEX IF NOT EXISTS tasks_organization_id_idx ON tasks (organization_id);
CREATE TYPE task_status AS ENUM (
'pending',
'initializing',
'active',
'paused',
'unknown',
'error'
);
CREATE VIEW
tasks_with_status
AS
SELECT
tasks.*,
CASE
WHEN tasks.workspace_id IS NULL OR latest_build.job_status IS NULL THEN 'pending'::task_status
WHEN latest_build.job_status = 'failed' THEN 'error'::task_status
WHEN latest_build.transition IN ('stop', 'delete')
AND latest_build.job_status = 'succeeded' THEN 'paused'::task_status
WHEN latest_build.transition = 'start'
AND latest_build.job_status = 'pending' THEN 'initializing'::task_status
WHEN latest_build.transition = 'start' AND latest_build.job_status IN ('running', 'succeeded') THEN
CASE
WHEN agent_status.none THEN 'initializing'::task_status
WHEN agent_status.connecting THEN 'initializing'::task_status
WHEN agent_status.connected THEN
CASE
WHEN app_status.any_unhealthy THEN 'error'::task_status
WHEN app_status.any_initializing THEN 'initializing'::task_status
WHEN app_status.all_healthy_or_disabled THEN 'active'::task_status
ELSE 'unknown'::task_status
END
ELSE 'unknown'::task_status
END
ELSE 'unknown'::task_status
END AS status
FROM
tasks
LEFT JOIN LATERAL (
SELECT workspace_build_number, workspace_agent_id, workspace_app_id
FROM task_workspace_apps task_app
WHERE task_id = tasks.id
ORDER BY workspace_build_number DESC
LIMIT 1
) task_app ON TRUE
LEFT JOIN LATERAL (
SELECT
workspace_build.transition,
provisioner_job.job_status,
workspace_build.job_id
FROM workspace_builds workspace_build
JOIN provisioner_jobs provisioner_job ON provisioner_job.id = workspace_build.job_id
WHERE workspace_build.workspace_id = tasks.workspace_id
AND workspace_build.build_number = task_app.workspace_build_number
) latest_build ON TRUE
CROSS JOIN LATERAL (
SELECT
COUNT(*) = 0 AS none,
bool_or(workspace_agent.lifecycle_state IN ('created', 'starting')) AS connecting,
bool_and(workspace_agent.lifecycle_state = 'ready') AS connected
FROM workspace_agents workspace_agent
WHERE workspace_agent.id = task_app.workspace_agent_id
) agent_status
CROSS JOIN LATERAL (
SELECT
bool_or(workspace_app.health = 'unhealthy') AS any_unhealthy,
bool_or(workspace_app.health = 'initializing') AS any_initializing,
bool_and(workspace_app.health IN ('healthy', 'disabled')) AS all_healthy_or_disabled
FROM workspace_apps workspace_app
WHERE workspace_app.id = task_app.workspace_app_id
) app_status
WHERE
tasks.deleted_at IS NULL;
@@ -0,0 +1,6 @@
INSERT INTO public.task_workspace_apps VALUES (
'f5a1c3e4-8b2d-4f6a-9d7e-2a8b5c9e1f3d', -- task_id
NULL, -- workspace_agent_id
NULL, -- workspace_app_id
99 -- workspace_build_number
) ON CONFLICT DO NOTHING;