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