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