use crate::InklogError;
use crate::LogRecord;
use oxcache::Cache;
use serde::{Deserialize, Serialize};
use std::sync::Arc;
use std::sync::LazyLock;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::time::Duration;
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct ObjectPoolConfig {
#[serde(default = "default_object_pool_max_capacity")]
pub max_capacity: usize,
pub ttl_secs: Option<u64>,
}
fn default_object_pool_max_capacity() -> usize {
1024
}
impl Default for ObjectPoolConfig {
fn default() -> Self {
Self {
max_capacity: 1024,
ttl_secs: None,
}
}
}
#[derive(Clone)]
pub struct ObjectPool<K, V>
where
K: oxcache::CacheKey + Send + Sync + 'static,
V: serde::Serialize + for<'de> serde::Deserialize<'de> + Send + Sync + Clone + 'static,
{
cache: Arc<Cache<K, V>>,
stats: Arc<PoolStats>,
}
impl<K, V> ObjectPool<K, V>
where
K: oxcache::CacheKey + Send + Sync + 'static,
V: serde::Serialize + for<'de> serde::Deserialize<'de> + Send + Sync + Clone + 'static,
{
pub async fn new() -> Result<Self, InklogError> {
Self::with_config(ObjectPoolConfig::default()).await
}
pub async fn with_config(config: ObjectPoolConfig) -> Result<Self, InklogError> {
let mut builder = Cache::builder();
builder = builder.capacity(config.max_capacity as u64);
if let Some(ttl_secs) = config.ttl_secs {
builder = builder.ttl(Duration::from_secs(ttl_secs));
}
let cache = builder.build().await.map_err(|e| {
let mut args = fluent_bundle::FluentArgs::new();
args.set("err", e.to_string());
InklogError::CacheError(crate::i18n::tr_args("cache-build_failed", args))
})?;
Ok(Self {
cache: Arc::new(cache),
stats: Arc::new(PoolStats::default()),
})
}
pub async fn get(&self, key: &K) -> Result<Option<V>, InklogError>
where
K: Clone,
{
let result = self.cache.get(key).await.map_err(|e| {
let mut args = fluent_bundle::FluentArgs::new();
args.set("err", e.to_string());
InklogError::CacheError(crate::i18n::tr_args("cache-get_failed", args))
})?;
if result.is_some() {
self.stats.hits.fetch_add(1, Ordering::Relaxed);
self.stats.items_reused.fetch_add(1, Ordering::Relaxed);
} else {
self.stats.misses.fetch_add(1, Ordering::Relaxed);
}
Ok(result)
}
pub async fn put(&self, key: &K, value: V) -> Result<(), InklogError>
where
K: Clone,
V: Clone,
{
self.cache.set(key, &value).await.map_err(|e| {
let mut args = fluent_bundle::FluentArgs::new();
args.set("err", e.to_string());
InklogError::CacheError(crate::i18n::tr_args("cache-set_failed", args))
})?;
self.stats.total_items.fetch_add(1, Ordering::Relaxed);
Ok(())
}
pub fn len(&self) -> usize {
self.stats.total_items.load(Ordering::Relaxed)
}
pub fn is_empty(&self) -> bool {
self.len() == 0
}
}
#[derive(Clone)]
pub struct ThreadLocalLogRecordPool {
capacity: usize,
}
impl ThreadLocalLogRecordPool {
pub fn new(capacity: usize) -> Self {
Self { capacity }
}
pub fn get(&self) -> LogRecord {
THREAD_LOCAL_LOG_RECORD_POOL.with(|pool| {
let mut pool = pool.borrow_mut();
pool.pop().unwrap_or_default()
})
}
pub fn put(&self, record: LogRecord) {
THREAD_LOCAL_LOG_RECORD_POOL.with(|pool| {
let mut pool = pool.borrow_mut();
if pool.len() < self.capacity {
let mut record = record;
record.reset();
pool.push(record);
}
});
}
pub fn len(&self) -> usize {
THREAD_LOCAL_LOG_RECORD_POOL.with(|pool| pool.borrow().len())
}
pub fn is_empty(&self) -> bool {
self.len() == 0
}
}
impl Default for ThreadLocalLogRecordPool {
fn default() -> Self {
Self::new(1024)
}
}
thread_local! {
static THREAD_LOCAL_LOG_RECORD_POOL: std::cell::RefCell<Vec<LogRecord>> =
std::cell::RefCell::new(Vec::with_capacity(1024));
}
#[derive(Clone)]
pub struct ThreadLocalStringPool {
capacity: usize,
}
impl ThreadLocalStringPool {
pub fn new(capacity: usize) -> Self {
Self { capacity }
}
pub fn get(&self) -> String {
THREAD_LOCAL_STRING_POOL.with(|pool| {
let mut pool = pool.borrow_mut();
pool.pop().unwrap_or_default()
})
}
pub fn put(&self, mut s: String) {
s.clear(); THREAD_LOCAL_STRING_POOL.with(|pool| {
let mut pool = pool.borrow_mut();
if pool.len() < self.capacity {
pool.push(s);
}
});
}
pub fn len(&self) -> usize {
THREAD_LOCAL_STRING_POOL.with(|pool| pool.borrow().len())
}
pub fn is_empty(&self) -> bool {
self.len() == 0
}
}
impl Default for ThreadLocalStringPool {
fn default() -> Self {
Self::new(1024)
}
}
thread_local! {
static THREAD_LOCAL_STRING_POOL: std::cell::RefCell<Vec<String>> =
std::cell::RefCell::new(Vec::with_capacity(1024));
}
static GLOBAL_LOG_RECORD_POOL: LazyLock<ThreadLocalLogRecordPool> =
LazyLock::new(|| ThreadLocalLogRecordPool::new(1024));
static GLOBAL_STRING_POOL: LazyLock<ThreadLocalStringPool> =
LazyLock::new(|| ThreadLocalStringPool::new(1024));
pub fn get_log_record() -> LogRecord {
GLOBAL_LOG_RECORD_POOL.get()
}
pub fn put_log_record(record: LogRecord) {
GLOBAL_LOG_RECORD_POOL.put(record)
}
pub fn get_string_buffer() -> String {
GLOBAL_STRING_POOL.get()
}
pub fn put_string_buffer(s: String) {
GLOBAL_STRING_POOL.put(s)
}
#[derive(Debug, Default)]
struct PoolStats {
pub(crate) total_items: AtomicUsize,
pub(crate) hits: AtomicUsize,
pub(crate) misses: AtomicUsize,
pub(crate) items_reused: AtomicUsize,
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_object_pool_new_default_capacity() {
let pool = ObjectPool::<String, String>::new()
.await
.expect("default pool should build");
assert_eq!(pool.len(), 0);
}
#[tokio::test]
async fn test_object_pool_with_config() {
let pool = ObjectPool::<String, i32>::with_config(ObjectPoolConfig {
max_capacity: 256,
ttl_secs: None,
})
.await
.expect("pool with config should build");
assert_eq!(pool.len(), 0);
}
#[tokio::test]
async fn test_object_pool_put_and_get() {
let pool = ObjectPool::<String, i32>::new().await.expect("build");
pool.put(&"a".to_string(), 1).await.expect("put");
pool.put(&"b".to_string(), 2).await.expect("put");
pool.put(&"c".to_string(), 3).await.expect("put");
assert_eq!(pool.get(&"a".to_string()).await.expect("get"), Some(1));
assert_eq!(pool.get(&"b".to_string()).await.expect("get"), Some(2));
assert_eq!(pool.get(&"c".to_string()).await.expect("get"), Some(3));
assert_eq!(pool.get(&"missing".to_string()).await.expect("get"), None);
}
#[tokio::test]
async fn test_object_pool_get_returns_result_on_cache_error() {
let pool = ObjectPool::<String, i32>::new().await.expect("build");
let result = pool.get(&"missing".to_string()).await;
assert!(result.is_ok());
assert_eq!(result.unwrap(), None);
}
#[tokio::test]
async fn test_object_pool_put_returns_result() {
let pool = ObjectPool::<String, i32>::new().await.expect("build");
let result = pool.put(&"key".to_string(), 42).await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_object_pool_with_ttl_config() {
let pool = ObjectPool::<String, String>::with_config(ObjectPoolConfig {
max_capacity: 256,
ttl_secs: Some(60),
})
.await
.expect("build with ttl");
pool.put(&"k".to_string(), "v".to_string())
.await
.expect("put");
assert_eq!(
pool.get(&"k".to_string()).await.expect("get"),
Some("v".to_string())
);
}
#[tokio::test]
async fn test_object_pool_is_empty() {
let pool = ObjectPool::<String, i32>::new().await.expect("build");
assert!(pool.is_empty());
assert_eq!(pool.len(), 0);
let result = pool.get(&"missing".to_string()).await.expect("get");
assert_eq!(result, None);
pool.put(&"key".to_string(), 42).await.expect("put");
let value = pool.get(&"key".to_string()).await.expect("get");
assert_eq!(value, Some(42));
}
#[test]
fn test_object_pool_config_default() {
let config = ObjectPoolConfig::default();
assert_eq!(config.max_capacity, 1024);
assert_eq!(config.ttl_secs, None);
}
#[test]
fn test_object_pool_config_with_ttl() {
let config = ObjectPoolConfig {
max_capacity: 256,
ttl_secs: Some(60),
};
assert_eq!(config.max_capacity, 256);
assert_eq!(config.ttl_secs, Some(60));
}
#[test]
fn test_thread_local_log_record_pool() {
let pool = ThreadLocalLogRecordPool::new(10);
while !pool.is_empty() {
let _ = pool.get();
}
assert!(pool.is_empty());
let record = pool.get();
assert_eq!(record.level, "INFO");
pool.put(record);
assert!(!pool.is_empty());
let record2 = pool.get();
assert_eq!(record2.level, "INFO");
}
#[test]
fn test_thread_local_log_record_pool_exceed_capacity() {
let pool = ThreadLocalLogRecordPool::new(3);
while !pool.is_empty() {
let _ = pool.get();
}
for _ in 0..3 {
let record = pool.get();
pool.put(record);
}
let extra = pool.get();
pool.put(extra);
assert!(pool.len() <= 3);
}
#[test]
fn test_thread_local_log_record_pool_default_trait() {
let pool = ThreadLocalLogRecordPool::default();
let r1 = pool.get();
pool.put(r1);
assert!(!pool.is_empty());
}
#[test]
fn test_thread_local_string_pool() {
let pool = ThreadLocalStringPool::new(10);
while !pool.is_empty() {
let _ = pool.get();
}
assert!(pool.is_empty());
let s = pool.get();
assert!(s.is_empty());
pool.put("test".to_string());
let s2 = pool.get();
assert_eq!(s2, "", "put() clears string contents");
}
#[test]
fn test_thread_local_string_pool_len_and_is_empty() {
let pool = ThreadLocalStringPool::new(10);
while !pool.is_empty() {
let _ = pool.get();
}
assert!(pool.is_empty());
assert_eq!(pool.len(), 0);
pool.put("first".to_string());
assert!(!pool.is_empty());
assert_eq!(pool.len(), 1);
pool.put("second".to_string());
assert_eq!(pool.len(), 2);
let s = pool.get();
assert_eq!(s, ""); assert_eq!(pool.len(), 1);
let s = pool.get();
assert_eq!(s, "");
assert_eq!(pool.len(), 0);
assert!(pool.is_empty());
}
#[test]
fn test_thread_local_string_pool_exceed_capacity_drops_excess() {
let pool = ThreadLocalStringPool::new(2);
while !pool.is_empty() {
let _ = pool.get();
}
pool.put("a".to_string());
pool.put("b".to_string());
assert_eq!(pool.len(), 2);
pool.put("c".to_string());
assert_eq!(
pool.len(),
2,
"pool should not grow beyond capacity; excess should be dropped"
);
let s1 = pool.get();
let s2 = pool.get();
let mut remaining = vec![s1, s2];
remaining.sort();
assert_eq!(remaining, vec!["".to_string(), "".to_string()]);
}
#[test]
fn test_thread_local_string_pool_default_trait() {
let pool = ThreadLocalStringPool::default();
let s = pool.get();
assert!(s.is_empty());
pool.put("default".to_string());
assert!(!pool.is_empty());
}
#[test]
fn test_global_log_record_functions() {
let record = get_log_record();
assert_eq!(record.level, "INFO");
let mut modified = record;
modified.message = "global test".to_string();
put_log_record(modified);
let record2 = get_log_record();
assert_eq!(record2.level, "INFO");
for _ in 0..5 {
let r = get_log_record();
put_log_record(r);
}
}
#[test]
fn test_global_string_buffer_functions() {
let s1 = get_string_buffer();
put_string_buffer(s1);
let s2 = get_string_buffer();
let _ = s2.capacity();
}
#[tokio::test(flavor = "multi_thread")]
async fn test_thread_local_pool_concurrent_isolation() {
use std::sync::Arc;
use std::sync::atomic::{AtomicUsize, Ordering};
let pool = Arc::new(ThreadLocalLogRecordPool::new(10));
let total_gets = Arc::new(AtomicUsize::new(0));
let mut handles = Vec::new();
for _ in 0..4 {
let pool_clone = Arc::clone(&pool);
let counter_clone = Arc::clone(&total_gets);
handles.push(tokio::task::spawn_blocking(move || {
for _ in 0..5 {
let _record = pool_clone.get();
counter_clone.fetch_add(1, Ordering::Relaxed);
}
}));
}
for h in handles {
h.await.expect("blocking task should not panic");
}
assert_eq!(total_gets.load(Ordering::Relaxed), 20);
}
#[test]
fn test_string_pool_clears_string_on_put() {
let pool = super::ThreadLocalStringPool::new(10);
let mut s = pool.get();
s.push_str("sensitive data");
pool.put(s);
let retrieved = pool.get();
assert_eq!(retrieved, "", "pooled string should be cleared on put");
}
}