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}