Skip to content

Commit 847afa7

Browse files
committed
add queue-state count regression benchmark
This adds a benchmark on top of #1203 to make the queue-state count query regression easy to reproduce and discuss. It compares the current `JobCountByQueueAndState` implementation against the legacy query shape on the same migrated `river_job` schema. The benchmark stays lightweight by default, but can be scaled locally with `RIVER_BENCH_QUEUE_STATE_COUNT_NUM_JOBS` to reproduce the planner regression with a couple hundred thousand rows and quantify the gap.
1 parent 3d3c21a commit 847afa7

1 file changed

Lines changed: 204 additions & 0 deletions

File tree

Lines changed: 204 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,204 @@
1+
package riverpgxv5_test
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"os"
7+
"strconv"
8+
"testing"
9+
10+
"github.com/jackc/pgx/v5/pgxpool"
11+
"github.com/stretchr/testify/require"
12+
13+
"github.com/riverqueue/river/riverdbtest"
14+
"github.com/riverqueue/river/riverdriver"
15+
"github.com/riverqueue/river/riverdriver/riverpgxv5"
16+
"github.com/riverqueue/river/rivershared/riversharedtest"
17+
)
18+
19+
func BenchmarkJobCountByQueueAndState(b *testing.B) {
20+
ctx := context.Background()
21+
22+
dbPool := riversharedtest.DBPool(ctx, b)
23+
driver := riverpgxv5.New(dbPool)
24+
schema := riverdbtest.TestSchema(ctx, b, driver, nil)
25+
numJobs := queueStateCountBenchmarkNumJobs(b)
26+
27+
seedQueueStateCountBenchmarkData(ctx, b, dbPool, schema, numJobs)
28+
29+
queueNamesTwo := queueStateCountBenchmarkQueueNames(2)
30+
queueNamesTen := queueStateCountBenchmarkQueueNames(10)
31+
32+
for _, benchmarkCase := range []struct {
33+
name string
34+
queueNames []string
35+
}{
36+
{name: "TwoQueues", queueNames: queueNamesTwo},
37+
{name: "TenQueues", queueNames: queueNamesTen},
38+
} {
39+
b.Run("Current/"+benchmarkCase.name, func(b *testing.B) {
40+
b.ReportAllocs()
41+
42+
params := &riverdriver.JobCountByQueueAndStateParams{
43+
QueueNames: benchmarkCase.queueNames,
44+
Schema: schema,
45+
}
46+
47+
b.ResetTimer()
48+
for range b.N {
49+
results, err := driver.GetExecutor().JobCountByQueueAndState(ctx, params)
50+
require.NoError(b, err)
51+
require.NotEmpty(b, results)
52+
}
53+
})
54+
55+
b.Run("Legacy/"+benchmarkCase.name, func(b *testing.B) {
56+
b.ReportAllocs()
57+
58+
query := legacyJobCountByQueueAndStateQuery(schema)
59+
60+
b.ResetTimer()
61+
for range b.N {
62+
rows, err := dbPool.Query(ctx, query, benchmarkCase.queueNames)
63+
require.NoError(b, err)
64+
65+
var numRows int
66+
for rows.Next() {
67+
var (
68+
countAvailable int64
69+
countRunning int64
70+
queue string
71+
)
72+
73+
require.NoError(b, rows.Scan(&queue, &countAvailable, &countRunning))
74+
numRows++
75+
}
76+
77+
rows.Close()
78+
require.NoError(b, rows.Err())
79+
require.Equal(b, len(benchmarkCase.queueNames), numRows)
80+
}
81+
})
82+
}
83+
}
84+
85+
func legacyJobCountByQueueAndStateQuery(schema string) string {
86+
return fmt.Sprintf(`
87+
WITH all_queues AS (
88+
SELECT DISTINCT unnest($1::text[])::text AS queue
89+
),
90+
91+
running_job_counts AS (
92+
SELECT
93+
queue,
94+
COUNT(*) AS count
95+
FROM %s.river_job
96+
WHERE queue = ANY($1::text[])
97+
AND state = 'running'
98+
GROUP BY queue
99+
),
100+
101+
available_job_counts AS (
102+
SELECT
103+
queue,
104+
COUNT(*) AS count
105+
FROM %s.river_job
106+
WHERE queue = ANY($1::text[])
107+
AND state = 'available'
108+
GROUP BY queue
109+
)
110+
111+
SELECT
112+
all_queues.queue,
113+
COALESCE(available_job_counts.count, 0) AS count_available,
114+
COALESCE(running_job_counts.count, 0) AS count_running
115+
FROM
116+
all_queues
117+
LEFT JOIN
118+
running_job_counts ON all_queues.queue = running_job_counts.queue
119+
LEFT JOIN
120+
available_job_counts ON all_queues.queue = available_job_counts.queue
121+
ORDER BY all_queues.queue ASC
122+
`, schema, schema)
123+
}
124+
125+
func queueStateCountBenchmarkNumJobs(b *testing.B) int {
126+
b.Helper()
127+
128+
numJobs := 20_000
129+
if numJobsEnv := os.Getenv("RIVER_BENCH_QUEUE_STATE_COUNT_NUM_JOBS"); numJobsEnv != "" {
130+
parsedNumJobs, err := strconv.Atoi(numJobsEnv)
131+
require.NoError(b, err)
132+
require.Greater(b, parsedNumJobs, 0)
133+
134+
numJobs = parsedNumJobs
135+
}
136+
137+
return numJobs
138+
}
139+
140+
func queueStateCountBenchmarkQueueNames(numQueues int) []string {
141+
queueNames := make([]string, numQueues)
142+
for i := range numQueues {
143+
queueNames[i] = fmt.Sprintf("queue_%03d", i+1)
144+
}
145+
146+
return queueNames
147+
}
148+
149+
func seedQueueStateCountBenchmarkData(ctx context.Context, b *testing.B, dbPool *pgxpool.Pool, schema string, numJobs int) {
150+
b.Helper()
151+
152+
query := fmt.Sprintf(`
153+
WITH generated_jobs AS (
154+
SELECT
155+
CASE gs %% 8
156+
WHEN 0 THEN 'running'
157+
WHEN 1 THEN 'available'
158+
WHEN 2 THEN 'completed'
159+
WHEN 3 THEN 'cancelled'
160+
WHEN 4 THEN 'discarded'
161+
WHEN 5 THEN 'retryable'
162+
WHEN 6 THEN 'scheduled'
163+
ELSE 'pending'
164+
END AS state,
165+
now() - ((gs %% 100000)::text || ' seconds')::interval AS scheduled_at,
166+
'queue_' || lpad(((gs %% 100) + 1)::text, 3, '0') AS queue
167+
FROM generate_series(1, %d) AS gs
168+
)
169+
INSERT INTO %s.river_job (
170+
args,
171+
finalized_at,
172+
kind,
173+
max_attempts,
174+
metadata,
175+
queue,
176+
scheduled_at,
177+
state
178+
)
179+
SELECT
180+
'{}'::jsonb,
181+
CASE
182+
WHEN state IN ('cancelled', 'completed', 'discarded') THEN scheduled_at + interval '1 second'
183+
ELSE NULL
184+
END AS finalized_at,
185+
'benchmark',
186+
25,
187+
'{}'::jsonb,
188+
queue,
189+
scheduled_at,
190+
state::%s.river_job_state
191+
FROM generated_jobs;
192+
193+
ANALYZE %s.river_job;
194+
`, numJobs, schema, schema, schema)
195+
196+
_, err := dbPool.Exec(ctx, query)
197+
require.NoError(b, err)
198+
199+
row := dbPool.QueryRow(ctx, "SELECT count(*) FROM "+schema+".river_job")
200+
201+
var numRows int
202+
require.NoError(b, row.Scan(&numRows))
203+
require.Equal(b, numJobs, numRows)
204+
}

0 commit comments

Comments
 (0)