Skip to main content

nexus_logbuf/
lib.rs

1//! High-performance lock-free ring buffers for variable-length messages.
2//!
3//! This crate provides bounded SPSC and MPSC byte ring buffers optimized for
4//! getting data off the hot path without disturbing it. No allocation, no
5//! formatting, no syscalls on the producer side.
6//!
7//! # Modules
8//!
9//! - [`queue`]: Low-level ring buffer primitives. No blocking, maximum control.
10//! - [`channel`]: Ergonomic channel API with backoff and parking for receivers.
11//!
12//! # Design
13//!
14//! - **Flat byte buffer** with free-running offsets, power-of-2 capacity
15//! - **len-as-commit**: Record's len field is the commit marker (non-zero = ready)
16//! - **Skip markers**: High bit of len distinguishes padding/aborted claims
17//! - **Consumer zeroing**: Consumer zeros records before releasing space
18//! - **Claim-based API**: `WriteClaim`/`ReadClaim` with RAII semantics
19//!
20//! # Channel Philosophy
21//!
22//! **Senders are never slowed down.** They use brief backoff (spin + yield) but
23//! never syscall. If the buffer is full, they return an error immediately.
24//!
25//! **Receivers can block.** They use `park_timeout` to wait for messages without
26//! burning CPU, but always with a timeout to check for disconnection.
27//!
28//! # Example (Queue API)
29//!
30//! ```
31//! use nexus_logbuf::queue::spsc;
32//!
33//! let (mut producer, mut consumer) = spsc::new(4096);
34//!
35//! // Producer (hot path)
36//! let payload = b"hello world";
37//! if let Ok(mut claim) = producer.try_claim(payload.len()) {
38//!     claim.copy_from_slice(payload);
39//!     claim.commit();
40//! }
41//!
42//! // Consumer (background thread)
43//! if let Some(record) = consumer.try_claim() {
44//!     assert_eq!(&*record, b"hello world");
45//!     // record dropped here -> zeros region, advances head
46//! }
47//! ```
48
49pub mod channel;
50pub mod queue;
51
52// Re-export for convenience (queue is the primitive layer)
53pub use queue::mpsc;
54pub use queue::spsc;
55
56/// Error returned from queue `try_claim` operations.
57#[derive(Debug, Clone, Copy, PartialEq, Eq)]
58pub enum TryClaimError {
59    /// The buffer is full.
60    Full,
61    /// The payload length was zero.
62    ZeroLength,
63}
64
65impl std::fmt::Display for TryClaimError {
66    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
67        match self {
68            Self::Full => write!(f, "buffer full"),
69            Self::ZeroLength => write!(f, "payload length must be non-zero"),
70        }
71    }
72}
73
74impl std::error::Error for TryClaimError {}
75
76/// Align a value up to the next multiple of 8.
77#[inline]
78pub(crate) const fn align8(n: usize) -> usize {
79    (n + 7) & !7
80}
81
82/// Record header constants.
83///
84/// The len field uses the high bit as a skip marker:
85/// - `len == 0`: Not committed, consumer waits
86/// - `len > 0, high bit clear`: Committed record, payload is `len` bytes
87/// - `len high bit set`: Skip marker, advance by `len & LEN_MASK` bytes
88pub(crate) const SKIP_BIT: u32 = 0x8000_0000;
89pub(crate) const LEN_MASK: u32 = 0x7FFF_FFFF;
90
91#[cfg(test)]
92mod tests {
93    use super::*;
94
95    #[test]
96    fn align8_works() {
97        assert_eq!(align8(0), 0);
98        assert_eq!(align8(1), 8);
99        assert_eq!(align8(7), 8);
100        assert_eq!(align8(8), 8);
101        assert_eq!(align8(9), 16);
102        assert_eq!(align8(15), 16);
103        assert_eq!(align8(16), 16);
104    }
105}