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
134fn atomic_u32_array<const N: usize>(init: u32) -> [AtomicU32; N] {
135 core::array::from_fn(|_| AtomicU32::new(init))
136}
137
138fn unsafe_cell_array<T, const N: usize>() -> [UnsafeCell<MaybeUninit<T>>; N] {
139 core::array::from_fn(|_| UnsafeCell::new(MaybeUninit::uninit()))
140}
141
142// Test-only hook state. These use `core` atomics directly rather than the
143// `crate::sync` shim: Loom's atomics are not const-constructible, and this
144// hook is scaffolding for a single-threaded test rather than part of the
145// protocol Loom models.
146#[cfg(test)]
147static TEST_AFTER_READ_TARGET: AtomicUsize = AtomicUsize::new(0);
148#[cfg(test)]
149static TEST_AFTER_READ_SEQ: core::sync::atomic::AtomicU32 = core::sync::atomic::AtomicU32::new(0);
150
151/// Outcome of a [`Consumer::poll_up_to`] or [`Consumer::poll_one`] call.
152///
153/// `read + dropped` accounts for every sequence the consumer advanced past, so
154/// the pair can be used to detect a lagging consumer without a separate probe.
155#[must_use]
156#[derive(Copy, Clone, Debug)]
157pub struct PollStats {
158 /// Number of items delivered to the hook.
159 pub read: usize,
160 /// Number of items skipped because the consumer lagged or slots were overwritten.
161 pub dropped: usize,
162 /// Newest sequence observed while polling.
163 pub newest: u32,
164}
165
166/// Overwrite ring for SPSC high-rate telemetry.
167/// Producer never waits; consumer may drop if it lags > N.
168pub struct SeqRing<T: Copy, const N: usize> {
169 next_seq: AtomicU32,
170 published_seq: AtomicU32,
171 slot_seq: [AtomicU32; N],
172 slots: [UnsafeCell<MaybeUninit<T>>; N],
173 producer_taken: AtomicBool,
174 consumer_taken: AtomicBool,
175}
176
177// SAFETY: SeqRing is Sync because the producer/consumer handles enforce SPSC usage,
178// and all shared state is accessed via atomics. Values are written before their
179// sequence numbers are published with Release and read with Acquire. T: Send ensures
180// values can be transferred across threads safely.
181unsafe impl<T: Copy + Send, const N: usize> Sync for SeqRing<T, N> {}
182
183impl<T: Copy, const N: usize> SeqRing<T, N> {
184 /// Create a new ring buffer.
185 ///
186 /// # Panics
187 /// Panics if `N == 0`.
188 pub fn new() -> Self {
189 assert!(N > 0);
190 Self {
191 next_seq: AtomicU32::new(0),
192 published_seq: AtomicU32::new(0),
193 slot_seq: atomic_u32_array::<N>(0),
194 slots: unsafe_cell_array::<T, N>(),
195 producer_taken: AtomicBool::new(false),
196 consumer_taken: AtomicBool::new(false),
197 }
198 }
199
200 /// Maximum number of items the ring can hold.
201 #[inline]
202 pub const fn capacity(&self) -> usize {
203 N
204 }
205
206 #[inline(always)]
207 const fn idx_for(seq: u32) -> usize {
208 ((seq.wrapping_sub(1)) as usize) % N
209 }
210
211 /// Try to create the producer handle.
212 ///
213 /// Returns `None` if a producer is already active. Prefer this over
214 /// [`producer`](Self::producer) when fallible bring-up is needed.
215 #[inline]
216 pub fn try_producer(&self) -> Option<Producer<'_, T, N>> {
217 if self.producer_taken.swap(true, Ordering::AcqRel) {
218 None
219 } else {
220 Some(Producer {
221 ring: self,
222 _not_sync: PhantomData,
223 })
224 }
225 }
226
227 /// Create the producer handle. Only one producer may be active.
228 ///
229 /// # Panics
230 /// Panics if a producer handle is already active.
231 #[inline]
232 pub fn producer(&self) -> Producer<'_, T, N> {
233 self.try_producer()
234 .expect("SeqRing::producer() called while a producer is active")
235 }
236
237 /// Try to create the consumer handle.
238 ///
239 /// Returns `None` if a consumer is already active. Prefer this over
240 /// [`consumer`](Self::consumer) when fallible bring-up is needed.
241 #[inline]
242 pub fn try_consumer(&self) -> Option<Consumer<'_, T, N>> {
243 if self.consumer_taken.swap(true, Ordering::AcqRel) {
244 None
245 } else {
246 Some(Consumer {
247 ring: self,
248 last_seq: 0,
249 dropped_accum: 0,
250 _not_sync: PhantomData,
251 })
252 }
253 }
254
255 /// Create the consumer handle. Only one consumer may be active.
256 ///
257 /// # Panics
258 /// Panics if a consumer handle is already active.
259 #[inline]
260 pub fn consumer(&self) -> Consumer<'_, T, N> {
261 self.try_consumer()
262 .expect("SeqRing::consumer() called while a consumer is active")
263 }
264
265 #[inline]
266 fn newest_seq(&self) -> u32 {
267 self.published_seq.load(Ordering::Acquire)
268 }
269
270 #[inline]
271 fn push_inner(&self, value: T) -> u32 {
272 let mut seq = self
273 .next_seq
274 .fetch_add(1, Ordering::Relaxed)
275 .wrapping_add(1);
276 if seq == 0 {
277 seq = 1;
278 self.next_seq.store(1, Ordering::Relaxed);
279 }
280
281 let idx = Self::idx_for(seq);
282 // Invalidate before writing so a concurrent reader of the previous
283 // sequence cannot observe the new value under the old sequence number.
284 // The Release fence keeps the invalidation ahead of the value write.
285 self.slot_seq[idx].store(0, Ordering::Relaxed);
286 fence(Ordering::Release);
287
288 // SAFETY: the producer is the only writer, and `idx` is in bounds
289 // because `idx_for` reduces modulo N. The write is volatile to match
290 // the volatile read in `read_seq_inner`: a consumer may be copying
291 // this slot concurrently, so the compiler must not split, duplicate,
292 // or move the store.
293 unsafe { core::ptr::write_volatile(self.slots[idx].get(), MaybeUninit::new(value)) };
294
295 self.slot_seq[idx].store(seq, Ordering::Release);
296 self.published_seq.store(seq, Ordering::Release);
297 seq
298 }
299
300 /// Advance past the reserved empty sequence `0`.
301 #[inline(always)]
302 const fn next_after(seq: u32) -> u32 {
303 match seq.wrapping_add(1) {
304 0 => 1,
305 n => n,
306 }
307 }
308
309 /// How many sequence numbers `push` actually assigned in `(from, to]`.
310 ///
311 /// Plain wrapping subtraction over-counts by one whenever the span crosses
312 /// the reserved value `0`, because `push` skips it. The span crosses `0`
313 /// exactly when `to` compares below `from`, since that is the only way the
314 /// walk from `from` up to `to` can pass through the wrap point.
315 #[inline(always)]
316 const fn seq_distance(from: u32, to: u32) -> u32 {
317 let raw = to.wrapping_sub(from);
318 if to < from { raw - 1 } else { raw }
319 }
320
321 #[inline]
322 fn read_seq_inner(&self, seq: u32) -> Option<T> {
323 let idx = Self::idx_for(seq);
324
325 let s1 = self.slot_seq[idx].load(Ordering::Acquire);
326 if s1 != seq {
327 return None;
328 }
329
330 // Copy the slot as raw bytes. The producer may be overwriting it right
331 // now, so the bytes are not trusted until the sequence re-check below
332 // passes — holding the copy as `MaybeUninit<T>` means a torn read
333 // cannot produce an invalid `T`, only bytes that are then discarded.
334 //
335 // SAFETY: `idx` is in bounds because `idx_for` reduces modulo N. The
336 // read is volatile so the compiler cannot split, duplicate, or hoist
337 // it, and `MaybeUninit<T>` has no validity invariant to violate.
338 let v: MaybeUninit<T> = unsafe { core::ptr::read_volatile(self.slots[idx].get()) };
339
340 #[cfg(test)]
341 self.test_after_read_hook(idx);
342
343 // Pin the copy above the re-check. An Acquire fence orders preceding
344 // loads ahead of what follows; a plain Acquire load on `s2` would only
345 // stop *later* accesses from moving up, which would let the copy sink
346 // past the check that is supposed to validate it.
347 fence(Ordering::Acquire);
348
349 let s2 = self.slot_seq[idx].load(Ordering::Relaxed);
350 if s2 != seq {
351 return None;
352 }
353
354 // SAFETY: the slot sequence matched `seq` both before and after the
355 // copy, and the producer invalidates the sequence before it touches a
356 // slot, so no write overlapped the read and the bytes are a complete,
357 // initialised `T`.
358 Some(unsafe { v.assume_init() })
359 }
360
361 #[cfg(test)]
362 fn test_after_read_hook(&self, idx: usize) {
363 let target = TEST_AFTER_READ_TARGET.load(Ordering::Acquire);
364 if target == self as *const _ as usize {
365 let seq = TEST_AFTER_READ_SEQ.load(Ordering::Relaxed);
366 self.slot_seq[idx].store(seq, Ordering::Release);
367 TEST_AFTER_READ_TARGET.store(0, Ordering::Release);
368 }
369 }
370}
371
372impl<T: Copy, const N: usize> Default for SeqRing<T, N> {
373 fn default() -> Self {
374 Self::new()
375 }
376}
377
378impl<T: Copy, const N: usize> core::fmt::Debug for SeqRing<T, N> {
379 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
380 f.debug_struct("SeqRing")
381 .field("capacity", &N)
382 .field("published_seq", &self.published_seq.load(Ordering::Relaxed))
383 .finish()
384 }
385}
386
387/// Producer handle for writing into the ring.
388///
389/// This handle is `!Sync` to prevent concurrent producers.
390pub struct Producer<'a, T: Copy, const N: usize> {
391 ring: &'a SeqRing<T, N>,
392 _not_sync: PhantomData<Cell<()>>,
393}
394
395impl<'a, T: Copy, const N: usize> Producer<'a, T, N> {
396 /// Write a value into the ring.
397 ///
398 /// Returns the sequence number assigned to the write (never 0).
399 #[inline]
400 pub fn push(&self, value: T) -> u32 {
401 self.ring.push_inner(value)
402 }
403}
404
405impl<'a, T: Copy, const N: usize> Drop for Producer<'a, T, N> {
406 fn drop(&mut self) {
407 self.ring.producer_taken.store(false, Ordering::Release);
408 }
409}
410
411impl<T: Copy, const N: usize> core::fmt::Debug for Producer<'_, T, N> {
412 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
413 f.debug_struct("seq_ring::Producer")
414 .field("capacity", &N)
415 .finish()
416 }
417}
418
419/// Consumer handle for reading from the ring.
420///
421/// This handle is `!Sync` to prevent concurrent consumers.
422pub struct Consumer<'a, T: Copy, const N: usize> {
423 ring: &'a SeqRing<T, N>,
424 last_seq: u32,
425 dropped_accum: usize,
426 _not_sync: PhantomData<Cell<()>>,
427}
428
429impl<'a, T: Copy, const N: usize> Consumer<'a, T, N> {
430 /// How many items have been dropped since consumer creation (or since reset).
431 ///
432 /// The counter saturates at [`usize::MAX`] rather than wrapping, so on a
433 /// 32-bit target a very long-lived lagging consumer reports "at least this
434 /// many" instead of overflowing. Call [`reset_dropped`](Self::reset_dropped)
435 /// periodically if exact long-run totals matter.
436 #[inline]
437 pub fn dropped(&self) -> usize {
438 self.dropped_accum
439 }
440
441 /// Reset the internal drop counter.
442 #[inline]
443 pub fn reset_dropped(&mut self) {
444 self.dropped_accum = 0;
445 }
446
447 /// Drain at most one item (in-order).
448 /// Returns true if an item was delivered to the hook.
449 #[inline]
450 pub fn poll_one(&mut self, hook: impl FnOnce(u32, &T)) -> bool {
451 let mut hook = Some(hook);
452 let stats = self.poll_up_to(1, |seq, v| {
453 if let Some(hook) = hook.take() {
454 hook(seq, v);
455 }
456 });
457 stats.read == 1
458 }
459
460 /// Drain at most one item (in-order), returning `(seq, value)`.
461 ///
462 /// Equivalent to [`poll_one`](Self::poll_one) without a hook. Drop
463 /// accounting and the `read + dropped` invariant are unchanged.
464 #[inline]
465 pub fn poll_one_value(&mut self) -> Option<(u32, T)> {
466 let mut result = None;
467 self.poll_one(|seq, v| result = Some((seq, *v)));
468 result
469 }
470
471 /// Drain up to `max` items (in-order).
472 /// Hook sees `&T` but it is a reference to a **local copy** inside poll.
473 ///
474 /// If `max == 0`, this returns immediately with `read = 0`, `dropped = 0`, and
475 /// `newest` set to the latest published sequence.
476 pub fn poll_up_to(&mut self, max: usize, mut hook: impl FnMut(u32, &T)) -> PollStats {
477 if max == 0 {
478 return PollStats {
479 read: 0,
480 dropped: 0,
481 newest: self.ring.newest_seq(),
482 };
483 }
484
485 let mut newest = self.ring.newest_seq();
486 if newest == 0 || newest == self.last_seq {
487 return PollStats {
488 read: 0,
489 dropped: 0,
490 newest,
491 };
492 }
493
494 let mut read = 0usize;
495 let mut dropped = 0usize;
496
497 while read < max {
498 newest = self.ring.newest_seq();
499 if self.last_seq == newest {
500 break;
501 }
502
503 let lag = SeqRing::<T, N>::seq_distance(self.last_seq, newest) as usize;
504 if lag > N {
505 let keep_from = newest.wrapping_sub((N - 1) as u32);
506 let resume_after = keep_from.wrapping_sub(1);
507 // Everything in (last_seq, keep_from) is gone; count what was
508 // really assigned rather than the raw sequence span.
509 let jumped = SeqRing::<T, N>::seq_distance(self.last_seq, resume_after) as usize;
510 dropped = dropped.saturating_add(jumped);
511 self.last_seq = resume_after;
512 continue;
513 }
514
515 let next = SeqRing::<T, N>::next_after(self.last_seq);
516
517 match self.ring.read_seq_inner(next) {
518 Some(v) => {
519 hook(next, &v);
520 self.last_seq = next;
521 read += 1;
522 }
523 None => {
524 self.last_seq = next;
525 dropped = dropped.saturating_add(1);
526 }
527 }
528 }
529
530 // Saturate rather than wrap. `usize` is 32 bits on every target this
531 // crate ships to, and the sequence space is also 32 bits, so a
532 // long-running consumer that lags can genuinely reach the top of the
533 // range. Overflow here would panic in debug and silently wrap in
534 // release — on an embedded target, in a hot path.
535 self.dropped_accum = self.dropped_accum.saturating_add(dropped);
536
537 PollStats {
538 read,
539 dropped,
540 newest,
541 }
542 }
543
544 /// "Give me the newest thing right now" (not in-order).
545 /// Returns true if it delivered something.
546 ///
547 /// This does not advance the consumer cursor.
548 #[inline]
549 pub fn latest(&self, hook: impl FnOnce(u32, &T)) -> bool {
550 let newest = self.ring.newest_seq();
551 if newest == 0 {
552 return false;
553 }
554 if let Some(v) = self.ring.read_seq_inner(newest) {
555 hook(newest, &v);
556 true
557 } else {
558 false
559 }
560 }
561
562 /// Read the newest item without a hook, returning `(seq, value)`.
563 ///
564 /// Equivalent to [`latest`](Self::latest). Does not advance the consumer
565 /// cursor.
566 #[inline]
567 pub fn latest_value(&self) -> Option<(u32, T)> {
568 let mut result = None;
569 self.latest(|seq, v| result = Some((seq, *v)));
570 result
571 }
572
573 /// Fast-forward consumer so the *next* `poll_one()` yields the newest item
574 /// (i.e. skip backlog).
575 ///
576 /// This does not modify the dropped counter.
577 #[inline]
578 pub fn skip_to_latest(&mut self) {
579 let newest = self.ring.newest_seq();
580 if newest != 0 {
581 self.last_seq = newest.wrapping_sub(1);
582 }
583 }
584}
585
586impl<'a, T: Copy, const N: usize> Drop for Consumer<'a, T, N> {
587 fn drop(&mut self) {
588 self.ring.consumer_taken.store(false, Ordering::Release);
589 }
590}
591
592impl<T: Copy, const N: usize> core::fmt::Debug for Consumer<'_, T, N> {
593 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
594 f.debug_struct("seq_ring::Consumer")
595 .field("capacity", &N)
596 .field("last_seq", &self.last_seq)
597 .field("dropped", &self.dropped_accum)
598 .finish()
599 }
600}
601
602impl<T: Copy, const N: usize> crate::traits::Sink<T> for Producer<'_, T, N> {
603 type Error = core::convert::Infallible;
604
605 #[inline]
606 fn try_push(&mut self, val: T) -> Result<(), core::convert::Infallible> {
607 self.push(val);
608 Ok(())
609 }
610}
611
612impl<T: Copy, const N: usize> crate::traits::Source<T> for Consumer<'_, T, N> {
613 #[inline]
614 fn try_pop(&mut self) -> Option<T> {
615 self.poll_one_value().map(|(_, v)| v)
616 }
617}
618
619#[cfg(test)]
620mod tests {
621 use super::{SeqRing, TEST_AFTER_READ_SEQ, TEST_AFTER_READ_TARGET};
622 use core::sync::atomic::Ordering;
623 use std::vec::Vec;
624
625 #[test]
626 fn poll_one_empty_returns_false() {
627 let ring = SeqRing::<u32, 4>::new();
628 let mut consumer = ring.consumer();
629 let ok = consumer.poll_one(|_, _| {});
630 assert!(!ok);
631 }
632
633 #[test]
634 fn polls_in_order() {
635 let ring = SeqRing::<u32, 8>::new();
636 let producer = ring.producer();
637 let mut consumer = ring.consumer();
638
639 producer.push(10);
640 producer.push(11);
641 producer.push(12);
642
643 let mut seen = Vec::new();
644 let stats = consumer.poll_up_to(10, |seq, v| seen.push((seq, *v)));
645
646 assert_eq!(stats.read, 3);
647 assert_eq!(stats.dropped, 0);
648 assert_eq!(stats.newest, 3);
649 assert_eq!(&seen[..], &[(1, 10), (2, 11), (3, 12)]);
650 }
651
652 #[test]
653 fn drops_when_consumer_lags() {
654 let ring = SeqRing::<u32, 4>::new();
655 let producer = ring.producer();
656 let mut consumer = ring.consumer();
657
658 for i in 0..10 {
659 producer.push(i);
660 }
661
662 let mut seen = Vec::new();
663 let stats = consumer.poll_up_to(10, |seq, v| seen.push((seq, *v)));
664
665 assert_eq!(stats.read, 4);
666 assert_eq!(stats.dropped, 6);
667 assert_eq!(stats.newest, 10);
668 assert_eq!(&seen[..], &[(7, 6), (8, 7), (9, 8), (10, 9)]);
669 }
670
671 #[test]
672 fn latest_reads_newest() {
673 let ring = SeqRing::<u32, 8>::new();
674 let producer = ring.producer();
675 let consumer = ring.consumer();
676
677 producer.push(1);
678 producer.push(2);
679
680 let mut got = None;
681 let ok = consumer.latest(|seq, v| got = Some((seq, *v)));
682
683 assert!(ok);
684 assert_eq!(got, Some((2, 2)));
685 }
686
687 #[test]
688 fn skip_to_latest_makes_next_poll_latest() {
689 let ring = SeqRing::<u32, 8>::new();
690 let producer = ring.producer();
691 let mut consumer = ring.consumer();
692
693 producer.push(10);
694 producer.push(11);
695 producer.push(12);
696
697 consumer.skip_to_latest();
698
699 let mut got = None;
700 let ok = consumer.poll_one(|seq, v| got = Some((seq, *v)));
701
702 assert!(ok);
703 assert_eq!(got, Some((3, 12)));
704 }
705
706 #[test]
707 fn poll_up_to_zero_returns_newest_only() {
708 let ring = SeqRing::<u32, 4>::new();
709 let producer = ring.producer();
710 let mut consumer = ring.consumer();
711
712 producer.push(42);
713
714 let stats = consumer.poll_up_to(0, |_, _| panic!("hook should not run"));
715
716 assert_eq!(stats.read, 0);
717 assert_eq!(stats.dropped, 0);
718 assert_eq!(stats.newest, 1);
719 }
720
721 #[test]
722 fn dropped_counter_can_reset() {
723 let ring = SeqRing::<u32, 2>::new();
724 let producer = ring.producer();
725 let mut consumer = ring.consumer();
726
727 for i in 0..5 {
728 producer.push(i);
729 }
730
731 let stats = consumer.poll_up_to(10, |_, _| {});
732
733 assert_eq!(consumer.dropped(), stats.dropped);
734
735 consumer.reset_dropped();
736
737 assert_eq!(consumer.dropped(), 0);
738 }
739
740 #[test]
741 fn latest_empty_returns_false() {
742 let ring = SeqRing::<u32, 4>::new();
743 let consumer = ring.consumer();
744
745 let ok = consumer.latest(|_, _| {});
746
747 assert!(!ok);
748 }
749
750 #[test]
751 fn latest_returns_false_when_slot_missing() {
752 let ring = SeqRing::<u32, 4>::new();
753 let consumer = ring.consumer();
754
755 ring.published_seq.store(1, Ordering::Release);
756
757 let ok = consumer.latest(|_, _| {});
758
759 assert!(!ok);
760 }
761
762 #[test]
763 fn poll_up_to_counts_dropped_when_slot_missing() {
764 let ring = SeqRing::<u32, 4>::new();
765 let mut consumer = ring.consumer();
766
767 ring.published_seq.store(1, Ordering::Release);
768
769 let stats = consumer.poll_up_to(1, |_, _| panic!("hook should not run"));
770
771 assert_eq!(stats.read, 0);
772 assert_eq!(stats.dropped, 1);
773 assert_eq!(consumer.dropped(), 1);
774 }
775
776 #[test]
777 fn read_seq_inner_detects_overwrite_during_read() {
778 let ring = SeqRing::<u32, 4>::new();
779 let producer = ring.producer();
780 let seq = producer.push(7);
781
782 TEST_AFTER_READ_SEQ.store(seq.wrapping_add(1), Ordering::Relaxed);
783 TEST_AFTER_READ_TARGET.store(&ring as *const _ as usize, Ordering::Release);
784
785 let got = ring.read_seq_inner(seq);
786
787 TEST_AFTER_READ_TARGET.store(0, Ordering::Release);
788
789 assert!(got.is_none());
790 }
791
792 #[test]
793 fn push_wraps_seq_from_zero_to_one() {
794 let ring = SeqRing::<u32, 4>::new();
795
796 ring.next_seq.store(u32::MAX, Ordering::Relaxed);
797
798 let seq = ring.producer().push(1);
799
800 assert_eq!(seq, 1);
801 assert_eq!(ring.next_seq.load(Ordering::Relaxed), 1);
802 }
803
804 #[test]
805 fn read_seq_inner_rejects_invalidated_slot() {
806 let ring = SeqRing::<u32, 4>::new();
807 let producer = ring.producer();
808 let seq = producer.push(7);
809
810 ring.slot_seq[SeqRing::<u32, 4>::idx_for(seq)].store(0, Ordering::Release);
811
812 assert!(ring.read_seq_inner(seq).is_none());
813 }
814
815 #[test]
816 fn consumer_skips_reserved_seq_zero_on_wrap() {
817 let ring = SeqRing::<u32, 4>::new();
818 let producer = ring.producer();
819 let mut consumer = ring.consumer();
820
821 ring.next_seq.store(u32::MAX - 1, Ordering::Relaxed);
822 assert_eq!(producer.push(10), u32::MAX);
823
824 consumer.skip_to_latest();
825 let mut got = None;
826 assert!(consumer.poll_one(|s, v| got = Some((s, *v))));
827 assert_eq!(got, Some((u32::MAX, 10)));
828
829 assert_eq!(producer.push(20), 1);
830
831 let mut got = None;
832 let stats = consumer.poll_up_to(4, |s, v| got = Some((s, *v)));
833
834 assert_eq!(stats.read, 1);
835 assert_eq!(stats.dropped, 0);
836 assert_eq!(got, Some((1, 20)));
837 }
838
839 #[test]
840 fn lag_across_wrap_counts_drops_exactly() {
841 let ring = SeqRing::<u32, 4>::new();
842 let producer = ring.producer();
843 let mut consumer = ring.consumer();
844
845 // Park the sequence just below the wrap and consume one item, so the
846 // consumer's cursor sits in the pre-wrap region.
847 ring.next_seq.store(u32::MAX - 6, Ordering::Relaxed);
848 assert_eq!(producer.push(100), u32::MAX - 5);
849
850 let mut got = None;
851 assert!(consumer.poll_one(|s, v| got = Some((s, *v))));
852 assert_eq!(got, Some((u32::MAX - 5, 100)));
853
854 // A fresh consumer counts every sequence published before it existed
855 // as dropped; clear that so the assertions below measure only the
856 // wrap-crossing jump.
857 consumer.reset_dropped();
858
859 // 15 more pushes: five before the wrap, then 1..=10 after it. `push`
860 // skips the reserved 0, so the raw sequence span is 16 while only 15
861 // items exist — the drop accounting must not count the gap.
862 let pushed: Vec<u32> = (0..15u32).map(|i| producer.push(i)).collect();
863 assert_eq!(pushed.last().copied(), Some(10));
864
865 let mut seen = Vec::new();
866 let stats = consumer.poll_up_to(16, |seq, v| seen.push((seq, *v)));
867
868 assert_eq!(stats.read, 4);
869 assert_eq!(stats.dropped, 11);
870 assert_eq!(stats.read + stats.dropped, pushed.len());
871
872 let seqs: Vec<u32> = seen.iter().map(|(s, _)| *s).collect();
873 assert_eq!(&seqs[..], &[7, 8, 9, 10]);
874 }
875
876 #[test]
877 fn dropped_accum_saturates_instead_of_overflowing() {
878 let ring = SeqRing::<u32, 4>::new();
879 let producer = ring.producer();
880 let mut consumer = ring.consumer();
881
882 // A consumer that starts at 0 against a producer near the top of the
883 // sequence space books close to 2^32 drops in one poll. On a 32-bit
884 // target that is most of `usize`, so a second poll must not overflow
885 // the accumulator — every target this crate ships to is 32-bit.
886 ring.next_seq.store(u32::MAX - 2, Ordering::Relaxed);
887 producer.push(1);
888 let _ = consumer.poll_up_to(4, |_, _| {});
889 let after_first = consumer.dropped();
890 assert!(after_first > 0);
891
892 for _ in 0..8 {
893 producer.push(2);
894 let _ = consumer.poll_up_to(4, |_, _| {});
895 }
896
897 assert!(
898 consumer.dropped() >= after_first,
899 "dropped counter went backwards — it wrapped instead of saturating"
900 );
901 }
902
903 #[test]
904 fn seq_distance_skips_the_reserved_zero() {
905 type R = SeqRing<u32, 4>;
906
907 // No wrap: plain difference.
908 assert_eq!(R::seq_distance(0, 0), 0);
909 assert_eq!(R::seq_distance(0, 5), 5);
910 assert_eq!(R::seq_distance(5, 9), 4);
911
912 // Spanning the wrap: one fewer than the raw span, because 0 is never
913 // assigned by `push`.
914 assert_eq!(R::seq_distance(u32::MAX, 1), 1);
915 assert_eq!(R::seq_distance(u32::MAX - 5, 6), 11);
916 assert_eq!(R::seq_distance(u32::MAX, u32::MAX), 0);
917 }
918
919 #[test]
920 fn concurrent_overwrite_never_yields_a_mismatched_value() {
921 use core::sync::atomic::AtomicBool;
922
923 // Each payload repeats its counter four times, so a torn read shows up
924 // as elements that disagree with each other. A small ring against an
925 // unthrottled producer keeps the consumer permanently behind, which is
926 // exactly the overwrite pressure the slot-invalidation guards against.
927 let ring = SeqRing::<[u32; 4], 2>::new();
928 let total = crate::test_support::iterations(20_000);
929 let done = AtomicBool::new(false);
930
931 std::thread::scope(|scope| {
932 scope.spawn(|| {
933 let producer = ring.producer();
934 for i in 0..total {
935 producer.push([i; 4]);
936 }
937 done.store(true, Ordering::Release);
938 });
939
940 scope.spawn(|| {
941 let mut consumer = ring.consumer();
942 let mut last_seq = 0u32;
943 let mut read_total = 0usize;
944
945 loop {
946 // Sample before polling: if the producer finishes after
947 // this load, the next iteration still drains the tail.
948 let finished = done.load(Ordering::Acquire);
949
950 let mut batch_last = last_seq;
951 let stats = consumer.poll_up_to(8, |seq, v| {
952 assert!(
953 seq > batch_last,
954 "sequence went backwards: {seq} after {batch_last}"
955 );
956 batch_last = seq;
957
958 // Pushes are consecutive from 0, so sequence `n`
959 // always carries payload `n - 1`. Anything else means
960 // a stale value surfaced under a fresh sequence, or a
961 // fresh value under a stale one.
962 let expected = seq - 1;
963 assert_eq!(
964 *v, [expected; 4],
965 "sequence {seq} carried a stale or torn payload"
966 );
967 });
968
969 last_seq = batch_last;
970 read_total += stats.read;
971
972 if finished && stats.read == 0 && stats.dropped == 0 {
973 break;
974 }
975 }
976
977 // Every published sequence was either delivered or counted as
978 // dropped — the consumer's accounting must be exact, not
979 // approximate.
980 assert_eq!(last_seq, total, "consumer stopped short of the tail");
981 assert_eq!(
982 read_total + consumer.dropped(),
983 total as usize,
984 "read + dropped must account for every published item"
985 );
986 });
987 });
988 }
989
990 #[test]
991 fn capacity_returns_n() {
992 let ring = SeqRing::<u32, 8>::new();
993 assert_eq!(ring.capacity(), 8);
994 }
995
996 #[test]
997 fn try_producer_and_try_consumer() {
998 let ring = SeqRing::<u32, 4>::new();
999 let p = ring.try_producer().expect("first producer");
1000 assert!(ring.try_producer().is_none());
1001 let mut c = ring.try_consumer().expect("first consumer");
1002 assert!(ring.try_consumer().is_none());
1003 p.push(7);
1004 let mut got = None;
1005 assert!(c.poll_one(|seq, v| got = Some((seq, *v))));
1006 assert_eq!(got, Some((1, 7)));
1007 drop(p);
1008 drop(c);
1009 assert!(ring.try_producer().is_some());
1010 assert!(ring.try_consumer().is_some());
1011 }
1012
1013 #[test]
1014 fn poll_one_value_and_latest_value() {
1015 let ring = SeqRing::<u32, 8>::new();
1016 let producer = ring.producer();
1017 let mut consumer = ring.consumer();
1018
1019 assert_eq!(consumer.poll_one_value(), None);
1020 assert_eq!(consumer.latest_value(), None);
1021
1022 producer.push(10);
1023 producer.push(20);
1024
1025 assert_eq!(consumer.latest_value(), Some((2, 20)));
1026 assert_eq!(consumer.poll_one_value(), Some((1, 10)));
1027 assert_eq!(consumer.poll_one_value(), Some((2, 20)));
1028 assert_eq!(consumer.poll_one_value(), None);
1029 // latest does not require an advanced cursor
1030 assert_eq!(consumer.latest_value(), Some((2, 20)));
1031 }
1032}