mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
fix(coderd/database): prevent AcquireProvisionerJob from grabbing canceled jobs (#21852)
The AcquireProvisionerJob query only checked started_at IS NULL, allowing it to acquire jobs that were canceled while pending (which have completed_at set but started_at still NULL). Added completed_at IS NULL check to the query to prevent this. Also fixed JobCompleteBuilder.Do() in dbfake to set started_at when completing jobs to match production behavior. Fixes coder/internal#1323
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 [ ])
|
||||
|
||||
@@ -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 [ ])
|
||||
|
||||
Reference in New Issue
Block a user