chore: compute job status as column (#10024)

* chore: provisioner job status as column
* use provisioner job status for workspace searching
This commit is contained in:
Steven Masley 2023-10-04 20:57:46 -05:00 committed by GitHub
parent d5040441aa
commit 5021e23105
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
24 changed files with 358 additions and 222 deletions

6
coderd/apidoc/docs.go generated
View File

@ -9124,7 +9124,8 @@ const docTemplate = `{
"succeeded",
"canceling",
"canceled",
"failed"
"failed",
"unknown"
],
"x-enum-varnames": [
"ProvisionerJobPending",
@ -9132,7 +9133,8 @@ const docTemplate = `{
"ProvisionerJobSucceeded",
"ProvisionerJobCanceling",
"ProvisionerJobCanceled",
"ProvisionerJobFailed"
"ProvisionerJobFailed",
"ProvisionerJobUnknown"
]
},
"codersdk.ProvisionerLogLevel": {

View File

@ -8211,7 +8211,8 @@
"succeeded",
"canceling",
"canceled",
"failed"
"failed",
"unknown"
],
"x-enum-varnames": [
"ProvisionerJobPending",
@ -8219,7 +8220,8 @@
"ProvisionerJobSucceeded",
"ProvisionerJobCanceling",
"ProvisionerJobCanceled",
"ProvisionerJobFailed"
"ProvisionerJobFailed",
"ProvisionerJobUnknown"
]
},
"codersdk.ProvisionerLogLevel": {

View File

@ -13,7 +13,6 @@ import (
"cdr.dev/slog"
"github.com/coder/coder/v2/coderd/database"
"github.com/coder/coder/v2/coderd/database/db2sdk"
"github.com/coder/coder/v2/coderd/database/dbauthz"
"github.com/coder/coder/v2/coderd/database/dbtime"
"github.com/coder/coder/v2/coderd/database/provisionerjobs"
@ -303,7 +302,7 @@ func getNextTransition(
// isEligibleForAutostart returns true if the workspace should be autostarted.
func isEligibleForAutostart(ws database.Workspace, build database.WorkspaceBuild, job database.ProvisionerJob, templateSchedule schedule.TemplateScheduleOptions, currentTick time.Time) bool {
// Don't attempt to autostart failed workspaces.
if db2sdk.ProvisionerJobStatus(job) == codersdk.ProvisionerJobFailed {
if codersdk.ProvisionerJobStatus(job.JobStatus) == codersdk.ProvisionerJobFailed {
return false
}
@ -337,7 +336,7 @@ func isEligibleForAutostart(ws database.Workspace, build database.WorkspaceBuild
// isEligibleForAutostart returns true if the workspace should be autostopped.
func isEligibleForAutostop(ws database.Workspace, build database.WorkspaceBuild, job database.ProvisionerJob, currentTick time.Time) bool {
if db2sdk.ProvisionerJobStatus(job) == codersdk.ProvisionerJobFailed {
if codersdk.ProvisionerJobStatus(job.JobStatus) == codersdk.ProvisionerJobFailed {
return false
}
@ -379,7 +378,7 @@ func isEligibleForFailedStop(build database.WorkspaceBuild, job database.Provisi
// If the template has specified a failure TLL.
return templateSchedule.FailureTTL > 0 &&
// And the job resulted in failure.
db2sdk.ProvisionerJobStatus(job) == codersdk.ProvisionerJobFailed &&
codersdk.ProvisionerJobStatus(job.JobStatus) == codersdk.ProvisionerJobFailed &&
build.Transition == database.WorkspaceTransitionStart &&
// And sufficient time has elapsed since the job has completed.
job.CompletedAt.Valid &&

View File

@ -71,31 +71,6 @@ func TemplateVersionParameter(param database.TemplateVersionParameter) (codersdk
}, nil
}
func ProvisionerJobStatus(provisionerJob database.ProvisionerJob) codersdk.ProvisionerJobStatus {
// The case where jobs are hung is handled by the unhang package. We can't
// just return Failed here when it's hung because that doesn't reflect in
// the database.
switch {
case provisionerJob.CanceledAt.Valid:
if !provisionerJob.CompletedAt.Valid {
return codersdk.ProvisionerJobCanceling
}
if provisionerJob.Error.String == "" {
return codersdk.ProvisionerJobCanceled
}
return codersdk.ProvisionerJobFailed
case !provisionerJob.StartedAt.Valid:
return codersdk.ProvisionerJobPending
case provisionerJob.CompletedAt.Valid:
if provisionerJob.Error.String == "" {
return codersdk.ProvisionerJobSucceeded
}
return codersdk.ProvisionerJobFailed
default:
return codersdk.ProvisionerJobRunning
}
}
func User(user database.User, organizationIDs []uuid.UUID) codersdk.User {
convertedUser := codersdk.User{
ID: user.ID,

View File

@ -4,13 +4,17 @@ import (
"crypto/rand"
"database/sql"
"encoding/json"
"fmt"
"testing"
"time"
"github.com/google/uuid"
"github.com/stretchr/testify/require"
"github.com/coder/coder/v2/coderd/database"
"github.com/coder/coder/v2/coderd/database/db2sdk"
"github.com/coder/coder/v2/coderd/database/dbgen"
"github.com/coder/coder/v2/coderd/database/dbtestutil"
"github.com/coder/coder/v2/coderd/database/dbtime"
"github.com/coder/coder/v2/codersdk"
"github.com/coder/coder/v2/provisionersdk/proto"
@ -110,11 +114,36 @@ func TestProvisionerJobStatus(t *testing.T) {
},
}
for _, tc := range cases {
// Share db for all job inserts.
db, _ := dbtestutil.NewDB(t)
org := dbgen.Organization(t, db, database.Organization{})
for i, tc := range cases {
tc := tc
i := i
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
actual := db2sdk.ProvisionerJobStatus(tc.job)
// Populate standard fields
now := dbtime.Now().Round(time.Minute)
tc.job.ID = uuid.New()
tc.job.CreatedAt = now
tc.job.UpdatedAt = now
tc.job.InitiatorID = org.ID
tc.job.OrganizationID = org.ID
tc.job.Input = []byte("{}")
tc.job.Provisioner = database.ProvisionerTypeEcho
// Unique tags for each job.
tc.job.Tags = map[string]string{fmt.Sprintf("%d", i): "true"}
inserted := dbgen.ProvisionerJob(t, db, nil, tc.job)
// Make sure the inserted job has the right values.
require.Equal(t, tc.job.StartedAt.Time.UTC(), inserted.StartedAt.Time.UTC(), "started at")
require.Equal(t, tc.job.CompletedAt.Time.UTC(), inserted.CompletedAt.Time.UTC(), "completed at")
require.Equal(t, tc.job.CanceledAt.Time.UTC(), inserted.CanceledAt.Time.UTC(), "cancelled at")
require.Equal(t, tc.job.Error, inserted.Error, "error")
require.Equal(t, tc.job.ErrorCode, inserted.ErrorCode, "error code")
actual := codersdk.ProvisionerJobStatus(inserted.JobStatus)
require.Equal(t, tc.status, actual)
})
}

View File

@ -20,7 +20,6 @@ import (
"golang.org/x/xerrors"
"github.com/coder/coder/v2/coderd/database"
"github.com/coder/coder/v2/coderd/database/db2sdk"
"github.com/coder/coder/v2/coderd/database/dbtime"
"github.com/coder/coder/v2/coderd/httpapi"
"github.com/coder/coder/v2/coderd/rbac"
@ -604,16 +603,6 @@ func (q *FakeQuerier) getGroupByIDNoLock(_ context.Context, id uuid.UUID) (datab
return database.Group{}, sql.ErrNoRows
}
// isNull is only used in dbfake, so reflect is ok. Use this to make the logic
// look more similar to the postgres.
func isNull(v interface{}) bool {
return !isNotNull(v)
}
func isNotNull(v interface{}) bool {
return reflect.ValueOf(v).FieldByName("Valid").Bool()
}
// ErrUnimplemented is returned by methods only used by the enterprise/tailnet.pgCoord. This coordinator explicitly
// depends on postgres triggers that announce changes on the pubsub. Implementing support for this in the fake
// database would strongly couple the FakeQuerier to the pubsub, which is undesirable. Furthermore, it makes little
@ -695,6 +684,36 @@ func minTime(t, u time.Time) time.Time {
return u
}
func provisonerJobStatus(j database.ProvisionerJob) database.ProvisionerJobStatus {
if isNotNull(j.CompletedAt) {
if j.Error.String != "" {
return database.ProvisionerJobStatusFailed
}
if isNotNull(j.CanceledAt) {
return database.ProvisionerJobStatusCanceled
}
return database.ProvisionerJobStatusSucceeded
}
if isNotNull(j.CanceledAt) {
return database.ProvisionerJobStatusCanceling
}
if isNull(j.StartedAt) {
return database.ProvisionerJobStatusPending
}
return database.ProvisionerJobStatusRunning
}
// isNull is only used in dbfake, so reflect is ok. Use this to make the logic
// look more similar to the postgres.
func isNull(v interface{}) bool {
return !isNotNull(v)
}
func isNotNull(v interface{}) bool {
return reflect.ValueOf(v).FieldByName("Valid").Bool()
}
func (*FakeQuerier) AcquireLock(_ context.Context, _ int64) error {
return xerrors.New("AcquireLock must only be called within a transaction")
}
@ -748,6 +767,7 @@ func (q *FakeQuerier) AcquireProvisionerJob(_ context.Context, arg database.Acqu
provisionerJob.StartedAt = arg.StartedAt
provisionerJob.UpdatedAt = arg.StartedAt.Time
provisionerJob.WorkerID = arg.WorkerID
provisionerJob.JobStatus = provisonerJobStatus(provisionerJob)
q.provisionerJobs[index] = provisionerJob
return provisionerJob, nil
}
@ -4077,7 +4097,7 @@ func (q *FakeQuerier) GetWorkspacesEligibleForTransition(ctx context.Context, no
if err != nil {
return nil, xerrors.Errorf("get provisioner job by ID: %w", err)
}
if db2sdk.ProvisionerJobStatus(job) == codersdk.ProvisionerJobFailed {
if codersdk.ProvisionerJobStatus(job.JobStatus) == codersdk.ProvisionerJobFailed {
workspaces = append(workspaces, workspace)
continue
}
@ -4464,6 +4484,7 @@ func (q *FakeQuerier) InsertProvisionerJob(_ context.Context, arg database.Inser
Input: arg.Input,
Tags: arg.Tags,
}
job.JobStatus = provisonerJobStatus(job)
q.provisionerJobs = append(q.provisionerJobs, job)
return job, nil
}
@ -5393,6 +5414,7 @@ func (q *FakeQuerier) UpdateProvisionerJobByID(_ context.Context, arg database.U
continue
}
job.UpdatedAt = arg.UpdatedAt
job.JobStatus = provisonerJobStatus(job)
q.provisionerJobs[index] = job
return nil
}
@ -5413,6 +5435,7 @@ func (q *FakeQuerier) UpdateProvisionerJobWithCancelByID(_ context.Context, arg
}
job.CanceledAt = arg.CanceledAt
job.CompletedAt = arg.CompletedAt
job.JobStatus = provisonerJobStatus(job)
q.provisionerJobs[index] = job
return nil
}
@ -5435,6 +5458,7 @@ func (q *FakeQuerier) UpdateProvisionerJobWithCompleteByID(_ context.Context, ar
job.CompletedAt = arg.CompletedAt
job.Error = arg.Error
job.ErrorCode = arg.ErrorCode
job.JobStatus = provisonerJobStatus(job)
q.provisionerJobs[index] = job
return nil
}
@ -6604,61 +6628,30 @@ func (q *FakeQuerier) GetAuthorizedWorkspaces(ctx context.Context, arg database.
// This logic should match the logic in the workspace.sql file.
var statusMatch bool
switch database.WorkspaceStatus(arg.Status) {
case database.WorkspaceStatusPending:
statusMatch = isNull(job.StartedAt)
case database.WorkspaceStatusStarting:
statusMatch = isNotNull(job.StartedAt) &&
isNull(job.CanceledAt) &&
isNull(job.CompletedAt) &&
time.Since(job.UpdatedAt) < 30*time.Second &&
statusMatch = job.JobStatus == database.ProvisionerJobStatusRunning &&
build.Transition == database.WorkspaceTransitionStart
case database.WorkspaceStatusRunning:
statusMatch = isNotNull(job.CompletedAt) &&
isNull(job.CanceledAt) &&
isNull(job.Error) &&
build.Transition == database.WorkspaceTransitionStart
case database.WorkspaceStatusStopping:
statusMatch = isNotNull(job.StartedAt) &&
isNull(job.CanceledAt) &&
isNull(job.CompletedAt) &&
time.Since(job.UpdatedAt) < 30*time.Second &&
statusMatch = job.JobStatus == database.ProvisionerJobStatusRunning &&
build.Transition == database.WorkspaceTransitionStop
case database.WorkspaceStatusStopped:
statusMatch = isNotNull(job.CompletedAt) &&
isNull(job.CanceledAt) &&
isNull(job.Error) &&
build.Transition == database.WorkspaceTransitionStop
case database.WorkspaceStatusFailed:
statusMatch = (isNotNull(job.CanceledAt) && isNotNull(job.Error)) ||
(isNotNull(job.CompletedAt) && isNotNull(job.Error))
case database.WorkspaceStatusCanceling:
statusMatch = isNotNull(job.CanceledAt) &&
isNull(job.CompletedAt)
case database.WorkspaceStatusCanceled:
statusMatch = isNotNull(job.CanceledAt) &&
isNotNull(job.CompletedAt)
case database.WorkspaceStatusDeleted:
statusMatch = isNotNull(job.StartedAt) &&
isNull(job.CanceledAt) &&
isNotNull(job.CompletedAt) &&
time.Since(job.UpdatedAt) < 30*time.Second &&
build.Transition == database.WorkspaceTransitionDelete &&
isNull(job.Error)
case database.WorkspaceStatusDeleting:
statusMatch = isNull(job.CompletedAt) &&
isNull(job.CanceledAt) &&
isNull(job.Error) &&
statusMatch = job.JobStatus == database.ProvisionerJobStatusRunning &&
build.Transition == database.WorkspaceTransitionDelete
case "started":
statusMatch = job.JobStatus == database.ProvisionerJobStatusSucceeded &&
build.Transition == database.WorkspaceTransitionStart
case database.WorkspaceStatusDeleted:
statusMatch = job.JobStatus == database.ProvisionerJobStatusSucceeded &&
build.Transition == database.WorkspaceTransitionDelete
case database.WorkspaceStatusStopped:
statusMatch = job.JobStatus == database.ProvisionerJobStatusSucceeded &&
build.Transition == database.WorkspaceTransitionStop
case database.WorkspaceStatusRunning:
statusMatch = job.JobStatus == database.ProvisionerJobStatusSucceeded &&
build.Transition == database.WorkspaceTransitionStart
default:
return nil, xerrors.Errorf("unknown workspace status in filter: %q", arg.Status)
statusMatch = job.JobStatus == database.ProvisionerJobStatus(arg.Status)
}
if !statusMatch {
continue

View File

@ -328,16 +328,16 @@ func GroupMember(t testing.TB, db database.Store, orig database.GroupMember) dat
// ProvisionerJob is a bit more involved to get the values such as "completedAt", "startedAt", "cancelledAt" set. ps
// can be set to nil if you are SURE that you don't require a provisionerdaemon to acquire the job in your test.
func ProvisionerJob(t testing.TB, db database.Store, ps pubsub.Pubsub, orig database.ProvisionerJob) database.ProvisionerJob {
id := takeFirst(orig.ID, uuid.New())
jobID := takeFirst(orig.ID, uuid.New())
// Always set some tags to prevent Acquire from grabbing jobs it should not.
if !orig.StartedAt.Time.IsZero() {
if orig.Tags == nil {
orig.Tags = make(database.StringMap)
}
// Make sure when we acquire the job, we only get this one.
orig.Tags[id.String()] = "true"
orig.Tags[jobID.String()] = "true"
}
jobID := takeFirst(orig.ID, uuid.New())
job, err := db.InsertProvisionerJob(genCtx, database.InsertProvisionerJobParams{
ID: jobID,
CreatedAt: takeFirst(orig.CreatedAt, dbtime.Now()),
@ -365,6 +365,8 @@ func ProvisionerJob(t testing.TB, db database.Store, ps pubsub.Pubsub, orig data
WorkerID: uuid.NullUUID{},
})
require.NoError(t, err)
// There is no easy way to make sure we acquire the correct job.
require.Equal(t, jobID, job.ID, "acquired incorrect job")
}
if !orig.CompletedAt.Time.IsZero() || orig.Error.String != "" {

View File

@ -89,6 +89,18 @@ CREATE TYPE parameter_type_system AS ENUM (
'hcl'
);
CREATE TYPE provisioner_job_status AS ENUM (
'pending',
'running',
'succeeded',
'canceling',
'canceled',
'failed',
'unknown'
);
COMMENT ON TYPE provisioner_job_status IS 'Computed status of a provisioner job. Jobs could be stuck in a hung state, these states do not guarantee any transition to another state.';
CREATE TYPE provisioner_job_type AS ENUM (
'template_version_import',
'workspace_build',
@ -500,9 +512,27 @@ CREATE TABLE provisioner_jobs (
file_id uuid NOT NULL,
tags jsonb DEFAULT '{"scope": "organization"}'::jsonb NOT NULL,
error_code text,
trace_metadata jsonb
trace_metadata jsonb,
job_status provisioner_job_status GENERATED ALWAYS AS (
CASE
WHEN (completed_at IS NOT NULL) THEN
CASE
WHEN (error <> ''::text) THEN 'failed'::provisioner_job_status
WHEN (canceled_at IS NOT NULL) THEN 'canceled'::provisioner_job_status
ELSE 'succeeded'::provisioner_job_status
END
ELSE
CASE
WHEN (error <> ''::text) THEN 'failed'::provisioner_job_status
WHEN (canceled_at IS NOT NULL) THEN 'canceling'::provisioner_job_status
WHEN (started_at IS NULL) THEN 'pending'::provisioner_job_status
ELSE 'running'::provisioner_job_status
END
END) STORED NOT NULL
);
COMMENT ON COLUMN provisioner_jobs.job_status IS 'Computed column to track the status of the job.';
CREATE TABLE replicas (
id uuid NOT NULL,
created_at timestamp with time zone NOT NULL,

View File

@ -0,0 +1,6 @@
BEGIN;
ALTER TABLE provisioner_jobs DROP COLUMN job_status;
DROP TYPE provisioner_job_status;
COMMIT;

View File

@ -0,0 +1,38 @@
BEGIN;
CREATE TYPE provisioner_job_status AS ENUM ('pending', 'running', 'succeeded', 'canceling', 'canceled', 'failed', 'unknown');
COMMENT ON TYPE provisioner_job_status IS 'Computed status of a provisioner job. Jobs could be stuck in a hung state, these states do not guarantee any transition to another state.';
ALTER TABLE provisioner_jobs ADD COLUMN
job_status provisioner_job_status NOT NULL GENERATED ALWAYS AS (
CASE
-- Completed means it is not in an "-ing" state
WHEN completed_at IS NOT NULL THEN
CASE
-- The order of these checks are important.
-- Check the error first, then cancelled, then completed.
WHEN error != '' THEN 'failed'::provisioner_job_status
WHEN canceled_at IS NOT NULL THEN 'canceled'::provisioner_job_status
ELSE 'succeeded'::provisioner_job_status
END
-- Not completed means it is in some "-ing" state
ELSE
CASE
-- This should never happen because all errors set
-- should also set a completed_at timestamp.
-- But if there is an error, we should always return
-- a failed state.
WHEN error != '' THEN 'failed'::provisioner_job_status
WHEN canceled_at IS NOT NULL THEN 'canceling'::provisioner_job_status
-- Not done and not started means it is pending
WHEN started_at IS NULL THEN 'pending'::provisioner_job_status
ELSE 'running'::provisioner_job_status
END
END
-- Stored so we do not have to recompute it every time.
) STORED;
COMMENT ON COLUMN provisioner_jobs.job_status IS 'Computed column to track the status of the job.';
COMMIT;

View File

@ -837,6 +837,80 @@ func AllParameterTypeSystemValues() []ParameterTypeSystem {
}
}
// Computed status of a provisioner job. Jobs could be stuck in a hung state, these states do not guarantee any transition to another state.
type ProvisionerJobStatus string
const (
ProvisionerJobStatusPending ProvisionerJobStatus = "pending"
ProvisionerJobStatusRunning ProvisionerJobStatus = "running"
ProvisionerJobStatusSucceeded ProvisionerJobStatus = "succeeded"
ProvisionerJobStatusCanceling ProvisionerJobStatus = "canceling"
ProvisionerJobStatusCanceled ProvisionerJobStatus = "canceled"
ProvisionerJobStatusFailed ProvisionerJobStatus = "failed"
ProvisionerJobStatusUnknown ProvisionerJobStatus = "unknown"
)
func (e *ProvisionerJobStatus) Scan(src interface{}) error {
switch s := src.(type) {
case []byte:
*e = ProvisionerJobStatus(s)
case string:
*e = ProvisionerJobStatus(s)
default:
return fmt.Errorf("unsupported scan type for ProvisionerJobStatus: %T", src)
}
return nil
}
type NullProvisionerJobStatus struct {
ProvisionerJobStatus ProvisionerJobStatus `json:"provisioner_job_status"`
Valid bool `json:"valid"` // Valid is true if ProvisionerJobStatus is not NULL
}
// Scan implements the Scanner interface.
func (ns *NullProvisionerJobStatus) Scan(value interface{}) error {
if value == nil {
ns.ProvisionerJobStatus, ns.Valid = "", false
return nil
}
ns.Valid = true
return ns.ProvisionerJobStatus.Scan(value)
}
// Value implements the driver Valuer interface.
func (ns NullProvisionerJobStatus) Value() (driver.Value, error) {
if !ns.Valid {
return nil, nil
}
return string(ns.ProvisionerJobStatus), nil
}
func (e ProvisionerJobStatus) Valid() bool {
switch e {
case ProvisionerJobStatusPending,
ProvisionerJobStatusRunning,
ProvisionerJobStatusSucceeded,
ProvisionerJobStatusCanceling,
ProvisionerJobStatusCanceled,
ProvisionerJobStatusFailed,
ProvisionerJobStatusUnknown:
return true
}
return false
}
func AllProvisionerJobStatusValues() []ProvisionerJobStatus {
return []ProvisionerJobStatus{
ProvisionerJobStatusPending,
ProvisionerJobStatusRunning,
ProvisionerJobStatusSucceeded,
ProvisionerJobStatusCanceling,
ProvisionerJobStatusCanceled,
ProvisionerJobStatusFailed,
ProvisionerJobStatusUnknown,
}
}
type ProvisionerJobType string
const (
@ -1671,6 +1745,8 @@ type ProvisionerJob struct {
Tags StringMap `db:"tags" json:"tags"`
ErrorCode sql.NullString `db:"error_code" json:"error_code"`
TraceMetadata pqtype.NullRawMessage `db:"trace_metadata" json:"trace_metadata"`
// Computed column to track the status of the job.
JobStatus ProvisionerJobStatus `db:"job_status" json:"job_status"`
}
type ProvisionerJobLog struct {

View File

@ -2988,7 +2988,7 @@ WHERE
SKIP LOCKED
LIMIT
1
) RETURNING id, created_at, updated_at, started_at, canceled_at, completed_at, error, organization_id, initiator_id, provisioner, storage_method, type, input, worker_id, file_id, tags, error_code, trace_metadata
) RETURNING id, created_at, updated_at, started_at, canceled_at, completed_at, error, organization_id, initiator_id, provisioner, storage_method, type, input, worker_id, file_id, tags, error_code, trace_metadata, job_status
`
type AcquireProvisionerJobParams struct {
@ -3031,13 +3031,14 @@ func (q *sqlQuerier) AcquireProvisionerJob(ctx context.Context, arg AcquireProvi
&i.Tags,
&i.ErrorCode,
&i.TraceMetadata,
&i.JobStatus,
)
return i, err
}
const getHungProvisionerJobs = `-- name: GetHungProvisionerJobs :many
SELECT
id, created_at, updated_at, started_at, canceled_at, completed_at, error, organization_id, initiator_id, provisioner, storage_method, type, input, worker_id, file_id, tags, error_code, trace_metadata
id, created_at, updated_at, started_at, canceled_at, completed_at, error, organization_id, initiator_id, provisioner, storage_method, type, input, worker_id, file_id, tags, error_code, trace_metadata, job_status
FROM
provisioner_jobs
WHERE
@ -3074,6 +3075,7 @@ func (q *sqlQuerier) GetHungProvisionerJobs(ctx context.Context, updatedAt time.
&i.Tags,
&i.ErrorCode,
&i.TraceMetadata,
&i.JobStatus,
); err != nil {
return nil, err
}
@ -3090,7 +3092,7 @@ func (q *sqlQuerier) GetHungProvisionerJobs(ctx context.Context, updatedAt time.
const getProvisionerJobByID = `-- name: GetProvisionerJobByID :one
SELECT
id, created_at, updated_at, started_at, canceled_at, completed_at, error, organization_id, initiator_id, provisioner, storage_method, type, input, worker_id, file_id, tags, error_code, trace_metadata
id, created_at, updated_at, started_at, canceled_at, completed_at, error, organization_id, initiator_id, provisioner, storage_method, type, input, worker_id, file_id, tags, error_code, trace_metadata, job_status
FROM
provisioner_jobs
WHERE
@ -3119,13 +3121,14 @@ func (q *sqlQuerier) GetProvisionerJobByID(ctx context.Context, id uuid.UUID) (P
&i.Tags,
&i.ErrorCode,
&i.TraceMetadata,
&i.JobStatus,
)
return i, err
}
const getProvisionerJobsByIDs = `-- name: GetProvisionerJobsByIDs :many
SELECT
id, created_at, updated_at, started_at, canceled_at, completed_at, error, organization_id, initiator_id, provisioner, storage_method, type, input, worker_id, file_id, tags, error_code, trace_metadata
id, created_at, updated_at, started_at, canceled_at, completed_at, error, organization_id, initiator_id, provisioner, storage_method, type, input, worker_id, file_id, tags, error_code, trace_metadata, job_status
FROM
provisioner_jobs
WHERE
@ -3160,6 +3163,7 @@ func (q *sqlQuerier) GetProvisionerJobsByIDs(ctx context.Context, ids []uuid.UUI
&i.Tags,
&i.ErrorCode,
&i.TraceMetadata,
&i.JobStatus,
); err != nil {
return nil, err
}
@ -3194,7 +3198,7 @@ queue_size AS (
SELECT COUNT(*) as count FROM unstarted_jobs
)
SELECT
pj.id, pj.created_at, pj.updated_at, pj.started_at, pj.canceled_at, pj.completed_at, pj.error, pj.organization_id, pj.initiator_id, pj.provisioner, pj.storage_method, pj.type, pj.input, pj.worker_id, pj.file_id, pj.tags, pj.error_code, pj.trace_metadata,
pj.id, pj.created_at, pj.updated_at, pj.started_at, pj.canceled_at, pj.completed_at, pj.error, pj.organization_id, pj.initiator_id, pj.provisioner, pj.storage_method, pj.type, pj.input, pj.worker_id, pj.file_id, pj.tags, pj.error_code, pj.trace_metadata, pj.job_status,
COALESCE(qp.queue_position, 0) AS queue_position,
COALESCE(qs.count, 0) AS queue_size
FROM
@ -3241,6 +3245,7 @@ func (q *sqlQuerier) GetProvisionerJobsByIDsWithQueuePosition(ctx context.Contex
&i.ProvisionerJob.Tags,
&i.ProvisionerJob.ErrorCode,
&i.ProvisionerJob.TraceMetadata,
&i.ProvisionerJob.JobStatus,
&i.QueuePosition,
&i.QueueSize,
); err != nil {
@ -3258,7 +3263,7 @@ func (q *sqlQuerier) GetProvisionerJobsByIDsWithQueuePosition(ctx context.Contex
}
const getProvisionerJobsCreatedAfter = `-- name: GetProvisionerJobsCreatedAfter :many
SELECT id, created_at, updated_at, started_at, canceled_at, completed_at, error, organization_id, initiator_id, provisioner, storage_method, type, input, worker_id, file_id, tags, error_code, trace_metadata FROM provisioner_jobs WHERE created_at > $1
SELECT id, created_at, updated_at, started_at, canceled_at, completed_at, error, organization_id, initiator_id, provisioner, storage_method, type, input, worker_id, file_id, tags, error_code, trace_metadata, job_status FROM provisioner_jobs WHERE created_at > $1
`
func (q *sqlQuerier) GetProvisionerJobsCreatedAfter(ctx context.Context, createdAt time.Time) ([]ProvisionerJob, error) {
@ -3289,6 +3294,7 @@ func (q *sqlQuerier) GetProvisionerJobsCreatedAfter(ctx context.Context, created
&i.Tags,
&i.ErrorCode,
&i.TraceMetadata,
&i.JobStatus,
); err != nil {
return nil, err
}
@ -3320,7 +3326,7 @@ INSERT INTO
trace_metadata
)
VALUES
($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12) RETURNING id, created_at, updated_at, started_at, canceled_at, completed_at, error, organization_id, initiator_id, provisioner, storage_method, type, input, worker_id, file_id, tags, error_code, trace_metadata
($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12) RETURNING id, created_at, updated_at, started_at, canceled_at, completed_at, error, organization_id, initiator_id, provisioner, storage_method, type, input, worker_id, file_id, tags, error_code, trace_metadata, job_status
`
type InsertProvisionerJobParams struct {
@ -3373,6 +3379,7 @@ func (q *sqlQuerier) InsertProvisionerJob(ctx context.Context, arg InsertProvisi
&i.Tags,
&i.ErrorCode,
&i.TraceMetadata,
&i.JobStatus,
)
return i, err
}
@ -9841,7 +9848,8 @@ LEFT JOIN LATERAL (
provisioner_jobs.updated_at,
provisioner_jobs.canceled_at,
provisioner_jobs.completed_at,
provisioner_jobs.error
provisioner_jobs.error,
provisioner_jobs.job_status
FROM
workspace_builds
LEFT JOIN
@ -9873,63 +9881,42 @@ WHERE
AND CASE
WHEN $2 :: text != '' THEN
CASE
WHEN $2 = 'pending' THEN
latest_build.started_at IS NULL
-- Some workspace specific status refer to the transition
-- type. By default, the standard provisioner job status
-- search strings are supported.
-- 'running' states
WHEN $2 = 'starting' THEN
latest_build.started_at IS NOT NULL AND
latest_build.canceled_at IS NULL AND
latest_build.completed_at IS NULL AND
latest_build.updated_at - INTERVAL '30 seconds' < NOW() AND
latest_build.job_status = 'running'::provisioner_job_status AND
latest_build.transition = 'start'::workspace_transition
WHEN $2 = 'running' THEN
latest_build.completed_at IS NOT NULL AND
latest_build.canceled_at IS NULL AND
latest_build.error IS NULL AND
latest_build.transition = 'start'::workspace_transition
WHEN $2 = 'stopping' THEN
latest_build.started_at IS NOT NULL AND
latest_build.canceled_at IS NULL AND
latest_build.completed_at IS NULL AND
latest_build.updated_at - INTERVAL '30 seconds' < NOW() AND
latest_build.job_status = 'running'::provisioner_job_status AND
latest_build.transition = 'stop'::workspace_transition
WHEN $2 = 'stopped' THEN
latest_build.completed_at IS NOT NULL AND
latest_build.canceled_at IS NULL AND
latest_build.error IS NULL AND
latest_build.transition = 'stop'::workspace_transition
WHEN $2 = 'failed' THEN
(latest_build.canceled_at IS NOT NULL AND
latest_build.error IS NOT NULL) OR
(latest_build.completed_at IS NOT NULL AND
latest_build.error IS NOT NULL)
WHEN $2 = 'canceling' THEN
latest_build.canceled_at IS NOT NULL AND
latest_build.completed_at IS NULL
WHEN $2 = 'canceled' THEN
latest_build.canceled_at IS NOT NULL AND
latest_build.completed_at IS NOT NULL
WHEN $2 = 'deleted' THEN
latest_build.started_at IS NOT NULL AND
latest_build.canceled_at IS NULL AND
latest_build.completed_at IS NOT NULL AND
latest_build.updated_at - INTERVAL '30 seconds' < NOW() AND
latest_build.transition = 'delete'::workspace_transition AND
-- If the error field is not null, the status is 'failed'
latest_build.error IS NULL
WHEN $2 = 'deleting' THEN
latest_build.completed_at IS NULL AND
latest_build.canceled_at IS NULL AND
latest_build.error IS NULL AND
latest_build.job_status = 'running' AND
latest_build.transition = 'delete'::workspace_transition
-- 'succeeded' states
WHEN $2 = 'deleted' THEN
latest_build.job_status = 'succeeded'::provisioner_job_status AND
latest_build.transition = 'delete'::workspace_transition
WHEN $2 = 'stopped' THEN
latest_build.job_status = 'succeeded'::provisioner_job_status AND
latest_build.transition = 'stop'::workspace_transition
WHEN $2 = 'started' THEN
latest_build.job_status = 'succeeded'::provisioner_job_status AND
latest_build.transition = 'start'::workspace_transition
-- Special case where the provisioner status and workspace status
-- differ. A workspace is "running" if the job is "succeeded" and
-- the transition is "start". This is because a workspace starts
-- running when a job is complete.
WHEN $2 = 'running' THEN
latest_build.job_status = 'succeeded'::provisioner_job_status AND
latest_build.transition = 'start'::workspace_transition
WHEN $2 != '' THEN
-- By default just match the job status exactly
latest_build.job_status = $2::provisioner_job_status
ELSE
true
END

View File

@ -96,7 +96,8 @@ LEFT JOIN LATERAL (
provisioner_jobs.updated_at,
provisioner_jobs.canceled_at,
provisioner_jobs.completed_at,
provisioner_jobs.error
provisioner_jobs.error,
provisioner_jobs.job_status
FROM
workspace_builds
LEFT JOIN
@ -128,63 +129,42 @@ WHERE
AND CASE
WHEN @status :: text != '' THEN
CASE
WHEN @status = 'pending' THEN
latest_build.started_at IS NULL
-- Some workspace specific status refer to the transition
-- type. By default, the standard provisioner job status
-- search strings are supported.
-- 'running' states
WHEN @status = 'starting' THEN
latest_build.started_at IS NOT NULL AND
latest_build.canceled_at IS NULL AND
latest_build.completed_at IS NULL AND
latest_build.updated_at - INTERVAL '30 seconds' < NOW() AND
latest_build.job_status = 'running'::provisioner_job_status AND
latest_build.transition = 'start'::workspace_transition
WHEN @status = 'running' THEN
latest_build.completed_at IS NOT NULL AND
latest_build.canceled_at IS NULL AND
latest_build.error IS NULL AND
latest_build.transition = 'start'::workspace_transition
WHEN @status = 'stopping' THEN
latest_build.started_at IS NOT NULL AND
latest_build.canceled_at IS NULL AND
latest_build.completed_at IS NULL AND
latest_build.updated_at - INTERVAL '30 seconds' < NOW() AND
latest_build.job_status = 'running'::provisioner_job_status AND
latest_build.transition = 'stop'::workspace_transition
WHEN @status = 'stopped' THEN
latest_build.completed_at IS NOT NULL AND
latest_build.canceled_at IS NULL AND
latest_build.error IS NULL AND
latest_build.transition = 'stop'::workspace_transition
WHEN @status = 'failed' THEN
(latest_build.canceled_at IS NOT NULL AND
latest_build.error IS NOT NULL) OR
(latest_build.completed_at IS NOT NULL AND
latest_build.error IS NOT NULL)
WHEN @status = 'canceling' THEN
latest_build.canceled_at IS NOT NULL AND
latest_build.completed_at IS NULL
WHEN @status = 'canceled' THEN
latest_build.canceled_at IS NOT NULL AND
latest_build.completed_at IS NOT NULL
WHEN @status = 'deleted' THEN
latest_build.started_at IS NOT NULL AND
latest_build.canceled_at IS NULL AND
latest_build.completed_at IS NOT NULL AND
latest_build.updated_at - INTERVAL '30 seconds' < NOW() AND
latest_build.transition = 'delete'::workspace_transition AND
-- If the error field is not null, the status is 'failed'
latest_build.error IS NULL
WHEN @status = 'deleting' THEN
latest_build.completed_at IS NULL AND
latest_build.canceled_at IS NULL AND
latest_build.error IS NULL AND
latest_build.job_status = 'running' AND
latest_build.transition = 'delete'::workspace_transition
-- 'succeeded' states
WHEN @status = 'deleted' THEN
latest_build.job_status = 'succeeded'::provisioner_job_status AND
latest_build.transition = 'delete'::workspace_transition
WHEN @status = 'stopped' THEN
latest_build.job_status = 'succeeded'::provisioner_job_status AND
latest_build.transition = 'stop'::workspace_transition
WHEN @status = 'started' THEN
latest_build.job_status = 'succeeded'::provisioner_job_status AND
latest_build.transition = 'start'::workspace_transition
-- Special case where the provisioner status and workspace status
-- differ. A workspace is "running" if the job is "succeeded" and
-- the transition is "start". This is because a workspace starts
-- running when a job is complete.
WHEN @status = 'running' THEN
latest_build.job_status = 'succeeded'::provisioner_job_status AND
latest_build.transition = 'start'::workspace_transition
WHEN @status != '' THEN
-- By default just match the job status exactly
latest_build.job_status = @status::provisioner_job_status
ELSE
true
END

View File

@ -10,13 +10,14 @@ import (
"sync/atomic"
"time"
"github.com/coder/coder/v2/codersdk"
"github.com/google/uuid"
"github.com/prometheus/client_golang/prometheus"
"tailscale.com/tailcfg"
"cdr.dev/slog"
"github.com/coder/coder/v2/coderd/database"
"github.com/coder/coder/v2/coderd/database/db2sdk"
"github.com/coder/coder/v2/coderd/database/dbauthz"
"github.com/coder/coder/v2/coderd/database/dbtime"
"github.com/coder/coder/v2/tailnet"
@ -119,7 +120,7 @@ func Workspaces(ctx context.Context, registerer prometheus.Registerer, db databa
gauge.Reset()
for _, job := range jobs {
status := db2sdk.ProvisionerJobStatus(job)
status := codersdk.ProvisionerJobStatus(job.JobStatus)
gauge.WithLabelValues(string(status)).Add(1)
}
}

View File

@ -17,7 +17,6 @@ import (
"cdr.dev/slog"
"github.com/coder/coder/v2/coderd/database"
"github.com/coder/coder/v2/coderd/database/db2sdk"
"github.com/coder/coder/v2/coderd/database/dbauthz"
"github.com/coder/coder/v2/coderd/database/pubsub"
"github.com/coder/coder/v2/coderd/httpapi"
@ -258,7 +257,7 @@ func convertProvisionerJob(pj database.GetProvisionerJobsByIDsWithQueuePositionR
if provisionerJob.WorkerID.Valid {
job.WorkerID = &provisionerJob.WorkerID.UUID
}
job.Status = db2sdk.ProvisionerJobStatus(provisionerJob)
job.Status = codersdk.ProvisionerJobStatus(pj.ProvisionerJob.JobStatus)
return job
}
@ -282,7 +281,7 @@ func fetchAndWriteLogs(ctx context.Context, db database.Store, jobID uuid.UUID,
}
func jobIsComplete(logger slog.Logger, job database.ProvisionerJob) bool {
status := db2sdk.ProvisionerJobStatus(job)
status := codersdk.ProvisionerJobStatus(job.JobStatus)
switch status {
case codersdk.ProvisionerJobCanceled:
return true

View File

@ -44,8 +44,10 @@ func TestConvertProvisionerJob_Unit(t *testing.T) {
expected codersdk.ProvisionerJob
}{
{
name: "empty",
input: database.ProvisionerJob{},
name: "empty",
input: database.ProvisionerJob{
JobStatus: database.ProvisionerJobStatusPending,
},
expected: codersdk.ProvisionerJob{
Status: codersdk.ProvisionerJobPending,
},
@ -55,6 +57,7 @@ func TestConvertProvisionerJob_Unit(t *testing.T) {
input: database.ProvisionerJob{
CanceledAt: validNullTimeMock,
CompletedAt: invalidNullTimeMock,
JobStatus: database.ProvisionerJobStatusCanceling,
},
expected: codersdk.ProvisionerJob{
CanceledAt: &validNullTimeMock.Time,
@ -67,6 +70,7 @@ func TestConvertProvisionerJob_Unit(t *testing.T) {
CanceledAt: validNullTimeMock,
CompletedAt: validNullTimeMock,
Error: errorMock,
JobStatus: database.ProvisionerJobStatusFailed,
},
expected: codersdk.ProvisionerJob{
CanceledAt: &validNullTimeMock.Time,
@ -80,6 +84,7 @@ func TestConvertProvisionerJob_Unit(t *testing.T) {
input: database.ProvisionerJob{
CanceledAt: validNullTimeMock,
CompletedAt: validNullTimeMock,
JobStatus: database.ProvisionerJobStatusCanceled,
},
expected: codersdk.ProvisionerJob{
CanceledAt: &validNullTimeMock.Time,
@ -91,6 +96,7 @@ func TestConvertProvisionerJob_Unit(t *testing.T) {
name: "job pending",
input: database.ProvisionerJob{
StartedAt: invalidNullTimeMock,
JobStatus: database.ProvisionerJobStatusPending,
},
expected: codersdk.ProvisionerJob{
Status: codersdk.ProvisionerJobPending,
@ -102,6 +108,7 @@ func TestConvertProvisionerJob_Unit(t *testing.T) {
CompletedAt: validNullTimeMock,
StartedAt: validNullTimeMock,
Error: errorMock,
JobStatus: database.ProvisionerJobStatusFailed,
},
expected: codersdk.ProvisionerJob{
CompletedAt: &validNullTimeMock.Time,
@ -115,6 +122,7 @@ func TestConvertProvisionerJob_Unit(t *testing.T) {
input: database.ProvisionerJob{
CompletedAt: validNullTimeMock,
StartedAt: validNullTimeMock,
JobStatus: database.ProvisionerJobStatusSucceeded,
},
expected: codersdk.ProvisionerJob{
CompletedAt: &validNullTimeMock.Time,
@ -156,7 +164,8 @@ func Test_logFollower_completeBeforeFollow(t *testing.T) {
Time: now.Add(-time.Second),
Valid: true,
},
Error: sql.NullString{},
Error: sql.NullString{},
JobStatus: database.ProvisionerJobStatusSucceeded,
}
// we need an HTTP server to get a websocket
@ -217,6 +226,7 @@ func Test_logFollower_completeBeforeSubscribe(t *testing.T) {
CanceledAt: sql.NullTime{},
CompletedAt: sql.NullTime{},
Error: sql.NullString{},
JobStatus: database.ProvisionerJobStatusRunning,
}
// we need an HTTP server to get a websocket
@ -238,6 +248,7 @@ func Test_logFollower_completeBeforeSubscribe(t *testing.T) {
Time: now.Add(-time.Millisecond),
Valid: true,
},
JobStatus: database.ProvisionerJobStatusSucceeded,
},
nil,
)
@ -293,6 +304,7 @@ func Test_logFollower_EndOfLogs(t *testing.T) {
CanceledAt: sql.NullTime{},
CompletedAt: sql.NullTime{},
Error: sql.NullString{},
JobStatus: database.ProvisionerJobStatusRunning,
}
// we need an HTTP server to get a websocket

View File

@ -14,7 +14,6 @@ import (
"cdr.dev/slog"
"github.com/coder/coder/v2/coderd/database"
"github.com/coder/coder/v2/coderd/database/db2sdk"
"github.com/coder/coder/v2/coderd/database/dbauthz"
"github.com/coder/coder/v2/coderd/database/dbtime"
"github.com/coder/coder/v2/coderd/database/pubsub"
@ -240,7 +239,7 @@ func unhangJob(ctx context.Context, log slog.Logger, db database.Store, pub pubs
}
if job.CompletedAt.Valid {
return jobInelligibleError{
Err: xerrors.Errorf("job is completed (status %s)", db2sdk.ProvisionerJobStatus(job)),
Err: xerrors.Errorf("job is completed (status %s)", job.JobStatus),
}
}
if job.UpdatedAt.After(time.Now().Add(-HungJobDuration)) {

View File

@ -718,7 +718,7 @@ func (b *Builder) checkTemplateJobStatus() error {
}
}
templateVersionJobStatus := db2sdk.ProvisionerJobStatus(*templateVersionJob)
templateVersionJobStatus := codersdk.ProvisionerJobStatus(templateVersionJob.JobStatus)
switch templateVersionJobStatus {
case codersdk.ProvisionerJobPending, codersdk.ProvisionerJobRunning:
msg := fmt.Sprintf("The provided template version is %s. Wait for it to complete importing!", templateVersionJobStatus)
@ -755,7 +755,7 @@ func (b *Builder) checkRunningBuild() error {
if err != nil {
return BuildError{http.StatusInternalServerError, "failed to fetch prior build", err}
}
if db2sdk.ProvisionerJobStatus(*job).Active() {
if codersdk.ProvisionerJobStatus(job.JobStatus).Active() {
msg := "A workspace build is already active."
return BuildError{
http.StatusConflict,

View File

@ -64,6 +64,7 @@ const (
ProvisionerJobCanceling ProvisionerJobStatus = "canceling"
ProvisionerJobCanceled ProvisionerJobStatus = "canceled"
ProvisionerJobFailed ProvisionerJobStatus = "failed"
ProvisionerJobUnknown ProvisionerJobStatus = "unknown"
)
// JobErrorCode defines the error code returned by job runner.

1
docs/api/schemas.md generated
View File

@ -3758,6 +3758,7 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in
| `canceling` |
| `canceled` |
| `failed` |
| `unknown` |
## codersdk.ProvisionerLogLevel

View File

@ -11,7 +11,6 @@ import (
"golang.org/x/xerrors"
"github.com/coder/coder/v2/coderd/database"
"github.com/coder/coder/v2/coderd/database/db2sdk"
"github.com/coder/coder/v2/coderd/database/dbauthz"
"github.com/coder/coder/v2/coderd/database/dbtime"
agpl "github.com/coder/coder/v2/coderd/schedule"
@ -242,7 +241,7 @@ func (s *EnterpriseTemplateScheduleStore) updateWorkspaceBuild(ctx context.Conte
if err != nil {
return xerrors.Errorf("get provisioner job %q: %w", build.JobID, err)
}
if db2sdk.ProvisionerJobStatus(job) != codersdk.ProvisionerJobSucceeded {
if codersdk.ProvisionerJobStatus(job.JobStatus) != codersdk.ProvisionerJobSucceeded {
// Only touch builds that are completed.
return nil
}

View File

@ -1755,7 +1755,8 @@ export type ProvisionerJobStatus =
| "failed"
| "pending"
| "running"
| "succeeded";
| "succeeded"
| "unknown";
export const ProvisionerJobStatuses: ProvisionerJobStatus[] = [
"canceled",
"canceling",
@ -1763,6 +1764,7 @@ export const ProvisionerJobStatuses: ProvisionerJobStatus[] = [
"pending",
"running",
"succeeded",
"unknown",
];
// From codersdk/workspaces.go

View File

@ -55,6 +55,7 @@ export const getStatus = (
text: "Canceled",
icon: <ErrorIcon />,
};
case "unknown":
case "failed":
return {
type: "error",

View File

@ -51,6 +51,8 @@ export const getDisplayWorkspaceBuildStatus = (
color: theme.palette.primary.main,
status: DisplayWorkspaceBuildStatusLanguage.running,
} as const;
// Just handle unknown as failed
case "unknown":
case "failed":
return {
type: "error",