Skip to content

Commit 1cdb9c0

Browse files
feat: prioritize human-initiated workspace builds over prebuilds in queue
This change implements a priority queue system for provisioner jobs to ensure that human-initiated workspace builds are processed before prebuild jobs, improving user experience during high queue periods. Changes: - Add priority column to provisioner_jobs table (1=human, 0=prebuild) - Update AcquireProvisionerJob query to order by priority DESC, created_at ASC - Set priority in workspace builder based on initiator (PrebuildsSystemUserID) - Expose priority field in API and SDK - Add comprehensive test for priority queue behavior Co-authored-by: kylecarbs <7122116+kylecarbs@users.noreply.github.com>
1 parent 52c4b61 commit 1cdb9c0

File tree

7 files changed

+129
-0
lines changed

7 files changed

+129
-0
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
-- Remove the priority-based index
2+
DROP INDEX IF EXISTS idx_provisioner_jobs_priority_created_at;
3+
4+
-- Remove the priority column
5+
ALTER TABLE provisioner_jobs DROP COLUMN IF EXISTS priority;
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
-- Add priority column to provisioner_jobs table to support prioritizing human-initiated jobs over prebuilds
2+
ALTER TABLE provisioner_jobs ADD COLUMN priority integer NOT NULL DEFAULT 0;
3+
4+
-- Create index for efficient priority-based ordering
5+
CREATE INDEX idx_provisioner_jobs_priority_created_at ON provisioner_jobs (organization_id, started_at, priority DESC, created_at ASC) WHERE started_at IS NULL;
6+
7+
-- Update existing jobs to set priority based on whether they are prebuilds
8+
-- Priority 1 = human-initiated jobs, Priority 0 = prebuilds
9+
UPDATE provisioner_jobs
10+
SET priority = CASE
11+
WHEN initiator_id = 'c42fdf75-3097-471c-8c33-fb52454d81c0' THEN 0 -- PrebuildsSystemUserID
12+
ELSE 1 -- Human-initiated
13+
END
14+
WHERE started_at IS NULL; -- Only update pending jobs

coderd/database/queries/provisionerjobs.sql

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ WHERE
2626
-- they are aliases and the code that calls this query already relies on a different type
2727
AND provisioner_tagset_contains(@provisioner_tags :: jsonb, potential_job.tags :: jsonb)
2828
ORDER BY
29+
potential_job.priority DESC,
2930
potential_job.created_at
3031
FOR UPDATE
3132
SKIP LOCKED

coderd/provisionerjobs.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -363,6 +363,7 @@ func convertProvisionerJob(pj database.GetProvisionerJobsByIDsWithQueuePositionR
363363
Tags: provisionerJob.Tags,
364364
QueuePosition: int(pj.QueuePosition),
365365
QueueSize: int(pj.QueueSize),
366+
Priority: provisionerJob.Priority,
366367
}
367368
// Applying values optional to the struct.
368369
if provisionerJob.StartedAt.Valid {

coderd/wsbuilder/priority_test.go

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
package wsbuilder_test
2+
3+
import (
4+
"database/sql"
5+
"encoding/json"
6+
"testing"
7+
"time"
8+
9+
"github.com/google/uuid"
10+
"github.com/stretchr/testify/require"
11+
"github.com/sqlc-dev/pqtype"
12+
13+
"github.com/coder/coder/v2/coderd/coderdtest"
14+
"github.com/coder/coder/v2/coderd/database"
15+
"github.com/coder/coder/v2/coderd/database/dbtestutil"
16+
"github.com/coder/coder/v2/testutil"
17+
)
18+
19+
func TestPriorityQueue(t *testing.T) {
20+
t.Parallel()
21+
22+
db, ps := dbtestutil.NewDB(t)
23+
client := coderdtest.New(t, &coderdtest.Options{
24+
IncludeProvisionerDaemon: true,
25+
Database: db,
26+
Pubsub: ps,
27+
})
28+
owner := coderdtest.CreateFirstUser(t, client)
29+
30+
// Create a template
31+
version := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, nil)
32+
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
33+
34+
ctx := testutil.Context(t, testutil.WaitMedium)
35+
36+
// Test priority setting by directly creating provisioner jobs
37+
// Create a human-initiated job
38+
humanJob, err := db.InsertProvisionerJob(ctx, database.InsertProvisionerJobParams{
39+
ID: uuid.New(),
40+
CreatedAt: time.Now(),
41+
UpdatedAt: time.Now(),
42+
InitiatorID: owner.UserID,
43+
OrganizationID: owner.OrganizationID,
44+
Provisioner: database.ProvisionerTypeEcho,
45+
Type: database.ProvisionerJobTypeWorkspaceBuild,
46+
StorageMethod: database.ProvisionerStorageMethodFile,
47+
FileID: uuid.New(),
48+
Input: json.RawMessage(`{}`),
49+
Tags: database.StringMap{},
50+
TraceMetadata: pqtype.NullRawMessage{},
51+
Priority: 1, // Human-initiated should have priority 1
52+
})
53+
require.NoError(t, err)
54+
55+
// Create a prebuild job
56+
prebuildJob, err := db.InsertProvisionerJob(ctx, database.InsertProvisionerJobParams{
57+
ID: uuid.New(),
58+
CreatedAt: time.Now().Add(time.Millisecond), // Slightly later
59+
UpdatedAt: time.Now().Add(time.Millisecond),
60+
InitiatorID: database.PrebuildsSystemUserID,
61+
OrganizationID: owner.OrganizationID,
62+
Provisioner: database.ProvisionerTypeEcho,
63+
Type: database.ProvisionerJobTypeWorkspaceBuild,
64+
StorageMethod: database.ProvisionerStorageMethodFile,
65+
FileID: uuid.New(),
66+
Input: json.RawMessage(`{}`),
67+
Tags: database.StringMap{},
68+
TraceMetadata: pqtype.NullRawMessage{},
69+
Priority: 0, // Prebuild should have priority 0
70+
})
71+
require.NoError(t, err)
72+
73+
// Verify that human job has higher priority than prebuild job
74+
require.Equal(t, int32(1), humanJob.Priority, "Human-initiated job should have priority 1")
75+
require.Equal(t, int32(0), prebuildJob.Priority, "Prebuild job should have priority 0")
76+
77+
// Test job acquisition order - human jobs should be acquired first
78+
// Even though the prebuild job was created later, the human job should be acquired first due to higher priority
79+
acquiredJob1, err := db.AcquireProvisionerJob(ctx, database.AcquireProvisionerJobParams{
80+
OrganizationID: owner.OrganizationID,
81+
StartedAt: sql.NullTime{Time: time.Now(), Valid: true},
82+
WorkerID: uuid.NullUUID{UUID: uuid.New(), Valid: true},
83+
Types: []database.ProvisionerType{database.ProvisionerTypeEcho},
84+
ProvisionerTags: json.RawMessage(`{}`),
85+
})
86+
require.NoError(t, err)
87+
require.Equal(t, int32(1), acquiredJob1.Priority, "First acquired job should be human-initiated due to higher priority")
88+
require.Equal(t, humanJob.ID, acquiredJob1.ID, "First acquired job should be the human job")
89+
90+
acquiredJob2, err := db.AcquireProvisionerJob(ctx, database.AcquireProvisionerJobParams{
91+
OrganizationID: owner.OrganizationID,
92+
StartedAt: sql.NullTime{Time: time.Now(), Valid: true},
93+
WorkerID: uuid.NullUUID{UUID: uuid.New(), Valid: true},
94+
Types: []database.ProvisionerType{database.ProvisionerTypeEcho},
95+
ProvisionerTags: json.RawMessage(`{}`),
96+
})
97+
require.NoError(t, err)
98+
require.Equal(t, int32(0), acquiredJob2.Priority, "Second acquired job should be prebuild")
99+
require.Equal(t, prebuildJob.ID, acquiredJob2.ID, "Second acquired job should be the prebuild job")
100+
}

coderd/wsbuilder/wsbuilder.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -371,6 +371,12 @@ func (b *Builder) buildTx(authFunc func(action policy.Action, object rbac.Object
371371
}
372372

373373
now := dbtime.Now()
374+
// Set priority: 1 for human-initiated jobs, 0 for prebuilds
375+
priority := int32(1) // Default to human-initiated
376+
if b.initiator == database.PrebuildsSystemUserID {
377+
priority = 0 // Prebuild jobs have lower priority
378+
}
379+
374380
provisionerJob, err := b.store.InsertProvisionerJob(b.ctx, database.InsertProvisionerJobParams{
375381
ID: uuid.New(),
376382
CreatedAt: now,
@@ -383,6 +389,7 @@ func (b *Builder) buildTx(authFunc func(action policy.Action, object rbac.Object
383389
FileID: templateVersionJob.FileID,
384390
Input: input,
385391
Tags: tags,
392+
Priority: priority,
386393
TraceMetadata: pqtype.NullRawMessage{
387394
Valid: true,
388395
RawMessage: traceMetadataRaw,

codersdk/provisionerdaemons.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,7 @@ type ProvisionerJob struct {
183183
Tags map[string]string `json:"tags" table:"tags"`
184184
QueuePosition int `json:"queue_position" table:"queue position"`
185185
QueueSize int `json:"queue_size" table:"queue size"`
186+
Priority int32 `json:"priority" table:"priority"`
186187
OrganizationID uuid.UUID `json:"organization_id" format:"uuid" table:"organization id"`
187188
Input ProvisionerJobInput `json:"input" table:"input,recursive_inline"`
188189
Type ProvisionerJobType `json:"type" table:"type"`

0 commit comments

Comments
 (0)