ph_eventing/seq_ring.rs
1//! Lock-free SPSC overwrite ring for high-rate telemetry in no-std contexts.
2//!
3//! # Overview
4//! - Single producer, single consumer.
5//! - Producer never blocks; new writes overwrite the oldest slots when the ring wraps.
6//! - Sequence numbers are monotonically increasing `u32`; `0` is reserved to mean "empty".
7//! - The consumer can drain in-order (`poll_one`/`poll_up_to`) or sample the newest value (`latest`).
8//! - If the consumer lags by more than `N`, it skips ahead and reports the number of dropped items.
9//! Two boundaries qualify that accounting: the sequence wrap can drop a few extra entries
10//! depending on `N`, and a gap of one whole sequence span aliases to "nothing new" and reports
11//! zero — see the two "Known limitation" sections below.
12//!
13//! # Memory ordering
14//! The producer invalidates the per-slot sequence, writes the value, publishes the new per-slot
15//! sequence, then publishes the newest sequence. The consumer validates the per-slot sequence
16//! before and after reading, which avoids observing a new value under an old sequence number when
17//! the producer overwrites a slot.
18//!
19//! The barriers on both sides are fences rather than ordered accesses on the sequence itself: a
20//! `Release` fence keeps the producer's invalidation ahead of its value write, and an `Acquire`
21//! fence keeps the consumer's copy ahead of its re-check. Plain `Release`/`Acquire` on the
22//! sequence stores and loads would leave the value access free to drift across the guard it is
23//! supposed to be bracketed by.
24//!
25//! Slot values are read and written with volatile accesses, and the consumer holds its copy as
26//! `MaybeUninit<T>` until the re-check passes. A copy that raced with an overwrite is therefore
27//! discarded as raw bytes and never materialises as a `T` that could violate the type's validity
28//! invariants — for reads that complete within one sequence span; the re-check compares sequence
29//! values, so it carries the counter-width ABA bound stated under "Known limitation: whole-span
30//! sequence aliasing" below.
31//!
32//! # Known deviation: the seqlock data race
33//!
34//! ## What it is
35//! This is a seqlock, and seqlocks are formally racy. The consumer may copy a slot while the
36//! producer overwrites it; the sequence re-check then discards the copy. Miri's data-race
37//! detector reports that copy as undefined behaviour, and it is right to: `read_volatile`
38//! constrains the compiler but does not make the access atomic.
39//!
40//! ## Why the design is this way
41//! It is a deliberate trade, not an oversight, and the alternatives were rejected for reasons
42//! worth stating plainly:
43//!
44//! - **Make the producer wait for the consumer.** This removes the race entirely, and removes the
45//! only property the type exists to provide. A telemetry producer in an interrupt handler cannot
46//! block on a consumer in a task loop.
47//! - **Copy the slot with atomic per-word operations.** Sound, and unavailable: the word count has
48//! to be computed from `size_of::<T>()`, which needs `generic_const_exprs` (unstable). Falling
49//! back to per-byte atomics does not work either — any `T` carrying padding has uninitialised
50//! bytes even after a typed write, and an atomic load of uninitialised memory is itself UB.
51//! - **Narrow the API so payloads live in atomics.** A ring restricted to, say, a `u32` or `u64`
52//! payload could store it in an `AtomicU32`/`AtomicU64` and would be **fully race-free**. This
53//! is a real option that was passed over in favour of accepting any `T: Copy`. So the honest
54//! framing is that generality was chosen over formal soundness — not that Rust makes soundness
55//! impossible here.
56//!
57//! ## What this actually costs you
58//! - **Nothing is known to miscompile.** Volatile seqlocks are used widely — the Linux kernel's
59//! `seqlock_t` is the same construct — and no compiler is known to break them. But "no known
60//! failure" is not a guarantee: the compiler is *permitted* to assume the race cannot happen.
61//! `read_volatile`/`write_volatile` block the optimisations that would plausibly exploit it
62//! (splitting, duplicating, hoisting the copy); nothing blocks the ones nobody has thought of.
63//! - **Loom shows it too, if you let it.** A model that asserts delivered payload *values*
64//! fails: Loom serialises the non-atomic slot memory (the copy returns the latest bytes)
65//! while C11 coherence lets both Relaxed sequence checks keep returning the stale
66//! pre-invalidation sequence — a non-atomic read establishes no happens-before to force the
67//! re-check forward. That is the formal gap of the abstract machine witnessed concretely; the
68//! fence pairing above is what closes it on real hardware, where observing the new value
69//! implies the earlier invalidation is visible to the fenced re-check. The shipped models
70//! therefore assert the sequence protocol and conservation, never payload values.
71//! - **Your own Miri runs will flag it.** If you run `cargo miri test` over a test that drives
72//! this ring from two threads, you will get a UB report pointing into this crate. That is the
73//! deviation, not a new bug. `scripts/miri.*` shows the split-pass approach: full checking
74//! everywhere else, race detector off for this ring alone.
75//! - **A raced copy is never returned** — within the span bound. The double sequence check
76//! discards it, and it is held as `MaybeUninit<T>` until validated, so it cannot even briefly
77//! exist as a `T` that violates the type's validity invariants. The check compares sequence
78//! values, so a read preempted for one whole span of publications can pass both checks against
79//! a rewritten slot; see "Known limitation: whole-span sequence aliasing".
80//!
81//! ## If that is not acceptable
82//! - [`crate::EventBuf`] is race-free by construction — its producer and consumer never touch the
83//! same slot, and it passes Miri with the detector on. Note it is **not a drop-in**: it applies
84//! backpressure instead of overwriting, so a full buffer rejects the push rather than dropping
85//! the oldest entry. That is a different contract, and the right one only if your producer can
86//! handle failure.
87//! - If you need overwrite semantics *and* a clean Miri run, keep the payload out of the ring:
88//! push a small index or handle into [`crate::EventBuf`], or into this ring accepting the
89//! caveat, and own the data elsewhere.
90//! - Keeping `T` small and padding-free does not remove the formal race, but it does remove any
91//! realistic tearing: a word-sized payload is copied by a single instruction on every target
92//! this crate supports.
93//!
94//! # Known limitation: extra drops at the sequence wrap
95//!
96//! Everywhere else these docs say the consumer keeps the last `N` entries and only loses data once
97//! it lags by more than `N`. That holds for all but one moment in the ring's life: the point where
98//! the sequence counter wraps, once every `2^32 - 1` pushes.
99//!
100//! Slots are addressed by `(seq - 1) % N`, but `push` skips the reserved value `0`, so a full
101//! cycle is `2^32 - 1` sequences rather than `2^32`. Unless `N` divides `2^32 - 1`, the slot walk
102//! does not line up across the wrap: the index jumps instead of advancing by one, and for a window
103//! straddling the wrap two live sequences can share a slot. The older of the two is overwritten
104//! before the consumer had its full `N` entries of slack.
105//!
106//! How much is lost depends entirely on `N`:
107//!
108//! | `N` | Entries lost, once per wrap |
109//! |-----|-----------------------------|
110//! | A power of two | Exactly 1 |
111//! | A divisor of `2^32 - 1` (3, 5, 15, 17, 51, 85, 255, 257, 65537, …) | 0 — the walk is seamless |
112//! | Anything else | Up to `N - 1`; e.g. `N = 48` loses 15, `N = 96` loses 33, `N = 121` loses 58 |
113//!
114//! **This is a data-loss bound, not a soundness problem.** The affected read fails its sequence
115//! check and is counted in [`PollStats::dropped`], so `read + dropped` still accounts for every
116//! published item and no stale or torn value is returned — both within the span bound of the
117//! "Known limitation: whole-span sequence aliasing" section below, which is where each of those
118//! guarantees runs out. It is indistinguishable from the ordinary lag-induced drops the consumer
119//! already reports.
120//!
121//! The same misalignment makes the lag-recovery jump resume up to one sequence later than it
122//! strictly needs to. That is bounded by the table above and reported identically.
123//!
124//! Practical advice: **prefer a power of two for `N`** — the cost is one lost entry per `2^32`
125//! pushes, which is beneath the noise floor for any workload that also tolerates overwrite. Pick a
126//! divisor of `2^32 - 1` if you want the wrap to be exactly seamless. Avoid values like 96 or 121
127//! if a burst of drops at a predictable interval would matter to you. If no loss is acceptable at
128//! all, [`crate::EventBuf`] applies backpressure instead and has no wrap boundary of this kind.
129//!
130//! # Known limitation: whole-span sequence aliasing
131//!
132//! Sequence arithmetic is modular. `push` skips the reserved value `0`, so the counter cycles
133//! through `2^32 - 1` distinct nonzero values, and every comparison and distance the consumer
134//! computes is exact only up to that span. Two consequences follow — both inherent to any
135//! fixed-width seqlock at its counter width:
136//!
137//! - **A whole-span gap from the resume cursor reports nothing.** If the distance from the
138//! consumer's resume cursor to the newest publication reaches exactly `2^32 - 1` (or any whole
139//! multiple), the published sequence aliases the cursor and `poll_one`/`poll_up_to` take their
140//! nothing-new early return: zero reads and zero drops. Larger distances report only the
141//! remainder modulo the span. The `read + dropped` conservation promise is therefore exact
142//! while the resume cursor stays within one span of the newest publication — residual backlog
143//! from a partial drain counts against that distance, so this is *not* simply "fewer than one
144//! span of publications between calls" (the sufficient call-cadence bound is below) — and
145//! silence after an extreme stall is not evidence that nothing was lost.
146//! - **A whole-span mid-read stall defeats the sequence re-check.** The torn-copy guard compares
147//! the slot's sequence before and after the copy. A consumer preempted *inside* that copy for
148//! exactly one whole span of publications sees the same sequence value on both sides of a slot
149//! that was rewritten in between — counter-width ABA — and a mixed copy would be accepted as
150//! `T`. The discard argument for the documented deviation is therefore bounded: it holds for
151//! any read that completes in less than one full span of producer publications.
152//!
153//! Reachability arithmetic, so the bound is a decision rather than a surprise: one span is
154//! ~4.29 billion publications. At a sustained 1 MHz push rate a poll gap must exceed ~71.6
155//! minutes — and the mid-read stall must hold the consumer *between two instructions of one
156//! copy* for that long — before either case is reachable; at 10 kHz it is ~5 days. The escape
157//! hatch is structural, and the bound is measured from the **resume cursor**, not from call
158//! cadence: aliasing needs the distance from the resume cursor to the newest publication to
159//! reach one whole span, and a partial drain leaves residual backlog that counts against it. A
160//! nonzero ordered poll (`poll_one`, or `poll_up_to` with a nonzero budget) always leaves the
161//! cursor at most `N - 1` behind the newest publication it observed at entry (each call freezes
162//! that entry sample as its drain goal, which is also what bounds the call) — the lag-recovery jump
163//! handles a lag over `N`, and draining even one item brings a lag of at most `N` below that —
164//! so keeping the publications between consecutive nonzero polls below one span *minus*
165//! `N - 1` suffices. [`Consumer::skip_to_latest`] leaves the cursor exactly **one** behind the
166//! newest it observed (so the next poll yields that newest item); its post-call allowance is
167//! therefore one span minus one, not a full span.
168//! `poll_up_to(0, …)` returns before touching the resume cursor, and the non-advancing
169//! [`Consumer::latest`] never moves it. Separately, bound consumer preemption during a single
170//! read to less than a span of publications. If neither bound can be stated for your system,
171//! [`crate::EventBuf`] has no sequence wrap of any kind.
172//!
173//! # Notes
174//! - `T` is `Copy` to allow returning values by copy without allocation.
175//! - The `&T` passed to hooks is a reference to a local copy made during the read.
176//! - Sequence arithmetic goes through `seq_distance`, which accounts for the reserved value `0`
177//! that `push` skips on wrap; raw wrapping subtraction over-counts by one across that boundary.
178
179use crate::sync::{AtomicBool, AtomicU32, Ordering, fence};
180// Slots stay on `core`'s cell rather than the Loom-tracked one. The seqlock's
181// slot access is racy by construction (see "Known deviation" above), so a
182// tracked cell would only re-report a documented deviation and mask everything
183// else Loom has to say. The sequence protocol — which is what the correctness
184// argument actually rests on — is built from the atomics above, and Loom
185// models that in full.
186use core::cell::{Cell, UnsafeCell};
187use core::marker::PhantomData;
188use core::mem::MaybeUninit;
189#[cfg(test)]
190use core::sync::atomic::AtomicUsize;
191
192// Helpers are `const fn` on the host path so `SeqRing::new` can be const.
193// Loom's atomics are not const-constructible, so the Loom build keeps the
194// non-const variants used by the non-const `new` below.
195//
196// Prefer `[const { … }; N]` over `array::from_fn`: the latter is not
197// const-callable with these constructors on the MSRV toolchain.
198#[cfg(not(loom))]
199const fn atomic_u32_array<const N: usize>() -> [AtomicU32; N] {
200 [const { AtomicU32::new(0) }; N]
201}
202
203#[cfg(loom)]
204fn atomic_u32_array<const N: usize>() -> [AtomicU32; N] {
205 core::array::from_fn(|_| AtomicU32::new(0))
206}
207
208#[cfg(not(loom))]
209const fn unsafe_cell_array<T, const N: usize>() -> [UnsafeCell<MaybeUninit<T>>; N] {
210 [const { UnsafeCell::new(MaybeUninit::uninit()) }; N]
211}
212
213#[cfg(loom)]
214fn unsafe_cell_array<T, const N: usize>() -> [UnsafeCell<MaybeUninit<T>>; N] {
215 core::array::from_fn(|_| UnsafeCell::new(MaybeUninit::uninit()))
216}
217
218// Test-only hook state. These use `core` atomics directly rather than the
219// `crate::sync` shim: Loom's atomics are not const-constructible, and this
220// hook is scaffolding for a single-threaded test rather than part of the
221// protocol Loom models.
222#[cfg(test)]
223static TEST_AFTER_READ_TARGET: AtomicUsize = AtomicUsize::new(0);
224#[cfg(test)]
225static TEST_AFTER_READ_SEQ: core::sync::atomic::AtomicU32 = core::sync::atomic::AtomicU32::new(0);
226
227/// Outcome of a [`Consumer::poll_up_to`] or [`Consumer::poll_one`] call.
228///
229/// `read + dropped` accounts for every sequence the consumer advanced past, so
230/// the pair can be used to detect a lagging consumer without a separate probe.
231#[must_use]
232#[derive(Copy, Clone, Debug)]
233pub struct PollStats {
234 /// Number of items delivered to the hook.
235 pub read: usize,
236 /// Number of items skipped because the consumer lagged or slots were overwritten.
237 pub dropped: usize,
238 /// Newest sequence sampled at poll entry — the frozen drain goal for
239 /// that call (later publications wait for the next poll).
240 pub newest: u32,
241}
242
243/// Overwrite ring for SPSC high-rate telemetry.
244/// Producer never waits; consumer may drop if it lags > N.
245pub struct SeqRing<T: Copy, const N: usize> {
246 next_seq: AtomicU32,
247 published_seq: AtomicU32,
248 slot_seq: [AtomicU32; N],
249 slots: [UnsafeCell<MaybeUninit<T>>; N],
250 producer_taken: AtomicBool,
251 consumer_taken: AtomicBool,
252}
253
254// SAFETY: SeqRing is Sync because the producer/consumer handles enforce SPSC usage,
255// and all shared state is accessed via atomics. Values are written before their
256// sequence numbers are published with Release and read with Acquire. T: Send ensures
257// values can be transferred across threads safely.
258unsafe impl<T: Copy + Send, const N: usize> Sync for SeqRing<T, N> {}
259
260impl<T: Copy, const N: usize> SeqRing<T, N> {
261 /// Create a new ring buffer.
262 ///
263 /// On the normal (non-Loom) build this is a `const fn`, so the ring can be
264 /// placed in a `static`: `static RING: SeqRing<u32, 64> = SeqRing::new();`.
265 /// Under `--cfg loom` it is deliberately non-const — Loom's atomics are
266 /// not const-constructible.
267 ///
268 /// # Capacity `0` is a build failure
269 /// The `N > 0` check is a *const* assertion, so a zero-capacity buffer
270 /// cannot be constructed at all -- there is no runtime panic left to
271 /// catch, and therefore no way to write the negative case as a `#[test]`.
272 /// This `compile_fail` doctest is that coverage, and pinning the error code
273 /// keeps it honest: without it the test would also pass on a typo.
274 ///
275 /// ```compile_fail,E0080
276 /// let _ = ph_eventing::SeqRing::<u32, 0>::new();
277 /// ```
278 ///
279 /// # Panics
280 /// Does not panic on the host path. Under Loom, where `new` is non-const,
281 /// `N == 0` is a runtime assertion instead.
282 #[cfg(not(loom))]
283 pub const fn new() -> Self {
284 const {
285 assert!(N > 0, "SeqRing capacity N must be > 0");
286 }
287 Self {
288 next_seq: AtomicU32::new(0),
289 published_seq: AtomicU32::new(0),
290 slot_seq: atomic_u32_array::<N>(),
291 slots: unsafe_cell_array::<T, N>(),
292 producer_taken: AtomicBool::new(false),
293 consumer_taken: AtomicBool::new(false),
294 }
295 }
296
297 /// Create a new ring buffer (Loom build — non-const).
298 ///
299 /// # Panics
300 /// Panics if `N == 0`.
301 #[cfg(loom)]
302 pub fn new() -> Self {
303 assert!(N > 0, "SeqRing capacity N must be > 0");
304 Self {
305 next_seq: AtomicU32::new(0),
306 published_seq: AtomicU32::new(0),
307 slot_seq: atomic_u32_array::<N>(),
308 slots: unsafe_cell_array::<T, N>(),
309 producer_taken: AtomicBool::new(false),
310 consumer_taken: AtomicBool::new(false),
311 }
312 }
313
314 /// Maximum number of items the ring can hold.
315 #[inline]
316 pub const fn capacity(&self) -> usize {
317 N
318 }
319
320 #[inline(always)]
321 const fn idx_for(seq: u32) -> usize {
322 ((seq.wrapping_sub(1)) as usize) % N
323 }
324
325 /// Try to create the producer handle.
326 ///
327 /// Returns `None` if a producer is already active — never panics. On the
328 /// targets this crate exists for a panic is a reset, so fallible bring-up
329 /// is the only handle-acquisition API. (The panicking `producer()` was
330 /// deprecated in 0.2.0 and removed in 0.3.0.)
331 #[inline]
332 pub fn try_producer(&self) -> Option<Producer<'_, T, N>> {
333 if self.producer_taken.swap(true, Ordering::AcqRel) {
334 None
335 } else {
336 Some(Producer {
337 ring: self,
338 _not_sync: PhantomData,
339 })
340 }
341 }
342
343 /// Try to create the consumer handle.
344 ///
345 /// Returns `None` if a consumer is already active — never panics. On the
346 /// targets this crate exists for a panic is a reset, so fallible bring-up
347 /// is the only handle-acquisition API. (The panicking `consumer()` was
348 /// deprecated in 0.2.0 and removed in 0.3.0.)
349 #[inline]
350 pub fn try_consumer(&self) -> Option<Consumer<'_, T, N>> {
351 if self.consumer_taken.swap(true, Ordering::AcqRel) {
352 None
353 } else {
354 Some(Consumer {
355 ring: self,
356 last_seq: 0,
357 dropped_accum: 0,
358 _not_sync: PhantomData,
359 })
360 }
361 }
362
363 #[inline]
364 fn newest_seq(&self) -> u32 {
365 self.published_seq.load(Ordering::Acquire)
366 }
367
368 #[inline]
369 fn push_inner(&self, value: T) -> u32 {
370 let mut seq = self
371 .next_seq
372 .fetch_add(1, Ordering::Relaxed)
373 .wrapping_add(1);
374 if seq == 0 {
375 seq = 1;
376 self.next_seq.store(1, Ordering::Relaxed);
377 }
378
379 let idx = Self::idx_for(seq);
380 // Invalidate before writing so a concurrent reader of the previous
381 // sequence cannot observe the new value under the old sequence number.
382 // The Release fence keeps the invalidation ahead of the value write.
383 self.slot_seq[idx].store(0, Ordering::Relaxed);
384 fence(Ordering::Release);
385
386 // SAFETY: the producer is the only writer, and `idx` is in bounds
387 // because `idx_for` reduces modulo N. The write is volatile to match
388 // the volatile read in `read_seq_inner`: a consumer may be copying
389 // this slot concurrently, so the compiler must not split, duplicate,
390 // or move the store.
391 unsafe { core::ptr::write_volatile(self.slots[idx].get(), MaybeUninit::new(value)) };
392
393 self.slot_seq[idx].store(seq, Ordering::Release);
394 self.published_seq.store(seq, Ordering::Release);
395 seq
396 }
397
398 /// Advance past the reserved empty sequence `0`.
399 #[inline(always)]
400 const fn next_after(seq: u32) -> u32 {
401 match seq.wrapping_add(1) {
402 0 => 1,
403 n => n,
404 }
405 }
406
407 /// How many sequence numbers `push` actually assigned in `(from, to]`.
408 ///
409 /// Plain wrapping subtraction over-counts by one whenever the span crosses
410 /// the reserved value `0`, because `push` skips it. The span crosses `0`
411 /// exactly when `to` compares below `from`, since that is the only way the
412 /// walk from `from` up to `to` can pass through the wrap point.
413 #[inline(always)]
414 const fn seq_distance(from: u32, to: u32) -> u32 {
415 let raw = to.wrapping_sub(from);
416 if to < from { raw - 1 } else { raw }
417 }
418
419 #[inline]
420 fn read_seq_inner(&self, seq: u32) -> Option<T> {
421 let idx = Self::idx_for(seq);
422
423 let s1 = self.slot_seq[idx].load(Ordering::Acquire);
424 if s1 != seq {
425 return None;
426 }
427
428 // Copy the slot as raw bytes. The producer may be overwriting it right
429 // now, so the bytes are not trusted until the sequence re-check below
430 // passes — holding the copy as `MaybeUninit<T>` means a torn read
431 // cannot produce an invalid `T`, only bytes that are then discarded.
432 //
433 // SAFETY: `idx` is in bounds because `idx_for` reduces modulo N. The
434 // read is volatile so the compiler cannot split, duplicate, or hoist
435 // it, and `MaybeUninit<T>` has no validity invariant to violate.
436 let v: MaybeUninit<T> = unsafe { core::ptr::read_volatile(self.slots[idx].get()) };
437
438 #[cfg(test)]
439 self.test_after_read_hook(idx);
440
441 // Pin the copy above the re-check. An Acquire fence orders preceding
442 // loads ahead of what follows; a plain Acquire load on `s2` would only
443 // stop *later* accesses from moving up, which would let the copy sink
444 // past the check that is supposed to validate it.
445 fence(Ordering::Acquire);
446
447 let s2 = self.slot_seq[idx].load(Ordering::Relaxed);
448 if s2 != seq {
449 return None;
450 }
451
452 // SAFETY: the slot sequence matched `seq` both before and after the
453 // copy, and the producer invalidates the sequence before it touches a
454 // slot, so no write overlapped the read and the bytes are a complete,
455 // initialised `T`.
456 Some(unsafe { v.assume_init() })
457 }
458
459 #[cfg(test)]
460 fn test_after_read_hook(&self, idx: usize) {
461 let target = TEST_AFTER_READ_TARGET.load(Ordering::Acquire);
462 if target == self as *const _ as usize {
463 let seq = TEST_AFTER_READ_SEQ.load(Ordering::Relaxed);
464 self.slot_seq[idx].store(seq, Ordering::Release);
465 TEST_AFTER_READ_TARGET.store(0, Ordering::Release);
466 }
467 }
468}
469
470impl<T: Copy, const N: usize> Default for SeqRing<T, N> {
471 fn default() -> Self {
472 Self::new()
473 }
474}
475
476impl<T: Copy, const N: usize> core::fmt::Debug for SeqRing<T, N> {
477 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
478 f.debug_struct("SeqRing")
479 .field("capacity", &N)
480 .field("published_seq", &self.published_seq.load(Ordering::Relaxed))
481 .finish()
482 }
483}
484
485/// Producer handle for writing into the ring.
486///
487/// This handle is `!Sync` to prevent concurrent producers.
488pub struct Producer<'a, T: Copy, const N: usize> {
489 ring: &'a SeqRing<T, N>,
490 _not_sync: PhantomData<Cell<()>>,
491}
492
493impl<'a, T: Copy, const N: usize> Producer<'a, T, N> {
494 /// Write a value into the ring.
495 ///
496 /// Returns the sequence number assigned to the write (never 0).
497 #[inline]
498 pub fn push(&self, value: T) -> u32 {
499 self.ring.push_inner(value)
500 }
501}
502
503impl<'a, T: Copy, const N: usize> Drop for Producer<'a, T, N> {
504 fn drop(&mut self) {
505 self.ring.producer_taken.store(false, Ordering::Release);
506 }
507}
508
509impl<T: Copy, const N: usize> core::fmt::Debug for Producer<'_, T, N> {
510 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
511 f.debug_struct("seq_ring::Producer")
512 .field("capacity", &N)
513 .finish()
514 }
515}
516
517/// Consumer handle for reading from the ring.
518///
519/// This handle is `!Sync` to prevent concurrent consumers.
520pub struct Consumer<'a, T: Copy, const N: usize> {
521 ring: &'a SeqRing<T, N>,
522 last_seq: u32,
523 dropped_accum: usize,
524 _not_sync: PhantomData<Cell<()>>,
525}
526
527impl<'a, T: Copy, const N: usize> Consumer<'a, T, N> {
528 /// How many items have been dropped since consumer creation (or since reset).
529 ///
530 /// The counter saturates at [`usize::MAX`] rather than wrapping, so on a
531 /// 32-bit target a very long-lived lagging consumer reports "at least this
532 /// many" instead of overflowing. Call [`reset_dropped`](Self::reset_dropped)
533 /// periodically if exact long-run totals matter.
534 #[inline]
535 pub fn dropped(&self) -> usize {
536 self.dropped_accum
537 }
538
539 /// Reset the internal drop counter.
540 #[inline]
541 pub fn reset_dropped(&mut self) {
542 self.dropped_accum = 0;
543 }
544
545 /// Drain at most one item (in-order). Bounded per call — this is
546 /// [`poll_up_to`](Self::poll_up_to)`(1, …)` and inherits its frozen
547 /// entry-sample window.
548 /// Returns true if an item was delivered to the hook.
549 #[inline]
550 pub fn poll_one(&mut self, hook: impl FnOnce(u32, &T)) -> bool {
551 let mut hook = Some(hook);
552 let stats = self.poll_up_to(1, |seq, v| {
553 if let Some(hook) = hook.take() {
554 hook(seq, v);
555 }
556 });
557 stats.read == 1
558 }
559
560 /// Drain at most one item (in-order), returning `(seq, value)`.
561 ///
562 /// Equivalent to [`poll_one`](Self::poll_one) without a hook. Drop
563 /// accounting and the `read + dropped` invariant are unchanged.
564 #[inline]
565 pub fn poll_one_value(&mut self) -> Option<(u32, T)> {
566 let mut result = None;
567 self.poll_one(|seq, v| result = Some((seq, *v)));
568 result
569 }
570
571 /// Drain up to `max` items (in-order) from the window that existed when
572 /// the call began.
573 /// Hook sees `&T` but it is a reference to a **local copy** inside poll.
574 ///
575 /// The newest published sequence is sampled **once at entry** and the
576 /// drain stops there: items the producer publishes while the poll runs
577 /// wait for the next call, and nothing is lost or double-counted by the
578 /// hand-off. Freezing the goal is what makes every call bounded — at
579 /// most one lag-recovery jump plus a walk of at most `N` slots plus
580 /// `max` reads, regardless of how fast the producer publishes. (The
581 /// previous formulation re-read the newest sequence every iteration, so
582 /// a producer that stayed ahead could starve the poll indefinitely.)
583 ///
584 /// If `max == 0`, this returns immediately with `read = 0`, `dropped = 0`, and
585 /// `newest` set to the latest published sequence. Otherwise
586 /// [`PollStats::newest`] reports the entry sample the drain ran against.
587 pub fn poll_up_to(&mut self, max: usize, mut hook: impl FnMut(u32, &T)) -> PollStats {
588 if max == 0 {
589 return PollStats {
590 read: 0,
591 dropped: 0,
592 newest: self.ring.newest_seq(),
593 };
594 }
595
596 // The frozen high-water mark: the drain goal for this entire call.
597 let newest = self.ring.newest_seq();
598 if newest == 0 || newest == self.last_seq {
599 return PollStats {
600 read: 0,
601 dropped: 0,
602 newest,
603 };
604 }
605
606 let mut read = 0usize;
607 let mut dropped = 0usize;
608
609 // At most one lag-recovery jump per call, computed against the frozen
610 // mark: the cursor only moves toward it below, so the distance never
611 // grows again within this call.
612 let lag = SeqRing::<T, N>::seq_distance(self.last_seq, newest) as usize;
613 if lag > N {
614 let keep_from = newest.wrapping_sub((N - 1) as u32);
615 let resume_after = keep_from.wrapping_sub(1);
616 // Everything in (last_seq, keep_from) is gone; count what was
617 // really assigned rather than the raw sequence span.
618 let jumped = SeqRing::<T, N>::seq_distance(self.last_seq, resume_after) as usize;
619 dropped = dropped.saturating_add(jumped);
620 self.last_seq = resume_after;
621 }
622
623 // Bounded by construction: after the jump at most `N` sequences lie
624 // between the cursor and the frozen mark, and every iteration —
625 // hit or miss — advances the cursor by exactly one toward it. A miss
626 // means the producer overwrote that slot after the entry sample; the
627 // item is genuinely gone and is counted as dropped.
628 while read < max && self.last_seq != newest {
629 let next = SeqRing::<T, N>::next_after(self.last_seq);
630
631 match self.ring.read_seq_inner(next) {
632 Some(v) => {
633 hook(next, &v);
634 self.last_seq = next;
635 read += 1;
636 }
637 None => {
638 self.last_seq = next;
639 dropped = dropped.saturating_add(1);
640 }
641 }
642 }
643
644 // Saturate rather than wrap. `usize` is 32 bits on every target this
645 // crate ships to, and the sequence space is also 32 bits, so a
646 // long-running consumer that lags can genuinely reach the top of the
647 // range. Overflow here would panic in debug and silently wrap in
648 // release — on an embedded target, in a hot path.
649 self.dropped_accum = self.dropped_accum.saturating_add(dropped);
650
651 PollStats {
652 read,
653 dropped,
654 newest,
655 }
656 }
657
658 /// "Give me the newest thing right now" (not in-order).
659 /// Returns true if it delivered something.
660 ///
661 /// This does not advance the consumer cursor.
662 #[inline]
663 pub fn latest(&self, hook: impl FnOnce(u32, &T)) -> bool {
664 let newest = self.ring.newest_seq();
665 if newest == 0 {
666 return false;
667 }
668 if let Some(v) = self.ring.read_seq_inner(newest) {
669 hook(newest, &v);
670 true
671 } else {
672 false
673 }
674 }
675
676 /// Read the newest item without a hook, returning `(seq, value)`.
677 ///
678 /// Equivalent to [`latest`](Self::latest). Does not advance the consumer
679 /// cursor.
680 #[inline]
681 pub fn latest_value(&self) -> Option<(u32, T)> {
682 let mut result = None;
683 self.latest(|seq, v| result = Some((seq, *v)));
684 result
685 }
686
687 /// Fast-forward consumer so the *next* `poll_one()` yields the newest item
688 /// (i.e. skip backlog).
689 ///
690 /// This does not modify the dropped counter.
691 #[inline]
692 pub fn skip_to_latest(&mut self) {
693 let newest = self.ring.newest_seq();
694 if newest != 0 {
695 self.last_seq = newest.wrapping_sub(1);
696 }
697 }
698}
699
700impl<'a, T: Copy, const N: usize> Drop for Consumer<'a, T, N> {
701 fn drop(&mut self) {
702 self.ring.consumer_taken.store(false, Ordering::Release);
703 }
704}
705
706impl<T: Copy, const N: usize> core::fmt::Debug for Consumer<'_, T, N> {
707 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
708 f.debug_struct("seq_ring::Consumer")
709 .field("capacity", &N)
710 .field("last_seq", &self.last_seq)
711 .field("dropped", &self.dropped_accum)
712 .finish()
713 }
714}
715
716impl<T: Copy, const N: usize> crate::traits::Sink<T> for Producer<'_, T, N> {
717 type Error = core::convert::Infallible;
718
719 #[inline]
720 fn try_push(&mut self, val: T) -> Result<(), core::convert::Infallible> {
721 self.push(val);
722 Ok(())
723 }
724}
725
726impl<T: Copy, const N: usize> crate::traits::Source<T> for Consumer<'_, T, N> {
727 #[inline]
728 fn try_pop(&mut self) -> Option<T> {
729 self.poll_one_value().map(|(_, v)| v)
730 }
731}
732
733#[cfg(test)]
734mod tests {
735 use super::{SeqRing, TEST_AFTER_READ_SEQ, TEST_AFTER_READ_TARGET};
736 use core::sync::atomic::Ordering;
737 use std::vec::Vec;
738
739 #[test]
740 fn poll_one_empty_returns_false() {
741 let ring = SeqRing::<u32, 4>::new();
742 let mut consumer = ring.try_consumer().unwrap();
743 let ok = consumer.poll_one(|_, _| {});
744 assert!(!ok);
745 }
746
747 #[test]
748 fn polls_in_order() {
749 let ring = SeqRing::<u32, 8>::new();
750 let producer = ring.try_producer().unwrap();
751 let mut consumer = ring.try_consumer().unwrap();
752
753 producer.push(10);
754 producer.push(11);
755 producer.push(12);
756
757 let mut seen = Vec::new();
758 let stats = consumer.poll_up_to(10, |seq, v| seen.push((seq, *v)));
759
760 assert_eq!(stats.read, 3);
761 assert_eq!(stats.dropped, 0);
762 assert_eq!(stats.newest, 3);
763 assert_eq!(&seen[..], &[(1, 10), (2, 11), (3, 12)]);
764 }
765
766 #[test]
767 fn drops_when_consumer_lags() {
768 let ring = SeqRing::<u32, 4>::new();
769 let producer = ring.try_producer().unwrap();
770 let mut consumer = ring.try_consumer().unwrap();
771
772 for i in 0..10 {
773 producer.push(i);
774 }
775
776 let mut seen = Vec::new();
777 let stats = consumer.poll_up_to(10, |seq, v| seen.push((seq, *v)));
778
779 assert_eq!(stats.read, 4);
780 assert_eq!(stats.dropped, 6);
781 assert_eq!(stats.newest, 10);
782 assert_eq!(&seen[..], &[(7, 6), (8, 7), (9, 8), (10, 9)]);
783 }
784
785 #[test]
786 fn latest_reads_newest() {
787 let ring = SeqRing::<u32, 8>::new();
788 let producer = ring.try_producer().unwrap();
789 let consumer = ring.try_consumer().unwrap();
790
791 producer.push(1);
792 producer.push(2);
793
794 let mut got = None;
795 let ok = consumer.latest(|seq, v| got = Some((seq, *v)));
796
797 assert!(ok);
798 assert_eq!(got, Some((2, 2)));
799 }
800
801 #[test]
802 fn skip_to_latest_makes_next_poll_latest() {
803 let ring = SeqRing::<u32, 8>::new();
804 let producer = ring.try_producer().unwrap();
805 let mut consumer = ring.try_consumer().unwrap();
806
807 producer.push(10);
808 producer.push(11);
809 producer.push(12);
810
811 consumer.skip_to_latest();
812
813 let mut got = None;
814 let ok = consumer.poll_one(|seq, v| got = Some((seq, *v)));
815
816 assert!(ok);
817 assert_eq!(got, Some((3, 12)));
818 }
819
820 #[test]
821 fn poll_up_to_zero_returns_newest_only() {
822 let ring = SeqRing::<u32, 4>::new();
823 let producer = ring.try_producer().unwrap();
824 let mut consumer = ring.try_consumer().unwrap();
825
826 producer.push(42);
827
828 let stats = consumer.poll_up_to(0, |_, _| panic!("hook should not run"));
829
830 assert_eq!(stats.read, 0);
831 assert_eq!(stats.dropped, 0);
832 assert_eq!(stats.newest, 1);
833 }
834
835 #[test]
836 fn dropped_counter_can_reset() {
837 let ring = SeqRing::<u32, 2>::new();
838 let producer = ring.try_producer().unwrap();
839 let mut consumer = ring.try_consumer().unwrap();
840
841 for i in 0..5 {
842 producer.push(i);
843 }
844
845 let stats = consumer.poll_up_to(10, |_, _| {});
846
847 assert_eq!(consumer.dropped(), stats.dropped);
848
849 consumer.reset_dropped();
850
851 assert_eq!(consumer.dropped(), 0);
852 }
853
854 #[test]
855 fn latest_empty_returns_false() {
856 let ring = SeqRing::<u32, 4>::new();
857 let consumer = ring.try_consumer().unwrap();
858
859 let ok = consumer.latest(|_, _| {});
860
861 assert!(!ok);
862 }
863
864 #[test]
865 fn latest_returns_false_when_slot_missing() {
866 let ring = SeqRing::<u32, 4>::new();
867 let consumer = ring.try_consumer().unwrap();
868
869 ring.published_seq.store(1, Ordering::Release);
870
871 let ok = consumer.latest(|_, _| {});
872
873 assert!(!ok);
874 }
875
876 #[test]
877 fn poll_up_to_counts_dropped_when_slot_missing() {
878 let ring = SeqRing::<u32, 4>::new();
879 let mut consumer = ring.try_consumer().unwrap();
880
881 ring.published_seq.store(1, Ordering::Release);
882
883 let stats = consumer.poll_up_to(1, |_, _| panic!("hook should not run"));
884
885 assert_eq!(stats.read, 0);
886 assert_eq!(stats.dropped, 1);
887 assert_eq!(consumer.dropped(), 1);
888 }
889
890 #[test]
891 fn read_seq_inner_detects_overwrite_during_read() {
892 let ring = SeqRing::<u32, 4>::new();
893 let producer = ring.try_producer().unwrap();
894 let seq = producer.push(7);
895
896 TEST_AFTER_READ_SEQ.store(seq.wrapping_add(1), Ordering::Relaxed);
897 TEST_AFTER_READ_TARGET.store(&ring as *const _ as usize, Ordering::Release);
898
899 let got = ring.read_seq_inner(seq);
900
901 TEST_AFTER_READ_TARGET.store(0, Ordering::Release);
902
903 assert!(got.is_none());
904 }
905
906 #[test]
907 fn push_wraps_seq_from_zero_to_one() {
908 let ring = SeqRing::<u32, 4>::new();
909
910 ring.next_seq.store(u32::MAX, Ordering::Relaxed);
911
912 let seq = ring.try_producer().unwrap().push(1);
913
914 assert_eq!(seq, 1);
915 assert_eq!(ring.next_seq.load(Ordering::Relaxed), 1);
916 }
917
918 #[test]
919 fn read_seq_inner_rejects_invalidated_slot() {
920 let ring = SeqRing::<u32, 4>::new();
921 let producer = ring.try_producer().unwrap();
922 let seq = producer.push(7);
923
924 ring.slot_seq[SeqRing::<u32, 4>::idx_for(seq)].store(0, Ordering::Release);
925
926 assert!(ring.read_seq_inner(seq).is_none());
927 }
928
929 #[test]
930 fn consumer_skips_reserved_seq_zero_on_wrap() {
931 let ring = SeqRing::<u32, 4>::new();
932 let producer = ring.try_producer().unwrap();
933 let mut consumer = ring.try_consumer().unwrap();
934
935 ring.next_seq.store(u32::MAX - 1, Ordering::Relaxed);
936 assert_eq!(producer.push(10), u32::MAX);
937
938 consumer.skip_to_latest();
939 let mut got = None;
940 assert!(consumer.poll_one(|s, v| got = Some((s, *v))));
941 assert_eq!(got, Some((u32::MAX, 10)));
942
943 assert_eq!(producer.push(20), 1);
944
945 let mut got = None;
946 let stats = consumer.poll_up_to(4, |s, v| got = Some((s, *v)));
947
948 assert_eq!(stats.read, 1);
949 assert_eq!(stats.dropped, 0);
950 assert_eq!(got, Some((1, 20)));
951 }
952
953 #[test]
954 fn poll_window_is_frozen_at_entry() {
955 // The bounded-poll pin: the drain goal is sampled once at entry, so
956 // an item published while the poll runs waits for the next call —
957 // freezing the goal is what bounds the call under continuous
958 // overwrite — and nothing is lost or double-counted at the hand-off.
959 let ring = SeqRing::<u32, 4>::new();
960 let producer = ring.try_producer().unwrap();
961 let mut consumer = ring.try_consumer().unwrap();
962
963 producer.push(10);
964 producer.push(20);
965
966 let mut seen = std::vec::Vec::new();
967 let stats = consumer.poll_up_to(4, |seq, v| {
968 if seq == 1 {
969 // Published mid-poll: must not extend this call's window.
970 producer.push(30);
971 }
972 seen.push((seq, *v));
973 });
974 assert_eq!(stats.read, 2);
975 assert_eq!(stats.dropped, 0);
976 assert_eq!(stats.newest, 2);
977 assert_eq!(seen, [(1, 10), (2, 20)]);
978
979 let stats = consumer.poll_up_to(4, |seq, v| assert_eq!((seq, *v), (3, 30)));
980 assert_eq!(stats.read, 1);
981 assert_eq!(stats.dropped, 0);
982 assert_eq!(stats.newest, 3);
983 }
984
985 #[test]
986 fn lag_across_wrap_counts_drops_exactly() {
987 let ring = SeqRing::<u32, 4>::new();
988 let producer = ring.try_producer().unwrap();
989 let mut consumer = ring.try_consumer().unwrap();
990
991 // Park the sequence just below the wrap and consume one item, so the
992 // consumer's cursor sits in the pre-wrap region.
993 ring.next_seq.store(u32::MAX - 6, Ordering::Relaxed);
994 assert_eq!(producer.push(100), u32::MAX - 5);
995
996 let mut got = None;
997 assert!(consumer.poll_one(|s, v| got = Some((s, *v))));
998 assert_eq!(got, Some((u32::MAX - 5, 100)));
999
1000 // A fresh consumer counts every sequence published before it existed
1001 // as dropped; clear that so the assertions below measure only the
1002 // wrap-crossing jump.
1003 consumer.reset_dropped();
1004
1005 // 15 more pushes: five before the wrap, then 1..=10 after it. `push`
1006 // skips the reserved 0, so the raw sequence span is 16 while only 15
1007 // items exist — the drop accounting must not count the gap.
1008 let pushed: Vec<u32> = (0..15u32).map(|i| producer.push(i)).collect();
1009 assert_eq!(pushed.last().copied(), Some(10));
1010
1011 let mut seen = Vec::new();
1012 let stats = consumer.poll_up_to(16, |seq, v| seen.push((seq, *v)));
1013
1014 assert_eq!(stats.read, 4);
1015 assert_eq!(stats.dropped, 11);
1016 assert_eq!(stats.read + stats.dropped, pushed.len());
1017
1018 let seqs: Vec<u32> = seen.iter().map(|(s, _)| *s).collect();
1019 assert_eq!(&seqs[..], &[7, 8, 9, 10]);
1020 }
1021
1022 #[test]
1023 fn dropped_accum_saturates_instead_of_overflowing() {
1024 let ring = SeqRing::<u32, 4>::new();
1025 let producer = ring.try_producer().unwrap();
1026 let mut consumer = ring.try_consumer().unwrap();
1027
1028 // A consumer that starts at 0 against a producer near the top of the
1029 // sequence space books close to 2^32 drops in one poll. On a 32-bit
1030 // target that is most of `usize`, so a second poll must not overflow
1031 // the accumulator — every target this crate ships to is 32-bit.
1032 ring.next_seq.store(u32::MAX - 2, Ordering::Relaxed);
1033 producer.push(1);
1034 let _ = consumer.poll_up_to(4, |_, _| {});
1035 let after_first = consumer.dropped();
1036 assert!(after_first > 0);
1037
1038 for _ in 0..8 {
1039 producer.push(2);
1040 let _ = consumer.poll_up_to(4, |_, _| {});
1041 }
1042
1043 assert!(
1044 consumer.dropped() >= after_first,
1045 "dropped counter went backwards — it wrapped instead of saturating"
1046 );
1047 }
1048
1049 #[test]
1050 fn seq_distance_skips_the_reserved_zero() {
1051 type R = SeqRing<u32, 4>;
1052
1053 // No wrap: plain difference.
1054 assert_eq!(R::seq_distance(0, 0), 0);
1055 assert_eq!(R::seq_distance(0, 5), 5);
1056 assert_eq!(R::seq_distance(5, 9), 4);
1057
1058 // Spanning the wrap: one fewer than the raw span, because 0 is never
1059 // assigned by `push`.
1060 assert_eq!(R::seq_distance(u32::MAX, 1), 1);
1061 assert_eq!(R::seq_distance(u32::MAX - 5, 6), 11);
1062 assert_eq!(R::seq_distance(u32::MAX, u32::MAX), 0);
1063 }
1064
1065 #[test]
1066 fn concurrent_overwrite_never_yields_a_mismatched_value() {
1067 use core::sync::atomic::AtomicBool;
1068
1069 // Each payload repeats its counter four times, so a torn read shows up
1070 // as elements that disagree with each other. A small ring against an
1071 // unthrottled producer keeps the consumer permanently behind, which is
1072 // exactly the overwrite pressure the slot-invalidation guards against.
1073 let ring = SeqRing::<[u32; 4], 2>::new();
1074 let total = crate::test_support::iterations(20_000);
1075 let done = AtomicBool::new(false);
1076
1077 std::thread::scope(|scope| {
1078 scope.spawn(|| {
1079 let producer = ring.try_producer().unwrap();
1080 for i in 0..total {
1081 producer.push([i; 4]);
1082 }
1083 done.store(true, Ordering::Release);
1084 });
1085
1086 scope.spawn(|| {
1087 let mut consumer = ring.try_consumer().unwrap();
1088 let mut last_seq = 0u32;
1089 let mut read_total = 0usize;
1090
1091 loop {
1092 // Sample before polling: if the producer finishes after
1093 // this load, the next iteration still drains the tail.
1094 let finished = done.load(Ordering::Acquire);
1095
1096 let mut batch_last = last_seq;
1097 let stats = consumer.poll_up_to(8, |seq, v| {
1098 assert!(
1099 seq > batch_last,
1100 "sequence went backwards: {seq} after {batch_last}"
1101 );
1102 batch_last = seq;
1103
1104 // Pushes are consecutive from 0, so sequence `n`
1105 // always carries payload `n - 1`. Anything else means
1106 // a stale value surfaced under a fresh sequence, or a
1107 // fresh value under a stale one.
1108 let expected = seq - 1;
1109 assert_eq!(
1110 *v, [expected; 4],
1111 "sequence {seq} carried a stale or torn payload"
1112 );
1113 });
1114
1115 last_seq = batch_last;
1116 read_total += stats.read;
1117
1118 if finished && stats.read == 0 && stats.dropped == 0 {
1119 break;
1120 }
1121 }
1122
1123 // Every published sequence was either delivered or counted as
1124 // dropped — the consumer's accounting must be exact, not
1125 // approximate.
1126 assert_eq!(last_seq, total, "consumer stopped short of the tail");
1127 assert_eq!(
1128 read_total + consumer.dropped(),
1129 total as usize,
1130 "read + dropped must account for every published item"
1131 );
1132 });
1133 });
1134 }
1135
1136 #[test]
1137 fn capacity_returns_n() {
1138 let ring = SeqRing::<u32, 8>::new();
1139 assert_eq!(ring.capacity(), 8);
1140 }
1141
1142 #[test]
1143 fn try_producer_and_try_consumer() {
1144 let ring = SeqRing::<u32, 4>::new();
1145 let p = ring.try_producer().expect("first producer");
1146 assert!(ring.try_producer().is_none());
1147 let mut c = ring.try_consumer().expect("first consumer");
1148 assert!(ring.try_consumer().is_none());
1149 p.push(7);
1150 let mut got = None;
1151 assert!(c.poll_one(|seq, v| got = Some((seq, *v))));
1152 assert_eq!(got, Some((1, 7)));
1153 drop(p);
1154 drop(c);
1155 assert!(ring.try_producer().is_some());
1156 assert!(ring.try_consumer().is_some());
1157 }
1158
1159 #[test]
1160 fn poll_one_value_and_latest_value() {
1161 let ring = SeqRing::<u32, 8>::new();
1162 let producer = ring.try_producer().unwrap();
1163 let mut consumer = ring.try_consumer().unwrap();
1164
1165 assert_eq!(consumer.poll_one_value(), None);
1166 assert_eq!(consumer.latest_value(), None);
1167
1168 producer.push(10);
1169 producer.push(20);
1170
1171 assert_eq!(consumer.latest_value(), Some((2, 20)));
1172 assert_eq!(consumer.poll_one_value(), Some((1, 10)));
1173 assert_eq!(consumer.poll_one_value(), Some((2, 20)));
1174 assert_eq!(consumer.poll_one_value(), None);
1175 // latest does not require an advanced cursor
1176 assert_eq!(consumer.latest_value(), Some((2, 20)));
1177 }
1178
1179 // Loom's `new` is deliberately non-const, so a `static` init only exists
1180 // on the host path.
1181 #[cfg(not(loom))]
1182 #[test]
1183 fn const_new_works_in_const_context() {
1184 static RING: SeqRing<u32, 4> = SeqRing::new();
1185 assert_eq!(RING.capacity(), 4);
1186 }
1187
1188 // See the matching test in `event_buf`: the value of the const `new` is
1189 // `'static`, `Send` handles off a `static`, not merely that the `static`
1190 // compiles. Pin the signatures so a lifetime regression fails the build.
1191 #[cfg(not(loom))]
1192 #[test]
1193 fn static_ring_yields_static_sendable_handles() {
1194 static RING: SeqRing<u32, 4> = SeqRing::new();
1195
1196 fn producer_for_isr() -> super::Producer<'static, u32, 4> {
1197 RING.try_producer().unwrap()
1198 }
1199 fn consumer_for_task() -> super::Consumer<'static, u32, 4> {
1200 RING.try_consumer().unwrap()
1201 }
1202 fn assert_send<T: Send>(_: &T) {}
1203
1204 let p = producer_for_isr();
1205 let mut c = consumer_for_task();
1206 assert_send(&p);
1207 assert_send(&c);
1208
1209 p.push(9);
1210 assert_eq!(c.poll_one_value(), Some((1, 9)));
1211 }
1212}