diff --git a/coderd/database/dbfake/dbfake.go b/coderd/database/dbfake/dbfake.go index 78783d78cf..f23be9ea3b 100644 --- a/coderd/database/dbfake/dbfake.go +++ b/coderd/database/dbfake/dbfake.go @@ -314,14 +314,15 @@ func (b WorkspaceBuildBuilder) doInTX() WorkspaceResponse { case database.ProvisionerJobStatusCanceled: // Set provisioner job status to 'canceled' b.logger.Debug(context.Background(), "canceling the provisioner job") + now := dbtime.Now() err = b.db.UpdateProvisionerJobWithCancelByID(ownerCtx, database.UpdateProvisionerJobWithCancelByIDParams{ ID: jobID, CanceledAt: sql.NullTime{ - Time: dbtime.Now(), + Time: now, Valid: true, }, CompletedAt: sql.NullTime{ - Time: dbtime.Now(), + Time: now, Valid: true, }, }) @@ -696,7 +697,7 @@ func (b JobCompleteBuilder) Pubsub(ps pubsub.Pubsub) JobCompleteBuilder { func (b JobCompleteBuilder) Do() JobCompleteResponse { r := JobCompleteResponse{CompletedAt: dbtime.Now()} - err := b.db.UpdateProvisionerJobWithCompleteByID(ownerCtx, database.UpdateProvisionerJobWithCompleteByIDParams{ + err := b.db.UpdateProvisionerJobWithCompleteWithStartedAtByID(ownerCtx, database.UpdateProvisionerJobWithCompleteWithStartedAtByIDParams{ ID: b.jobID, UpdatedAt: r.CompletedAt, Error: sql.NullString{}, @@ -705,6 +706,10 @@ func (b JobCompleteBuilder) Do() JobCompleteResponse { Time: r.CompletedAt, Valid: true, }, + StartedAt: sql.NullTime{ + Time: r.CompletedAt, + Valid: true, + }, }) require.NoError(b.t, err, "complete job") if b.ps != nil { diff --git a/coderd/database/querier_test.go b/coderd/database/querier_test.go index 8e00b5dad3..1f53a4ced7 100644 --- a/coderd/database/querier_test.go +++ b/coderd/database/querier_test.go @@ -1645,6 +1645,53 @@ func TestAcquireProvisionerJob(t *testing.T) { require.NoError(t, err, "mark job %d/%d as complete", idx+1, numJobs) } }) + + t.Run("SkipsCanceledPendingJobs", func(t *testing.T) { + t.Parallel() + var ( + db, _ = dbtestutil.NewDB(t) + ctx = testutil.Context(t, testutil.WaitMedium) + org = dbgen.Organization(t, db, database.Organization{}) + now = dbtime.Now() + ) + + // Insert a pending job (started_at is NULL). + job, err := db.InsertProvisionerJob(ctx, database.InsertProvisionerJobParams{ + ID: uuid.New(), + CreatedAt: now, + UpdatedAt: now, + InitiatorID: uuid.New(), + OrganizationID: org.ID, + Provisioner: database.ProvisionerTypeEcho, + Type: database.ProvisionerJobTypeWorkspaceBuild, + StorageMethod: database.ProvisionerStorageMethodFile, + FileID: uuid.New(), + Input: json.RawMessage(`{}`), + Tags: database.StringMap{}, + TraceMetadata: pqtype.NullRawMessage{}, + }) + require.NoError(t, err) + + // Cancel it while still pending. In production (workspacebuilds.go), canceling + // a pending build sets completed_at but leaves started_at NULL since no + // provisioner ever started the job. + err = db.UpdateProvisionerJobWithCancelByID(ctx, database.UpdateProvisionerJobWithCancelByIDParams{ + ID: job.ID, + CanceledAt: sql.NullTime{Time: now, Valid: true}, + CompletedAt: sql.NullTime{Time: now, Valid: true}, + }) + require.NoError(t, err) + + // AcquireProvisionerJob should skip this job since it's already completed. + _, err = db.AcquireProvisionerJob(ctx, database.AcquireProvisionerJobParams{ + OrganizationID: org.ID, + StartedAt: sql.NullTime{Time: now, Valid: true}, + WorkerID: uuid.NullUUID{UUID: uuid.New(), Valid: true}, + Types: []database.ProvisionerType{database.ProvisionerTypeEcho}, + ProvisionerTags: json.RawMessage(`{}`), + }) + require.ErrorIs(t, err, sql.ErrNoRows) + }) } func TestUserLastSeenFilter(t *testing.T) { diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index 7a238e7232..571835ed1d 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -10251,6 +10251,7 @@ WHERE provisioner_jobs AS potential_job WHERE potential_job.started_at IS NULL + AND potential_job.completed_at IS NULL AND potential_job.organization_id = $3 -- Ensure the caller has the correct provisioner. AND potential_job.provisioner = ANY($4 :: provisioner_type [ ]) diff --git a/coderd/database/queries/provisionerjobs.sql b/coderd/database/queries/provisionerjobs.sql index 02d67d628a..0f1b1db94d 100644 --- a/coderd/database/queries/provisionerjobs.sql +++ b/coderd/database/queries/provisionerjobs.sql @@ -19,6 +19,7 @@ WHERE provisioner_jobs AS potential_job WHERE potential_job.started_at IS NULL + AND potential_job.completed_at IS NULL AND potential_job.organization_id = @organization_id -- Ensure the caller has the correct provisioner. AND potential_job.provisioner = ANY(@types :: provisioner_type [ ])