use parking_lot::Mutex;
use std::sync::Arc;
use std::sync::atomic::AtomicU64;
#[derive(Debug, Clone)]
pub struct PoolConfig {
pub symbol_size: u16,
pub initial_size: usize,
pub max_size: usize,
pub allow_growth: bool,
pub growth_increment: usize,
}
impl PoolConfig {
#[must_use]
#[inline]
pub fn normalized(mut self) -> Self {
if self.max_size < self.initial_size {
self.max_size = self.initial_size;
}
if self.growth_increment == 0 {
self.growth_increment = 1;
}
self
}
}
impl Default for PoolConfig {
#[inline]
fn default() -> Self {
Self {
symbol_size: 1024,
initial_size: 0,
max_size: 1024,
allow_growth: true,
growth_increment: 64,
}
}
}
#[derive(Debug)]
pub struct SymbolBuffer {
data: Box<[u8]>,
checked_out: bool,
owner_pool_id: Option<u64>,
}
impl SymbolBuffer {
#[must_use]
#[inline]
pub fn new(symbol_size: u16) -> Self {
let len = symbol_size as usize;
Self {
data: vec![0u8; len].into_boxed_slice(),
checked_out: false,
owner_pool_id: None,
}
}
#[must_use]
#[inline]
pub fn as_slice(&self) -> &[u8] {
&self.data
}
#[must_use]
#[inline]
pub fn as_mut_slice(&mut self) -> &mut [u8] {
&mut self.data
}
#[must_use]
#[inline]
pub fn len(&self) -> usize {
self.data.len()
}
#[must_use]
#[inline]
pub fn is_empty(&self) -> bool {
self.data.is_empty()
}
fn mark_in_use(&mut self, owner_pool_id: u64) {
self.checked_out = true;
self.owner_pool_id = Some(owner_pool_id);
}
fn mark_free(&mut self) {
self.checked_out = false;
self.owner_pool_id = None;
}
}
impl Drop for SymbolBuffer {
fn drop(&mut self) {
self.data.fill(0);
}
}
#[derive(Debug, Clone, Default)]
pub struct PoolStats {
pub allocations: u64,
pub deallocations: u64,
pub pool_hits: u64,
pub pool_misses: u64,
pub peak_usage: usize,
pub current_usage: usize,
pub growth_events: u64,
}
#[derive(Debug, Clone, Copy)]
pub struct PoolExhausted;
impl std::fmt::Display for PoolExhausted {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "symbol pool exhausted")
}
}
impl std::error::Error for PoolExhausted {}
#[derive(Debug)]
pub struct SymbolPool {
free_list: Vec<SymbolBuffer>,
allocated: usize,
pool_id: u64,
config: PoolConfig,
stats: PoolStats,
}
impl SymbolPool {
#[must_use]
pub fn new(config: PoolConfig) -> Self {
let config = config.normalized();
let mut pool = Self {
free_list: Vec::with_capacity(config.initial_size),
allocated: 0,
pool_id: next_symbol_pool_id(),
config,
stats: PoolStats::default(),
};
pool.warm(pool.config.initial_size);
pool
}
pub fn warm(&mut self, count: usize) {
let max_free = self.config.max_size.saturating_sub(self.allocated);
let target = count.min(max_free);
while self.free_list.len() < target {
self.free_list
.push(SymbolBuffer::new(self.config.symbol_size));
}
}
fn grow(&mut self) -> bool {
if !self.config.allow_growth {
return false;
}
if self.free_list.len() + self.allocated >= self.config.max_size {
return false;
}
let available = self
.config
.max_size
.saturating_sub(self.free_list.len() + self.allocated);
let grow_by = self.config.growth_increment.min(available);
for _ in 0..grow_by {
self.free_list
.push(SymbolBuffer::new(self.config.symbol_size));
}
if grow_by > 0 {
self.stats.growth_events += 1;
true
} else {
false
}
}
pub fn allocate(&mut self) -> Result<SymbolBuffer, PoolExhausted> {
if let Some(mut buffer) = self.free_list.pop() {
buffer.mark_in_use(self.pool_id);
self.allocated += 1;
self.stats.allocations += 1;
self.stats.pool_hits += 1;
self.stats.current_usage = self.allocated;
self.stats.peak_usage = self.stats.peak_usage.max(self.allocated);
return Ok(buffer);
}
if self.grow() {
return self.allocate();
}
self.stats.pool_misses += 1;
Err(PoolExhausted)
}
pub fn try_allocate(&mut self) -> Option<SymbolBuffer> {
self.allocate().ok()
}
pub fn deallocate(&mut self, mut buffer: SymbolBuffer) {
let expected_len = self.config.symbol_size as usize;
assert_eq!(
buffer.len(),
expected_len,
"Cannot deallocate buffer of size {} into pool of size {}",
buffer.len(),
self.config.symbol_size
);
assert!(
buffer.checked_out,
"Cannot deallocate buffer that is not currently checked out from a pool"
);
assert_eq!(
buffer.owner_pool_id,
Some(self.pool_id),
"Cannot deallocate buffer checked out from a different pool"
);
buffer.as_mut_slice().fill(0);
buffer.mark_free();
self.free_list.push(buffer);
self.allocated = self.allocated.saturating_sub(1);
self.stats.deallocations += 1;
self.stats.current_usage = self.allocated;
}
#[must_use]
pub fn stats(&self) -> &PoolStats {
&self.stats
}
pub fn reset_stats(&mut self) {
self.stats = PoolStats::default();
self.stats.current_usage = self.allocated;
self.stats.peak_usage = self.allocated;
}
pub fn shrink_to_fit(&mut self) {
let target = self.config.initial_size.min(self.config.max_size);
if self.free_list.len() > target {
self.free_list.truncate(target);
}
}
}
#[inline]
fn next_symbol_pool_id() -> u64 {
static NEXT_SYMBOL_POOL_ID: AtomicU64 = AtomicU64::new(1);
NEXT_SYMBOL_POOL_ID.fetch_add(1, std::sync::atomic::Ordering::Relaxed)
}
#[derive(Debug, Clone)]
#[allow(clippy::struct_field_names)]
pub struct ResourceLimits {
pub max_symbol_memory: usize,
pub max_encoding_ops: usize,
pub max_decoding_ops: usize,
pub max_symbols_in_flight: usize,
pub max_per_object_memory: usize,
}
impl Default for ResourceLimits {
#[inline]
fn default() -> Self {
Self {
max_symbol_memory: usize::MAX,
max_encoding_ops: usize::MAX,
max_decoding_ops: usize::MAX,
max_symbols_in_flight: usize::MAX,
max_per_object_memory: usize::MAX,
}
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct ResourceUsage {
pub symbol_memory: usize,
pub encoding_ops: usize,
pub decoding_ops: usize,
pub symbols_in_flight: usize,
}
impl ResourceUsage {
fn add(&mut self, other: Self) {
self.symbol_memory = self.symbol_memory.saturating_add(other.symbol_memory);
self.encoding_ops = self.encoding_ops.saturating_add(other.encoding_ops);
self.decoding_ops = self.decoding_ops.saturating_add(other.decoding_ops);
self.symbols_in_flight = self
.symbols_in_flight
.saturating_add(other.symbols_in_flight);
}
fn sub(&mut self, other: Self) {
self.symbol_memory = self.symbol_memory.saturating_sub(other.symbol_memory);
self.encoding_ops = self.encoding_ops.saturating_sub(other.encoding_ops);
self.decoding_ops = self.decoding_ops.saturating_sub(other.decoding_ops);
self.symbols_in_flight = self
.symbols_in_flight
.saturating_sub(other.symbols_in_flight);
}
}
#[derive(Debug, Clone, Copy, Default)]
pub struct ResourceRequest {
pub usage: ResourceUsage,
}
pub trait ResourceObserver: Send + Sync {
fn on_pressure_change(&self, pressure: f64);
fn on_limit_approached(&self, resource: ResourceKind, usage_percent: f64);
fn on_limit_exceeded(&self, resource: ResourceKind);
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ResourceKind {
SymbolMemory,
EncodingOps,
DecodingOps,
SymbolsInFlight,
}
#[derive(Debug, Clone, Copy)]
pub struct ResourceExhausted;
impl std::fmt::Display for ResourceExhausted {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "resource limits exceeded")
}
}
impl std::error::Error for ResourceExhausted {}
struct ResourceTrackerInner {
limits: ResourceLimits,
current: ResourceUsage,
observers: Vec<Arc<dyn ResourceObserver>>,
last_pressure: f64,
}
#[derive(Clone)]
pub struct ResourceTracker {
inner: Arc<Mutex<ResourceTrackerInner>>,
}
impl ResourceTracker {
#[must_use]
#[inline]
pub fn new(limits: ResourceLimits) -> Self {
Self {
inner: Arc::new(Mutex::new(ResourceTrackerInner {
limits,
current: ResourceUsage::default(),
observers: Vec::new(),
last_pressure: 0.0,
})),
}
}
#[must_use]
#[inline]
pub fn usage(&self) -> ResourceUsage {
self.inner.lock().current
}
#[must_use]
#[inline]
pub fn limits(&self) -> ResourceLimits {
self.inner.lock().limits.clone()
}
pub fn add_observer(&self, observer: Box<dyn ResourceObserver>) {
self.inner.lock().observers.push(Arc::from(observer));
}
#[must_use]
pub fn pressure(&self) -> f64 {
let inner = self.inner.lock();
compute_pressure(&inner.current, &inner.limits)
}
#[must_use]
#[inline]
pub fn can_acquire(&self, request: &ResourceRequest) -> bool {
let inner = self.inner.lock();
let mut projected = inner.current;
projected.add(request.usage);
within_limits(&projected, &inner.limits)
}
pub fn try_acquire_encoding(
&self,
memory_needed: usize,
) -> Result<ResourceGuard, ResourceExhausted> {
self.try_acquire(ResourceUsage {
symbol_memory: memory_needed,
encoding_ops: 1,
decoding_ops: 0,
symbols_in_flight: 0,
})
}
pub fn try_acquire_decoding(
&self,
memory_needed: usize,
) -> Result<ResourceGuard, ResourceExhausted> {
self.try_acquire(ResourceUsage {
symbol_memory: memory_needed,
encoding_ops: 0,
decoding_ops: 1,
symbols_in_flight: 0,
})
}
#[allow(clippy::significant_drop_tightening)] pub fn try_acquire(&self, usage: ResourceUsage) -> Result<ResourceGuard, ResourceExhausted> {
let batch = {
let mut inner = self.inner.lock();
let mut projected = inner.current;
projected.add(usage);
if !within_limits(&projected, &inner.limits) {
let batch = prepare_limit_exceeded(&inner, &projected);
drop(inner);
batch.dispatch();
return Err(ResourceExhausted);
}
inner.current = projected;
prepare_pressure_notifications(&mut inner)
};
batch.dispatch();
Ok(ResourceGuard {
inner: Arc::clone(&self.inner),
acquired: usage,
})
}
}
pub struct ResourceGuard {
inner: Arc<Mutex<ResourceTrackerInner>>,
acquired: ResourceUsage,
}
impl Drop for ResourceGuard {
#[allow(clippy::significant_drop_tightening)] fn drop(&mut self) {
let batch = {
let mut inner = self.inner.lock();
inner.current.sub(self.acquired);
prepare_pressure_notifications(&mut inner)
};
batch.dispatch();
}
}
struct NotificationBatch {
observers: Vec<Arc<dyn ResourceObserver>>,
pressure_change: Option<f64>,
approached: Vec<(ResourceKind, f64)>,
exceeded: Vec<ResourceKind>,
}
impl NotificationBatch {
fn empty() -> Self {
Self {
observers: Vec::new(),
pressure_change: None,
approached: Vec::new(),
exceeded: Vec::new(),
}
}
fn dispatch(self) {
for obs in &self.observers {
if let Some(p) = self.pressure_change {
obs.on_pressure_change(p);
}
for (kind, ratio) in &self.approached {
obs.on_limit_approached(*kind, *ratio);
}
for kind in &self.exceeded {
obs.on_limit_exceeded(*kind);
}
}
}
}
fn within_limits(usage: &ResourceUsage, limits: &ResourceLimits) -> bool {
usage.symbol_memory <= limits.max_symbol_memory
&& usage.encoding_ops <= limits.max_encoding_ops
&& usage.decoding_ops <= limits.max_decoding_ops
&& usage.symbols_in_flight <= limits.max_symbols_in_flight
}
fn compute_pressure(usage: &ResourceUsage, limits: &ResourceLimits) -> f64 {
let ratios = [
ratio(usage.symbol_memory, limits.max_symbol_memory),
ratio(usage.encoding_ops, limits.max_encoding_ops),
ratio(usage.decoding_ops, limits.max_decoding_ops),
ratio(usage.symbols_in_flight, limits.max_symbols_in_flight),
];
ratios.into_iter().fold(0.0_f64, f64::max).clamp(0.0, 1.0)
}
#[allow(clippy::cast_precision_loss)]
fn ratio(value: usize, limit: usize) -> f64 {
if limit == 0 {
if value == 0 { 0.0 } else { 1.0 }
} else {
(value as f64 / limit as f64).min(1.0)
}
}
fn prepare_pressure_notifications(inner: &mut ResourceTrackerInner) -> NotificationBatch {
let pressure = compute_pressure(&inner.current, &inner.limits);
let mut batch = NotificationBatch::empty();
batch.observers.clone_from(&inner.observers);
if (pressure - inner.last_pressure).abs() > f64::EPSILON {
inner.last_pressure = pressure;
batch.pressure_change = Some(pressure);
}
if pressure >= 0.8 {
let ratios = [
(
ResourceKind::SymbolMemory,
ratio(inner.current.symbol_memory, inner.limits.max_symbol_memory),
),
(
ResourceKind::EncodingOps,
ratio(inner.current.encoding_ops, inner.limits.max_encoding_ops),
),
(
ResourceKind::DecodingOps,
ratio(inner.current.decoding_ops, inner.limits.max_decoding_ops),
),
(
ResourceKind::SymbolsInFlight,
ratio(
inner.current.symbols_in_flight,
inner.limits.max_symbols_in_flight,
),
),
];
for (kind, ratio) in ratios {
if ratio >= 0.8 {
batch.approached.push((kind, ratio));
}
}
}
batch
}
fn prepare_limit_exceeded(
inner: &ResourceTrackerInner,
projected: &ResourceUsage,
) -> NotificationBatch {
let mut batch = NotificationBatch::empty();
batch.observers.clone_from(&inner.observers);
let limits = &inner.limits;
if projected.symbol_memory > limits.max_symbol_memory {
batch.exceeded.push(ResourceKind::SymbolMemory);
}
if projected.encoding_ops > limits.max_encoding_ops {
batch.exceeded.push(ResourceKind::EncodingOps);
}
if projected.decoding_ops > limits.max_decoding_ops {
batch.exceeded.push(ResourceKind::DecodingOps);
}
if projected.symbols_in_flight > limits.max_symbols_in_flight {
batch.exceeded.push(ResourceKind::SymbolsInFlight);
}
batch
}
#[cfg(test)]
mod tests {
use super::*;
use std::panic::{AssertUnwindSafe, catch_unwind};
#[test]
fn test_pool_allocate_deallocate() {
let mut pool = SymbolPool::new(PoolConfig::default());
let buf = pool.allocate().expect("should allocate");
assert_eq!(buf.len(), 1024);
pool.deallocate(buf);
assert_eq!(pool.stats.allocations, 1);
assert_eq!(pool.stats.deallocations, 1);
assert_eq!(pool.stats.current_usage, 0);
}
#[test]
fn test_pool_exhaustion() {
let config = PoolConfig {
initial_size: 1,
max_size: 1,
allow_growth: false,
..Default::default()
};
let mut pool = SymbolPool::new(config);
let _buf1 = pool.allocate().expect("should allocate");
assert!(pool.allocate().is_err());
assert_eq!(pool.stats.pool_misses, 1);
}
#[test]
fn test_pool_growth() {
let config = PoolConfig {
initial_size: 1,
max_size: 5,
growth_increment: 2,
allow_growth: true,
..Default::default()
};
let mut pool = SymbolPool::new(config);
let _buf1 = pool.allocate().expect("1");
let _buf2 = pool.allocate().expect("2"); let _buf3 = pool.allocate().expect("3");
assert_eq!(pool.stats.growth_events, 1);
assert_eq!(pool.stats.current_usage, 3);
}
#[test]
fn test_resource_tracker_acquire_release() {
let limits = ResourceLimits {
max_encoding_ops: 2,
..Default::default()
};
let tracker = ResourceTracker::new(limits);
let g1 = tracker.try_acquire_encoding(100).expect("1");
assert_eq!(tracker.usage().encoding_ops, 1);
let _g2 = tracker.try_acquire_encoding(100).expect("2");
assert_eq!(tracker.usage().encoding_ops, 2);
assert!(tracker.try_acquire_encoding(100).is_err());
drop(g1);
assert_eq!(tracker.usage().encoding_ops, 1);
let _g3 = tracker.try_acquire_encoding(100).expect("3");
}
#[test]
fn test_resource_pressure() {
let limits = ResourceLimits {
max_symbol_memory: 100,
..Default::default()
};
let tracker = ResourceTracker::new(limits);
assert!((tracker.pressure() - 0.0).abs() < f64::EPSILON);
let _g1 = tracker
.try_acquire(ResourceUsage {
symbol_memory: 50,
..Default::default()
})
.unwrap();
assert!((tracker.pressure() - 0.5).abs() < f64::EPSILON);
let _g2 = tracker
.try_acquire(ResourceUsage {
symbol_memory: 50,
..Default::default()
})
.unwrap();
assert!((tracker.pressure() - 1.0).abs() < f64::EPSILON);
}
#[test]
fn test_reset_stats_preserves_current_usage() {
let mut pool = SymbolPool::new(PoolConfig::default());
let _buf = pool.allocate().expect("should allocate");
assert_eq!(pool.stats.current_usage, 1);
assert_eq!(pool.stats.allocations, 1);
pool.reset_stats();
assert_eq!(pool.stats.current_usage, 1);
assert_eq!(pool.stats.peak_usage, 1);
assert_eq!(pool.stats.allocations, 0);
assert_eq!(pool.stats.deallocations, 0);
}
#[test]
fn test_within_limits_ignores_per_object_memory() {
let limits = ResourceLimits {
max_symbol_memory: 1000,
max_per_object_memory: 100,
..Default::default()
};
let usage = ResourceUsage {
symbol_memory: 500,
..Default::default()
};
assert!(within_limits(&usage, &limits));
}
#[test]
fn pool_config_debug_clone() {
let cfg = PoolConfig::default();
let cfg2 = cfg;
assert!(format!("{cfg2:?}").contains("PoolConfig"));
}
#[test]
fn pool_config_default_values() {
let cfg = PoolConfig::default();
assert_eq!(cfg.symbol_size, 1024);
assert_eq!(cfg.initial_size, 0);
assert_eq!(cfg.max_size, 1024);
assert!(cfg.allow_growth);
assert_eq!(cfg.growth_increment, 64);
}
#[test]
fn pool_config_normalized_clamps() {
let cfg = PoolConfig {
initial_size: 10,
max_size: 5,
growth_increment: 0,
..Default::default()
}
.normalized();
assert!(cfg.max_size >= cfg.initial_size);
assert!(cfg.growth_increment >= 1);
}
#[test]
fn symbol_buffer_debug_new_len_empty() {
let buf = SymbolBuffer::new(64);
assert_eq!(buf.len(), 64);
assert!(!buf.is_empty());
assert!(format!("{buf:?}").contains("SymbolBuffer"));
}
#[test]
fn symbol_buffer_as_slice() {
let mut buf = SymbolBuffer::new(4);
assert_eq!(buf.as_slice().len(), 4);
buf.as_mut_slice()[0] = 0xFF;
assert_eq!(buf.as_slice()[0], 0xFF);
}
#[test]
fn symbol_buffer_zero_size_is_empty() {
let buf = SymbolBuffer::new(0);
assert!(buf.is_empty());
assert_eq!(buf.len(), 0);
}
#[test]
fn pool_stats_debug_clone_default() {
let stats = PoolStats::default();
let stats2 = stats;
assert_eq!(stats2.allocations, 0);
assert!(format!("{stats2:?}").contains("PoolStats"));
}
#[test]
fn pool_exhausted_debug_clone_copy() {
let e = PoolExhausted;
let e2 = e;
assert!(format!("{e2:?}").contains("PoolExhausted"));
}
#[test]
fn pool_exhausted_display_error() {
let e = PoolExhausted;
assert!(e.to_string().contains("exhausted"));
let err: Box<dyn std::error::Error> = Box::new(e);
assert!(!err.to_string().is_empty());
}
#[test]
fn resource_limits_debug_clone_default() {
let lim = ResourceLimits::default();
let lim2 = lim;
assert_eq!(lim2.max_symbol_memory, usize::MAX);
assert!(format!("{lim2:?}").contains("ResourceLimits"));
}
#[test]
fn resource_usage_debug_clone_copy_default_eq() {
let u = ResourceUsage::default();
let u2 = u;
assert_eq!(u, u2);
assert!(format!("{u:?}").contains("ResourceUsage"));
}
#[test]
fn resource_usage_ne() {
let u1 = ResourceUsage::default();
let u2 = ResourceUsage {
symbol_memory: 100,
..Default::default()
};
assert_ne!(u1, u2);
}
#[test]
fn resource_request_debug_clone_copy_default() {
let req = ResourceRequest::default();
let req2 = req;
assert_eq!(req2.usage, ResourceUsage::default());
assert!(format!("{req2:?}").contains("ResourceRequest"));
}
#[test]
fn resource_kind_debug_clone_copy_eq() {
let k = ResourceKind::SymbolMemory;
let k2 = k;
assert_eq!(k, k2);
assert!(format!("{k:?}").contains("SymbolMemory"));
}
#[test]
fn resource_kind_all_variants() {
let variants = [
ResourceKind::SymbolMemory,
ResourceKind::EncodingOps,
ResourceKind::DecodingOps,
ResourceKind::SymbolsInFlight,
];
for (i, v) in variants.iter().enumerate() {
for (j, v2) in variants.iter().enumerate() {
if i == j {
assert_eq!(v, v2);
} else {
assert_ne!(v, v2);
}
}
}
}
#[test]
fn resource_exhausted_debug_clone_copy() {
let e = ResourceExhausted;
let e2 = e;
assert!(format!("{e2:?}").contains("ResourceExhausted"));
}
#[test]
fn resource_exhausted_display_error() {
let e = ResourceExhausted;
assert!(e.to_string().contains("resource limits exceeded"));
let err: Box<dyn std::error::Error> = Box::new(e);
assert!(!err.to_string().is_empty());
}
#[test]
fn symbol_pool_debug() {
let pool = SymbolPool::new(PoolConfig::default());
assert!(format!("{pool:?}").contains("SymbolPool"));
}
#[test]
fn symbol_pool_warm() {
let mut pool = SymbolPool::new(PoolConfig {
initial_size: 0,
max_size: 10,
..Default::default()
});
pool.warm(5);
for _ in 0..5 {
assert!(pool.allocate().is_ok());
}
}
#[test]
fn symbol_pool_warm_respects_max_with_live_allocations() {
let mut pool = SymbolPool::new(PoolConfig {
initial_size: 0,
max_size: 4,
allow_growth: false,
..Default::default()
});
pool.warm(4);
let b1 = pool.allocate().expect("alloc 1");
let b2 = pool.allocate().expect("alloc 2");
let b3 = pool.allocate().expect("alloc 3");
pool.warm(4);
let b4 = pool.allocate().expect("alloc 4");
assert!(
pool.allocate().is_err(),
"pool exceeded max_size after warm"
);
pool.deallocate(b1);
pool.deallocate(b2);
pool.deallocate(b3);
pool.deallocate(b4);
}
#[test]
fn deallocate_rejects_invalid_buffers_without_mutating_state() {
let mut pool = SymbolPool::new(PoolConfig {
symbol_size: 1024,
initial_size: 0,
max_size: 8,
allow_growth: false,
growth_increment: 1,
});
let baseline = pool.stats().clone();
for invalid in [SymbolBuffer::new(8), SymbolBuffer::new(1024)] {
let result = catch_unwind(AssertUnwindSafe(|| {
pool.deallocate(invalid);
}));
assert!(result.is_err(), "invalid buffer return must panic");
assert_eq!(pool.stats().deallocations, baseline.deallocations);
assert_eq!(pool.stats().current_usage, baseline.current_usage);
}
}
#[test]
fn deallocate_rejects_cross_pool_buffers_without_mutating_state() {
let config = PoolConfig {
symbol_size: 8,
initial_size: 1,
max_size: 1,
allow_growth: false,
growth_increment: 1,
};
let mut pool = SymbolPool::new(config.clone());
let valid = pool.allocate().expect("valid alloc");
let mut other_pool = SymbolPool::new(config);
let foreign = other_pool.allocate().expect("foreign alloc");
let result = catch_unwind(AssertUnwindSafe(|| {
pool.deallocate(foreign);
}));
assert!(result.is_err(), "cross-pool return must panic");
assert_eq!(pool.stats().deallocations, 0);
assert_eq!(pool.stats().current_usage, 1);
assert!(
pool.try_allocate().is_none(),
"foreign buffer must not expand capacity"
);
assert_eq!(other_pool.stats().current_usage, 1);
assert!(
other_pool.try_allocate().is_none(),
"foreign pool must still account for its live checkout"
);
pool.deallocate(valid);
assert_eq!(pool.stats().deallocations, 1);
assert_eq!(pool.stats().current_usage, 0);
}
#[test]
fn symbol_pool_shrink_to_fit() {
let mut pool = SymbolPool::new(PoolConfig {
initial_size: 2,
max_size: 10,
allow_growth: true,
growth_increment: 4,
..Default::default()
});
pool.warm(8);
pool.shrink_to_fit();
}
#[test]
fn symbol_pool_try_allocate() {
let mut pool = SymbolPool::new(PoolConfig {
initial_size: 1,
max_size: 1,
allow_growth: false,
..Default::default()
});
assert!(pool.try_allocate().is_some());
assert!(pool.try_allocate().is_none());
}
#[test]
fn symbol_pool_reuses_zeroed_buffers_after_deallocate() {
let mut pool = SymbolPool::new(PoolConfig {
symbol_size: 8,
initial_size: 1,
max_size: 1,
allow_growth: false,
growth_increment: 1,
});
let mut buffer = pool.allocate().expect("allocate");
buffer.as_mut_slice().fill(0xAA);
pool.deallocate(buffer);
let reused = pool.allocate().expect("reallocate");
assert!(
reused.as_slice().iter().all(|byte| *byte == 0),
"reused buffers must not retain prior payload bytes"
);
}
#[test]
fn resource_tracker_clone() {
let tracker = ResourceTracker::new(ResourceLimits::default());
let tracker2 = tracker;
assert_eq!(tracker2.usage(), ResourceUsage::default());
}
}