feat(spmc): add competing queues - #305
Merged
Merged
Conversation
tisonkun
reviewed
Sep 13, 2026
Comment on lines
+34
to
+37
| pub use crate::internal::competing_queue::RecvError; | ||
| pub use crate::internal::competing_queue::SendError; | ||
| pub use crate::internal::competing_queue::TryRecvError; | ||
| pub use crate::internal::competing_queue::TrySendError; |
Member
There was a problem hiding this comment.
I disagree this style just for "reusing code".
Duplicate the code a bit is helpful to decouple concepts for evolution.
At least we should never public an internal struct but have another place to hold it. Or else we don't do software engineering but putting code randomly.
mpmc::RecvError/SendError/TryRecvError/TrySendError and their spmc counterparts were pub re-exports of the exact same internal::competing_queue types, so the two modules' errors were literally interchangeable and an internal implementation detail leaked into the public API surface. Give mpmc and spmc their own duplicated public error types, converted from the internal errors at the endpoint boundary via From impls. The internal Shared<T> competing-consumer core stays reused between the two modules; only the public error surface is decoupled, per review feedback on PR apache#305. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
tisonkun
approved these changes
Sep 20, 2026
tisonkun
left a comment
Member
There was a problem hiding this comment.
Generally LGTM. But I think bounded and unbounded impls can use different queue for performance consideration.
Also I have to push some commits to apply the comments above about not reusing errors since they are logically different concept that happens to have similar/same structure. We should not reuse just because code looks the same.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Add opt-in bounded and unbounded
asyncband::spmcqueues for distributing work from one producer to competing consumers. Senders are non-cloneable and require&mut self; receivers are cloneable, and each consumed value is delivered to exactly one receiver.Fixes #212.
Design Notes
SPMC owns its queue implementation and its four public error types, following MPMC's single-lock coordination design. MPMC and SPMC have independent type identities and can evolve separately; there is no shared internal error API or error conversion layer. The queues reuse the existing private mutex and waiter-list primitives. SPMC tracks the presence of its unique sender with a boolean. The MPMC library implementation is unchanged relative to
mainat7a4e000. Benchmark adapters, batch constants, and runtime setup live in a sharedbenchmarks/channelsfixture; MPMC and SPMC retain separate workloads, and only the MPMC workload requires cloneable senders.Validation
cargo x test: 634 tests and doctests passed, including 20 explicit SPMC tests and five SPMC compile-fail checks.cargo x check: the feature matrix passed, including standalonempmcandspmc.cargo x lint: Clippy, rustfmt, Taplo, typos, HawkEye, and rustdoc passed.cargo x bench --no-run: both benchmark targets compiled; all 167 MPMC/SPMC benchmark cases passed in test mode after the fixture reorganization. The performance report below records three serial runs of all 64 SPMC comparison cases at687d2e0, with 100 samples per case.cargo +nightly miri test --package tests-integration --test spmc_test): 18 passed; the two OS-backed Tokio concurrency tests were ignored, as intended.Performance report
Measured on 2026-09-20 at
687d2e0with an Apple M4 Max (14 CPU cores), 36 GiB RAM, macOS 26.6.2, and Rust 1.99.0-nightly (3d6c19bb9,aarch64-apple-darwin), using the default optimized bench profile. Peers: async-channel 2.5.0 and flume 0.12.0; runtime: Tokio 1.53.1; harness: Divan 0.1.21.Each sample transfers 16,384
usizevalues from one producer to 1, 2, 4, or 8 competing consumers; bounded capacity is 64. The sender moves into one task without cloning or an added synchronization wrapper. Consumers drain until disconnection without fixed quotas, and each sample checks the total count and checksum. Runtime, channel, and task creation are outside the measured region; data operations, disconnection, and task completion are inside.The table reports the median of three run medians, in milliseconds per batch, from three serial runs of 100 samples per case with one batch per sample. No builds or tests ran alongside these measurements. Lower elapsed time is better.
currentdenotes Tokio current-thread;4denotes four worker threads. MPMC uses one producer. The peer ratio is SPMC time divided by the faster of async-channel and flume, calculated before rounding.The largest SPMC/faster-peer ratio is 1.00x on this host; none of the measured configurations reaches the 3x investigation or 10x rejection thresholds from #208. These measurements do not establish performance across platforms or workloads.
Unbounded sends are synchronous for every implementation, so the producer can fill the queue before consumers run on a current-thread executor. Those rows measure draining rather than parallel consumer contention; use the four-worker results and targeted-wakeup tests to assess 1P/8C behavior.
Reproduce by running
cargo x bench --no-run, then execute the ecosystem benchmark binary printed by Cargo with--bench --color never --sample-count 100 'spmc::'three times serially, without concurrent builds or tests. The default harness uses 20 samples with one batch per sample.