Skip to main content

proxima_core/
buffer.rs

1//! `BufferPool`/`PooledBuf` — per-worker byte-buffer reservoir. Folded in
2//! from the former `proxima-lib` satellite crate: BufferPool leans on
3//! `thread_local::ThreadLocal` for its per-worker routing, which has no
4//! no_std/alloc analog (it resolves OS thread identity), so the whole
5//! pool is std-only; alloc-only builds carry no public API here.
6
7#[cfg(feature = "std")]
8use alloc::sync::Arc;
9
10#[cfg(feature = "std")]
11use bytes::BytesMut;
12#[cfg(feature = "std")]
13use crossbeam_queue::ArrayQueue;
14#[cfg(feature = "std")]
15use thread_local::ThreadLocal;
16
17#[cfg(feature = "std")]
18pub const DEFAULT_POOL_PER_WORKER: usize = 256;
19#[cfg(feature = "std")]
20pub const DEFAULT_BUFFER_BYTES: usize = 16 * 1024;
21
22/// Per-worker `BytesMut` reservoir backed by a thread-local
23/// `ArrayQueue`. Lock-free in the steady state because each queue is
24/// only ever touched by one worker.
25#[cfg(feature = "std")]
26pub struct BufferPool {
27    inner: Arc<BufferPoolInner>,
28}
29
30#[cfg(feature = "std")]
31struct BufferPoolInner {
32    per_worker: ThreadLocal<ArrayQueue<BytesMut>>,
33    capacity_per_worker: usize,
34    buffer_bytes: usize,
35}
36
37#[cfg(feature = "std")]
38impl BufferPool {
39    #[must_use]
40    pub fn new() -> Self {
41        Self::with_config(DEFAULT_POOL_PER_WORKER, DEFAULT_BUFFER_BYTES)
42    }
43
44    #[must_use]
45    pub fn with_config(per_worker_capacity: usize, buffer_bytes: usize) -> Self {
46        Self {
47            inner: Arc::new(BufferPoolInner {
48                per_worker: ThreadLocal::new(),
49                capacity_per_worker: per_worker_capacity.max(1),
50                buffer_bytes: buffer_bytes.max(1),
51            }),
52        }
53    }
54
55    pub fn acquire(&self) -> PooledBuf {
56        let queue = self
57            .inner
58            .per_worker
59            .get_or(|| ArrayQueue::new(self.inner.capacity_per_worker));
60        let buffer = queue
61            .pop()
62            .map(|mut buf| {
63                buf.clear();
64                buf
65            })
66            .unwrap_or_else(|| BytesMut::with_capacity(self.inner.buffer_bytes));
67        PooledBuf {
68            inner: buffer,
69            pool: self.inner.clone(),
70            return_on_drop: true,
71        }
72    }
73
74    #[must_use]
75    pub fn buffer_bytes(&self) -> usize {
76        self.inner.buffer_bytes
77    }
78
79    #[must_use]
80    pub fn local_inventory(&self) -> usize {
81        match self.inner.per_worker.get() {
82            Some(queue) => queue.len(),
83            None => 0,
84        }
85    }
86}
87
88#[cfg(feature = "std")]
89impl Default for BufferPool {
90    fn default() -> Self {
91        Self::new()
92    }
93}
94
95#[cfg(feature = "std")]
96impl Clone for BufferPool {
97    fn clone(&self) -> Self {
98        Self {
99            inner: self.inner.clone(),
100        }
101    }
102}
103
104#[cfg(feature = "std")]
105pub struct PooledBuf {
106    inner: BytesMut,
107    pool: Arc<BufferPoolInner>,
108    return_on_drop: bool,
109}
110
111#[cfg(feature = "std")]
112impl PooledBuf {
113    /// Borrow the buffer mutably. The buffer is cleared before being handed
114    /// out, so the caller starts with `len() == 0` and the prior allocation.
115    pub fn buffer_mut(&mut self) -> &mut BytesMut {
116        &mut self.inner
117    }
118
119    /// Take ownership of the inner `BytesMut`. After `take`, the `PooledBuf`
120    /// is dropped without returning anything to the pool.
121    pub fn take(mut self) -> BytesMut {
122        self.return_on_drop = false;
123        core::mem::take(&mut self.inner)
124    }
125}
126
127#[cfg(feature = "std")]
128impl Drop for PooledBuf {
129    fn drop(&mut self) {
130        if !self.return_on_drop {
131            return;
132        }
133        let buffer = core::mem::take(&mut self.inner);
134        let queue = self
135            .pool
136            .per_worker
137            .get_or(|| ArrayQueue::new(self.pool.capacity_per_worker));
138        // push fails (returns Err with the buffer) when the queue is at capacity;
139        // dropping the returned buffer frees it through normal allocator path.
140        let _ = queue.push(buffer);
141    }
142}
143
144#[cfg(all(test, feature = "std"))]
145#[allow(clippy::unwrap_used, clippy::expect_used)]
146mod tests {
147    use super::*;
148
149    #[test]
150    fn acquire_returns_buffer_with_zero_length() {
151        let pool = BufferPool::new();
152        let buf = pool.acquire();
153        assert_eq!(buf.inner.len(), 0);
154    }
155
156    #[test]
157    fn returned_buffer_is_reused_by_next_acquire_on_same_thread() {
158        let pool = BufferPool::with_config(4, 64);
159        let mut first = pool.acquire();
160        first.buffer_mut().extend_from_slice(b"abc");
161        let initial_capacity = first.inner.capacity();
162        drop(first);
163        assert_eq!(pool.local_inventory(), 1);
164        let second = pool.acquire();
165        assert_eq!(
166            second.inner.capacity(),
167            initial_capacity,
168            "returned buffer must be reused with its allocated capacity",
169        );
170        assert_eq!(pool.local_inventory(), 0);
171    }
172
173    #[test]
174    fn capacity_overflow_drops_extra_buffers() {
175        let pool = BufferPool::with_config(2, 64);
176        let one = pool.acquire();
177        let two = pool.acquire();
178        let three = pool.acquire();
179        drop(one);
180        drop(two);
181        drop(three);
182        assert!(
183            pool.local_inventory() <= 2,
184            "local queue must respect its capacity"
185        );
186    }
187
188    #[test]
189    fn take_consumes_buffer_without_returning_to_pool() {
190        let pool = BufferPool::with_config(4, 64);
191        let buf = pool.acquire();
192        let _owned = buf.take();
193        assert_eq!(pool.local_inventory(), 0);
194    }
195}