photon_ring/wait.rs
1// Copyright 2026 Photon Ring Contributors
2// SPDX-License-Identifier: MIT OR Apache-2.0
3
4//! Wait strategies for blocking receive operations.
5//!
6//! [`WaitStrategy`] controls how a consumer thread waits when no message is
7//! available. Choose based on your latency vs CPU usage requirements:
8//!
9//! | Strategy | Latency | CPU usage | Best for |
10//! |---|---|---|---|
11//! | `BusySpin` | Lowest (~0 ns wakeup) | 100% core | HFT, dedicated cores |
12//! | `YieldSpin` | Low (~1-5 us wakeup) | High | Shared cores, SMT |
13//! | `Park` | Medium (~10-50 us wakeup) | Near zero | Background consumers |
14//! | `Adaptive` | Auto-scaling | Varies | General purpose |
15
16/// Strategy for blocking `recv()` and `SubscriberGroup::recv()`.
17///
18/// Controls how the consumer thread waits when no message is available.
19/// Choose based on your latency vs CPU usage requirements:
20///
21/// | Strategy | Latency | CPU usage | Best for |
22/// |---|---|---|---|
23/// | `BusySpin` | Lowest (~0 ns wakeup) | 100% core | HFT, dedicated cores |
24/// | `YieldSpin` | Low (~1-5 us wakeup) | High | Shared cores, SMT |
25/// | `Park` | Medium (~10-50 us wakeup) | Near zero | Background consumers |
26/// | `Adaptive` | Auto-scaling | Varies | General purpose |
27#[derive(Debug, Clone, Copy, PartialEq, Eq)]
28pub enum WaitStrategy {
29 /// Pure busy-spin with no PAUSE instruction. Minimum wakeup latency
30 /// but consumes 100% of one CPU core. Use on dedicated, pinned cores.
31 BusySpin,
32
33 /// Spin with `thread::yield_now()` between iterations. Yields the
34 /// OS time slice to other threads on the same core. Good for SMT.
35 #[cfg(feature = "std")]
36 YieldSpin,
37
38 /// `thread::park()` / `unpark()` based waiting. Near-zero CPU usage
39 /// when idle but higher wakeup latency (~10-50 us depending on OS).
40 #[cfg(feature = "std")]
41 Park,
42
43 /// Three-phase escalation: busy-spin for `spin_iters` iterations,
44 /// then yield for `yield_iters`, then park. Balances latency and CPU.
45 ///
46 /// On `no_std` (without the `std` feature), the yield and park phases
47 /// fall back to `core::hint::spin_loop()`.
48 Adaptive {
49 /// Number of bare-spin iterations before escalating.
50 spin_iters: u32,
51 /// Number of yield iterations before parking (or PAUSE-spinning on `no_std`).
52 yield_iters: u32,
53 },
54}
55
56impl Default for WaitStrategy {
57 fn default() -> Self {
58 WaitStrategy::Adaptive {
59 spin_iters: 64,
60 yield_iters: 64,
61 }
62 }
63}
64
65impl WaitStrategy {
66 /// Execute one wait iteration. Called by `recv_with` on each loop when
67 /// `try_recv` returns `Empty`.
68 ///
69 /// `iter` is the zero-based iteration count since the last successful
70 /// receive — it drives the phase transitions in `Adaptive`.
71 #[inline]
72 pub(crate) fn wait(&self, iter: u32) {
73 match self {
74 WaitStrategy::BusySpin => {
75 // No hint, no yield — pure busy loop.
76 }
77 #[cfg(feature = "std")]
78 WaitStrategy::YieldSpin => {
79 std::thread::yield_now();
80 }
81 #[cfg(feature = "std")]
82 WaitStrategy::Park => {
83 std::thread::park();
84 }
85 WaitStrategy::Adaptive {
86 spin_iters,
87 yield_iters,
88 } => {
89 if iter < *spin_iters {
90 // Phase 1: bare spin
91 } else if iter < spin_iters + yield_iters {
92 // Phase 2: yield (or spin_loop on no_std)
93 #[cfg(feature = "std")]
94 {
95 std::thread::yield_now();
96 }
97 #[cfg(not(feature = "std"))]
98 {
99 core::hint::spin_loop();
100 }
101 } else {
102 // Phase 3: park (or spin_loop on no_std)
103 #[cfg(feature = "std")]
104 {
105 std::thread::park();
106 }
107 #[cfg(not(feature = "std"))]
108 {
109 core::hint::spin_loop();
110 }
111 }
112 }
113 }
114 }
115}
116
117#[cfg(test)]
118mod tests {
119 use super::*;
120
121 #[test]
122 fn default_is_adaptive() {
123 let ws = WaitStrategy::default();
124 assert_eq!(
125 ws,
126 WaitStrategy::Adaptive {
127 spin_iters: 64,
128 yield_iters: 64,
129 }
130 );
131 }
132
133 #[test]
134 fn busy_spin_returns_immediately() {
135 let ws = WaitStrategy::BusySpin;
136 // Should not block — just verify it completes.
137 for i in 0..1000 {
138 ws.wait(i);
139 }
140 }
141
142 #[cfg(feature = "std")]
143 #[test]
144 fn yield_spin_returns() {
145 let ws = WaitStrategy::YieldSpin;
146 for i in 0..100 {
147 ws.wait(i);
148 }
149 }
150
151 #[test]
152 fn adaptive_phases() {
153 let ws = WaitStrategy::Adaptive {
154 spin_iters: 4,
155 yield_iters: 4,
156 };
157 // Phase 1 (spin): iters 0..4
158 for i in 0..4 {
159 ws.wait(i);
160 }
161 // Phase 2 (yield): iters 4..8
162 for i in 4..8 {
163 ws.wait(i);
164 }
165 // Phase 3 (park/spin_loop): iter 8+
166 // On std this would park, but since no one unparks we only test
167 // non-park path here. The park path is tested via recv_with integration.
168 #[cfg(not(feature = "std"))]
169 {
170 ws.wait(8);
171 ws.wait(100);
172 }
173 }
174
175 #[test]
176 fn clone_and_copy() {
177 let ws = WaitStrategy::BusySpin;
178 let ws2 = ws;
179 #[allow(clippy::clone_on_copy)]
180 let ws3 = ws.clone();
181 assert_eq!(ws, ws2);
182 assert_eq!(ws, ws3);
183 }
184
185 #[test]
186 fn debug_format() {
187 use alloc::format;
188 let ws = WaitStrategy::BusySpin;
189 let s = format!("{ws:?}");
190 assert!(s.contains("BusySpin"));
191 }
192}