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