Skip to content

perf(queue): reduce dispatch and resize overhead - #178

Closed
appleboy wants to merge 2 commits into
masterfrom
perf/queue-dispatch
Closed

appleboy wants to merge 2 commits into
masterfrom
perf/queue-dispatch

Conversation

@appleboy

@appleboy appleboy commented Oct 31, 2026 •

Copy link
Copy Markdown
Member

Summary

A backlog alternating between 1,024 and 1,025 tasks previously resized the Ring on every enqueue/dequeue pair. Shrink only at 25% utilization to avoid that oscillation. Reuse a single dispatcher and worker goroutines instead of spawning Request and work goroutines for every task.

Keep handler timeout/retry/panic logic intact, make Start idempotent, and run optional afterFn callbacks independently while retaining Release tracking. Public interfaces are unchanged.

Performance results

Measured on 2026-10-31, comparing baseline 9721be66093a0e2889129e4751154d8388698b98, the Ring-only change, and the combined implementation in a67a8e5 (the current head only removes local report artifacts; Go source is unchanged).

All summary numbers below are medians of six samples. For Queue benchmarks, ns/task is amortized end-to-end work cost, not an individual task's queuing delay or p99. Time reduction = 1 - after/before; derived throughput gain = before/after - 1.

End-to-end results

Scenario Before ns/task After ns/task Time reduction Derived throughput gain Before → after B/task Before → after allocs/task
Queue, 1 worker 2,537.5 1,196 52.9% 112.2% 1,000 → 854 17 → 11
Queue, 4 workers 1,610.5 1,438.5 10.7% 12.0% 1,001 → 854 17 → 11
Queue, 12 workers 2,065 1,859 10.0% 11.1% 1,001.5 → 854 17 → 11
Queue, 4 workers + afterFn 1,654 1,358.5 17.9% 21.8% 1,001 → 894 17 → 13

General tasks use 17 → 11 allocations/task (35.3% fewer), and approximately 1,000–1,002 → 854 B/task (14.6–14.7% lower). With afterFn, the results are 17 → 13 allocations/task (23.5% fewer) and 1,001 → 894 B/task (10.7% lower).

The 4/12-worker timing ranges overlap; median differences must not be treated as proof of statistical significance. Allocation-count reductions were consistent across all recorded samples. Empty-task results do not imply the same gain for real CPU-heavy or I/O-heavy handlers.

Ring capacity-boundary results

One operation is a successful enqueue/dequeue pair while holding the backlog near the stated capacity boundary. Previously, a backlog alternating between 1,024 and 1,025 caused capacity to alternate between 1,024 and 2,048, allocating and copying twice while holding the Ring mutex. Growing at full and shrinking only at 25% utilization eliminates this steady-state oscillation; minimum capacity remains 2.

Backlog Before ns/op After ns/op Before B/op After B/op Before → after allocs/op
Ring backlog 64 351.25 9.3555 3,456 0 2 → 0
Ring backlog 1,024 3,453.5 9.1425 51,200 0 2 → 0

This targeted microbenchmark is not an overall Queue speedup multiplier. The first growth still allocates; amortization over many iterations rounds the measured steady-state B/op and allocs/op to zero. Later shrinking can retain a larger backing array in exchange for fewer allocations and copies.

Separate contributions and full sample ranges

Ring-only retains the original queue.go and applies only the new ring.go. Each row includes the complete median tuple and minimum/maximum time from the local measurement summary. For Ring, an operation is one enqueue/dequeue pair; for Queue, it is one completed task.

Scenario Version Median ns/op Median B/op Median allocs/op Time range, ns/op
Ring backlog 64 baseline 351.25 3,456 2 334.4–405.3
Ring backlog 64 ring-only 9.321 0 0 9.294–9.341
Ring backlog 64 optimized 9.3555 0 0 9.3–9.414
Ring backlog 1,024 baseline 3,453.5 51,200 2 3,332–3,559
Ring backlog 1,024 ring-only 9.148 0 0 9.129–9.215
Ring backlog 1,024 optimized 9.1425 0 0 9.111–9.201
Queue, 1 worker baseline 2,537.5 1,000 17 2,511–2,557
Queue, 1 worker ring-only 2,537.5 998 17 2,532–2,554
Queue, 1 worker optimized 1,196 854 11 1,183–1,256
Queue, 4 workers baseline 1,610.5 1,001 17 1,544–1,658
Queue, 4 workers ring-only 1,669 998 17 1,588–1,708
Queue, 4 workers optimized 1,438.5 854 11 1,299–1,607
Queue, 12 workers baseline 2,065 1,001.5 17 1,894–2,118
Queue, 12 workers ring-only 2,067 998 17 1,915–2,079
Queue, 12 workers optimized 1,859 854 11 1,846–1,952
Queue, 4 workers + afterFn baseline 1,654 1,001 17 1,536–1,688
Queue, 4 workers + afterFn ring-only 1,636 998 17 1,563–1,758
Queue, 4 workers + afterFn optimized 1,358.5 894 13 1,341–1,421

The Ring fix primarily benefits the capacity boundary. The batched end-to-end workload does not exhibit the same oscillation: Ring-only still uses 17 allocations/task, while adding the scheduler change brings general tasks down to 11. Small Ring-only timing differences are not claimed as gains.

Goroutine creation per task

Configuration Before After
Without afterFn 3: Request, work, handler 1: handler
With afterFn 3: callback runs inside work 2: handler and callback

These counts come from the code paths, not runtime-profile sampling, and exclude persistent goroutines created at initialization or worker growth. The handler goroutine remains necessary to preserve timeout behavior when a handler ignores cancellation. Callbacks remain independent of worker slots and tracked by Release. Idle queues now retain workers until retirement or release; workers block while waiting for tasks.

Environment, methodology, and limitations

  • Apple M4 Pro; darwin/arm64; Go 1.26.10; GOMAXPROCS=12 via -cpu=12.
  • Identical performance_test.go in all three versions; 500ms per case, 3 rounds × 2 samples = 6 samples per case/version (108 samples across six cases and three versions).
  • Interleaved order: baseline → Ring-only → optimized; optimized → baseline → Ring-only; Ring-only → optimized → baseline.
  • Versions ran sequentially, without concurrent test/lint runs and without race instrumentation for benchmarks.
  • One producer, empty handlers, batches of up to 1,024 submissions. Each batch waits for handler completion signals, or callback completion signals in the afterFn case. Final Release is included in timing and successful task counts must equal submissions.
  • Every enqueue error is checked; rejection of tasks from a full queue is not counted as successful throughput.
  • NewPool is called before ResetTimer. Asynchronous startup and final release can still contribute amortized costs.
  • Real business handlers, multiple producers, external brokers, long-running steady state, RSS, p95, and p99 were not measured. Runtime scheduling, GC, and host background load can affect timing; no statistical-significance claim is made.
  • Baseline and final correctness checks passed under the race detector. Queue statement coverage was 93.2%; lint reported 0 issues. Four lifecycle scenarios passed 30 repetitions at each of CPU settings 1, 4, and 12, as detailed below.

Raw benchmark data

All 108 original samples, execution timestamps, environment headers, and benchmark/implementation SHA256 hashes are embedded below. The report and raw-data files remain local and are not part of the PR file changes.

baseline: all six samples per case and source hashes
# baseline
# baseline commit: 9721be66093a0e2889129e4751154d8388698b98
# 2026-10-31T10:45:24.614465+08:00
# go test -run ^$ -bench ^(BenchmarkRingBoundary|BenchmarkQueueEndToEnd|BenchmarkQueueAfterFnEndToEnd)$ -benchmem -benchtime=500ms -count=2 -cpu=12 .
# performance_test.go SHA256: a57ed948549c3348af240f0c69a8bd652cf8b752d72a93208cf892b4a5ffc231
# queue.go SHA256: fd7b78f7269d316ffa25bfe4ebd0a81664431d6d08534df8e29fc357e6545fdf
# ring.go SHA256: 9d2cf12d3f5a02c7cebcc75c2d032122243384063d3b1f837fbe0d0eb4b9600f

# Round 1
goos: darwin
goarch: arm64
pkg: github.com/golang-queue/queue
cpu: Apple M4 Pro
BenchmarkRingBoundary/backlog=64-12 	 1707652	       339.7 ns/op	    3456 B/op	       2 allocs/op
BenchmarkRingBoundary/backlog=64-12 	 1729177	       362.8 ns/op	    3456 B/op	       2 allocs/op
BenchmarkRingBoundary/backlog=1024-12         	  161654	      3559 ns/op	   51200 B/op	       2 allocs/op
BenchmarkRingBoundary/backlog=1024-12         	  165234	      3538 ns/op	   51200 B/op	       2 allocs/op
BenchmarkQueueEndToEnd/workers=1-12           	  232165	      2550 ns/op	    1000 B/op	      17 allocs/op
BenchmarkQueueEndToEnd/workers=1-12           	  231847	      2535 ns/op	    1000 B/op	      17 allocs/op
BenchmarkQueueEndToEnd/workers=4-12           	  403672	      1558 ns/op	    1001 B/op	      17 allocs/op
BenchmarkQueueEndToEnd/workers=4-12           	  395530	      1544 ns/op	    1001 B/op	      17 allocs/op
BenchmarkQueueEndToEnd/workers=12-12          	  280562	      2086 ns/op	    1002 B/op	      17 allocs/op
BenchmarkQueueEndToEnd/workers=12-12          	  281010	      2116 ns/op	    1002 B/op	      17 allocs/op
BenchmarkQueueAfterFnEndToEnd-12              	  393162	      1552 ns/op	    1001 B/op	      17 allocs/op
BenchmarkQueueAfterFnEndToEnd-12              	  407179	      1688 ns/op	    1001 B/op	      17 allocs/op
PASS
ok  	github.com/golang-queue/queue	8.581s

# Round 2
goos: darwin
goarch: arm64
pkg: github.com/golang-queue/queue
cpu: Apple M4 Pro
BenchmarkRingBoundary/backlog=64-12 	 1528264	       405.3 ns/op	    3456 B/op	       2 allocs/op
BenchmarkRingBoundary/backlog=64-12 	 1663270	       336.9 ns/op	    3456 B/op	       2 allocs/op
BenchmarkRingBoundary/backlog=1024-12         	  177080	      3332 ns/op	   51200 B/op	       2 allocs/op
BenchmarkRingBoundary/backlog=1024-12         	  179100	      3431 ns/op	   51200 B/op	       2 allocs/op
BenchmarkQueueEndToEnd/workers=1-12           	  230928	      2526 ns/op	     999 B/op	      17 allocs/op
BenchmarkQueueEndToEnd/workers=1-12           	  235040	      2540 ns/op	    1000 B/op	      17 allocs/op
BenchmarkQueueEndToEnd/workers=4-12           	  398450	      1653 ns/op	    1001 B/op	      17 allocs/op
BenchmarkQueueEndToEnd/workers=4-12           	  371175	      1639 ns/op	    1001 B/op	      17 allocs/op
BenchmarkQueueEndToEnd/workers=12-12          	  268393	      2006 ns/op	    1001 B/op	      17 allocs/op
BenchmarkQueueEndToEnd/workers=12-12          	  302671	      2044 ns/op	    1001 B/op	      17 allocs/op
BenchmarkQueueAfterFnEndToEnd-12              	  360284	      1536 ns/op	    1001 B/op	      17 allocs/op
BenchmarkQueueAfterFnEndToEnd-12              	  379545	      1645 ns/op	    1001 B/op	      17 allocs/op
PASS
ok  	github.com/golang-queue/queue	8.341s

# Round 3
goos: darwin
goarch: arm64
pkg: github.com/golang-queue/queue
cpu: Apple M4 Pro
BenchmarkRingBoundary/backlog=64-12 	 1761234	       364.3 ns/op	    3456 B/op	       2 allocs/op
BenchmarkRingBoundary/backlog=64-12 	 1764464	       334.4 ns/op	    3456 B/op	       2 allocs/op
BenchmarkRingBoundary/backlog=1024-12         	  181333	      3465 ns/op	   51200 B/op	       2 allocs/op
BenchmarkRingBoundary/backlog=1024-12         	  172911	      3442 ns/op	   51200 B/op	       2 allocs/op
BenchmarkQueueEndToEnd/workers=1-12           	  236456	      2511 ns/op	    1000 B/op	      17 allocs/op
BenchmarkQueueEndToEnd/workers=1-12           	  234081	      2557 ns/op	    1000 B/op	      17 allocs/op
BenchmarkQueueEndToEnd/workers=4-12           	  389548	      1658 ns/op	    1001 B/op	      17 allocs/op
BenchmarkQueueEndToEnd/workers=4-12           	  376849	      1582 ns/op	    1001 B/op	      17 allocs/op
BenchmarkQueueEndToEnd/workers=12-12          	  276811	      2118 ns/op	    1002 B/op	      17 allocs/op
BenchmarkQueueEndToEnd/workers=12-12          	  299121	      1894 ns/op	    1001 B/op	      17 allocs/op
BenchmarkQueueAfterFnEndToEnd-12              	  372774	      1677 ns/op	    1002 B/op	      17 allocs/op
BenchmarkQueueAfterFnEndToEnd-12              	  386558	      1663 ns/op	    1002 B/op	      17 allocs/op
PASS
ok  	github.com/golang-queue/queue	8.411s
ring-only: all six samples per case and source hashes
# ring-only
# baseline commit: 9721be66093a0e2889129e4751154d8388698b98
# 2026-10-31T10:45:24.614786+08:00
# go test -run ^$ -bench ^(BenchmarkRingBoundary|BenchmarkQueueEndToEnd|BenchmarkQueueAfterFnEndToEnd)$ -benchmem -benchtime=500ms -count=2 -cpu=12 .
# performance_test.go SHA256: a57ed948549c3348af240f0c69a8bd652cf8b752d72a93208cf892b4a5ffc231
# queue.go SHA256: fd7b78f7269d316ffa25bfe4ebd0a81664431d6d08534df8e29fc357e6545fdf
# ring.go SHA256: d38f2e662c201c46278d084a751b26188a7c119c4ddd016630a6a0288f51d5d7

# Round 1
goos: darwin
goarch: arm64
pkg: github.com/golang-queue/queue
cpu: Apple M4 Pro
BenchmarkRingBoundary/backlog=64-12 	63411094	         9.324 ns/op	       0 B/op	       0 allocs/op
BenchmarkRingBoundary/backlog=64-12 	63519507	         9.294 ns/op	       0 B/op	       0 allocs/op
BenchmarkRingBoundary/backlog=1024-12         	65887003	         9.155 ns/op	       0 B/op	       0 allocs/op
BenchmarkRingBoundary/backlog=1024-12         	65422425	         9.215 ns/op	       0 B/op	       0 allocs/op
BenchmarkQueueEndToEnd/workers=1-12           	  223208	      2549 ns/op	     999 B/op	      17 allocs/op
BenchmarkQueueEndToEnd/workers=1-12           	  240972	      2554 ns/op	     998 B/op	      17 allocs/op
BenchmarkQueueEndToEnd/workers=4-12           	  398856	      1692 ns/op	     999 B/op	      17 allocs/op
BenchmarkQueueEndToEnd/workers=4-12           	  363988	      1655 ns/op	     998 B/op	      17 allocs/op
BenchmarkQueueEndToEnd/workers=12-12          	  293664	      2074 ns/op	     999 B/op	      17 allocs/op
BenchmarkQueueEndToEnd/workers=12-12          	  294046	      2065 ns/op	     998 B/op	      17 allocs/op
BenchmarkQueueAfterFnEndToEnd-12              	  337825	      1647 ns/op	     998 B/op	      17 allocs/op
BenchmarkQueueAfterFnEndToEnd-12              	  368608	      1632 ns/op	     998 B/op	      17 allocs/op
PASS
ok  	github.com/golang-queue/queue	8.146s

# Round 2
goos: darwin
goarch: arm64
pkg: github.com/golang-queue/queue
cpu: Apple M4 Pro
BenchmarkRingBoundary/backlog=64-12 	64417164	         9.331 ns/op	       0 B/op	       0 allocs/op
BenchmarkRingBoundary/backlog=64-12 	64936533	         9.296 ns/op	       0 B/op	       0 allocs/op
BenchmarkRingBoundary/backlog=1024-12         	65695532	         9.129 ns/op	       0 B/op	       0 allocs/op
BenchmarkRingBoundary/backlog=1024-12         	65610518	         9.148 ns/op	       0 B/op	       0 allocs/op
BenchmarkQueueEndToEnd/workers=1-12           	  231363	      2537 ns/op	     998 B/op	      17 allocs/op
BenchmarkQueueEndToEnd/workers=1-12           	  237847	      2532 ns/op	     998 B/op	      17 allocs/op
BenchmarkQueueEndToEnd/workers=4-12           	  352791	      1708 ns/op	     999 B/op	      17 allocs/op
BenchmarkQueueEndToEnd/workers=4-12           	  362088	      1683 ns/op	     998 B/op	      17 allocs/op
BenchmarkQueueEndToEnd/workers=12-12          	  314890	      1915 ns/op	     998 B/op	      17 allocs/op
BenchmarkQueueEndToEnd/workers=12-12          	  289844	      1917 ns/op	     998 B/op	      17 allocs/op
BenchmarkQueueAfterFnEndToEnd-12              	  400255	      1563 ns/op	     998 B/op	      17 allocs/op
BenchmarkQueueAfterFnEndToEnd-12              	  346850	      1640 ns/op	     998 B/op	      17 allocs/op
PASS
ok  	github.com/golang-queue/queue	8.015s

# Round 3
goos: darwin
goarch: arm64
pkg: github.com/golang-queue/queue
cpu: Apple M4 Pro
BenchmarkRingBoundary/backlog=64-12 	64178487	         9.318 ns/op	       0 B/op	       0 allocs/op
BenchmarkRingBoundary/backlog=64-12 	63515869	         9.341 ns/op	       0 B/op	       0 allocs/op
BenchmarkRingBoundary/backlog=1024-12         	66197764	         9.148 ns/op	       0 B/op	       0 allocs/op
BenchmarkRingBoundary/backlog=1024-12         	66093248	         9.136 ns/op	       0 B/op	       0 allocs/op
BenchmarkQueueEndToEnd/workers=1-12           	  230487	      2535 ns/op	     999 B/op	      17 allocs/op
BenchmarkQueueEndToEnd/workers=1-12           	  235363	      2538 ns/op	     998 B/op	      17 allocs/op
BenchmarkQueueEndToEnd/workers=4-12           	  385228	      1588 ns/op	     998 B/op	      17 allocs/op
BenchmarkQueueEndToEnd/workers=4-12           	  348844	      1655 ns/op	     998 B/op	      17 allocs/op
BenchmarkQueueEndToEnd/workers=12-12          	  298677	      2069 ns/op	     999 B/op	      17 allocs/op
BenchmarkQueueEndToEnd/workers=12-12          	  285526	      2079 ns/op	     998 B/op	      17 allocs/op
BenchmarkQueueAfterFnEndToEnd-12              	  394726	      1758 ns/op	     998 B/op	      17 allocs/op
BenchmarkQueueAfterFnEndToEnd-12              	  407175	      1583 ns/op	     998 B/op	      17 allocs/op
PASS
ok  	github.com/golang-queue/queue	8.129s
optimized: all six samples per case and source hashes
# optimized
# baseline commit: 9721be66093a0e2889129e4751154d8388698b98
# 2026-10-31T10:45:24.614969+08:00
# go test -run ^$ -bench ^(BenchmarkRingBoundary|BenchmarkQueueEndToEnd|BenchmarkQueueAfterFnEndToEnd)$ -benchmem -benchtime=500ms -count=2 -cpu=12 .
# performance_test.go SHA256: a57ed948549c3348af240f0c69a8bd652cf8b752d72a93208cf892b4a5ffc231
# queue.go SHA256: 65e453712c611f4dccff15fe5fa8c64ed4967958171ab2041d87bf2cd87cb64d
# ring.go SHA256: d38f2e662c201c46278d084a751b26188a7c119c4ddd016630a6a0288f51d5d7

# Round 1
goos: darwin
goarch: arm64
pkg: github.com/golang-queue/queue
cpu: Apple M4 Pro
BenchmarkRingBoundary/backlog=64-12 	62200502	         9.414 ns/op	       0 B/op	       0 allocs/op
BenchmarkRingBoundary/backlog=64-12 	64260751	         9.370 ns/op	       0 B/op	       0 allocs/op
BenchmarkRingBoundary/backlog=1024-12         	65092399	         9.111 ns/op	       0 B/op	       0 allocs/op
BenchmarkRingBoundary/backlog=1024-12         	65145985	         9.156 ns/op	       0 B/op	       0 allocs/op
BenchmarkQueueEndToEnd/workers=1-12           	  498918	      1199 ns/op	     854 B/op	      11 allocs/op
BenchmarkQueueEndToEnd/workers=1-12           	  504519	      1256 ns/op	     854 B/op	      11 allocs/op
BenchmarkQueueEndToEnd/workers=4-12           	  431584	      1299 ns/op	     854 B/op	      11 allocs/op
BenchmarkQueueEndToEnd/workers=4-12           	  491500	      1408 ns/op	     854 B/op	      11 allocs/op
BenchmarkQueueEndToEnd/workers=12-12          	  318118	      1854 ns/op	     854 B/op	      11 allocs/op
BenchmarkQueueEndToEnd/workers=12-12          	  333901	      1864 ns/op	     854 B/op	      11 allocs/op
BenchmarkQueueAfterFnEndToEnd-12              	  450871	      1341 ns/op	     894 B/op	      13 allocs/op
BenchmarkQueueAfterFnEndToEnd-12              	  460434	      1344 ns/op	     894 B/op	      13 allocs/op
PASS
ok  	github.com/golang-queue/queue	8.159s

# Round 2
goos: darwin
goarch: arm64
pkg: github.com/golang-queue/queue
cpu: Apple M4 Pro
BenchmarkRingBoundary/backlog=64-12 	63710948	         9.300 ns/op	       0 B/op	       0 allocs/op
BenchmarkRingBoundary/backlog=64-12 	64966119	         9.350 ns/op	       0 B/op	       0 allocs/op
BenchmarkRingBoundary/backlog=1024-12         	64224645	         9.129 ns/op	       0 B/op	       0 allocs/op
BenchmarkRingBoundary/backlog=1024-12         	65879168	         9.124 ns/op	       0 B/op	       0 allocs/op
BenchmarkQueueEndToEnd/workers=1-12           	  489147	      1183 ns/op	     854 B/op	      11 allocs/op
BenchmarkQueueEndToEnd/workers=1-12           	  507361	      1195 ns/op	     854 B/op	      11 allocs/op
BenchmarkQueueEndToEnd/workers=4-12           	  460804	      1530 ns/op	     854 B/op	      11 allocs/op
BenchmarkQueueEndToEnd/workers=4-12           	  391165	      1607 ns/op	     854 B/op	      11 allocs/op
BenchmarkQueueEndToEnd/workers=12-12          	  293264	      1930 ns/op	     854 B/op	      11 allocs/op
BenchmarkQueueEndToEnd/workers=12-12          	  327092	      1952 ns/op	     854 B/op	      11 allocs/op
BenchmarkQueueAfterFnEndToEnd-12              	  379497	      1374 ns/op	     894 B/op	      13 allocs/op
BenchmarkQueueAfterFnEndToEnd-12              	  398148	      1421 ns/op	     894 B/op	      13 allocs/op
PASS
ok  	github.com/golang-queue/queue	8.479s

# Round 3
goos: darwin
goarch: arm64
pkg: github.com/golang-queue/queue
cpu: Apple M4 Pro
BenchmarkRingBoundary/backlog=64-12 	63923698	         9.321 ns/op	       0 B/op	       0 allocs/op
BenchmarkRingBoundary/backlog=64-12 	64846461	         9.361 ns/op	       0 B/op	       0 allocs/op
BenchmarkRingBoundary/backlog=1024-12         	64768880	         9.201 ns/op	       0 B/op	       0 allocs/op
BenchmarkRingBoundary/backlog=1024-12         	65377280	         9.172 ns/op	       0 B/op	       0 allocs/op
BenchmarkQueueEndToEnd/workers=1-12           	  485888	      1197 ns/op	     854 B/op	      11 allocs/op
BenchmarkQueueEndToEnd/workers=1-12           	  501968	      1194 ns/op	     854 B/op	      11 allocs/op
BenchmarkQueueEndToEnd/workers=4-12           	  463135	      1469 ns/op	     854 B/op	      11 allocs/op
BenchmarkQueueEndToEnd/workers=4-12           	  437142	      1349 ns/op	     854 B/op	      11 allocs/op
BenchmarkQueueEndToEnd/workers=12-12          	  314808	      1846 ns/op	     854 B/op	      11 allocs/op
BenchmarkQueueEndToEnd/workers=12-12          	  335983	      1853 ns/op	     854 B/op	      11 allocs/op
BenchmarkQueueAfterFnEndToEnd-12              	  441286	      1373 ns/op	     894 B/op	      13 allocs/op
BenchmarkQueueAfterFnEndToEnd-12              	  480596	      1341 ns/op	     894 B/op	      13 allocs/op
PASS
ok  	github.com/golang-queue/queue	8.062s

Related issues

N/A — no GitHub or Jira issue was supplied.

Architecture / flow

flowchart TD
    Ring[ring.go: Ring.Request with quarter-full shrink] --> Dispatcher[queue.go: single start dispatcher]
    Dispatcher --> Workers[queue.go: reusable consume workers]
    Workers --> Handler[Existing handle timeout and retry logic]
    Handler --> Work[queue.go: work completion and metrics]
    Work --> Wake[queue.go: schedule notification]
    Wake --> Dispatcher
    Work --> Callback[queue.go: independent tracked afterFn]
    Resize[queue.go: UpdateWorkerCount] --> Wake
    style Ring fill:#dff0d8,stroke:#3c763d
    style Dispatcher fill:#dff0d8,stroke:#3c763d
    style Workers fill:#dff0d8,stroke:#3c763d
    style Work fill:#dff0d8,stroke:#3c763d
    style Wake fill:#dff0d8,stroke:#3c763d
    style Callback fill:#dff0d8,stroke:#3c763d
Loading

AI authorship

  • AI was used
  • Tool: OpenAI Codex
  • AI-authored files: queue.go, ring.go, options.go, performance_test.go, scheduling_test.go.
  • Human line-by-line reviewed: None — not yet reviewed by a human.

Change classification and scope

  • Core change — scheduling, dynamic concurrency, callback lifecycle, and shutdown affect every Queue consumer.
  • Scope: Ring shrink hysteresis, reusable scheduling, and regression/benchmark tests. No dependency, public interface, or handler cancellation redesign.
  • Recommend two or more reviewers, including the module owner, before merging.

Verification

Setup

Check out perf/queue-dispatch at dc8131533355d3440557aad8a4c8ec26c1486ec1 and run commands from the repository root. Prerequisites: Go 1.26.10, Git, and a working race-detector toolchain. Dependencies use go.mod/go.tools.mod; no external broker, accounts, secrets, services, or test data are required. go mod download prepares dependencies; the lint tool is pinned in go.tools.mod. RTK is optional for reviewers: commands below invoke Go directly.

Automated checks

Command from repository root Expected success Status Observed result
go tool -modfile=go.tools.mod golangci-lint fmt Formatting produces no further source changes Passed Benchmark source hashes still match the measured version
go test -race -covermode=atomic -coverprofile=/tmp/queue-pr-coverage.out ./... All tests pass, no races or goleak failures Passed Queue coverage 93.2%; PR-preparation run reused passing cached results; subsequent artifact removal changes no Go source
go tool -modfile=go.tools.mod golangci-lint run No diagnostics Passed 0 issues
`go test -race -run 'Test(AfterFnCanWaitForNextTask QueueResizePauseAndDrain ReusableWorkerOutcomes BlockingBackendAndRepeatedStart)$' -count=30 -cpu=1,4,12 -timeout=90s .`
git diff --check and git diff --staged --check before commit No whitespace errors Passed No diagnostics

Behavioral scenarios

Starting state: each test creates its own in-memory Queue/backend; no persistent state. The following named tests all Passed in the full race suite. Run an individual scenario with go test -race -run '^TEST_NAME$' -timeout=30s ., substituting the exact name in the first column.

Test name / action Expected observable result Observed result
TestRingShrinkHysteresisAndFIFO: fill 16, drain to 8 then 4, refill and drain Capacity remains 16 at half-full, halves to 8 at quarter-full; FIFO survives resize; final capacity 2 Passed
TestQueueResizePauseAndDrain: enqueue 16 gated jobs; resize 2 → 4 → 1 → 0 → 2 Active count follows the tested phases, no new work while paused, all 16 unique tasks succeed after drain Passed
TestReusableWorkerOutcomes: error, panic, retry success, noncooperative timed-out handler, then success Worker continues; 2 successes and 3 failures; Release completes while timed-out handler is still waiting Passed
TestAfterFnCanWaitForNextTask: one worker; callback enqueues and waits for a second task Second task runs without callback occupying its slot; both tasks/callbacks finish Passed on original baseline and final version
TestReleaseWaitsForAfterFn: hold callback behind a channel, invoke Release, then unblock Release waits until callback is unblocked Passed
TestBlockingBackendAndRepeatedStart: call Start twice, process 64 jobs, shut down blocking backend Request remains serialized, 64 successes, Release completes Passed

Completion is signaled by test channels; progress waits are bounded at 5 seconds, and the callback dependency test has a 1-second bound. The noncooperative handler is released during test cleanup. Existing shutdown, max-capacity, retry, and panic tests also passed.

Performance reproduction

Passed: run the command below in original, Ring-only, and final source directories using identical performance_test.go. Use baseline commit 9721be66093a0e2889129e4751154d8388698b98 with the current performance_test.go copied in. For Ring-only, also copy the current ring.go. Execute three rounds with two samples per round in this order: baseline → Ring-only → optimized; optimized → baseline → Ring-only; Ring-only → optimized → baseline:

go test -run '^$'   -bench '^(BenchmarkRingBoundary|BenchmarkQueueEndToEnd|BenchmarkQueueAfterFnEndToEnd)$'   -benchmem -benchtime=500ms -count=2 -cpu=12 .

Expected: completed-task counts equal submissions; Ring boundary steady-state allocations disappear; general task allocations drop. Exact timing is machine-dependent. Observed medians, ranges, and all raw samples are in the Performance results section above. This benchmark uses one producer, bounded batches, and includes final Release; it does not measure real broker traffic or tail latency.

Not run: production/multi-producer soak or external-broker load tests; this scope has no production workload or external service fixture.

Cleanup: tests release their queues and require no service cleanup. Remove /tmp/queue-pr-coverage.out if desired. Benchmarks do not persist application data. Formatting can update local files; inspect git diff afterward.

Security check

  • No secrets or credentials included in the scoped diff.
  • N/A — no authentication, authorization, or external security-sensitive interface changed.

Risk and rollback

  • Worker reuse changes goroutine lifetime and wakeups; review retirement, shutdown handoff, and callback tracking carefully. Idle queues now retain configured workers, and quarter-full shrinking may retain larger buffers longer.
  • Timeout still cannot forcibly terminate a handler that ignores context. Blocking backends must still unblock Request during Shutdown.
  • Roll back by reverting implementation commit a67a8e5; there is no data/schema migration.

Reviewer guide

Read queue.go Start/work/consume/start and ring.go Request line by line, then the lifecycle tests. Inspect the benchmark methodology in performance_test.go. This PR includes only the three existing Go files and two test files; the plan, report files, and raw-data files are retained locally; complete measurement data is embedded in this description.

- Reuse dispatch and worker goroutines while preserving callback independence
- Shrink ring buffers at quarter capacity to prevent resize oscillation
- Add lifecycle regressions and reproducible performance comparisons
@appleboy

Copy link
Copy Markdown
Member Author

Superseded by two independently reviewable PRs: #179 (Ring resize hysteresis) and #180 (Queue dispatcher/worker reuse). Both target master directly, contain disjoint code/test files, and can be merged in either order. Each description includes fresh standalone benchmark comparisons and raw samples; plan/report artifacts remain local. The original branch is retained for historical reference.

@appleboy appleboy closed this Oct 31, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant