Skip to content

fix(coderd/database): consider tag sets when calculating queue position #16685

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 20 commits into from
Mar 3, 2025
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
1e35c53
fix(coderd/database): remove linux build tags from db package
evgeniy-scherbina Feb 19, 2025
5c49221
fix(coderd/database): consider tag sets when calculating queue position
evgeniy-scherbina Feb 20, 2025
4257ca0
test(coderd/database): skip tests when PostgreSQL is unavailable
evgeniy-scherbina Feb 25, 2025
30f007c
test(coderd/database): create default provisioner in tests
evgeniy-scherbina Feb 25, 2025
acb93ac
fix(coderd/database): correctly calculate pending and ranked jobs
evgeniy-scherbina Feb 26, 2025
5aa0ffa
test(coderd/database): improve test coverage
evgeniy-scherbina Feb 26, 2025
c1f421d
refactor(coderd/database): use empty tag sets
evgeniy-scherbina Feb 26, 2025
e7693ee
test(coderd/database): improve test coverage
evgeniy-scherbina Feb 27, 2025
a9ed7d2
test(coderd/database): wrap test cases in subtests
evgeniy-scherbina Feb 27, 2025
574cdf6
refactor(coderd/database): minor refactoring in tests
evgeniy-scherbina Feb 27, 2025
8b3446e
fix(coderd/database): linter
evgeniy-scherbina Feb 27, 2025
c4316fd
refactor(coderd/database): use job_status generated column
evgeniy-scherbina Feb 27, 2025
b4ea4db
perf: add index on provisioner_jobs table for status field
evgeniy-scherbina Feb 27, 2025
675b3e9
Merge remote-tracking branch 'origin/main' into 15843-queue-position
evgeniy-scherbina Feb 28, 2025
1f8e4e3
fix(coderd/database): increment migration number
evgeniy-scherbina Feb 28, 2025
f9c9711
fix(coderd/database): update dump.sql
evgeniy-scherbina Feb 28, 2025
61a9f58
perf: optimize get-queue-position sql query
evgeniy-scherbina Feb 28, 2025
4f77f67
chore: update dbmem
evgeniy-scherbina Feb 28, 2025
359a9e0
Merge remote-tracking branch 'origin/main' into 15843-queue-position
evgeniy-scherbina Mar 3, 2025
6f4da84
fix(coderd/database): increment migration number
evgeniy-scherbina Mar 3, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions coderd/database/db_test.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
//go:build linux

package database_test

import (
Expand Down
11 changes: 9 additions & 2 deletions coderd/database/dbtestutil/postgres_test.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
//go:build linux

package dbtestutil_test

import (
Expand All @@ -21,6 +19,9 @@ func TestMain(m *testing.M) {

func TestOpen(t *testing.T) {
t.Parallel()
if !dbtestutil.WillUsePostgres() {
t.Skip("this test requires postgres")
}

connect, err := dbtestutil.Open(t)
require.NoError(t, err)
Expand All @@ -35,6 +36,9 @@ func TestOpen(t *testing.T) {

func TestOpen_InvalidDBFrom(t *testing.T) {
t.Parallel()
if !dbtestutil.WillUsePostgres() {
t.Skip("this test requires postgres")
}

_, err := dbtestutil.Open(t, dbtestutil.WithDBFrom("__invalid__"))
require.Error(t, err)
Expand All @@ -44,6 +48,9 @@ func TestOpen_InvalidDBFrom(t *testing.T) {

func TestOpen_ValidDBFrom(t *testing.T) {
t.Parallel()
if !dbtestutil.WillUsePostgres() {
t.Skip("this test requires postgres")
}

// first check if we can create a new template db
dsn, err := dbtestutil.Open(t, dbtestutil.WithDBFrom(""))
Expand Down
2 changes: 0 additions & 2 deletions coderd/database/migrations/migrate_test.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
//go:build linux

package migrations_test

import (
Expand Down
2 changes: 0 additions & 2 deletions coderd/database/pubsub/pubsub_linux_test.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
//go:build linux

package pubsub_test

import (
Expand Down
2 changes: 2 additions & 0 deletions coderd/database/querier.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

240 changes: 236 additions & 4 deletions coderd/database/querier_test.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
//go:build linux

package database_test

import (
Expand Down Expand Up @@ -1258,6 +1256,15 @@ func TestQueuePosition(t *testing.T) {
time.Sleep(time.Millisecond)
}

// Create default provisioner daemon:
dbgen.ProvisionerDaemon(t, db, database.ProvisionerDaemon{
Name: "default_provisioner",
Provisioners: []database.ProvisionerType{database.ProvisionerTypeEcho},
// Ensure the `tags` field is NOT NULL for the default provisioner;
// otherwise, it won't be able to pick up any jobs.
Tags: database.StringMap{"a": "1"},
})

queued, err := db.GetProvisionerJobsByIDsWithQueuePosition(ctx, jobIDs)
require.NoError(t, err)
require.Len(t, queued, jobCount)
Expand Down Expand Up @@ -2164,11 +2171,125 @@ func TestGetProvisionerJobsByIDsWithQueuePosition(t *testing.T) {
t.SkipNow()
}

now := dbtime.Now()
ctx := testutil.Context(t, testutil.WaitShort)

testCases := []struct {
name string
jobTags []database.StringMap
daemonTags []database.StringMap
queueSizes []int64
queuePositions []int64
}{
{
name: "test-case-1",
jobTags: []database.StringMap{
{"a": "1", "b": "2"},
{"a": "1"},
{"a": "1", "c": "3"},
},
daemonTags: []database.StringMap{
{"a": "1", "b": "2"},
{"a": "1"},
},
queueSizes: []int64{2, 2, 0},
queuePositions: []int64{1, 1, 0},
},
{
name: "test-case-2",
jobTags: []database.StringMap{
{"a": "1", "b": "2"},
{"a": "1"},
{"a": "1", "c": "3"},
},
daemonTags: []database.StringMap{
{"a": "1", "b": "2"},
{"a": "1"},
{"a": "1", "b": "2", "c": "3"},
},
queueSizes: []int64{3, 3, 3},
queuePositions: []int64{1, 1, 3},
},
}

for _, tc := range testCases {
db, _ := dbtestutil.NewDB(t)

// Create provisioner jobs based on provided tags:
allJobs := make([]database.ProvisionerJob, len(tc.jobTags))
for idx, tags := range tc.jobTags {
// Make sure jobs are stored in correct order, first job should have the earliest createdAt timestamp.
// Example for 3 jobs:
// job_1 createdAt: now - 3 minutes
// job_2 createdAt: now - 2 minutes
// job_3 createdAt: now - 1 minute
timeOffsetInMinutes := len(tc.jobTags) - idx
timeOffset := time.Duration(timeOffsetInMinutes) * time.Minute
createdAt := now.Add(-timeOffset)

allJobs[idx] = dbgen.ProvisionerJob(t, db, nil, database.ProvisionerJob{
CreatedAt: createdAt,
Tags: tags,
})
}

// Create provisioner daemons based on provided tags:
for idx, tags := range tc.daemonTags {
dbgen.ProvisionerDaemon(t, db, database.ProvisionerDaemon{
Name: fmt.Sprintf("prov_%v", idx),
Provisioners: []database.ProvisionerType{database.ProvisionerTypeEcho},
Tags: tags,
})
}

// Assert invariant: the jobs are in pending status
for idx, job := range allJobs {
require.Equal(t, database.ProvisionerJobStatusPending, job.JobStatus, "expected job %d to have status %s", idx, database.ProvisionerJobStatusPending)
}

var jobIDs []uuid.UUID
for _, job := range allJobs {
jobIDs = append(jobIDs, job.ID)
}

// When: we fetch the jobs by their IDs
actualJobs, err := db.GetProvisionerJobsByIDsWithQueuePosition(ctx, jobIDs)
require.NoError(t, err)
require.Len(t, actualJobs, len(allJobs), "should return all jobs")

// Then: the jobs should be returned in the correct order (by IDs in the input slice)
for idx, job := range actualJobs {
assert.EqualValues(t, allJobs[idx], job.ProvisionerJob)
}

// Then: the queue size should be set correctly
var queueSizes []int64
for _, job := range actualJobs {
queueSizes = append(queueSizes, job.QueueSize)
}
assert.EqualValues(t, tc.queueSizes, queueSizes, "expected queue positions to be set correctly")

// Then: the queue position should be set correctly:
var queuePositions []int64
for _, job := range actualJobs {
queuePositions = append(queuePositions, job.QueuePosition)
}
assert.EqualValues(t, tc.queuePositions, queuePositions, "expected queue positions to be set correctly")
}
}

func TestGetProvisionerJobsByIDsWithQueuePosition_MixedStatuses(t *testing.T) {
t.Parallel()
if !dbtestutil.WillUsePostgres() {
t.SkipNow()
}

db, _ := dbtestutil.NewDB(t)
now := dbtime.Now()
ctx := testutil.Context(t, testutil.WaitShort)

// Given the following provisioner jobs:
defaultTags := database.StringMap{"a": "1"}
// Create the following provisioner jobs:
allJobs := []database.ProvisionerJob{
// Pending. This will be the last in the queue because
// it was created most recently.
Expand All @@ -2178,6 +2299,7 @@ func TestGetProvisionerJobsByIDsWithQueuePosition(t *testing.T) {
CanceledAt: sql.NullTime{},
CompletedAt: sql.NullTime{},
Error: sql.NullString{},
Tags: defaultTags,
}),

// Another pending. This will come first in the queue
Expand All @@ -2188,6 +2310,7 @@ func TestGetProvisionerJobsByIDsWithQueuePosition(t *testing.T) {
CanceledAt: sql.NullTime{},
CompletedAt: sql.NullTime{},
Error: sql.NullString{},
Tags: defaultTags,
}),

// Running
Expand All @@ -2197,6 +2320,7 @@ func TestGetProvisionerJobsByIDsWithQueuePosition(t *testing.T) {
CanceledAt: sql.NullTime{},
CompletedAt: sql.NullTime{},
Error: sql.NullString{},
Tags: defaultTags,
}),

// Succeeded
Expand All @@ -2206,6 +2330,7 @@ func TestGetProvisionerJobsByIDsWithQueuePosition(t *testing.T) {
CanceledAt: sql.NullTime{},
CompletedAt: sql.NullTime{Valid: true, Time: now},
Error: sql.NullString{},
Tags: defaultTags,
}),

// Canceling
Expand All @@ -2215,6 +2340,7 @@ func TestGetProvisionerJobsByIDsWithQueuePosition(t *testing.T) {
CanceledAt: sql.NullTime{Valid: true, Time: now},
CompletedAt: sql.NullTime{},
Error: sql.NullString{},
Tags: defaultTags,
}),

// Canceled
Expand All @@ -2224,6 +2350,7 @@ func TestGetProvisionerJobsByIDsWithQueuePosition(t *testing.T) {
CanceledAt: sql.NullTime{Valid: true, Time: now},
CompletedAt: sql.NullTime{Valid: true, Time: now},
Error: sql.NullString{},
Tags: defaultTags,
}),

// Failed
Expand All @@ -2233,9 +2360,17 @@ func TestGetProvisionerJobsByIDsWithQueuePosition(t *testing.T) {
CanceledAt: sql.NullTime{},
CompletedAt: sql.NullTime{},
Error: sql.NullString{String: "failed", Valid: true},
Tags: defaultTags,
}),
}

// Create default provisioner daemon:
dbgen.ProvisionerDaemon(t, db, database.ProvisionerDaemon{
Name: "default_provisioner",
Provisioners: []database.ProvisionerType{database.ProvisionerTypeEcho},
Tags: defaultTags,
})

// Assert invariant: the jobs are in the expected order
require.Len(t, allJobs, 7, "expected 7 jobs")
for idx, status := range []database.ProvisionerJobStatus{
Expand Down Expand Up @@ -2266,9 +2401,11 @@ func TestGetProvisionerJobsByIDsWithQueuePosition(t *testing.T) {
}

// Then: the queue size should be set correctly
var queueSizes []int64
for _, job := range actualJobs {
assert.EqualValues(t, job.QueueSize, 2, "should have queue size 2")
queueSizes = append(queueSizes, job.QueueSize)
}
assert.EqualValues(t, []int64{2, 2, 0, 0, 0, 0, 0}, queueSizes, "expected queue positions to be set correctly")

// Then: the queue position should be set correctly:
var queuePositions []int64
Expand All @@ -2278,6 +2415,101 @@ func TestGetProvisionerJobsByIDsWithQueuePosition(t *testing.T) {
assert.EqualValues(t, []int64{2, 1, 0, 0, 0, 0, 0}, queuePositions, "expected queue positions to be set correctly")
}

func TestGetProvisionerJobsByIDsWithQueuePosition_OrderValidation(t *testing.T) {
t.Parallel()
if !dbtestutil.WillUsePostgres() {
t.SkipNow()
}

db, _ := dbtestutil.NewDB(t)
now := dbtime.Now()
ctx := testutil.Context(t, testutil.WaitShort)

defaultTags := database.StringMap{"a": "1"}
// Create the following provisioner jobs:
allJobs := []database.ProvisionerJob{
dbgen.ProvisionerJob(t, db, nil, database.ProvisionerJob{
CreatedAt: now.Add(-4 * time.Minute),
Tags: defaultTags,
}),

dbgen.ProvisionerJob(t, db, nil, database.ProvisionerJob{
CreatedAt: now.Add(-5 * time.Minute),
Tags: defaultTags,
}),

dbgen.ProvisionerJob(t, db, nil, database.ProvisionerJob{
CreatedAt: now.Add(-6 * time.Minute),
Tags: defaultTags,
}),

dbgen.ProvisionerJob(t, db, nil, database.ProvisionerJob{
CreatedAt: now.Add(-3 * time.Minute),
Tags: defaultTags,
}),

dbgen.ProvisionerJob(t, db, nil, database.ProvisionerJob{
CreatedAt: now.Add(-2 * time.Minute),
Tags: defaultTags,
}),

dbgen.ProvisionerJob(t, db, nil, database.ProvisionerJob{
CreatedAt: now.Add(-1 * time.Minute),
Tags: defaultTags,
}),
}

// Create default provisioner daemon:
dbgen.ProvisionerDaemon(t, db, database.ProvisionerDaemon{
Name: "default_provisioner",
Provisioners: []database.ProvisionerType{database.ProvisionerTypeEcho},
Tags: defaultTags,
})

// Assert invariant: the jobs are in the expected order
require.Len(t, allJobs, 6, "expected 7 jobs")
for idx, status := range []database.ProvisionerJobStatus{
database.ProvisionerJobStatusPending,
database.ProvisionerJobStatusPending,
database.ProvisionerJobStatusPending,
database.ProvisionerJobStatusPending,
database.ProvisionerJobStatusPending,
database.ProvisionerJobStatusPending,
} {
require.Equal(t, status, allJobs[idx].JobStatus, "expected job %d to have status %s", idx, status)
}

var jobIDs []uuid.UUID
for _, job := range allJobs {
jobIDs = append(jobIDs, job.ID)
}

// When: we fetch the jobs by their IDs
actualJobs, err := db.GetProvisionerJobsByIDsWithQueuePosition(ctx, jobIDs)
require.NoError(t, err)
require.Len(t, actualJobs, len(allJobs), "should return all jobs")

// Then: the jobs should be returned in the correct order (by IDs in the input slice)
for idx, job := range actualJobs {
assert.EqualValues(t, allJobs[idx], job.ProvisionerJob)
assert.EqualValues(t, allJobs[idx].CreatedAt, job.ProvisionerJob.CreatedAt)
}

// Then: the queue size should be set correctly
var queueSizes []int64
for _, job := range actualJobs {
queueSizes = append(queueSizes, job.QueueSize)
}
assert.EqualValues(t, []int64{6, 6, 6, 6, 6, 6}, queueSizes, "expected queue positions to be set correctly")

// Then: the queue position should be set correctly:
var queuePositions []int64
for _, job := range actualJobs {
queuePositions = append(queuePositions, job.QueuePosition)
}
assert.EqualValues(t, []int64{3, 2, 1, 4, 5, 6}, queuePositions, "expected queue positions to be set correctly")
}

func TestGroupRemovalTrigger(t *testing.T) {
t.Parallel()

Expand Down
Loading
Loading