Skip to main content

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}