Skip to main content

ph_eventing/
event_buf.rs

1//! Bounded SPSC event buffer with backpressure — no heap, no alloc.
2//!
3//! [`EventBuf`] is a fixed-size, lock-free, single-producer single-consumer
4//! ring buffer that **rejects** pushes when full instead of overwriting.
5//! This gives the producer explicit backpressure so no events are silently
6//! lost.
7//!
8//! # When to use
9//! Use `EventBuf` when every event matters and the producer can afford to
10//! handle a "buffer full" signal (retry, log, or apply its own policy).
11//! If losing old events is acceptable, prefer [`crate::SeqRing`].
12//! If you only need a single-owner ring, see [`crate::RingBuf`].
13//!
14//! # Memory ordering
15//! This is a classic Lamport SPSC queue:
16//! - The producer owns `head` (Relaxed load, Release store) and reads
17//!   `tail` with Acquire to see consumer progress.
18//! - The consumer owns `tail` (Relaxed load, Release store) and reads
19//!   `head` with Acquire to see producer progress.
20//! - A slot is written before `head` is advanced and read before `tail` is
21//!   advanced, so the Release/Acquire pairs on the cursors act as the
22//!   publication fence.
23//! - The producer and consumer never touch the same slot: `push` writes at
24//!   `head` only while `head - tail < N`, so there is no data race on the
25//!   slots themselves, only on the cursors.
26//! - [`EventBuf::len`] is the one observer that reads both cursors, so it
27//!   brackets its `head` load between two `tail` samples to get a consistent
28//!   pair.
29//!
30//! # Example
31//! ```
32//! use ph_eventing::EventBuf;
33//!
34//! let buf = EventBuf::<u32, 4>::new();
35//! let producer = buf.producer();
36//! let consumer = buf.consumer();
37//!
38//! assert!(producer.push(1).is_ok());
39//! assert!(producer.push(2).is_ok());
40//! assert_eq!(consumer.peek(), Some(1));
41//! assert_eq!(consumer.pop(), Some(1));
42//! assert_eq!(consumer.pop(), Some(2));
43//! assert_eq!(consumer.pop(), None); // empty
44//! ```
45
46use crate::sync::{AtomicBool, AtomicU32, Ordering, TrackedCell, fence};
47use core::cell::Cell;
48use core::marker::PhantomData;
49use core::mem::MaybeUninit;
50
51fn slot_array<T, const N: usize>() -> [TrackedCell<MaybeUninit<T>>; N] {
52    core::array::from_fn(|_| TrackedCell::new(MaybeUninit::uninit()))
53}
54
55/// How many times [`EventBuf::len`] retries its snapshot before falling back
56/// to a clamped estimate. Bounded so `len` is always wait-free.
57const RETRY_LIMIT: usize = 2;
58
59/// Bounded SPSC event buffer with backpressure.
60///
61/// When the buffer is full, [`Producer::push`] returns `Err(val)` instead
62/// of overwriting, giving the producer a chance to retry, drop, or log.
63/// The consumer drains items with [`Consumer::pop`] or [`Consumer::drain`],
64/// and can inspect the oldest item with [`Consumer::peek`] without consuming it.
65///
66/// # Panics
67/// - `EventBuf::new()` panics if `N == 0`.
68/// - `producer()` / `consumer()` panic if called while another handle of
69///   the same kind is already active. Use [`EventBuf::try_producer`] /
70///   [`EventBuf::try_consumer`] for a fallible alternative.
71pub struct EventBuf<T: Copy, const N: usize> {
72    head: AtomicU32,
73    tail: AtomicU32,
74    slots: [TrackedCell<MaybeUninit<T>>; N],
75    producer_taken: AtomicBool,
76    consumer_taken: AtomicBool,
77}
78
79// SAFETY: EventBuf is Sync because the producer/consumer handles enforce
80// SPSC usage, and the head/tail cursors are accessed via atomics with
81// Release/Acquire ordering that guarantees slot visibility. T: Send ensures
82// values can be transferred across threads safely.
83unsafe impl<T: Copy + Send, const N: usize> Sync for EventBuf<T, N> {}
84
85impl<T: Copy, const N: usize> EventBuf<T, N> {
86    /// Create a new, empty event buffer.
87    ///
88    /// # Panics
89    /// Panics if `N == 0`.
90    pub fn new() -> Self {
91        assert!(N > 0, "EventBuf capacity N must be > 0");
92        Self {
93            head: AtomicU32::new(0),
94            tail: AtomicU32::new(0),
95            slots: slot_array::<T, N>(),
96            producer_taken: AtomicBool::new(false),
97            consumer_taken: AtomicBool::new(false),
98        }
99    }
100
101    #[inline(always)]
102    const fn slot_index(pos: u32) -> usize {
103        (pos as usize) % N
104    }
105
106    /// Maximum number of items the buffer can hold.
107    #[inline]
108    pub const fn capacity(&self) -> usize {
109        N
110    }
111
112    /// Approximate number of items currently buffered.
113    ///
114    /// Returns a consistent `(tail, head)` snapshot. The value may still be
115    /// stale by the time the caller acts on it, but it will never spuriously
116    /// exceed [`capacity`](Self::capacity).
117    ///
118    /// This never blocks: it makes a bounded number of attempts and then falls
119    /// back to a clamped estimate, so a busy consumer cannot stall the caller.
120    #[inline]
121    pub fn len(&self) -> usize {
122        // Seqlock-style read. Sampling `tail` on both sides of the `head` load
123        // and requiring the samples to match means `head` was observed while
124        // `tail` held still, so `head.wrapping_sub(tail)` cannot appear as a
125        // huge unsigned value after a concurrent consumer advance.
126        //
127        // The two barriers pin the `head` load between the samples. They also
128        // make equality a sound bound: if `h` observes a producer publication
129        // that reused consumer-freed space, the following Acquire fence
130        // synchronizes through that Relaxed load. The producer acquired the
131        // newer `tail` before publishing `h`, so `t2` cannot then observe an
132        // older tail. Thus `t1 == t2` implies `h - t1 <= N`.
133        //
134        // This depends on EventBuf's backpressure protocol: the producer reads
135        // `tail` with Acquire before advancing `head`. It is not a generic
136        // double-sampling property. See AGENTS.md for the full happens-before
137        // chain.
138        for _ in 0..RETRY_LIMIT {
139            let t1 = self.tail.load(Ordering::Acquire);
140            let h = self.head.load(Ordering::Relaxed);
141            fence(Ordering::Acquire);
142            let t2 = self.tail.load(Ordering::Relaxed);
143
144            if t1 == t2 {
145                return h.wrapping_sub(t1) as usize;
146            }
147        }
148
149        // The consumer moved during every attempt. Read `tail` first so a
150        // further advance can only make the count an over-estimate rather
151        // than an underflow, then clamp to preserve the capacity bound.
152        let t = self.tail.load(Ordering::Acquire);
153        let h = self.head.load(Ordering::Relaxed);
154        (h.wrapping_sub(t) as usize).min(N)
155    }
156
157    /// Returns `true` if the buffer contains no items (approximate).
158    #[inline]
159    pub fn is_empty(&self) -> bool {
160        self.len() == 0
161    }
162
163    /// Returns `true` if the buffer is at capacity (approximate).
164    #[inline]
165    pub fn is_full(&self) -> bool {
166        self.len() >= N
167    }
168
169    /// Try to create the producer handle.
170    ///
171    /// Returns `None` if a producer is already active. Prefer this over
172    /// [`producer`](Self::producer) when fallible bring-up is needed.
173    #[inline]
174    pub fn try_producer(&self) -> Option<Producer<'_, T, N>> {
175        if self.producer_taken.swap(true, Ordering::AcqRel) {
176            None
177        } else {
178            Some(Producer {
179                buf: self,
180                _not_sync: PhantomData,
181            })
182        }
183    }
184
185    /// Create the producer handle. Only one producer may be active.
186    ///
187    /// # Panics
188    /// Panics if a producer handle is already active.
189    #[inline]
190    pub fn producer(&self) -> Producer<'_, T, N> {
191        self.try_producer()
192            .expect("EventBuf: only one Producer may be active at a time")
193    }
194
195    /// Try to create the consumer handle.
196    ///
197    /// Returns `None` if a consumer is already active. Prefer this over
198    /// [`consumer`](Self::consumer) when fallible bring-up is needed.
199    #[inline]
200    pub fn try_consumer(&self) -> Option<Consumer<'_, T, N>> {
201        if self.consumer_taken.swap(true, Ordering::AcqRel) {
202            None
203        } else {
204            Some(Consumer {
205                buf: self,
206                _not_sync: PhantomData,
207            })
208        }
209    }
210
211    /// Create the consumer handle. Only one consumer may be active.
212    ///
213    /// # Panics
214    /// Panics if a consumer handle is already active.
215    #[inline]
216    pub fn consumer(&self) -> Consumer<'_, T, N> {
217        self.try_consumer()
218            .expect("EventBuf: only one Consumer may be active at a time")
219    }
220}
221
222impl<T: Copy, const N: usize> Default for EventBuf<T, N> {
223    fn default() -> Self {
224        Self::new()
225    }
226}
227
228impl<T: Copy, const N: usize> core::fmt::Debug for EventBuf<T, N> {
229    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
230        f.debug_struct("EventBuf")
231            .field("len", &self.len())
232            .field("capacity", &N)
233            .finish()
234    }
235}
236
237/// Write handle for an [`EventBuf`].
238///
239/// Dropping the producer releases the slot so a new one can be created.
240pub struct Producer<'a, T: Copy, const N: usize> {
241    buf: &'a EventBuf<T, N>,
242    _not_sync: PhantomData<Cell<()>>,
243}
244
245impl<T: Copy, const N: usize> Producer<'_, T, N> {
246    /// Try to push a value into the buffer.
247    ///
248    /// Returns `Ok(())` on success, or `Err(val)` if the buffer is full
249    /// (the value is returned to the caller so nothing is lost).
250    #[inline]
251    pub fn push(&self, val: T) -> Result<(), T> {
252        let head = self.buf.head.load(Ordering::Relaxed);
253        let tail = self.buf.tail.load(Ordering::Acquire);
254        if head.wrapping_sub(tail) as usize >= N {
255            return Err(val);
256        }
257        let idx = EventBuf::<T, N>::slot_index(head);
258        // SAFETY: producer is the only writer to this slot; the consumer
259        // will not read it until head is advanced (Release below).
260        self.buf.slots[idx].with_mut(|slot| unsafe { (*slot).write(val) });
261        self.buf.head.store(head.wrapping_add(1), Ordering::Release);
262        Ok(())
263    }
264}
265
266impl<T: Copy, const N: usize> Drop for Producer<'_, T, N> {
267    fn drop(&mut self) {
268        self.buf.producer_taken.store(false, Ordering::Release);
269    }
270}
271
272impl<T: Copy, const N: usize> core::fmt::Debug for Producer<'_, T, N> {
273    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
274        f.debug_struct("event_buf::Producer")
275            .field("capacity", &N)
276            .finish()
277    }
278}
279
280/// Read handle for an [`EventBuf`].
281///
282/// Dropping the consumer releases the slot so a new one can be created.
283pub struct Consumer<'a, T: Copy, const N: usize> {
284    buf: &'a EventBuf<T, N>,
285    _not_sync: PhantomData<Cell<()>>,
286}
287
288impl<T: Copy, const N: usize> Consumer<'_, T, N> {
289    /// Pop the oldest item from the buffer.
290    ///
291    /// Returns `None` if the buffer is empty.
292    #[inline]
293    pub fn pop(&self) -> Option<T> {
294        let tail = self.buf.tail.load(Ordering::Relaxed);
295        let head = self.buf.head.load(Ordering::Acquire);
296        if tail == head {
297            return None;
298        }
299        let idx = EventBuf::<T, N>::slot_index(tail);
300        // SAFETY: consumer is the only reader of this slot; the producer
301        // will not overwrite it until tail is advanced (Release below).
302        let val = self.buf.slots[idx].with(|slot| unsafe { (*slot).assume_init_read() });
303        self.buf.tail.store(tail.wrapping_add(1), Ordering::Release);
304        Some(val)
305    }
306
307    /// Copy the oldest item without removing it.
308    ///
309    /// Returns `None` if the buffer is empty. The consumer cursor is not
310    /// advanced, so a following [`pop`](Self::pop) returns the same value.
311    #[inline]
312    pub fn peek(&self) -> Option<T> {
313        let tail = self.buf.tail.load(Ordering::Relaxed);
314        let head = self.buf.head.load(Ordering::Acquire);
315        if tail == head {
316            return None;
317        }
318        let idx = EventBuf::<T, N>::slot_index(tail);
319        // SAFETY: same slot exclusivity as `pop` — the producer will not
320        // overwrite this slot until `tail` advances. `T: Copy`, so reading
321        // without advancing leaves a valid value for a later `pop`.
322        Some(self.buf.slots[idx].with(|slot| unsafe { (*slot).assume_init_read() }))
323    }
324
325    /// Drain up to `max` items, passing each to `hook`.
326    ///
327    /// Returns the number of items consumed.
328    #[inline]
329    pub fn drain(&self, max: usize, mut hook: impl FnMut(T)) -> usize {
330        let mut count = 0;
331        while count < max {
332            match self.pop() {
333                Some(val) => {
334                    hook(val);
335                    count += 1;
336                }
337                None => break,
338            }
339        }
340        count
341    }
342}
343
344impl<T: Copy, const N: usize> Drop for Consumer<'_, T, N> {
345    fn drop(&mut self) {
346        self.buf.consumer_taken.store(false, Ordering::Release);
347    }
348}
349
350impl<T: Copy, const N: usize> core::fmt::Debug for Consumer<'_, T, N> {
351    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
352        f.debug_struct("event_buf::Consumer")
353            .field("capacity", &N)
354            .finish()
355    }
356}
357
358impl<T: Copy, const N: usize> crate::traits::Sink<T> for Producer<'_, T, N> {
359    type Error = T;
360
361    #[inline]
362    fn try_push(&mut self, val: T) -> Result<(), T> {
363        self.push(val)
364    }
365}
366
367impl<T: Copy, const N: usize> crate::traits::Source<T> for Consumer<'_, T, N> {
368    #[inline]
369    fn try_pop(&mut self) -> Option<T> {
370        self.pop()
371    }
372}
373
374#[cfg(test)]
375mod tests {
376    use super::*;
377
378    #[test]
379    fn new_buf_is_empty() {
380        let buf = EventBuf::<u32, 4>::new();
381        assert!(buf.is_empty());
382        assert!(!buf.is_full());
383        assert_eq!(buf.len(), 0);
384        assert_eq!(buf.capacity(), 4);
385    }
386
387    #[test]
388    fn push_and_pop_fifo() {
389        let buf = EventBuf::<u32, 4>::new();
390        let p = buf.producer();
391        let c = buf.consumer();
392
393        assert!(p.push(10).is_ok());
394        assert!(p.push(20).is_ok());
395        assert!(p.push(30).is_ok());
396
397        assert_eq!(c.pop(), Some(10));
398        assert_eq!(c.pop(), Some(20));
399        assert_eq!(c.pop(), Some(30));
400        assert_eq!(c.pop(), None);
401    }
402
403    #[test]
404    fn push_rejects_when_full() {
405        let buf = EventBuf::<u32, 2>::new();
406        let p = buf.producer();
407        let c = buf.consumer();
408
409        assert!(p.push(1).is_ok());
410        assert!(p.push(2).is_ok());
411        assert_eq!(p.push(3), Err(3)); // full — value returned
412
413        // drain one, then push succeeds
414        assert_eq!(c.pop(), Some(1));
415        assert!(p.push(3).is_ok());
416    }
417
418    #[test]
419    fn drain_returns_count() {
420        let buf = EventBuf::<u32, 8>::new();
421        let p = buf.producer();
422        let c = buf.consumer();
423
424        for i in 0..5 {
425            p.push(i).unwrap();
426        }
427
428        let mut out = std::vec::Vec::new();
429        let n = c.drain(3, |v| out.push(v));
430        assert_eq!(n, 3);
431        assert_eq!(out, [0, 1, 2]);
432
433        // remaining
434        let n = c.drain(100, |v| out.push(v));
435        assert_eq!(n, 2);
436        assert_eq!(out, [0, 1, 2, 3, 4]);
437    }
438
439    #[test]
440    fn drain_on_empty_returns_zero() {
441        let buf = EventBuf::<u32, 4>::new();
442        let _p = buf.producer();
443        let c = buf.consumer();
444
445        let n = c.drain(10, |_| panic!("should not be called"));
446        assert_eq!(n, 0);
447    }
448
449    #[test]
450    fn producer_consumer_can_be_recreated() {
451        let buf = EventBuf::<u32, 4>::new();
452        {
453            let p = buf.producer();
454            p.push(1).unwrap();
455        }
456        // producer dropped — can create a new one
457        let p = buf.producer();
458        p.push(2).unwrap();
459
460        {
461            let c = buf.consumer();
462            assert_eq!(c.pop(), Some(1));
463        }
464        // consumer dropped — can create a new one
465        let c = buf.consumer();
466        assert_eq!(c.pop(), Some(2));
467        assert_eq!(c.pop(), None);
468    }
469
470    #[test]
471    #[should_panic(expected = "only one Producer")]
472    fn double_producer_panics() {
473        let buf = EventBuf::<u32, 4>::new();
474        let _p1 = buf.producer();
475        let _p2 = buf.producer();
476    }
477
478    #[test]
479    #[should_panic(expected = "only one Consumer")]
480    fn double_consumer_panics() {
481        let buf = EventBuf::<u32, 4>::new();
482        let _c1 = buf.consumer();
483        let _c2 = buf.consumer();
484    }
485
486    #[test]
487    fn wraps_around_correctly() {
488        let buf = EventBuf::<u32, 3>::new();
489        let p = buf.producer();
490        let c = buf.consumer();
491
492        // fill, drain, fill again — exercises the wrap
493        for round in 0u32..4 {
494            let base = round * 3;
495            for i in 0..3 {
496                assert!(p.push(base + i).is_ok());
497            }
498            assert_eq!(p.push(99), Err(99)); // full
499            for i in 0..3 {
500                assert_eq!(c.pop(), Some(base + i));
501            }
502            assert_eq!(c.pop(), None); // empty
503        }
504    }
505
506    #[test]
507    fn default_is_new() {
508        let buf: EventBuf<u8, 4> = EventBuf::default();
509        assert!(buf.is_empty());
510    }
511
512    #[test]
513    fn len_and_full_track_state() {
514        let buf = EventBuf::<u32, 3>::new();
515        let p = buf.producer();
516        let c = buf.consumer();
517
518        assert_eq!(buf.len(), 0);
519        assert!(buf.is_empty());
520
521        p.push(1).unwrap();
522        assert_eq!(buf.len(), 1);
523
524        p.push(2).unwrap();
525        p.push(3).unwrap();
526        assert_eq!(buf.len(), 3);
527        assert!(buf.is_full());
528
529        c.pop();
530        assert_eq!(buf.len(), 2);
531        assert!(!buf.is_full());
532    }
533
534    #[test]
535    fn len_stays_within_capacity_while_consumer_drains() {
536        let buf = EventBuf::<u32, 8>::new();
537        let done = AtomicBool::new(false);
538        let pushes = crate::test_support::iterations(200_000);
539
540        std::thread::scope(|scope| {
541            scope.spawn(|| {
542                let p = buf.producer();
543                for i in 0..pushes {
544                    let _ = p.push(i);
545                }
546                done.store(true, Ordering::Release);
547            });
548
549            scope.spawn(|| {
550                let c = buf.consumer();
551                while !done.load(Ordering::Acquire) {
552                    c.pop();
553                }
554            });
555
556            // `len` races both handles; it may be stale, but it must never
557            // report more than the buffer can hold.
558            while !done.load(Ordering::Acquire) {
559                let observed = buf.len();
560                assert!(
561                    observed <= buf.capacity(),
562                    "len() reported {observed} for a capacity-{} buffer",
563                    buf.capacity()
564                );
565            }
566        });
567    }
568
569    #[test]
570    fn concurrent_spsc_preserves_fifo_and_loses_nothing() {
571        let buf = EventBuf::<u32, 4>::new();
572        let total = crate::test_support::iterations(50_000);
573
574        let received = std::thread::scope(|scope| {
575            scope.spawn(|| {
576                let p = buf.producer();
577                // Backpressure means push can fail; retry so the stream is
578                // complete and any gap in the consumer's view is a real bug.
579                for i in 0..total {
580                    let mut val = i;
581                    while let Err(rejected) = p.push(val) {
582                        val = rejected;
583                        std::thread::yield_now();
584                    }
585                }
586            });
587
588            let consumer = scope.spawn(|| {
589                let c = buf.consumer();
590                let mut seen = 0u32;
591                while seen < total {
592                    match c.pop() {
593                        // Strict FIFO: the nth item popped must be n.
594                        Some(val) => {
595                            assert_eq!(val, seen, "out-of-order pop at index {seen}");
596                            seen += 1;
597                        }
598                        None => std::thread::yield_now(),
599                    }
600                }
601                seen
602            });
603
604            consumer.join().unwrap()
605        });
606
607        assert_eq!(received, total);
608        assert_eq!(buf.len(), 0);
609    }
610
611    #[test]
612    fn handles_are_send() {
613        fn assert_send<T: Send>() {}
614        assert_send::<super::Producer<'_, u32, 4>>();
615        assert_send::<super::Consumer<'_, u32, 4>>();
616    }
617
618    #[test]
619    fn try_producer_and_try_consumer() {
620        let buf = EventBuf::<u32, 4>::new();
621        let p = buf.try_producer().expect("first producer");
622        assert!(buf.try_producer().is_none());
623        let c = buf.try_consumer().expect("first consumer");
624        assert!(buf.try_consumer().is_none());
625        p.push(1).unwrap();
626        assert_eq!(c.pop(), Some(1));
627        drop(p);
628        drop(c);
629        assert!(buf.try_producer().is_some());
630        assert!(buf.try_consumer().is_some());
631    }
632
633    #[test]
634    fn peek_copies_without_advancing() {
635        let buf = EventBuf::<u32, 4>::new();
636        let p = buf.producer();
637        let c = buf.consumer();
638
639        assert_eq!(c.peek(), None);
640        p.push(10).unwrap();
641        p.push(20).unwrap();
642        assert_eq!(c.peek(), Some(10));
643        assert_eq!(c.peek(), Some(10));
644        assert_eq!(buf.len(), 2);
645        assert_eq!(c.pop(), Some(10));
646        assert_eq!(c.peek(), Some(20));
647        assert_eq!(c.pop(), Some(20));
648        assert_eq!(c.peek(), None);
649    }
650}