use std::collections::HashMap;
use std::sync::{Arc, Mutex};
pub type PoolPrepareCache = PreparedStatementCache<()>;
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct PrepareCacheStats {
pub hits: u64,
pub misses: u64,
pub evictions: u64,
pub size: usize,
}
struct Entry<V> {
value: Arc<V>,
last_used: u64,
}
pub struct PreparedStatementCache<V> {
capacity: usize,
inner: Mutex<LruState<V>>,
}
struct LruState<V> {
map: HashMap<Arc<str>, Entry<V>>,
clock: u64,
hits: u64,
misses: u64,
evictions: u64,
}
impl<V> PreparedStatementCache<V> {
pub fn new(capacity: usize) -> Self {
Self {
capacity: capacity.max(1),
inner: Mutex::new(LruState {
map: HashMap::new(),
clock: 0,
hits: 0,
misses: 0,
evictions: 0,
}),
}
}
pub fn get_or_prepare(
&self,
sql: impl Into<Arc<str>>,
prepare: impl FnOnce(&str) -> V,
) -> (Arc<V>, bool) {
let key: Arc<str> = sql.into();
let mut state = self
.inner
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
state.clock += 1;
let clock = state.clock;
if let Some(entry) = state.map.get_mut(&key) {
entry.last_used = clock;
let value = Arc::clone(&entry.value);
state.hits += 1;
return (value, true);
}
state.misses += 1;
let value = Arc::new(prepare(&key));
if state.map.len() >= self.capacity
&& let Some(oldest_key) = state
.map
.iter()
.min_by_key(|(_, entry)| entry.last_used)
.map(|(key, _)| Arc::clone(key))
{
state.map.remove(&oldest_key);
state.evictions += 1;
}
state.map.insert(
Arc::clone(&key),
Entry {
value: Arc::clone(&value),
last_used: clock,
},
);
(value, false)
}
pub fn stats(&self) -> PrepareCacheStats {
let state = self
.inner
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
PrepareCacheStats {
hits: state.hits,
misses: state.misses,
evictions: state.evictions,
size: state.map.len(),
}
}
pub fn capacity(&self) -> usize {
self.capacity
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::atomic::{AtomicU32, Ordering};
#[test]
fn test_second_access_is_cache_hit() {
let cache: PreparedStatementCache<String> = PreparedStatementCache::new(8);
let prepares = AtomicU32::new(0);
let (v1, hit1) = cache.get_or_prepare("SELECT 1", |sql| {
prepares.fetch_add(1, Ordering::SeqCst);
format!("prepared:{sql}")
});
assert!(!hit1, "首次应未命中");
assert_eq!(&*v1, "prepared:SELECT 1");
let (v2, hit2) = cache.get_or_prepare("SELECT 1", |sql| {
prepares.fetch_add(1, Ordering::SeqCst);
format!("prepared:{sql}")
});
assert!(hit2, "二次应命中");
assert_eq!(&*v2, "prepared:SELECT 1", "命中应返回同一准备产物");
assert_eq!(prepares.load(Ordering::SeqCst), 1, "prepare 只应执行一次");
let (_, hit3) = cache.get_or_prepare("SELECT 2", |_| String::new());
assert!(!hit3);
let stats = cache.stats();
assert_eq!((stats.hits, stats.misses), (1, 2));
assert_eq!(stats.size, 2);
}
#[test]
fn test_lru_eviction_respects_recency() {
let cache: PreparedStatementCache<()> = PreparedStatementCache::new(2);
assert!(!cache.get_or_prepare("a", |_| ()).1, "a 首次未命中");
assert!(!cache.get_or_prepare("b", |_| ()).1, "b 首次未命中");
assert!(cache.get_or_prepare("a", |_| ()).1);
assert!(!cache.get_or_prepare("c", |_| ()).1);
let stats = cache.stats();
assert_eq!(stats.evictions, 1);
assert_eq!(stats.size, 2);
assert_eq!(cache.capacity(), 2);
assert!(!cache.get_or_prepare("b", |_| ()).1, "b 应已被淘汰");
}
}