Skip to main content

ipfrs_tensorlogic/
tensor_pool.rs

1//! Slab-based Reusable Buffer Pool for Arrow IPC Zero-Copy Tensor Operations
2//!
3//! This module provides a `TensorPool` — a production-grade, thread-safe buffer pool
4//! organized into power-of-two size buckets. It is designed for zero-copy Arrow IPC
5//! tensor operations where repeated allocation/deallocation of large byte buffers
6//! is a key performance bottleneck.
7//!
8//! # Size Classes
9//!
10//! | Bucket | Min Size | Max Size |
11//! |--------|----------|----------|
12//! |   0    |    0 B   |   255 B  |  (allocated as 256 B)
13//! |   1    |  256 B   |   511 B  |  (allocated as 512 B)
14//! |   2    |  512 B   |  1023 B  |  (allocated as 1 KiB)
15//! |   3    |   1 KiB  |  2047 B  |  (allocated as 2 KiB)
16//! |   4    |   2 KiB  |  4095 B  |  (allocated as 4 KiB)
17//! |   5    |   4 KiB  |  8191 B  |  (allocated as 8 KiB)
18//! |   6    |   8 KiB  | 16383 B  |  (allocated as 16 KiB)
19//! |   7    |  16 KiB  |  ∞       |  (allocated as exactly requested, up to 32 MiB cap)
20//!
21//! # Example
22//!
23//! ```
24//! use ipfrs_tensorlogic::tensor_pool::{TensorPool, TensorPoolConfig};
25//!
26//! let pool = TensorPool::new(TensorPoolConfig::default());
27//!
28//! // Acquire a buffer that fits at least 1000 bytes
29//! let mut buf = pool.acquire(1000);
30//! buf.resize(1000, 0u8);
31//! assert_eq!(buf.len(), 1000);
32//!
33//! // Return buffer to pool
34//! pool.release(buf);
35//!
36//! // Next acquire should reuse the buffer
37//! let buf2 = pool.acquire(1000);
38//! let snap = pool.stats();
39//! assert_eq!(snap.total_reuses, 1);
40//! pool.release(buf2);
41//! ```
42
43use std::sync::atomic::{AtomicU64, Ordering};
44use std::sync::Mutex;
45
46// ---------------------------------------------------------------------------
47// Constants
48// ---------------------------------------------------------------------------
49
50/// Number of buckets (power-of-two size classes)
51pub const NUM_BUCKETS: usize = 8;
52
53/// Minimum size for bucket 0 (256 bytes)
54const BUCKET_MIN_SIZE: usize = 256;
55
56/// Size threshold above which everything is routed to bucket 7 (32 MiB).
57#[allow(dead_code)]
58pub const BUCKET_7_THRESHOLD: usize = 32 * 1024 * 1024;
59
60// ---------------------------------------------------------------------------
61// bucket_for helper
62// ---------------------------------------------------------------------------
63
64/// Returns the bucket index (0..=7) for a given size.
65///
66/// Bucket 0 holds buffers sized up to and including 256 B, bucket 1 up to 512 B, …
67/// Bucket 7 holds everything > 16 KiB (including up to and above 32 MiB).
68///
69/// # Arguments
70/// * `size` — the minimum number of bytes required
71///
72/// # Returns
73/// Bucket index in `0..NUM_BUCKETS`.
74///
75/// # Examples
76/// ```
77/// use ipfrs_tensorlogic::tensor_pool::bucket_for;
78/// assert_eq!(bucket_for(0), 0);
79/// assert_eq!(bucket_for(256), 0);  // exactly fits bucket 0
80/// assert_eq!(bucket_for(257), 1);  // spills into bucket 1
81/// assert_eq!(bucket_for(32 * 1024 * 1024), 7);
82/// ```
83pub fn bucket_for(size: usize) -> usize {
84    // Map size → smallest power-of-two bucket that can hold it.
85    // Bucket 0 → capacity 256
86    // Bucket k → capacity 256 << k
87    // Bucket 7 → capacity 256 << 7 = 32 MiB (anything ≥ 32 MiB also lands here)
88    if size == 0 {
89        return 0;
90    }
91    // Walk up from bucket 0
92    for bucket in 0..NUM_BUCKETS {
93        let bucket_capacity = BUCKET_MIN_SIZE << bucket;
94        if size <= bucket_capacity {
95            return bucket;
96        }
97    }
98    // size > 32 MiB — goes to the last bucket
99    NUM_BUCKETS - 1
100}
101
102/// Returns the capacity that a buffer in the given bucket should have.
103#[inline]
104fn capacity_for_bucket(bucket: usize) -> usize {
105    BUCKET_MIN_SIZE << bucket.min(NUM_BUCKETS - 1)
106}
107
108// ---------------------------------------------------------------------------
109// TensorPoolStats
110// ---------------------------------------------------------------------------
111
112/// Atomic counters tracking pool activity.
113///
114/// All fields use `AtomicU64` and `Relaxed` ordering for maximum throughput.
115/// Call [`TensorPoolStats::snapshot`] to obtain a consistent plain-struct view.
116pub struct TensorPoolStats {
117    /// Total number of times `acquire` was called
118    pub total_acquired: AtomicU64,
119    /// Total number of times `release` was called
120    pub total_released: AtomicU64,
121    /// Fresh allocations that bypassed the pool (pool was empty for that bucket)
122    pub total_allocs: AtomicU64,
123    /// Pool-hit reuses (buffer served from the free list)
124    pub total_reuses: AtomicU64,
125    /// Running total of bytes currently held by all pooled buffers
126    pub total_bytes_pooled: AtomicU64,
127}
128
129impl Default for TensorPoolStats {
130    fn default() -> Self {
131        Self {
132            total_acquired: AtomicU64::new(0),
133            total_released: AtomicU64::new(0),
134            total_allocs: AtomicU64::new(0),
135            total_reuses: AtomicU64::new(0),
136            total_bytes_pooled: AtomicU64::new(0),
137        }
138    }
139}
140
141impl TensorPoolStats {
142    /// Capture a consistent snapshot of all counters.
143    pub fn snapshot(&self) -> TensorPoolSnapshot {
144        TensorPoolSnapshot {
145            total_acquired: self.total_acquired.load(Ordering::Relaxed),
146            total_released: self.total_released.load(Ordering::Relaxed),
147            total_allocs: self.total_allocs.load(Ordering::Relaxed),
148            total_reuses: self.total_reuses.load(Ordering::Relaxed),
149            total_bytes_pooled: self.total_bytes_pooled.load(Ordering::Relaxed),
150        }
151    }
152}
153
154// ---------------------------------------------------------------------------
155// TensorPoolSnapshot
156// ---------------------------------------------------------------------------
157
158/// Plain-struct snapshot of [`TensorPoolStats`].
159#[derive(Debug, Clone, PartialEq, Eq)]
160pub struct TensorPoolSnapshot {
161    /// Total number of times `acquire` was called
162    pub total_acquired: u64,
163    /// Total number of times `release` was called
164    pub total_released: u64,
165    /// Fresh allocations that bypassed the pool
166    pub total_allocs: u64,
167    /// Pool-hit reuses
168    pub total_reuses: u64,
169    /// Running total of bytes currently held by all pooled buffers
170    pub total_bytes_pooled: u64,
171}
172
173// ---------------------------------------------------------------------------
174// TensorPoolConfig
175// ---------------------------------------------------------------------------
176
177/// Configuration for [`TensorPool`].
178#[derive(Debug, Clone)]
179pub struct TensorPoolConfig {
180    /// Maximum number of free buffers to retain per bucket.
181    ///
182    /// When a buffer is released back to the pool and the corresponding free list
183    /// already has `max_per_bucket` entries, the buffer is simply dropped.
184    pub max_per_bucket: usize,
185}
186
187impl Default for TensorPoolConfig {
188    fn default() -> Self {
189        Self { max_per_bucket: 16 }
190    }
191}
192
193// ---------------------------------------------------------------------------
194// PooledBuffer
195// ---------------------------------------------------------------------------
196
197/// An owned, pool-tracked byte buffer.
198///
199/// Created exclusively by [`TensorPool::acquire`] and must be returned to the
200/// same pool via [`TensorPool::release`].  There is **no** `Drop` guard —
201/// callers are responsible for calling `release`.  This is an intentional
202/// design choice to avoid the need for `Arc<TensorPool>` references inside the
203/// buffer struct and to keep zero-copy paths maximally thin.
204pub struct PooledBuffer {
205    /// The actual byte storage
206    inner: Vec<u8>,
207    /// Which bucket this buffer belongs to
208    pub(crate) bucket: usize,
209}
210
211impl PooledBuffer {
212    /// Construct a new `PooledBuffer` from a raw `Vec<u8>` and a bucket index.
213    ///
214    /// This is not part of the public API — callers should use [`TensorPool::acquire`].
215    pub(crate) fn new(inner: Vec<u8>, bucket: usize) -> Self {
216        Self { inner, bucket }
217    }
218
219    /// Returns the bucket index this buffer is classified under.
220    #[inline]
221    pub fn bucket(&self) -> usize {
222        self.bucket
223    }
224
225    /// Immutable view of the buffer contents.
226    #[inline]
227    pub fn as_slice(&self) -> &[u8] {
228        &self.inner
229    }
230
231    /// Mutable view of the buffer contents.
232    #[inline]
233    pub fn as_mut_slice(&mut self) -> &mut [u8] {
234        &mut self.inner
235    }
236
237    /// The total pre-allocated capacity of the underlying `Vec<u8>`.
238    #[inline]
239    pub fn capacity(&self) -> usize {
240        self.inner.capacity()
241    }
242
243    /// The current logical length (number of initialized bytes).
244    #[inline]
245    pub fn len(&self) -> usize {
246        self.inner.len()
247    }
248
249    /// Returns `true` if `len() == 0`.
250    #[inline]
251    pub fn is_empty(&self) -> bool {
252        self.inner.is_empty()
253    }
254
255    /// Resize the buffer, filling any new bytes with `val`.
256    ///
257    /// Delegates directly to [`Vec::resize`].
258    #[inline]
259    pub fn resize(&mut self, new_len: usize, val: u8) {
260        self.inner.resize(new_len, val);
261    }
262
263    /// Consume the `PooledBuffer` and return the raw inner `Vec<u8>`.
264    ///
265    /// The caller takes ownership; the buffer is **not** returned to the pool.
266    /// Prefer [`TensorPool::release`] to reuse buffers.
267    pub fn into_inner(self) -> Vec<u8> {
268        self.inner
269    }
270}
271
272// ---------------------------------------------------------------------------
273// TensorPool
274// ---------------------------------------------------------------------------
275
276/// Slab-based, thread-safe buffer pool for zero-copy Arrow IPC tensor operations.
277///
278/// Internally maintains 8 free lists — one per power-of-two size class from
279/// 256 B to 32 MiB.  All free lists are protected by individual `Mutex` locks
280/// so contention is minimised.
281///
282/// # Thread Safety
283///
284/// `TensorPool` is `Send + Sync` and can be wrapped in an `Arc` for sharing
285/// across threads / async tasks.
286pub struct TensorPool {
287    /// Free lists, one per bucket.  We use a fixed-size array of `Mutex<Vec<…>>`
288    /// rather than a `Vec` to make the structure `Sync` without extra indirection.
289    free_lists: [Mutex<Vec<Vec<u8>>>; NUM_BUCKETS],
290    /// Live counters
291    stats: TensorPoolStats,
292    /// Configuration
293    config: TensorPoolConfig,
294}
295
296impl Default for TensorPool {
297    fn default() -> Self {
298        Self::new(TensorPoolConfig::default())
299    }
300}
301
302impl TensorPool {
303    /// Create a new `TensorPool` with the supplied configuration.
304    pub fn new(config: TensorPoolConfig) -> Self {
305        Self {
306            // Array-init: Rust does not support [expr; N] for non-Copy types, so
307            // we use `std::array::from_fn`.
308            free_lists: std::array::from_fn(|_| Mutex::new(Vec::new())),
309            stats: TensorPoolStats::default(),
310            config,
311        }
312    }
313
314    // -----------------------------------------------------------------------
315    // Public API
316    // -----------------------------------------------------------------------
317
318    /// Acquire a buffer whose capacity is at least `min_bytes`.
319    ///
320    /// If the corresponding free list is non-empty, a buffer is popped and
321    /// returned (incrementing `total_reuses`).  Otherwise a fresh `Vec<u8>` is
322    /// allocated (incrementing `total_allocs`).
323    ///
324    /// In both cases `total_acquired` is incremented.
325    pub fn acquire(&self, min_bytes: usize) -> PooledBuffer {
326        let bucket = bucket_for(min_bytes);
327        let cap = capacity_for_bucket(bucket);
328
329        self.stats.total_acquired.fetch_add(1, Ordering::Relaxed);
330
331        // Try to pop from the free list under a short-lived lock.
332        let maybe_buf = {
333            let mut list = self.free_lists[bucket]
334                .lock()
335                .expect("TensorPool free-list mutex poisoned");
336            list.pop()
337        };
338
339        match maybe_buf {
340            Some(mut buf) => {
341                // Reuse: the buffer was previously cleared on release.
342                self.stats.total_reuses.fetch_add(1, Ordering::Relaxed);
343                // Ensure capacity is still sufficient (it always should be, but
344                // be defensive in case someone tampered with the inner vec).
345                if buf.capacity() < cap {
346                    buf.reserve(cap - buf.capacity());
347                }
348                PooledBuffer::new(buf, bucket)
349            }
350            None => {
351                // Fresh allocation
352                self.stats.total_allocs.fetch_add(1, Ordering::Relaxed);
353                let buf = Vec::with_capacity(cap);
354                PooledBuffer::new(buf, bucket)
355            }
356        }
357    }
358
359    /// Release a buffer back into the pool.
360    ///
361    /// The buffer contents are cleared (length reset to 0, capacity retained)
362    /// before being added to the free list.  If the free list for the buffer's
363    /// bucket already holds `max_per_bucket` entries, the buffer is simply
364    /// dropped (freeing its memory).
365    ///
366    /// Increments `total_released` in both cases.  Adjusts `total_bytes_pooled`
367    /// by the buffer's capacity when it is successfully pooled.
368    pub fn release(&self, buf: PooledBuffer) {
369        let bucket = buf.bucket;
370        let cap = buf.capacity();
371        let mut inner = buf.into_inner();
372
373        self.stats.total_released.fetch_add(1, Ordering::Relaxed);
374
375        // Clear contents before returning to pool.
376        inner.clear();
377
378        let mut list = self.free_lists[bucket]
379            .lock()
380            .expect("TensorPool free-list mutex poisoned");
381
382        if list.len() < self.config.max_per_bucket {
383            self.stats
384                .total_bytes_pooled
385                .fetch_add(cap as u64, Ordering::Relaxed);
386            list.push(inner);
387        }
388        // else: buffer is simply dropped here, memory freed.
389    }
390
391    /// Return the number of free (available) buffers in the specified bucket.
392    ///
393    /// # Panics
394    ///
395    /// Panics in debug mode if `bucket >= NUM_BUCKETS`.
396    pub fn pool_depth(&self, bucket: usize) -> usize {
397        debug_assert!(bucket < NUM_BUCKETS, "bucket index out of range");
398        if bucket >= NUM_BUCKETS {
399            return 0;
400        }
401        self.free_lists[bucket]
402            .lock()
403            .expect("TensorPool free-list mutex poisoned")
404            .len()
405    }
406
407    /// Capture a snapshot of the pool statistics.
408    pub fn stats(&self) -> TensorPoolSnapshot {
409        self.stats.snapshot()
410    }
411
412    /// Drain excess buffers from every bucket, keeping at most `max_per_bucket`
413    /// buffers per bucket.
414    ///
415    /// Buffers that are drained are dropped, releasing their memory.  The
416    /// `total_bytes_pooled` counter is decremented accordingly.
417    pub fn prune(&self, max_per_bucket: usize) {
418        for (bucket, free_list) in self.free_lists.iter().enumerate() {
419            let mut list = free_list
420                .lock()
421                .expect("TensorPool free-list mutex poisoned");
422
423            if list.len() > max_per_bucket {
424                let excess = list.drain(max_per_bucket..).collect::<Vec<_>>();
425                let freed_bytes: u64 = excess.iter().map(|v| v.capacity() as u64).sum();
426                drop(excess);
427                // Subtract freed bytes from the pooled-bytes counter, saturating at 0.
428                let _ = self.stats.total_bytes_pooled.fetch_update(
429                    Ordering::Relaxed,
430                    Ordering::Relaxed,
431                    |prev| Some(prev.saturating_sub(freed_bytes)),
432                );
433                let _ = bucket; // suppress unused-var lint in release builds
434            }
435        }
436    }
437}
438
439// ---------------------------------------------------------------------------
440// Tests
441// ---------------------------------------------------------------------------
442
443#[cfg(test)]
444mod tests {
445    use super::*;
446
447    // ------------------------------------------------------------------
448    // Helper
449    // ------------------------------------------------------------------
450
451    fn make_pool() -> TensorPool {
452        TensorPool::new(TensorPoolConfig::default())
453    }
454
455    // ------------------------------------------------------------------
456    // bucket_for edge-cases
457    // ------------------------------------------------------------------
458
459    #[test]
460    fn bucket_for_zero_is_zero() {
461        assert_eq!(bucket_for(0), 0);
462    }
463
464    #[test]
465    fn bucket_for_one_is_zero() {
466        assert_eq!(bucket_for(1), 0);
467    }
468
469    #[test]
470    fn bucket_for_255_is_zero() {
471        assert_eq!(bucket_for(255), 0);
472    }
473
474    #[test]
475    fn bucket_for_256_is_zero() {
476        // 256 exactly fills bucket 0's capacity (256 B), so it maps to bucket 0.
477        assert_eq!(bucket_for(256), 0);
478    }
479
480    #[test]
481    fn bucket_for_257_is_one() {
482        // 257 exceeds bucket 0 capacity (256 B), so it must go to bucket 1 (512 B).
483        assert_eq!(bucket_for(257), 1);
484    }
485
486    #[test]
487    fn bucket_for_512_is_one() {
488        // 512 exactly fills bucket 1's capacity (512 B).
489        assert_eq!(bucket_for(512), 1);
490    }
491
492    #[test]
493    fn bucket_for_513_is_two() {
494        assert_eq!(bucket_for(513), 2);
495    }
496
497    #[test]
498    fn bucket_for_32mb_is_seven() {
499        assert_eq!(bucket_for(32 * 1024 * 1024), 7);
500    }
501
502    #[test]
503    fn bucket_for_above_32mb_is_seven() {
504        assert_eq!(bucket_for(64 * 1024 * 1024), 7);
505    }
506
507    // ------------------------------------------------------------------
508    // Acquire / release round-trip
509    // ------------------------------------------------------------------
510
511    #[test]
512    fn acquire_release_round_trip() {
513        let pool = make_pool();
514
515        let buf = pool.acquire(100);
516        assert!(buf.capacity() >= 100);
517        pool.release(buf);
518
519        // The buffer should now be in the pool.
520        assert_eq!(pool.pool_depth(0), 1);
521    }
522
523    #[test]
524    fn released_buffer_has_zero_len() {
525        let pool = make_pool();
526
527        let mut buf = pool.acquire(100);
528        buf.resize(50, 0xAB);
529        assert_eq!(buf.len(), 50);
530
531        pool.release(buf);
532
533        // Retrieve and confirm length is reset.
534        let buf2 = pool.acquire(100);
535        assert_eq!(buf2.len(), 0, "released buffer must have length 0");
536        pool.release(buf2);
537    }
538
539    // ------------------------------------------------------------------
540    // Reuse counter
541    // ------------------------------------------------------------------
542
543    #[test]
544    fn reuse_counter_increments_on_pool_hit() {
545        let pool = make_pool();
546
547        // First acquire: pool is empty → fresh alloc.
548        let buf = pool.acquire(100);
549        pool.release(buf);
550
551        // Second acquire: pool has one entry → reuse.
552        let buf2 = pool.acquire(100);
553        pool.release(buf2);
554
555        let snap = pool.stats();
556        assert_eq!(snap.total_reuses, 1);
557    }
558
559    // ------------------------------------------------------------------
560    // Fresh alloc counter
561    // ------------------------------------------------------------------
562
563    #[test]
564    fn fresh_alloc_counter_increments_on_miss() {
565        let pool = make_pool();
566
567        // Pool is empty → fresh allocation.
568        let buf = pool.acquire(200);
569        pool.release(buf);
570
571        let snap = pool.stats();
572        assert_eq!(snap.total_allocs, 1);
573        assert_eq!(snap.total_reuses, 0);
574    }
575
576    // ------------------------------------------------------------------
577    // pool_depth reporting
578    // ------------------------------------------------------------------
579
580    #[test]
581    fn pool_depth_reflects_releases() {
582        let pool = make_pool();
583
584        let b1 = pool.acquire(100);
585        let b2 = pool.acquire(100);
586        let b3 = pool.acquire(100);
587
588        assert_eq!(pool.pool_depth(0), 0);
589
590        pool.release(b1);
591        assert_eq!(pool.pool_depth(0), 1);
592
593        pool.release(b2);
594        assert_eq!(pool.pool_depth(0), 2);
595
596        pool.release(b3);
597        assert_eq!(pool.pool_depth(0), 3);
598    }
599
600    // ------------------------------------------------------------------
601    // prune
602    // ------------------------------------------------------------------
603
604    #[test]
605    fn prune_drains_excess_buffers() {
606        let pool = TensorPool::new(TensorPoolConfig { max_per_bucket: 10 });
607
608        // Fill bucket 0 with 8 entries.
609        let buffers: Vec<_> = (0..8).map(|_| pool.acquire(100)).collect();
610        for buf in buffers {
611            pool.release(buf);
612        }
613        assert_eq!(pool.pool_depth(0), 8);
614
615        // Prune to 3.
616        pool.prune(3);
617        assert_eq!(pool.pool_depth(0), 3);
618    }
619
620    #[test]
621    fn prune_keeps_buckets_at_max_when_under_limit() {
622        let pool = make_pool();
623
624        let buffers: Vec<_> = (0..3).map(|_| pool.acquire(100)).collect();
625        for buf in buffers {
626            pool.release(buf);
627        }
628
629        // Prune with a limit higher than current depth — should be a no-op.
630        pool.prune(5);
631        assert_eq!(pool.pool_depth(0), 3);
632    }
633
634    // ------------------------------------------------------------------
635    // resize on PooledBuffer
636    // ------------------------------------------------------------------
637
638    #[test]
639    fn resize_works_on_pooled_buffer() {
640        let pool = make_pool();
641        let mut buf = pool.acquire(128);
642
643        buf.resize(64, 0xFF);
644        assert_eq!(buf.len(), 64);
645        assert!(buf.as_slice().iter().all(|&b| b == 0xFF));
646
647        buf.resize(0, 0);
648        assert_eq!(buf.len(), 0);
649
650        pool.release(buf);
651    }
652
653    // ------------------------------------------------------------------
654    // Stats snapshot correctness
655    // ------------------------------------------------------------------
656
657    #[test]
658    fn stats_snapshot_total_acquired() {
659        let pool = make_pool();
660
661        for _ in 0..5 {
662            let buf = pool.acquire(100);
663            pool.release(buf);
664        }
665
666        let snap = pool.stats();
667        assert_eq!(snap.total_acquired, 5);
668    }
669
670    #[test]
671    fn stats_snapshot_total_released() {
672        let pool = make_pool();
673
674        for _ in 0..3 {
675            let buf = pool.acquire(100);
676            pool.release(buf);
677        }
678
679        let snap = pool.stats();
680        assert_eq!(snap.total_released, 3);
681    }
682
683    #[test]
684    fn stats_allocs_plus_reuses_equals_acquired() {
685        let pool = make_pool();
686
687        // 4 acquires: first is a fresh alloc, rest should hit the pool since we
688        // release before re-acquiring.
689        for _ in 0..4 {
690            let buf = pool.acquire(100);
691            pool.release(buf);
692        }
693
694        let snap = pool.stats();
695        assert_eq!(
696            snap.total_allocs + snap.total_reuses,
697            snap.total_acquired,
698            "allocs + reuses must equal total acquired"
699        );
700    }
701
702    // ------------------------------------------------------------------
703    // Bucket capacity correctness
704    // ------------------------------------------------------------------
705
706    #[test]
707    fn acquired_buffer_has_correct_bucket_capacity() {
708        let pool = make_pool();
709
710        // 600 > 512 (bucket 1 cap), so it falls into bucket 2 (capacity 1024).
711        let buf = pool.acquire(600);
712        assert_eq!(buf.bucket(), 2);
713        assert!(buf.capacity() >= 1024);
714
715        pool.release(buf);
716    }
717
718    // ------------------------------------------------------------------
719    // max_per_bucket cap
720    // ------------------------------------------------------------------
721
722    #[test]
723    fn release_drops_buffer_when_list_full() {
724        let pool = TensorPool::new(TensorPoolConfig { max_per_bucket: 2 });
725
726        let b1 = pool.acquire(100);
727        let b2 = pool.acquire(100);
728        let b3 = pool.acquire(100); // Will be dropped when released (list already full)
729
730        pool.release(b1);
731        pool.release(b2);
732        assert_eq!(pool.pool_depth(0), 2);
733
734        pool.release(b3); // Should be silently dropped
735        assert_eq!(pool.pool_depth(0), 2);
736    }
737}