Skip to content

perf(queue): reuse workers and consolidate task results - #180

Merged
appleboy merged 2 commits into
masterfrom
perf/queue-worker-reuse
Nov 1, 2026
Merged

appleboy merged 2 commits into
masterfrom
perf/queue-worker-reuse

Conversation

@appleboy

@appleboy appleboy commented Oct 31, 2026 •

Copy link
Copy Markdown
Member

Summary and scope

Short tasks previously created a Request goroutine, a work goroutine, and a handler goroutine for every job. Request was already serialized because the dispatcher waited for each result. Call Request directly from one persistent dispatcher and reuse work goroutines to remove per-task startup/teardown and routineGroup accounting.

Specific changes:

  • The dispatcher owns worker creation/retirement and checks the current concurrency limit; completion notifications are coalesced wakeups. The completion path no longer acquires the scheduling mutex.
  • Reuse consume workers instead of creating a new work goroutine per task. Task delivery still uses a channel; this does not eliminate all channel synchronization.
  • Keep the independent handler goroutine and preserve timeout/retry/panic behavior. A noncooperative timed-out handler must not permanently consume a Queue slot.
  • Keep afterFn independent of worker slots in a tracked goroutine. A callback may enqueue/wait for another task; Release still waits for callbacks.
  • Consolidate the separate error and panic channels into one buffered result channel. Each handler produces one terminal result; both normal waiting and shutdown waiting decode it. Capacity 1 lets a handler finish after the caller times out without blocking its result send. Retry timers remain unchanged.
  • Make Start idempotent so repeated calls cannot create parallel dispatchers. Dynamic retirement lets current work finish; blocking backends retain serialized Request calls and must unblock on Shutdown.

Per-task goroutine creation is 3 → 1 without afterFn and 3 → 2 with afterFn, excluding persistent workers created at initialization or growth. These counts are derived from code paths, not runtime-profile samples.

This PR changes queue.go, options.go, scheduling_test.go, queue_performance_test.go, handle_result_test.go, and handle_performance_test.go. It makes no Ring changes. Ring PR #179 has already merged into master; compatibility with that master version is tested below. Both performance experiments used the original Ring implementation to isolate Queue changes. Split from #178; no issue-closing keyword is intended.

Flow

flowchart TD
    Backend[Unchanged backend Request] --> Dispatcher[queue.go: persistent start dispatcher]
    Dispatcher --> Worker[queue.go: reusable consume workers]
    Worker --> Handler[Independent handler: timeout and retries preserved]
    Handler --> Result[One buffered result: error or panic]
    Result --> Work[queue.go: work completion and metrics]
    Work --> Wake[queue.go: schedule wakeup]
    Wake --> Dispatcher
    Work --> Callback[queue.go: independent tracked afterFn]
    Resize[UpdateWorkerCount] --> Wake
    style Dispatcher fill:#dff0d8,stroke:#3c763d
    style Worker fill:#dff0d8,stroke:#3c763d
    style Work fill:#dff0d8,stroke:#3c763d
    style Wake fill:#dff0d8,stroke:#3c763d
    style Callback fill:#dff0d8,stroke:#3c763d
Loading

Stage 2: result-channel consolidation (2026-11-01)

Incremental comparison: worker-reuse baseline 4d0f9d945db6594c8ea1c7241be2f8fdffc55357 versus the channel-only implementation now committed as 43b2074d24e7bdf138a5870db818c0f970871d0d. The measured queue.go and both benchmark sources are byte-identical to the committed versions (SHA256 recorded below). The experimental benchmark filename was handle_experiment_benchmark_test.go; it is committed unchanged as handle_performance_test.go.

Previously every handle invocation allocated two buffered channels even though a handler reports either an error/success or a panic, never both. One result struct carries err and panicValue through a single capacity-1 channel. The measured saving is two allocations and approximately 128 bytes per task. The normal result select and the shutdown wait both rethrow panic results and return error results as before. Results are not pooled across tasks: a timed-out handler may still be running and must retain its own destination. Handler goroutines, context timeouts, retry counts, and time.After retry delays remain unchanged. Timer reuse is not part of this PR.

End-to-end results

Values are medians of six samples. Time reduction is 1 - after/before.

Scenario Before ns/task After ns/task Time reduction Before → after B/task Before → after allocs/task
1 worker 1,259 1,111.5 11.7% 860 → 732 11 → 9
4 workers 1,419.5 1,335 6.0% 859 → 731 11 → 9
12 workers 1,858.5 1,797.5 3.3% 856.5 → 729 11 → 9
4 workers + afterFn 1,418.5 1,344.5 5.2% 898 → 770 13 → 11
Scenario Before range ns/task After range ns/task
1 worker 1,195–1,275 1,091–1,136
4 workers 1,347–1,518 1,294–1,354
12 workers 1,795–1,994 1,759–1,886
4 workers + afterFn 1,364–1,696 1,312–1,427

Direct handle cost

This isolates handle, without enqueue/dispatcher overhead. Each invocation resets attempts and RetryCount, fails the specified number of times, then succeeds. Retry delay is deliberately 1ns and timeout is one minute; this is a timer/handler microbenchmark, not production backoff latency.

Retries Before ns/op (range) After ns/op (range) Before → after B/op Before → after allocs/op
0 834.15 (817.9–863.3) 717.3 (716.6–751.2) 704 → 576 10 → 8
1 1,090.5 (1,071–1,170) 1,014 (999.3–1,049) 984 → 856 14 → 12
3 1,679 (1,642–1,793) 1,609.5 (1,587–1,731) 1,544 → 1,416 22 → 20
10 3,610 (3,521–3,771) 3,491 (3,473–3,578) 3,504 → 3,376 50 → 48

No-retry direct handle time decreases 14.0%; this component result is separate from end-to-end throughput.

Method, interpretation, and reproduction

Apple M4 Pro, darwin/arm64, Go 1.26.10, CPU setting 12, 500ms per case, two samples per round over three rounds. Runs were sequential without race instrumentation. The original experiment interleaved baseline, channel-only, and a separate timer-reuse candidate; only baseline and channel-only results are included here. Round orders were baseline/channel/timer, timer/baseline/channel, and channel/timer/baseline. The timer candidate is excluded from this commit.

End-to-end tests use a single producer, empty handlers, and batches up to 1,024; final Release is timed and successful task counts are checked. This measures amortized task cost, not p99 or individual queue latency. Multiworker/callback timing ranges overlap, so no statistical-significance or production speedup claim is made. Allocation reductions are more consistent than timing. Different dates produced different baseline timings; do not combine stage 1 and stage 2 timing percentages. Combined Ring + final Queue performance has not been measured.

For reproduction, export 4d0f9d9 and 43b2074d24e7bdf138a5870db818c0f970871d0d into separate disposable directories. Copy handle_performance_test.go from the latter into the baseline; queue_performance_test.go already exists in both. Run in each directory, interleaving versions over three rounds:

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

Expected: benchmark success/attempt checks pass, general tasks show 11 → 9 allocations and callbacks 13 → 11; exact timing is not an assertion. Delete disposable directories afterward.

Raw samples and source hashes

Channel experiment: baseline, all six samples per case
# Variant: baseline
# Baseline: 4d0f9d945db6594c8ea1c7241be2f8fdffc55357
# Date: 2026-11-01T15:27:52.065498+08:00
# go test -run ^$ -bench ^(BenchmarkQueueEndToEnd|BenchmarkQueueAfterFnEndToEnd|BenchmarkHandleRetry)$ -benchmem -benchtime=500ms -count=2 -cpu=12 .
# queue.go SHA256: 65e453712c611f4dccff15fe5fa8c64ed4967958171ab2041d87bf2cd87cb64d
# queue_performance_test.go SHA256: e6ef2c4acd222d077b82bbbbd9cf04e2411ea0eed3513a9d7a17d484e061859f
# handle_experiment_benchmark_test.go SHA256: 477fa3a0fd21f188b1e43c616df0d4b3f6dc1e5cd5914320b383819684c83562

# Round 1
goos: darwin
goarch: arm64
pkg: github.com/golang-queue/queue
cpu: Apple M4 Pro
BenchmarkHandleRetry/retries=0-12   	  678914	       859.9 ns/op	     704 B/op	      10 allocs/op
BenchmarkHandleRetry/retries=0-12   	  682956	       863.3 ns/op	     704 B/op	      10 allocs/op
BenchmarkHandleRetry/retries=1-12   	  527547	      1170 ns/op	     984 B/op	      14 allocs/op
BenchmarkHandleRetry/retries=1-12   	  501952	      1162 ns/op	     984 B/op	      14 allocs/op
BenchmarkHandleRetry/retries=3-12   	  337850	      1793 ns/op	    1544 B/op	      22 allocs/op
BenchmarkHandleRetry/retries=3-12   	  322239	      1791 ns/op	    1544 B/op	      22 allocs/op
BenchmarkHandleRetry/retries=10-12  	  160275	      3771 ns/op	    3504 B/op	      50 allocs/op
BenchmarkHandleRetry/retries=10-12  	  157854	      3771 ns/op	    3504 B/op	      50 allocs/op
BenchmarkQueueEndToEnd/workers=1-12 	  466574	      1257 ns/op	     860 B/op	      11 allocs/op
BenchmarkQueueEndToEnd/workers=1-12 	  476461	      1268 ns/op	     860 B/op	      11 allocs/op
BenchmarkQueueEndToEnd/workers=4-12 	  423295	      1480 ns/op	     859 B/op	      11 allocs/op
BenchmarkQueueEndToEnd/workers=4-12 	  416662	      1442 ns/op	     859 B/op	      11 allocs/op
BenchmarkQueueEndToEnd/workers=12-12         	  325519	      1879 ns/op	     856 B/op	      11 allocs/op
BenchmarkQueueEndToEnd/workers=12-12         	  326677	      1994 ns/op	     857 B/op	      11 allocs/op
BenchmarkQueueAfterFnEndToEnd-12             	  419122	      1382 ns/op	     898 B/op	      13 allocs/op
BenchmarkQueueAfterFnEndToEnd-12             	  353954	      1528 ns/op	     898 B/op	      13 allocs/op
PASS
ok  	github.com/golang-queue/queue	10.551s

# Round 2
goos: darwin
goarch: arm64
pkg: github.com/golang-queue/queue
cpu: Apple M4 Pro
BenchmarkHandleRetry/retries=0-12   	  667488	       828.4 ns/op	     704 B/op	      10 allocs/op
BenchmarkHandleRetry/retries=0-12   	  704672	       839.9 ns/op	     704 B/op	      10 allocs/op
BenchmarkHandleRetry/retries=1-12   	  530014	      1094 ns/op	     984 B/op	      14 allocs/op
BenchmarkHandleRetry/retries=1-12   	  541147	      1087 ns/op	     984 B/op	      14 allocs/op
BenchmarkHandleRetry/retries=3-12   	  369104	      1673 ns/op	    1544 B/op	      22 allocs/op
BenchmarkHandleRetry/retries=3-12   	  361305	      1685 ns/op	    1544 B/op	      22 allocs/op
BenchmarkHandleRetry/retries=10-12  	  165858	      3572 ns/op	    3504 B/op	      50 allocs/op
BenchmarkHandleRetry/retries=10-12  	  164350	      3648 ns/op	    3504 B/op	      50 allocs/op
BenchmarkQueueEndToEnd/workers=1-12 	  478042	      1275 ns/op	     861 B/op	      11 allocs/op
BenchmarkQueueEndToEnd/workers=1-12 	  483187	      1261 ns/op	     859 B/op	      11 allocs/op
BenchmarkQueueEndToEnd/workers=4-12 	  407452	      1347 ns/op	     858 B/op	      11 allocs/op
BenchmarkQueueEndToEnd/workers=4-12 	  387643	      1348 ns/op	     859 B/op	      11 allocs/op
BenchmarkQueueEndToEnd/workers=12-12         	  321548	      1795 ns/op	     856 B/op	      11 allocs/op
BenchmarkQueueEndToEnd/workers=12-12         	  332612	      1833 ns/op	     857 B/op	      11 allocs/op
BenchmarkQueueAfterFnEndToEnd-12             	  413868	      1454 ns/op	     898 B/op	      13 allocs/op
BenchmarkQueueAfterFnEndToEnd-12             	  397903	      1364 ns/op	     898 B/op	      13 allocs/op
PASS
ok  	github.com/golang-queue/queue	9.806s

# Round 3
goos: darwin
goarch: arm64
pkg: github.com/golang-queue/queue
cpu: Apple M4 Pro
BenchmarkHandleRetry/retries=0-12   	  730474	       824.4 ns/op	     704 B/op	      10 allocs/op
BenchmarkHandleRetry/retries=0-12   	  721536	       817.9 ns/op	     704 B/op	      10 allocs/op
BenchmarkHandleRetry/retries=1-12   	  555777	      1082 ns/op	     984 B/op	      14 allocs/op
BenchmarkHandleRetry/retries=1-12   	  576550	      1071 ns/op	     984 B/op	      14 allocs/op
BenchmarkHandleRetry/retries=3-12   	  361040	      1648 ns/op	    1544 B/op	      22 allocs/op
BenchmarkHandleRetry/retries=3-12   	  367850	      1642 ns/op	    1544 B/op	      22 allocs/op
BenchmarkHandleRetry/retries=10-12  	  166089	      3524 ns/op	    3504 B/op	      50 allocs/op
BenchmarkHandleRetry/retries=10-12  	  168981	      3521 ns/op	    3504 B/op	      50 allocs/op
BenchmarkQueueEndToEnd/workers=1-12 	  515749	      1195 ns/op	     860 B/op	      11 allocs/op
BenchmarkQueueEndToEnd/workers=1-12 	  493501	      1217 ns/op	     860 B/op	      11 allocs/op
BenchmarkQueueEndToEnd/workers=4-12 	  435442	      1518 ns/op	     859 B/op	      11 allocs/op
BenchmarkQueueEndToEnd/workers=4-12 	  412221	      1397 ns/op	     858 B/op	      11 allocs/op
BenchmarkQueueEndToEnd/workers=12-12         	  332084	      1873 ns/op	     857 B/op	      11 allocs/op
BenchmarkQueueEndToEnd/workers=12-12         	  348021	      1844 ns/op	     856 B/op	      11 allocs/op
BenchmarkQueueAfterFnEndToEnd-12             	  417793	      1696 ns/op	     899 B/op	      13 allocs/op
BenchmarkQueueAfterFnEndToEnd-12             	  413421	      1383 ns/op	     898 B/op	      13 allocs/op
PASS
ok  	github.com/golang-queue/queue	10.730s
Channel experiment: channel, all six samples per case
# Variant: channel
# Baseline: 4d0f9d945db6594c8ea1c7241be2f8fdffc55357
# Date: 2026-11-01T15:27:52.065498+08:00
# go test -run ^$ -bench ^(BenchmarkQueueEndToEnd|BenchmarkQueueAfterFnEndToEnd|BenchmarkHandleRetry)$ -benchmem -benchtime=500ms -count=2 -cpu=12 .
# queue.go SHA256: ef2a5345576e8ab36abb566fcb8f395bce9a5e17c233186622bced6d5a32cd5c
# queue_performance_test.go SHA256: e6ef2c4acd222d077b82bbbbd9cf04e2411ea0eed3513a9d7a17d484e061859f
# handle_experiment_benchmark_test.go SHA256: 477fa3a0fd21f188b1e43c616df0d4b3f6dc1e5cd5914320b383819684c83562

# Round 1
goos: darwin
goarch: arm64
pkg: github.com/golang-queue/queue
cpu: Apple M4 Pro
BenchmarkHandleRetry/retries=0-12   	  722173	       751.2 ns/op	     576 B/op	       8 allocs/op
BenchmarkHandleRetry/retries=0-12   	  830186	       744.7 ns/op	     576 B/op	       8 allocs/op
BenchmarkHandleRetry/retries=1-12   	  566004	      1049 ns/op	     856 B/op	      12 allocs/op
BenchmarkHandleRetry/retries=1-12   	  588592	      1033 ns/op	     856 B/op	      12 allocs/op
BenchmarkHandleRetry/retries=3-12   	  356461	      1658 ns/op	    1416 B/op	      20 allocs/op
BenchmarkHandleRetry/retries=3-12   	  351812	      1731 ns/op	    1416 B/op	      20 allocs/op
BenchmarkHandleRetry/retries=10-12  	  170574	      3578 ns/op	    3376 B/op	      48 allocs/op
BenchmarkHandleRetry/retries=10-12  	  164877	      3544 ns/op	    3376 B/op	      48 allocs/op
BenchmarkQueueEndToEnd/workers=1-12 	  527564	      1136 ns/op	     732 B/op	       9 allocs/op
BenchmarkQueueEndToEnd/workers=1-12 	  531265	      1132 ns/op	     732 B/op	       9 allocs/op
BenchmarkQueueEndToEnd/workers=4-12 	  451188	      1349 ns/op	     731 B/op	       9 allocs/op
BenchmarkQueueEndToEnd/workers=4-12 	  433540	      1354 ns/op	     731 B/op	       9 allocs/op
BenchmarkQueueEndToEnd/workers=12-12         	  336216	      1771 ns/op	     728 B/op	       9 allocs/op
BenchmarkQueueEndToEnd/workers=12-12         	  340942	      1833 ns/op	     729 B/op	       9 allocs/op
BenchmarkQueueAfterFnEndToEnd-12             	  424195	      1312 ns/op	     770 B/op	      11 allocs/op
BenchmarkQueueAfterFnEndToEnd-12             	  483804	      1323 ns/op	     770 B/op	      11 allocs/op
PASS
ok  	github.com/golang-queue/queue	10.190s

# Round 2
goos: darwin
goarch: arm64
pkg: github.com/golang-queue/queue
cpu: Apple M4 Pro
BenchmarkHandleRetry/retries=0-12   	  833685	       717.4 ns/op	     576 B/op	       8 allocs/op
BenchmarkHandleRetry/retries=0-12   	  801495	       716.6 ns/op	     576 B/op	       8 allocs/op
BenchmarkHandleRetry/retries=1-12   	  588046	      1000 ns/op	     856 B/op	      12 allocs/op
BenchmarkHandleRetry/retries=1-12   	  607502	      1026 ns/op	     856 B/op	      12 allocs/op
BenchmarkHandleRetry/retries=3-12   	  385189	      1587 ns/op	    1416 B/op	      20 allocs/op
BenchmarkHandleRetry/retries=3-12   	  366690	      1593 ns/op	    1416 B/op	      20 allocs/op
BenchmarkHandleRetry/retries=10-12  	  175395	      3474 ns/op	    3376 B/op	      48 allocs/op
BenchmarkHandleRetry/retries=10-12  	  173970	      3476 ns/op	    3376 B/op	      48 allocs/op
BenchmarkQueueEndToEnd/workers=1-12 	  552405	      1104 ns/op	     731 B/op	       9 allocs/op
BenchmarkQueueEndToEnd/workers=1-12 	  551660	      1091 ns/op	     732 B/op	       9 allocs/op
BenchmarkQueueEndToEnd/workers=4-12 	  475479	      1294 ns/op	     731 B/op	       9 allocs/op
BenchmarkQueueEndToEnd/workers=4-12 	  478230	      1320 ns/op	     731 B/op	       9 allocs/op
BenchmarkQueueEndToEnd/workers=12-12         	  345267	      1781 ns/op	     728 B/op	       9 allocs/op
BenchmarkQueueEndToEnd/workers=12-12         	  341312	      1814 ns/op	     729 B/op	       9 allocs/op
BenchmarkQueueAfterFnEndToEnd-12             	  446781	      1366 ns/op	     770 B/op	      11 allocs/op
BenchmarkQueueAfterFnEndToEnd-12             	  484192	      1319 ns/op	     770 B/op	      11 allocs/op
PASS
ok  	github.com/golang-queue/queue	10.615s

# Round 3
goos: darwin
goarch: arm64
pkg: github.com/golang-queue/queue
cpu: Apple M4 Pro
BenchmarkHandleRetry/retries=0-12   	  786201	       716.8 ns/op	     576 B/op	       8 allocs/op
BenchmarkHandleRetry/retries=0-12   	  838134	       717.2 ns/op	     576 B/op	       8 allocs/op
BenchmarkHandleRetry/retries=1-12   	  554506	      1002 ns/op	     856 B/op	      12 allocs/op
BenchmarkHandleRetry/retries=1-12   	  590911	       999.3 ns/op	     856 B/op	      12 allocs/op
BenchmarkHandleRetry/retries=3-12   	  372080	      1619 ns/op	    1416 B/op	      20 allocs/op
BenchmarkHandleRetry/retries=3-12   	  366787	      1600 ns/op	    1416 B/op	      20 allocs/op
BenchmarkHandleRetry/retries=10-12  	  174360	      3473 ns/op	    3376 B/op	      48 allocs/op
BenchmarkHandleRetry/retries=10-12  	  172578	      3506 ns/op	    3376 B/op	      48 allocs/op
BenchmarkQueueEndToEnd/workers=1-12 	  540442	      1108 ns/op	     733 B/op	       9 allocs/op
BenchmarkQueueEndToEnd/workers=1-12 	  552378	      1115 ns/op	     731 B/op	       9 allocs/op
BenchmarkQueueEndToEnd/workers=4-12 	  475754	      1321 ns/op	     731 B/op	       9 allocs/op
BenchmarkQueueEndToEnd/workers=4-12 	  486946	      1351 ns/op	     730 B/op	       9 allocs/op
BenchmarkQueueEndToEnd/workers=12-12         	  342422	      1759 ns/op	     729 B/op	       9 allocs/op
BenchmarkQueueEndToEnd/workers=12-12         	  367762	      1886 ns/op	     730 B/op	       9 allocs/op
BenchmarkQueueAfterFnEndToEnd-12             	  481567	      1427 ns/op	     770 B/op	      11 allocs/op
BenchmarkQueueAfterFnEndToEnd-12             	  492300	      1411 ns/op	     770 B/op	      11 allocs/op
PASS
ok  	github.com/golang-queue/queue	10.329s

Stage 1: worker reuse measurements (2026-10-31)

Historical measurements on 2026-10-31 compare baseline 9721be66093a0e2889129e4751154d8388698b98 with worker reuse before channel consolidation at 4d0f9d945db6594c8ea1c7241be2f8fdffc55357. These replace the combined-change numbers from #178. They describe the first commit, not the final head. Do not add or multiply these timing gains with the separate November 1 experiment. Every summary value is the median of six samples.

Scenario Before ns/task After ns/task Time reduction Derived throughput gain
1 worker 2,608.5 1,271 51.3% 105.2%
4 workers 1,850 1,495.5 19.2% 23.7%
12 workers 2,037 1,939.5 4.8% 5.0%
4 workers + afterFn 1,778 1,440.5 19.0% 23.4%

Time reduction = 1 - after/before; throughput gain = before/after - 1.

Scenario Before → after B/op Before → after allocs/op
1 worker 1,000 → 860 17 → 11
4 workers 1,001 → 859 17 → 11
12 workers 1,001.5 → 857 17 → 11
4 workers + afterFn 1,001 → 898 17 → 13
Scenario Before time range, ns/op After time range, ns/op
1 worker 2,588–2,837 1,212–1,300
4 workers 1,755–2,521 1,371–1,650
12 workers 1,934–2,269 1,873–2,009
4 workers + afterFn 1,639–1,887 1,383–1,503

Measurement method and limitations

  • Apple M4 Pro, darwin/arm64, Go 1.26.10, GOMAXPROCS=12; 500ms per case, three rounds with two samples per round.
  • Interleaved sequence: baseline → Ring-only → Queue-only; Queue-only → baseline → Ring-only; Ring-only → Queue-only → baseline. Runs were sequential, without simultaneous test/lint runs or race instrumentation.
  • Baseline included both benchmark families. Each changed version ran only its relevant family; the matching benchmark source file is byte-identical between that version and baseline.
  • These are microbenchmarks on one machine. Scheduler, GC, and background load cause variability. No statistical-significance claim is made; the ranges above should be considered alongside medians.
  • Single producer, empty handlers, batches of up to 1,024 tasks. Each batch waits for handler completion signals (callback signals for afterFn); final Release is timed and success count must equal submissions. Every enqueue error is checked. Queue construction precedes ResetTimer, but asynchronous startup can contribute amortized cost.
  • ns/task measures amortized end-to-end cost, not individual queuing latency or p99. No real handler, external-broker, multi-producer, RSS, or long-duration soak results are claimed. The original Ring implementation is identical in before/after versions.

Raw samples and source hashes

All relevant samples are embedded here; no report, plan, or raw-data files are added to the repository. Baseline output below is filtered to this PR’s benchmark family; other case lines are omitted, but environment, command, round markers, and hashes are preserved.

baseline — six samples per case
# baseline
# source commit: 9721be66093a0e2889129e4751154d8388698b98
# 2026-10-31T10:59:28.726129+08:00
# go test -run ^$ -bench ^(BenchmarkRingBoundary|BenchmarkQueueEndToEnd|BenchmarkQueueAfterFnEndToEnd)$ -benchmem -benchtime=500ms -count=2 -cpu=12 .
# ring.go SHA256: 9d2cf12d3f5a02c7cebcc75c2d032122243384063d3b1f837fbe0d0eb4b9600f
# queue.go SHA256: fd7b78f7269d316ffa25bfe4ebd0a81664431d6d08534df8e29fc357e6545fdf
# ring_performance_test.go SHA256: 133d56843339be04d5a3253f903571f4effe06103173cf3a37b1f0c5b79ed11b
# queue_performance_test.go SHA256: e6ef2c4acd222d077b82bbbbd9cf04e2411ea0eed3513a9d7a17d484e061859f

# Round 1
goos: darwin
goarch: arm64
pkg: github.com/golang-queue/queue
cpu: Apple M4 Pro
BenchmarkQueueEndToEnd/workers=1-12 	  211312	      2837 ns/op	    1000 B/op	      17 allocs/op
BenchmarkQueueEndToEnd/workers=1-12 	  216106	      2703 ns/op	    1000 B/op	      17 allocs/op
BenchmarkQueueEndToEnd/workers=4-12 	  223156	      2521 ns/op	    1001 B/op	      17 allocs/op
BenchmarkQueueEndToEnd/workers=4-12 	  332484	      1830 ns/op	    1001 B/op	      17 allocs/op
BenchmarkQueueEndToEnd/workers=12-12         	  294852	      2156 ns/op	    1002 B/op	      17 allocs/op
BenchmarkQueueEndToEnd/workers=12-12         	  288481	      2048 ns/op	    1002 B/op	      17 allocs/op
BenchmarkQueueAfterFnEndToEnd-12             	  279088	      1849 ns/op	    1001 B/op	      17 allocs/op
BenchmarkQueueAfterFnEndToEnd-12             	  341097	      1887 ns/op	    1001 B/op	      17 allocs/op
PASS
ok  	github.com/golang-queue/queue	9.165s

# Round 2
goos: darwin
goarch: arm64
pkg: github.com/golang-queue/queue
cpu: Apple M4 Pro
BenchmarkQueueEndToEnd/workers=1-12 	  227174	      2588 ns/op	    1000 B/op	      17 allocs/op
BenchmarkQueueEndToEnd/workers=1-12 	  220702	      2594 ns/op	    1000 B/op	      17 allocs/op
BenchmarkQueueEndToEnd/workers=4-12 	  365462	      1870 ns/op	    1001 B/op	      17 allocs/op
BenchmarkQueueEndToEnd/workers=4-12 	  335932	      1818 ns/op	    1001 B/op	      17 allocs/op
BenchmarkQueueEndToEnd/workers=12-12         	  284256	      2269 ns/op	    1002 B/op	      17 allocs/op
BenchmarkQueueEndToEnd/workers=12-12         	  278961	      2026 ns/op	    1001 B/op	      17 allocs/op
BenchmarkQueueAfterFnEndToEnd-12             	  337782	      1826 ns/op	    1001 B/op	      17 allocs/op
BenchmarkQueueAfterFnEndToEnd-12             	  335761	      1730 ns/op	    1001 B/op	      17 allocs/op
PASS
ok  	github.com/golang-queue/queue	9.081s

# Round 3
goos: darwin
goarch: arm64
pkg: github.com/golang-queue/queue
cpu: Apple M4 Pro
BenchmarkQueueEndToEnd/workers=1-12 	  195511	      2604 ns/op	    1001 B/op	      17 allocs/op
BenchmarkQueueEndToEnd/workers=1-12 	  223450	      2613 ns/op	    1001 B/op	      17 allocs/op
BenchmarkQueueEndToEnd/workers=4-12 	  289236	      1951 ns/op	    1001 B/op	      17 allocs/op
BenchmarkQueueEndToEnd/workers=4-12 	  374748	      1755 ns/op	    1001 B/op	      17 allocs/op
BenchmarkQueueEndToEnd/workers=12-12         	  314367	      1934 ns/op	    1001 B/op	      17 allocs/op
BenchmarkQueueEndToEnd/workers=12-12         	  319140	      1938 ns/op	    1001 B/op	      17 allocs/op
BenchmarkQueueAfterFnEndToEnd-12             	  357900	      1639 ns/op	    1002 B/op	      17 allocs/op
BenchmarkQueueAfterFnEndToEnd-12             	  342828	      1680 ns/op	    1000 B/op	      17 allocs/op
PASS
ok  	github.com/golang-queue/queue	8.472s
queue — six samples per case
# queue
# source commit: 4d0f9d945db6594c8ea1c7241be2f8fdffc55357
# 2026-10-31T10:59:28.726691+08:00
# go test -run ^$ -bench ^(BenchmarkQueueEndToEnd|BenchmarkQueueAfterFnEndToEnd)$ -benchmem -benchtime=500ms -count=2 -cpu=12 .
# ring.go SHA256: 9d2cf12d3f5a02c7cebcc75c2d032122243384063d3b1f837fbe0d0eb4b9600f
# queue.go SHA256: 65e453712c611f4dccff15fe5fa8c64ed4967958171ab2041d87bf2cd87cb64d
# queue_performance_test.go SHA256: e6ef2c4acd222d077b82bbbbd9cf04e2411ea0eed3513a9d7a17d484e061859f

# Round 1
goos: darwin
goarch: arm64
pkg: github.com/golang-queue/queue
cpu: Apple M4 Pro
BenchmarkQueueEndToEnd/workers=1-12 	  434113	      1300 ns/op	     859 B/op	      11 allocs/op
BenchmarkQueueEndToEnd/workers=1-12 	  470713	      1273 ns/op	     860 B/op	      11 allocs/op
BenchmarkQueueEndToEnd/workers=4-12 	  380390	      1571 ns/op	     860 B/op	      11 allocs/op
BenchmarkQueueEndToEnd/workers=4-12 	  401127	      1650 ns/op	     858 B/op	      11 allocs/op
BenchmarkQueueEndToEnd/workers=12-12         	  288250	      1977 ns/op	     858 B/op	      11 allocs/op
BenchmarkQueueEndToEnd/workers=12-12         	  309080	      2009 ns/op	     857 B/op	      11 allocs/op
BenchmarkQueueAfterFnEndToEnd-12             	  419684	      1448 ns/op	     898 B/op	      13 allocs/op
BenchmarkQueueAfterFnEndToEnd-12             	  397272	      1443 ns/op	     898 B/op	      13 allocs/op
PASS
ok  	github.com/golang-queue/queue	5.284s

# Round 2
goos: darwin
goarch: arm64
pkg: github.com/golang-queue/queue
cpu: Apple M4 Pro
BenchmarkQueueEndToEnd/workers=1-12 	  486424	      1269 ns/op	     860 B/op	      11 allocs/op
BenchmarkQueueEndToEnd/workers=1-12 	  488043	      1297 ns/op	     860 B/op	      11 allocs/op
BenchmarkQueueEndToEnd/workers=4-12 	  394556	      1618 ns/op	     859 B/op	      11 allocs/op
BenchmarkQueueEndToEnd/workers=4-12 	  389336	      1420 ns/op	     859 B/op	      11 allocs/op
BenchmarkQueueEndToEnd/workers=12-12         	  307015	      1902 ns/op	     857 B/op	      11 allocs/op
BenchmarkQueueEndToEnd/workers=12-12         	  300020	      2000 ns/op	     856 B/op	      11 allocs/op
BenchmarkQueueAfterFnEndToEnd-12             	  403342	      1438 ns/op	     898 B/op	      13 allocs/op
BenchmarkQueueAfterFnEndToEnd-12             	  419671	      1503 ns/op	     898 B/op	      13 allocs/op
PASS
ok  	github.com/golang-queue/queue	6.004s

# Round 3
goos: darwin
goarch: arm64
pkg: github.com/golang-queue/queue
cpu: Apple M4 Pro
BenchmarkQueueEndToEnd/workers=1-12 	  488893	      1212 ns/op	     860 B/op	      11 allocs/op
BenchmarkQueueEndToEnd/workers=1-12 	  497556	      1226 ns/op	     861 B/op	      11 allocs/op
BenchmarkQueueEndToEnd/workers=4-12 	  444980	      1385 ns/op	     858 B/op	      11 allocs/op
BenchmarkQueueEndToEnd/workers=4-12 	  435084	      1371 ns/op	     859 B/op	      11 allocs/op
BenchmarkQueueEndToEnd/workers=12-12         	  316830	      1877 ns/op	     858 B/op	      11 allocs/op
BenchmarkQueueEndToEnd/workers=12-12         	  329625	      1873 ns/op	     857 B/op	      11 allocs/op
BenchmarkQueueAfterFnEndToEnd-12             	  431907	      1383 ns/op	     899 B/op	      13 allocs/op
BenchmarkQueueAfterFnEndToEnd-12             	  465993	      1399 ns/op	     898 B/op	      13 allocs/op
PASS
ok  	github.com/golang-queue/queue	5.598s

Verification

Setup: check out perf/queue-worker-reuse at 43b2074d24e7bdf138a5870db818c0f970871d0d; run from the repository root with Go 1.26.10 and a working race-detector toolchain. Run go mod download for dependencies; lint tooling is pinned in go.tools.mod. No external broker, account, secret, or persistent fixture is required.

Check / command Expected behavior Status and observed result
go tool -modfile=go.tools.mod golangci-lint fmt No further formatting changes Passed
go test -race ./... All tests pass without races or goleak failures Passed on this standalone branch
go tool -modfile=go.tools.mod golangci-lint run No diagnostics Passed: 0 issues
go test -race -run 'Test(ResultProtocol|AfterFnCanWaitForNextTask|QueueResizePauseAndDrain|ReusableWorkerOutcomes|BlockingBackendAndRepeatedStart)' -count=10 -cpu=1,4,12 -timeout=90s . Result and worker lifecycle scenarios pass 10 times at each CPU setting Passed on this standalone branch
git diff --check and staged check before commit No whitespace errors Passed

For focused reproduction, run go test -race -run '^TEST_NAME$' -timeout=30s . with a name below. Every scenario starts with fresh in-memory state and Passed in the full race suite and combined-branches compatibility check:

Scenario Concrete trigger Expected observable result
TestQueueResizePauseAndDrain Enqueue 16 gated jobs; resize 2 → 4 → 1 → 0 → 2 Tested concurrency phases honored; pause starts no new work; 16 unique successes after drain
TestReusableWorkerOutcomes Error, panic, retry success, noncooperative timed-out handler, then success Continue processing; 2 successes, 3 failures; Release does not wait indefinitely for the timed-out handler
TestAfterFnCanWaitForNextTask One worker; callback enqueues and waits for a second task Second task and both callbacks finish without a slot deadlock
TestReleaseWaitsForAfterFn Hold callback behind a channel while calling Release, then unblock Release waits for callback completion
TestBlockingBackendAndRepeatedStart Start twice, submit 64 tasks, shut down a blocking backend Request stays serialized; 64 successes; Release returns

Progress signals use channels with 5-second test bounds; the callback dependency check has a 1-second bound. Cleanup releases queues and the deliberately blocked timed-out handler. No service/data cleanup is needed.

Additional result-protocol regressions (all passed with race detection):

Scenario Trigger and expected result
TestResultProtocolShutdown Shutdown cancels the handler context; success, error, and panic retain their original outcomes
TestResultProtocolLateCompletion Handler returns or panics after caller timeout; buffered delivery completes without leaking a blocked sender
TestResultProtocolRetryCancellation A 10ms timeout interrupts a one-hour retry delay
TestResultProtocolRepeatedRetries Ten retries give exactly eleven attempts; final success or exhausted error is preserved

Run go test -race -run '^TestResultProtocol' -timeout=30s . for focused reproduction. The full race suite and lint passed on the final sources. An isolated integration copy of master eaf4947 (including merged Ring PR #179) with these six PR files also passed go test -race ./.... This is correctness compatibility evidence, not a combined performance measurement.

To reproduce stage 1 separately, export 9721be66093a0e2889129e4751154d8388698b98 and 4d0f9d9, copy queue_performance_test.go from the latter into the former, and run the end-to-end benchmark families with the same 500ms/count=2/cpu=12 settings for three rounds. Stage 1 expected allocations are 17 → 11 (13 with callback); the final channel change is measured separately above.

Not run: production, external-broker, multi-producer, or long-duration soak tests; no representative service workload is available in scope.

Classification and AI authorship

  • Core change: shared scheduling, concurrency, and result lifecycle behavior; recommend the module owner plus another reviewer.
  • AI tool: OpenAI Codex.
  • AI-authored files: queue.go, options.go, scheduling_test.go, queue_performance_test.go, handle_result_test.go, handle_performance_test.go.
  • Human line-by-line reviewed: None recorded; automated reviews do not count as human review.
  • Goal, scope, and performance evidence are documented here. Plan/report/blog artifacts remain local and are not committed.
  • No credentials included or authentication/authorization interfaces changed.

Risk, rollback, and reviewer focus

Review Start/work/consume/start ownership, retirement, shutdown handoff, and routineGroup tracking of callbacks. Review both result receive paths in handle for error/panic equivalence. Keep the result channel buffered with capacity 1: handlers ignoring cancellation may complete after timeout. Do not reuse a result channel across tasks. Idle queues retain workers until retirement/release; callbacks may overlap. Blocking backends must still unblock Request during Shutdown.

The channel consolidation is a separate follow-up commit 43b2074d24e7bdf138a5870db818c0f970871d0d and can be reverted independently. To roll back the whole PR, revert its two commits in reverse order (or the resulting squash commit if squash-merged). No data/schema migration is involved.

Preserve timeout and callback independence while reducing per-task scheduling overhead. Add lifecycle regressions and end-to-end benchmarks.
- Return errors and panics through one buffered result channel
- Preserve late completion and shutdown behavior without changing retry timers
- Add result lifecycle regressions and focused allocation benchmarks
@appleboy appleboy changed the title perf(queue): reuse dispatch and worker goroutines perf(queue): reuse workers and consolidate task results Nov 1, 2026
@appleboy
appleboy merged commit f38971e into master Nov 1, 2026
6 checks passed
@appleboy
appleboy deleted the perf/queue-worker-reuse branch November 1, 2026 09:29
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