Skip to main content

hydra_sync/
channel.rs

1//! A single-producer, single-consumer (SPSC) `lock-free` ring buffer for [`Bytes`](https://crates.io/crates/bytes) handles.
2use anyhow::Result;
3use bytes::Bytes;
4use std::cell::UnsafeCell;
5use std::mem::MaybeUninit;
6use std::ops::{Deref, DerefMut};
7use std::sync::Arc;
8use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
9use tokio::sync::Notify;
10
11#[allow(unused)]
12/// Cache line ~size on x86_64 / ARM.
13const CACHE_LINE: usize = 64;
14
15/// Forces a value onto its own cache line, preventing false sharing between threads.
16#[repr(C, align(64))]
17struct CachePadded<T>(T);
18
19impl<T> CachePadded<T> {
20    #[inline(always)]
21    const fn pad(val: T) -> Self {
22        Self(val)
23    }
24}
25
26impl<T> Deref for CachePadded<T> {
27    type Target = T;
28    #[inline(always)]
29    fn deref(&self) -> &T {
30        &self.0
31    }
32}
33
34impl<T> DerefMut for CachePadded<T> {
35    #[inline(always)]
36    fn deref_mut(&mut self) -> &mut T {
37        &mut self.0
38    }
39}
40
41/// The ring buffer is full; the push was rejected.
42#[derive(Debug, Clone, Copy, PartialEq, Eq)]
43pub struct FullError;
44impl std::error::Error for FullError {}
45impl std::fmt::Display for FullError {
46    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
47        write!(f, "ring buffer full")
48    }
49}
50
51/// The ring buffer is empty; the pop was rejected.
52#[derive(Debug, Clone, Copy, PartialEq, Eq)]
53pub struct EmptyError;
54impl std::error::Error for EmptyError {}
55impl std::fmt::Display for EmptyError {
56    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
57        write!(f, "ring buffer empty")
58    }
59}
60
61/// The peer handle was dropped; the channel is disconnected.
62#[derive(Debug, Clone, Copy, PartialEq, Eq)]
63pub struct ClosedError;
64impl std::error::Error for ClosedError {}
65impl std::fmt::Display for ClosedError {
66    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
67        write!(f, "ring buffer peer dropped")
68    }
69}
70
71/// Single ring buffer slot handle. (Wrapped in `UnsafeCell` because we can't trust the rust compiler)
72type Slot = UnsafeCell<MaybeUninit<Bytes>>;
73
74/// **Optimized** SPSC ring buffer.
75///
76/// Each slot stores immutable `Bytes` handle (24 bytes), `push/pop` move the handle —
77/// the ring does not copy packet payload data. [Reference](https://rigtorp.se/ringbuffer/)
78pub struct Ring {
79    slots: Box<[Slot]>,
80    capacity: usize,
81    head: CachePadded<AtomicUsize>,
82    tail: CachePadded<AtomicUsize>,
83    /// Set when either handle is dropped; peers check this to detect disconnects.
84    closed: CachePadded<AtomicBool>,
85    /// Wakes a consumer blocked on an empty ring (`pop_async`).
86    data_available: CachePadded<Arc<Notify>>,
87    /// Wakes a producer blocked on a full ring (`push_async`).
88    space_available: CachePadded<Arc<Notify>>,
89}
90
91unsafe impl Send for Ring {}
92unsafe impl Sync for Ring {}
93
94impl Ring {
95    #[inline]
96    /// Create a ring buffer with the given `capacity`,
97    /// (must be a `power of two`, or it will choose `next power of two`) with minimum of capacity 2.
98    pub fn create(mut capacity: usize) -> Result<Self> {
99        if !capacity.is_power_of_two() {
100            capacity = capacity.next_power_of_two();
101        }
102        capacity = capacity.max(2); // clamp at min 2
103
104        let slots: Box<[Slot]> = (0..capacity)
105            .map(|_| UnsafeCell::new(MaybeUninit::uninit()))
106            .collect();
107
108        Ok(Self {
109            slots,
110            capacity,
111            head: CachePadded::pad(AtomicUsize::new(0)),
112            tail: CachePadded::pad(AtomicUsize::new(0)),
113            closed: CachePadded::pad(AtomicBool::new(false)),
114            data_available: CachePadded::pad(Arc::new(Notify::new())),
115            space_available: CachePadded::pad(Arc::new(Notify::new())),
116        })
117    }
118
119    #[inline(always)]
120    /// Cheap owned handle to await `space_available` without holding any ring reference.
121    pub fn space_notify(&self) -> Arc<Notify> {
122        Arc::clone(&self.space_available)
123    }
124
125    #[inline(always)]
126    /// Cheap owned handle to await `data_available` without holding any ring reference.
127    pub fn data_notify(&self) -> Arc<Notify> {
128        Arc::clone(&self.data_available)
129    }
130
131    #[inline]
132    /// Split into a [`Producer`] and [`Consumer`] handles.
133    pub fn into_split(self) -> (Producer, Consumer) {
134        let ring = Arc::new(self);
135        (
136            Producer {
137                ring: Arc::clone(&ring),
138                head_local: 0,
139                tail_cached: 0,
140            },
141            Consumer {
142                ring,
143                tail_local: 0,
144                head_cached: 0,
145            },
146        )
147    }
148
149    #[inline]
150    /// Approximate number of occupied slots `(maybe stale)`.
151    pub fn len(&self) -> usize {
152        let h = self.head.load(Ordering::Relaxed);
153        let t = self.tail.load(Ordering::Relaxed);
154        (h.wrapping_sub(t)) & (self.capacity - 1)
155    }
156
157    #[inline]
158    /// Returns `true` if the buffer appears full `(maybe stale)`.
159    pub fn is_full(&self) -> bool {
160        let h = self.head.load(Ordering::Relaxed);
161        let t = self.tail.load(Ordering::Relaxed);
162        ((h + 1) & (self.capacity - 1)) == t
163    }
164
165    #[inline]
166    /// Returns `true` if the buffer appears empty `(maybe stale)`.
167    pub fn is_empty(&self) -> bool {
168        self.head.load(Ordering::Relaxed) == self.tail.load(Ordering::Relaxed)
169    }
170
171    #[inline]
172    /// Returns `true` if either handle has been dropped `(peer disconnected)`.
173    pub fn is_closed(&self) -> bool {
174        self.closed.load(Ordering::Acquire)
175    }
176
177    #[inline]
178    /// Approximate number of free slots `(maybe stale)`.
179    pub fn free_slots(&self) -> usize {
180        self.capacity - 1 - self.len()
181    }
182
183    #[inline]
184    /// Total capacity of the ring buffer `(including the wasted sentinel slot)`.
185    pub fn capacity(&self) -> usize {
186        self.capacity
187    }
188
189    #[inline(always)]
190    /// Raw mutable pointer to the slot array.
191    ///
192    /// SAFETY: `idx < capacity` always (masked by caller).
193    /// `UnsafeCell` ensures that rust compiler won't do some UB stuffs,
194    /// `(it gives a proper mutable pointer to the slot)`.
195    fn slot_ptr(&self, idx: usize) -> *mut MaybeUninit<Bytes> {
196        unsafe { self.slots.get_unchecked(idx).get() }
197    }
198}
199
200impl Drop for Ring {
201    fn drop(&mut self) {
202        let h = *self.head.get_mut();
203        let t = *self.tail.get_mut();
204        let mut idx = t;
205        while idx != h {
206            unsafe {
207                (*self.slots[idx].get()).assume_init_drop();
208            }
209            idx = (idx + 1) & (self.capacity - 1);
210        }
211    }
212}
213
214/// **Write half** of a ring buffer, only one `Producer` may exist per ring.
215pub struct Producer {
216    /// Shared ring buffer.
217    ring: Arc<Ring>,
218    /// Producer's exclusive local copy of the head index.
219    /// (only the producer ever writes `head`, so this never goes stale)
220    head_local: usize,
221    /// Producer's cached copy of the consumer's tail index.
222    tail_cached: usize,
223}
224
225impl Producer {
226    /// Push `Bytes` by ownership. Returns `Err(FullError)` if the ring is full.
227    /// The `Bytes` handle is moved into the ring slot (no payload data copy).
228    #[inline(always)]
229    pub fn push(&mut self, data: Bytes) -> Result<(), FullError> {
230        let head = self.head_local;
231        let next = (head + 1) & (self.ring.capacity - 1);
232
233        // check if full by comparing the next head index with the cached tail index
234        if next == self.tail_cached {
235            self.tail_cached = self.ring.tail.load(Ordering::Acquire);
236            if next == self.tail_cached {
237                return Err(FullError);
238            }
239        }
240
241        // write slots in place
242        unsafe {
243            self.ring.slot_ptr(head).write(MaybeUninit::new(data));
244        }
245
246        // update the head index with 'release ordering',
247        // so that the data is visible to the consumer
248        self.head_local = next;
249        self.ring.head.store(next, Ordering::Release);
250        // wake a consumer blocked in `pop_async` (permit stored if none waiting)
251        self.ring.data_available.notify_one();
252        Ok(())
253    }
254
255    #[inline(always)]
256    /// Similar to [`push`](Self::push), but takes a slice of bytes, which `allocates and copies the data`.
257    pub fn push_bytes(&mut self, data: &[u8]) -> Result<(), FullError> {
258        self.push(Bytes::copy_from_slice(data)) // <- this allocates and copies the data
259    }
260
261    #[inline(always)]
262    /// Push as many packets as possible, returns the `number of packets` pushed.
263    pub fn push_batch(&mut self, packets: &[Bytes]) -> usize {
264        if packets.is_empty() {
265            return 0;
266        }
267
268        let head = self.head_local;
269        let cap_mask = self.ring.capacity - 1;
270
271        let mut used = (head.wrapping_sub(self.tail_cached)) & cap_mask;
272        let mut free = (self.ring.capacity - 1) - used;
273
274        // if the cached tail index is stale, refresh it and recalculate free slots
275        if free < packets.len() {
276            self.tail_cached = self.ring.tail.load(Ordering::Acquire);
277            used = (head.wrapping_sub(self.tail_cached)) & cap_mask;
278            free = (self.ring.capacity - 1) - used;
279        }
280
281        // push packets, up to the number of free slots
282        let n = packets.len().min(free);
283        if n == 0 {
284            return 0;
285        }
286
287        // write all packets in place, wrapping around if necessary
288        #[allow(clippy::needless_range_loop)]
289        for i in 0..n {
290            let idx = (head + i) & cap_mask;
291            unsafe {
292                self.ring
293                    .slot_ptr(idx)
294                    .write(MaybeUninit::new(packets.get_unchecked(i).clone())); // TODO: this clone can be copy or atomic ref count
295            }
296        }
297
298        // update the head index
299        self.head_local = (head + n) & cap_mask;
300        self.ring.head.store(self.head_local, Ordering::Release);
301        n
302    }
303
304    /// Async push: resolves once `data` has a slot. Waits on a [`Notify`] when
305    /// full instead of spinning, returns `Err(ClosedError)` if the consumer was
306    /// dropped. The lock-free fast path is identical to [`push`](Self::push).
307    pub async fn push_async(&mut self, data: Bytes) -> Result<(), ClosedError> {
308        loop {
309            if self.try_push_shared(&data) {
310                return Ok(());
311            }
312            if self.ring.closed.load(Ordering::Acquire) {
313                return Err(ClosedError);
314            }
315            self.ring.space_available.notified().await;
316        }
317    }
318
319    #[inline(always)]
320    /// Like [`push`](Self::push), but clones `src` into the slot **only after** capacity is confirmed.
321    fn try_push_shared(&mut self, src: &Bytes) -> bool {
322        let head = self.head_local;
323        let next = (head + 1) & (self.ring.capacity - 1);
324
325        if next == self.tail_cached {
326            self.tail_cached = self.ring.tail.load(Ordering::Acquire);
327            if next == self.tail_cached {
328                return false;
329            }
330        }
331
332        unsafe {
333            self.ring
334                .slot_ptr(head)
335                .write(MaybeUninit::new(src.clone()));
336        }
337        // both push paths MUST advance `head_local` together with the shared
338        // atomic, or a later sync `push` would recompute a stale index and
339        // overwrite a live slot
340        self.head_local = next;
341        self.ring.head.store(next, Ordering::Release);
342        // wake a consumer blocked in `pop_async`
343        self.ring.data_available.notify_one();
344        true
345    }
346
347    #[inline(always)]
348    /// Approximate number of occupied slots `(maybe stale)`.
349    pub fn len(&self) -> usize {
350        (self.head_local.wrapping_sub(self.tail_cached)) & (self.ring.capacity - 1)
351    }
352
353    #[inline(always)]
354    /// Returns `true` if the ring appears full `(maybe stale)`.
355    pub fn is_full(&self) -> bool {
356        self.len() == self.ring.capacity - 1
357    }
358
359    #[inline(always)]
360    /// Returns `true` if the ring appears empty `(maybe stale)`.
361    pub fn is_empty(&self) -> bool {
362        self.len() == 0
363    }
364
365    #[inline(always)]
366    /// Returns `true` if the consumer handle has been dropped `(zombie ring)`.
367    pub fn is_closed(&self) -> bool {
368        self.ring.is_closed()
369    }
370
371    #[inline(always)]
372    /// Cheap owned handle to await space-available **without** holding this `Producer`.
373    pub fn space_notify(&self) -> Arc<Notify> {
374        self.ring.space_notify()
375    }
376
377    /// Approximate number of free slots `(maybe stale)`.
378    #[inline]
379    pub fn free_slots(&self) -> usize {
380        (self.ring.capacity - 1) - self.len()
381    }
382
383    /// Usable capacity `(total slots minus sentinel)`.
384    #[inline]
385    pub fn capacity(&self) -> usize {
386        self.ring.capacity - 1
387    }
388}
389
390impl Drop for Producer {
391    fn drop(&mut self) {
392        self.ring.closed.store(true, Ordering::Release);
393        // wake any consumer blocked on pop so it can observe the disconnect;
394        // late arrivals check the flag themselves before awaiting.
395        self.ring.data_available.notify_waiters();
396    }
397}
398
399/// **Read half** of a ring buffer, only one `Consumer` may exist per ring.
400pub struct Consumer {
401    /// Shared ring buffer.
402    ring: Arc<Ring>,
403    /// Consumer's exclusive local copy of the tail index.
404    /// (only the consumer ever writes `tail`, so this never goes stale)
405    tail_local: usize,
406    /// Consumer's cached copy of the producer's head index.
407    head_cached: usize,
408}
409
410impl Consumer {
411    /// Pop a single `Bytes`. Returns `Err(EmptyError)` if the ring is empty.
412    #[inline(always)]
413    pub fn pop(&mut self) -> Result<Bytes, EmptyError> {
414        let tail = self.tail_local;
415
416        // check against the cached head index to see if the ring is empty
417        if tail == self.head_cached {
418            self.head_cached = self.ring.head.load(Ordering::Acquire);
419            if tail == self.head_cached {
420                return Err(EmptyError);
421            }
422        }
423
424        // pull data
425        let data = unsafe { self.ring.slot_ptr(tail).read().assume_init() };
426
427        // update the tail index with 'release ordering',
428        // so that the producer sees the slot as free
429        let next = (tail + 1) & (self.ring.capacity - 1);
430        self.tail_local = next;
431        self.ring.tail.store(next, Ordering::Release);
432        // wake a producer blocked in `push_async` (permit stored if none waiting)
433        self.ring.space_available.notify_one();
434        Ok(data)
435    }
436
437    /// Pop up to `buf.len()` packets into pre-allocated `Bytes` slots.
438    /// Returns the `number of packets` popped.
439    #[inline(always)]
440    pub fn pop_batch(&mut self, buf: &mut [Bytes]) -> usize {
441        if buf.is_empty() {
442            return 0;
443        }
444
445        let tail = self.tail_local;
446        let cap_mask = self.ring.capacity - 1;
447
448        let mut available = (self.head_cached.wrapping_sub(tail)) & cap_mask;
449
450        // check cached head index, if it's stale, refresh it and recalculate available slots
451        if available < buf.len() {
452            self.head_cached = self.ring.head.load(Ordering::Acquire);
453            available = (self.head_cached.wrapping_sub(tail)) & cap_mask;
454        }
455
456        let n = buf.len().min(available);
457        if n == 0 {
458            return 0;
459        }
460
461        // pull data
462        for i in 0..n {
463            let idx = (tail + i) & cap_mask;
464            unsafe {
465                // safety; we have already checked that there are enough available slots
466                *buf.get_unchecked_mut(i) = self.ring.slot_ptr(idx).read().assume_init();
467            }
468        }
469
470        // update & return
471        self.tail_local = (tail + n) & cap_mask;
472        self.ring.tail.store(self.tail_local, Ordering::Release);
473        n
474    }
475
476    /// Async pulls the next packet. Waits on a [`Notify`] when
477    /// empty instead of spinning, returns `Err(ClosedError)` if the producer
478    /// was dropped. The lock-free fast path is identical to [`pop`](Self::pop).
479    pub async fn pop_async(&mut self) -> Result<Bytes, ClosedError> {
480        loop {
481            if let Ok(data) = self.pop() {
482                return Ok(data);
483            }
484            if self.ring.closed.load(Ordering::Acquire) {
485                return Err(ClosedError);
486            }
487            self.ring.data_available.notified().await;
488        }
489    }
490
491    #[inline(always)]
492    /// Approximate number of occupied slots (maybe stale).
493    pub fn len(&self) -> usize {
494        (self.head_cached.wrapping_sub(self.tail_local)) & (self.ring.capacity - 1)
495    }
496
497    #[inline]
498    /// Returns `true` if the ring appears empty `(maybe stale)`.
499    pub fn is_empty(&self) -> bool {
500        self.len() == 0
501    }
502
503    #[inline(always)]
504    /// Returns `true` if the producer handle has been dropped.
505    pub fn is_closed(&self) -> bool {
506        self.ring.is_closed()
507    }
508
509    #[inline]
510    /// Approximate number of available items `(maybe stale)`.
511    pub fn available(&self) -> usize {
512        self.len()
513    }
514
515    #[inline]
516    /// Usable capacity `(total slots minus sentinel)`.
517    pub fn capacity(&self) -> usize {
518        self.ring.capacity - 1
519    }
520}
521
522impl Drop for Consumer {
523    fn drop(&mut self) {
524        self.ring.closed.store(true, Ordering::Release);
525        // wake any producer blocked on push so it can observe the disconnect;
526        // late arrivals check the flag themselves before awaiting.
527        self.ring.space_available.notify_waiters();
528    }
529}