use std::collections::{HashMap, HashSet};
use std::sync::Arc;
use std::sync::Mutex;
pub fn canonicalize_cache_key(key: &str) -> String {
key.trim().to_lowercase()
}
fn matches_pattern(pattern: &str, text: &str) -> bool {
let p: Vec<char> = pattern.chars().collect();
let t: Vec<char> = text.chars().collect();
let mut pi = 0usize;
let mut ti = 0usize;
let mut star_pi: Option<usize> = None;
let mut star_ti = 0usize;
while ti < t.len() {
if pi < p.len() && (p[pi] == t[ti] || p[pi] == '?') {
pi += 1;
ti += 1;
} else if pi < p.len() && p[pi] == '*' {
star_pi = Some(pi);
star_ti = ti;
pi += 1;
} else if let Some(spi) = star_pi {
pi = spi + 1;
star_ti += 1;
ti = star_ti;
} else {
return false;
}
}
while pi < p.len() && p[pi] == '*' {
pi += 1;
}
pi == p.len()
}
pub trait SyncCache: Send + Sync {
fn get(&self, key: &str) -> Option<Vec<u8>>;
fn get_many(&self, keys: &[&str]) -> HashMap<String, Vec<u8>> {
keys.iter()
.filter_map(|&key| self.get(key).map(|v| (key.to_string(), v)))
.collect()
}
fn set(&self, key: &str, value: Vec<u8>);
fn set_many(&self, items: &[(String, Vec<u8>)]) {
for (key, value) in items {
self.set(key, value.clone());
}
}
fn delete(&self, key: &str) -> bool;
fn delete_many(&self, keys: &[&str]) -> usize {
keys.iter().filter(|&&key| self.delete(key)).count()
}
fn contains(&self, key: &str) -> bool;
fn clear(&self);
fn len(&self) -> usize;
fn is_empty(&self) -> bool;
fn invalidate(&self, pattern: &str) -> usize {
let keys = self.find_keys_by_pattern(pattern);
self.delete_many(&keys.iter().map(|s| s.as_str()).collect::<Vec<_>>())
}
fn find_keys_by_pattern(&self, pattern: &str) -> Vec<String>;
fn get_stats(&self) -> HashMap<String, u64> {
HashMap::new()
}
}
pub type SharedCache = Arc<dyn SyncCache>;
pub struct OxcacheSyncCache {
backend: oxcache::backend::DashMapMemoryBackend,
key_index: Mutex<HashSet<String>>,
}
impl OxcacheSyncCache {
pub fn new() -> Self {
Self {
backend: oxcache::backend::DashMapMemoryBackend::new(),
key_index: Mutex::new(HashSet::new()),
}
}
pub fn with_capacity(capacity: usize) -> Self {
Self {
backend: oxcache::backend::DashMapMemoryBackend::builder()
.capacity(capacity)
.build(),
key_index: Mutex::new(HashSet::with_capacity(capacity)),
}
}
pub fn inner(&self) -> &oxcache::backend::DashMapMemoryBackend {
&self.backend
}
}
impl Default for OxcacheSyncCache {
fn default() -> Self {
Self::new()
}
}
impl std::fmt::Debug for OxcacheSyncCache {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let len = self.key_index.lock().map(|idx| idx.len()).unwrap_or(0);
f.debug_struct("OxcacheSyncCache")
.field("backend", &"DashMapMemoryBackend")
.field("key_count", &len)
.finish()
}
}
impl SyncCache for OxcacheSyncCache {
fn get(&self, key: &str) -> Option<Vec<u8>> {
use oxcache::backend::SyncCacheReader;
self.backend.get(key).ok().flatten()
}
fn set(&self, key: &str, value: Vec<u8>) {
use oxcache::backend::SyncCacheWriter;
let mut idx = match self.key_index.lock() {
Ok(idx) => idx,
Err(_) => {
log::warn!(
"cache key_index poisoned; set falling back to backend-only for key={:?}",
key
);
if let Err(e) = self.backend.set(key, value, None) {
log::warn!("cache backend set failed for key={:?}: {}", key, e);
}
return;
}
};
if let Err(e) = self.backend.set(key, value, None) {
log::warn!("cache backend set failed for key={:?}: {}", key, e);
return;
}
idx.insert(key.to_string());
}
fn delete(&self, key: &str) -> bool {
use oxcache::backend::{SyncCacheReader, SyncCacheWriter};
let mut idx = match self.key_index.lock() {
Ok(idx) => idx,
Err(_) => {
log::warn!(
"cache key_index poisoned; delete falling back to backend-only for key={:?}",
key
);
let existed = self.backend.exists(key).unwrap_or(false);
if existed {
if let Err(e) = self.backend.delete(key) {
log::warn!("cache backend delete failed for key={:?}: {}", key, e);
}
}
return existed;
}
};
let existed = self.backend.exists(key).unwrap_or(false);
if existed {
if let Err(e) = self.backend.delete(key) {
log::warn!("cache backend delete failed for key={:?}: {}", key, e);
return existed;
}
idx.remove(key);
}
existed
}
fn contains(&self, key: &str) -> bool {
use oxcache::backend::SyncCacheReader;
self.backend.exists(key).unwrap_or(false)
}
fn clear(&self) {
use oxcache::backend::SyncCacheWriter;
let mut idx = match self.key_index.lock() {
Ok(idx) => idx,
Err(_) => {
log::warn!("cache key_index poisoned; clear falling back to backend-only");
if let Err(e) = self.backend.clear() {
log::warn!("cache backend clear failed: {}", e);
}
return;
}
};
if let Err(e) = self.backend.clear() {
log::warn!("cache backend clear failed: {}", e);
return;
}
idx.clear();
}
fn len(&self) -> usize {
use oxcache::backend::SyncCacheReader;
self.backend.len().unwrap_or(0) as usize
}
fn is_empty(&self) -> bool {
self.len() == 0
}
fn find_keys_by_pattern(&self, pattern: &str) -> Vec<String> {
use oxcache::backend::SyncCacheReader;
let mut idx = match self.key_index.lock() {
Ok(idx) => idx,
Err(_) => return Vec::new(),
};
let mut result = Vec::new();
let mut stale_keys = Vec::new();
for k in idx.iter() {
if !matches_pattern(pattern, k) {
continue;
}
if self.backend.exists(k).unwrap_or(false) {
result.push(k.clone());
} else {
stale_keys.push(k.clone());
}
}
for k in stale_keys {
idx.remove(&k);
}
result
}
fn get_many(&self, keys: &[&str]) -> HashMap<String, Vec<u8>> {
use oxcache::backend::SyncCacheReader;
let mut results = HashMap::with_capacity(keys.len());
for &key in keys {
if let Ok(Some(value)) = self.backend.get(key) {
results.insert(key.to_string(), value);
}
}
results
}
fn set_many(&self, items: &[(String, Vec<u8>)]) {
use oxcache::backend::SyncCacheWriter;
let mut idx = match self.key_index.lock() {
Ok(idx) => idx,
Err(_) => {
log::warn!(
"cache key_index poisoned; set_many falling back to backend-only for {} items",
items.len()
);
for (key, value) in items {
if let Err(e) = self.backend.set(key, value.clone(), None) {
log::warn!("cache backend set failed for key={:?}: {}", key, e);
}
}
return;
}
};
let mut succeeded: Vec<&String> = Vec::with_capacity(items.len());
for (key, value) in items {
if let Err(e) = self.backend.set(key, value.clone(), None) {
log::warn!("cache backend set failed for key={:?}: {}", key, e);
} else {
succeeded.push(key);
}
}
for key in succeeded {
idx.insert(key.clone());
}
}
fn delete_many(&self, keys: &[&str]) -> usize {
use oxcache::backend::{SyncCacheReader, SyncCacheWriter};
let mut idx = match self.key_index.lock() {
Ok(idx) => idx,
Err(_) => {
log::warn!(
"cache key_index poisoned; delete_many falling back to backend-only for {} keys",
keys.len()
);
let mut deleted = 0usize;
for &key in keys {
let existed = self.backend.exists(key).unwrap_or(false);
if existed {
if let Err(e) = self.backend.delete(key) {
log::warn!("cache backend delete failed for key={:?}: {}", key, e);
} else {
deleted += 1;
}
}
}
return deleted;
}
};
let mut deleted = 0usize;
for &key in keys {
let existed = self.backend.exists(key).unwrap_or(false);
if existed {
if let Err(e) = self.backend.delete(key) {
log::warn!("cache backend delete failed for key={:?}: {}", key, e);
} else {
idx.remove(key);
deleted += 1;
}
}
}
deleted
}
fn get_stats(&self) -> HashMap<String, u64> {
use oxcache::backend::SyncCacheReader;
let mut stats = HashMap::new();
let len = self.backend.len().unwrap_or(0);
stats.insert("total_keys".to_string(), len);
stats.insert(
"capacity".to_string(),
SyncCacheReader::capacity(&self.backend).unwrap_or(0),
);
if let Ok(backend_stats) = self.backend.stats() {
for (k, v) in backend_stats {
if let Ok(n) = v.parse::<u64>() {
stats.entry(k).or_insert(n);
} else if let Ok(f) = v.parse::<f64>() {
let lower = k.to_lowercase();
let converted = if lower.contains("rate")
|| lower.contains("ratio")
|| lower.contains("pct")
{
(f * 100.0).round() as u64
} else {
f.round() as u64
};
log::debug!(
"cache stat {:?} parsed as f64={} converted to u64={}",
k,
f,
converted
);
stats.entry(k).or_insert(converted);
} else {
log::warn!(
"cache stat {:?} value {:?} could not be parsed as u64 or f64; dropped",
k,
v
);
}
}
}
stats
}
}
pub type DashMapCache = OxcacheSyncCache;
pub use oxcache::cache::Cache;
pub use oxcache::traits::CacheKey;
#[cfg(test)]
mod tests {
use super::*;
use crate::cache::DashMapCache;
#[test]
fn test_canonicalize_cache_key_trims_whitespace() {
assert_eq!(canonicalize_cache_key(" user:123 "), "user:123");
assert_eq!(canonicalize_cache_key("key"), "key");
assert_eq!(canonicalize_cache_key("\tkey\n"), "key");
}
#[test]
fn test_canonicalize_cache_key_lowercase() {
assert_eq!(canonicalize_cache_key("USER:123"), "user:123");
assert_eq!(canonicalize_cache_key("MixedCase"), "mixedcase");
assert_eq!(canonicalize_cache_key("USER:ABC"), "user:abc");
}
#[test]
fn test_canonicalize_cache_key_combined() {
assert_eq!(canonicalize_cache_key(" USER:123 "), "user:123");
assert_eq!(canonicalize_cache_key("\tMIXED_CASE\n"), "mixed_case");
}
#[test]
fn test_matches_pattern_exact() {
assert!(matches_pattern("user:1", "user:1"));
assert!(!matches_pattern("user:1", "user:2"));
}
#[test]
fn test_matches_pattern_prefix_wildcard() {
assert!(matches_pattern("user:*", "user:1"));
assert!(matches_pattern("user:*", "user:2"));
assert!(matches_pattern("user:*", "user:"));
assert!(!matches_pattern("user:*", "session:1"));
}
#[test]
fn test_matches_pattern_suffix_wildcard() {
assert!(matches_pattern("*:1", "user:1"));
assert!(matches_pattern("*:1", "admin:1"));
assert!(!matches_pattern("*:1", "user:2"));
}
#[test]
fn test_matches_pattern_middle_wildcard() {
assert!(matches_pattern("*session*", "my_session_data"));
assert!(matches_pattern("*session*", "session"));
assert!(!matches_pattern("*session*", "user:1"));
}
#[test]
fn test_matches_pattern_single_char_wildcard() {
assert!(matches_pattern("user:?", "user:1"));
assert!(matches_pattern("user:?", "user:a"));
assert!(!matches_pattern("user:?", "user:12"));
}
#[test]
fn test_matches_pattern_combined_wildcards() {
assert!(matches_pattern("?ser:*", "user:1"));
assert!(matches_pattern("?ser:*", "aser:xyz"));
assert!(!matches_pattern("?ser:*", "xser1:"));
}
#[test]
fn test_matches_pattern_empty_pattern_and_text() {
assert!(matches_pattern("", ""));
assert!(matches_pattern("*", ""));
assert!(!matches_pattern("", "a"));
assert!(matches_pattern("*", "anything"));
}
#[test]
fn test_synccache_trait_get_set() {
let cache: Box<dyn SyncCache> = Box::new(DashMapCache::new());
cache.set("test_key", b"test_value".to_vec());
assert_eq!(cache.get("test_key"), Some(b"test_value".to_vec()));
assert!(cache.contains("test_key"));
}
#[test]
fn test_synccache_trait_delete() {
let cache: Box<dyn SyncCache> = Box::new(DashMapCache::new());
cache.set("key1", b"value1".to_vec());
assert!(cache.delete("key1"));
assert!(!cache.contains("key1"));
assert_eq!(cache.get("key1"), None);
}
#[test]
fn test_synccache_trait_clear() {
let cache: Box<dyn SyncCache> = Box::new(DashMapCache::new());
cache.set("key1", b"v1".to_vec());
cache.set("key2", b"v2".to_vec());
cache.clear();
assert!(cache.is_empty());
assert_eq!(cache.len(), 0);
}
#[test]
fn test_synccache_trait_len_and_is_empty() {
let cache: Box<dyn SyncCache> = Box::new(DashMapCache::new());
assert!(cache.is_empty());
assert_eq!(cache.len(), 0);
cache.set("key1", b"v1".to_vec());
cache.set("key2", b"v2".to_vec());
assert!(!cache.is_empty());
assert_eq!(cache.len(), 2);
}
#[test]
fn test_synccache_trait_get_many() {
let cache: Box<dyn SyncCache> = Box::new(DashMapCache::new());
cache.set("key1", b"v1".to_vec());
cache.set("key2", b"v2".to_vec());
cache.set("key3", b"v3".to_vec());
let results = cache.get_many(&["key1", "key2", "nonexistent"]);
assert_eq!(results.len(), 2);
assert_eq!(results.get("key1"), Some(&b"v1".to_vec()));
}
#[test]
fn test_synccache_trait_set_many() {
let cache: Box<dyn SyncCache> = Box::new(DashMapCache::new());
let items = vec![
("k1".to_string(), b"v1".to_vec()),
("k2".to_string(), b"v2".to_vec()),
];
cache.set_many(&items);
assert_eq!(cache.len(), 2);
assert_eq!(cache.get("k1"), Some(b"v1".to_vec()));
assert_eq!(cache.get("k2"), Some(b"v2".to_vec()));
}
#[test]
fn test_synccache_trait_delete_many() {
let cache: Box<dyn SyncCache> = Box::new(DashMapCache::new());
cache.set("k1", b"v1".to_vec());
cache.set("k2", b"v2".to_vec());
cache.set("k3", b"v3".to_vec());
let deleted = cache.delete_many(&["k1", "k3", "nonexistent"]);
assert_eq!(deleted, 2);
assert_eq!(cache.len(), 1);
}
#[test]
fn test_synccache_trait_invalidate() {
let cache: Box<dyn SyncCache> = Box::new(DashMapCache::new());
cache.set("user:1", b"v1".to_vec());
cache.set("user:2", b"v2".to_vec());
cache.set("session:1", b"s1".to_vec());
let deleted = cache.invalidate("user:*");
assert_eq!(deleted, 2);
assert_eq!(cache.len(), 1);
assert!(cache.contains("session:1"));
}
#[test]
fn test_synccache_trait_find_keys_by_pattern() {
let cache: Box<dyn SyncCache> = Box::new(DashMapCache::new());
cache.set("user:1", b"v1".to_vec());
cache.set("user:2", b"v2".to_vec());
cache.set("admin:1", b"a1".to_vec());
let keys = cache.find_keys_by_pattern("user:*");
assert_eq!(keys.len(), 2);
assert!(keys.contains(&"user:1".to_string()));
assert!(keys.contains(&"user:2".to_string()));
}
#[test]
fn test_synccache_trait_find_keys_by_pattern_suffix() {
let cache: Box<dyn SyncCache> = Box::new(DashMapCache::new());
cache.set("user:1", b"v1".to_vec());
cache.set("admin:1", b"a1".to_vec());
cache.set("user:2", b"v2".to_vec());
let keys = cache.find_keys_by_pattern("*:1");
assert_eq!(keys.len(), 2);
assert!(keys.contains(&"user:1".to_string()));
assert!(keys.contains(&"admin:1".to_string()));
}
#[test]
fn test_synccache_trait_find_keys_by_pattern_no_match() {
let cache: Box<dyn SyncCache> = Box::new(DashMapCache::new());
cache.set("user:1", b"v1".to_vec());
let keys = cache.find_keys_by_pattern("session:*");
assert!(keys.is_empty());
}
#[test]
fn test_synccache_trait_get_stats() {
let cache: Box<dyn SyncCache> = Box::new(DashMapCache::new());
cache.set("k1", b"v1".to_vec());
cache.set("k2", b"v2".to_vec());
let stats = cache.get_stats();
assert!(stats.contains_key("total_keys"));
assert_eq!(stats.get("total_keys"), Some(&2));
assert!(stats.contains_key("capacity"));
}
#[test]
fn test_shared_cache_type_alias() {
let cache: SharedCache = Arc::new(DashMapCache::new());
cache.set("key", b"value".to_vec());
assert!(cache.contains("key"));
}
#[test]
fn test_oxcache_sync_cache_with_capacity() {
let cache = OxcacheSyncCache::with_capacity(100);
cache.set("k1", b"v1".to_vec());
assert_eq!(cache.get("k1"), Some(b"v1".to_vec()));
assert_eq!(cache.len(), 1);
}
#[test]
fn test_oxcache_sync_cache_default() {
let cache = OxcacheSyncCache::default();
assert!(cache.is_empty());
}
#[test]
fn test_oxcache_sync_cache_inner() {
let cache = OxcacheSyncCache::new();
let _backend: &oxcache::backend::DashMapMemoryBackend = cache.inner();
}
#[test]
fn test_oxcache_sync_cache_debug() {
let cache = OxcacheSyncCache::new();
let debug_str = format!("{:?}", cache);
assert!(debug_str.contains("OxcacheSyncCache"));
}
#[test]
fn test_oxcache_sync_cache_delete_nonexistent_returns_false() {
let cache = OxcacheSyncCache::new();
assert!(!cache.delete("nonexistent"));
}
struct MinimalCache {
data: std::sync::Mutex<HashMap<String, Vec<u8>>>,
}
impl MinimalCache {
fn new() -> Self {
Self {
data: std::sync::Mutex::new(HashMap::new()),
}
}
}
impl SyncCache for MinimalCache {
fn get(&self, key: &str) -> Option<Vec<u8>> {
self.data.lock().unwrap().get(key).cloned()
}
fn set(&self, key: &str, value: Vec<u8>) {
self.data.lock().unwrap().insert(key.to_string(), value);
}
fn delete(&self, key: &str) -> bool {
self.data.lock().unwrap().remove(key).is_some()
}
fn contains(&self, key: &str) -> bool {
self.data.lock().unwrap().contains_key(key)
}
fn clear(&self) {
self.data.lock().unwrap().clear();
}
fn len(&self) -> usize {
self.data.lock().unwrap().len()
}
fn is_empty(&self) -> bool {
self.len() == 0
}
fn find_keys_by_pattern(&self, pattern: &str) -> Vec<String> {
self.data
.lock()
.unwrap()
.keys()
.filter(|k| matches_pattern(pattern, k))
.cloned()
.collect()
}
}
#[test]
fn test_default_get_many_implementation() {
let cache = MinimalCache::new();
cache.set("key1", b"v1".to_vec());
cache.set("key2", b"v2".to_vec());
let results = cache.get_many(&["key1", "key2", "missing"]);
assert_eq!(results.len(), 2);
assert_eq!(results.get("key1"), Some(&b"v1".to_vec()));
assert_eq!(results.get("key2"), Some(&b"v2".to_vec()));
assert!(!results.contains_key("missing"));
}
#[test]
fn test_default_get_many_empty() {
let cache = MinimalCache::new();
let results = cache.get_many(&[]);
assert!(results.is_empty());
}
#[test]
fn test_default_set_many_implementation() {
let cache = MinimalCache::new();
let items = vec![
("k1".to_string(), b"v1".to_vec()),
("k2".to_string(), b"v2".to_vec()),
("k3".to_string(), b"v3".to_vec()),
];
cache.set_many(&items);
assert_eq!(cache.len(), 3);
assert_eq!(cache.get("k1"), Some(b"v1".to_vec()));
assert_eq!(cache.get("k2"), Some(b"v2".to_vec()));
assert_eq!(cache.get("k3"), Some(b"v3".to_vec()));
}
#[test]
fn test_default_set_many_empty() {
let cache = MinimalCache::new();
cache.set_many(&[]);
assert!(cache.is_empty());
}
#[test]
fn test_default_delete_many_implementation() {
let cache = MinimalCache::new();
cache.set("k1", b"v1".to_vec());
cache.set("k2", b"v2".to_vec());
cache.set("k3", b"v3".to_vec());
let deleted = cache.delete_many(&["k1", "k3", "missing"]);
assert_eq!(deleted, 2);
assert_eq!(cache.len(), 1);
assert!(cache.contains("k2"));
}
#[test]
fn test_default_delete_many_empty() {
let cache = MinimalCache::new();
let deleted = cache.delete_many(&[]);
assert_eq!(deleted, 0);
}
#[test]
fn test_default_get_stats_implementation() {
let cache = MinimalCache::new();
cache.set("k1", b"v1".to_vec());
cache.set("k2", b"v2".to_vec());
let stats = cache.get_stats();
assert!(stats.is_empty());
}
#[test]
fn test_default_invalidate_implementation() {
let cache = MinimalCache::new();
cache.set("user:1", b"v1".to_vec());
cache.set("user:2", b"v2".to_vec());
cache.set("session:1", b"s1".to_vec());
let deleted = cache.invalidate("user:*");
assert_eq!(deleted, 2);
assert_eq!(cache.len(), 1);
assert!(cache.contains("session:1"));
}
fn poison_key_index(cache: &OxcacheSyncCache) {
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
let _guard = cache.key_index.lock().unwrap();
panic!("intentional panic to poison key_index mutex");
}));
assert!(result.is_err(), "poisoning panic should be caught");
}
#[test]
fn test_set_with_poisoned_mutex_falls_back_to_backend() {
let cache = OxcacheSyncCache::new();
poison_key_index(&cache);
cache.set("poisoned_key", b"poisoned_value".to_vec());
assert!(cache.contains("poisoned_key"));
assert_eq!(cache.get("poisoned_key"), Some(b"poisoned_value".to_vec()));
}
#[test]
fn test_delete_with_poisoned_mutex_falls_back_to_backend() {
let cache = OxcacheSyncCache::new();
cache.set("to_delete", b"value".to_vec());
assert!(cache.contains("to_delete"));
poison_key_index(&cache);
let deleted = cache.delete("to_delete");
assert!(deleted, "delete should report the key existed");
assert!(!cache.contains("to_delete"));
}
#[test]
fn test_delete_with_poisoned_mutex_nonexistent_key() {
let cache = OxcacheSyncCache::new();
poison_key_index(&cache);
let deleted = cache.delete("nonexistent_key");
assert!(!deleted);
}
#[test]
fn test_clear_with_poisoned_mutex_falls_back_to_backend() {
let cache = OxcacheSyncCache::new();
cache.set("k1", b"v1".to_vec());
cache.set("k2", b"v2".to_vec());
assert_eq!(cache.len(), 2);
poison_key_index(&cache);
cache.clear();
assert_eq!(cache.len(), 0);
}
#[test]
fn test_find_keys_by_pattern_with_poisoned_mutex_returns_empty() {
let cache = OxcacheSyncCache::new();
cache.set("user:1", b"v1".to_vec());
cache.set("user:2", b"v2".to_vec());
poison_key_index(&cache);
let keys = cache.find_keys_by_pattern("user:*");
assert!(keys.is_empty(), "poisoned mutex should yield empty result");
}
#[test]
fn test_set_many_with_poisoned_mutex_falls_back_to_backend() {
let cache = OxcacheSyncCache::new();
poison_key_index(&cache);
let items = vec![
("batch_k1".to_string(), b"v1".to_vec()),
("batch_k2".to_string(), b"v2".to_vec()),
];
cache.set_many(&items);
assert!(cache.contains("batch_k1"));
assert!(cache.contains("batch_k2"));
assert_eq!(cache.get("batch_k2"), Some(b"v2".to_vec()));
}
#[test]
fn test_delete_many_with_poisoned_mutex_falls_back_to_backend() {
let cache = OxcacheSyncCache::new();
cache.set("del_k1", b"v1".to_vec());
cache.set("del_k2", b"v2".to_vec());
cache.set("del_k3", b"v3".to_vec());
poison_key_index(&cache);
let deleted = cache.delete_many(&["del_k1", "del_k3", "nonexistent"]);
assert_eq!(deleted, 2, "should report 2 keys deleted");
assert!(!cache.contains("del_k1"));
assert!(cache.contains("del_k2"));
assert!(!cache.contains("del_k3"));
}
#[test]
fn test_delete_many_with_poisoned_mutex_all_nonexistent() {
let cache = OxcacheSyncCache::new();
poison_key_index(&cache);
let deleted = cache.delete_many(&["no_k1", "no_k2"]);
assert_eq!(deleted, 0);
}
#[test]
fn test_get_with_poisoned_mutex_still_works() {
let cache = OxcacheSyncCache::new();
cache.set("k", b"v".to_vec());
poison_key_index(&cache);
assert_eq!(cache.get("k"), Some(b"v".to_vec()));
assert!(cache.contains("k"));
}
#[test]
fn test_find_keys_by_pattern_removes_stale_keys() {
let cache = OxcacheSyncCache::new();
{
let mut idx = cache.key_index.lock().unwrap();
idx.insert("stale_key_1".to_string());
idx.insert("stale_key_2".to_string());
}
let result = cache.find_keys_by_pattern("stale_*");
assert!(
result.is_empty(),
"Stale keys should not appear in results"
);
let idx = cache.key_index.lock().unwrap();
assert!(
!idx.contains("stale_key_1"),
"Stale key 1 should be removed from index"
);
assert!(
!idx.contains("stale_key_2"),
"Stale key 2 should be removed from index"
);
}
}