use crate::backend::interface::{BackendKind, CacheConnector, CacheReader, CacheWriter};
use crate::backend::score::{BackendScore, Scores};
use crate::error::Result;
use crate::impl_backend_builder;
use async_trait::async_trait;
use dashmap::DashMap;
use std::collections::HashMap;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc;
use std::time::{Duration, Instant};
#[derive(Clone, Debug)]
pub(crate) struct CacheEntry {
value: Vec<u8>,
expires_at: Option<Instant>,
}
#[derive(Clone)]
pub struct DashMapMemoryBackend {
cache: Arc<DashMap<String, CacheEntry>>,
hits: Arc<AtomicUsize>,
misses: Arc<AtomicUsize>,
capacity: usize,
default_ttl: Option<Duration>,
}
impl_backend_builder!(DashMapMemoryBackend, DashMapBackendBuilder);
impl DashMapMemoryBackend {
fn evict_if_full(&self) {
if let Some(key) = self
.cache
.iter()
.filter_map(|r| {
let entry = r.value();
entry.expires_at.map(|exp| (r.key().clone(), exp))
})
.min_by_key(|(_, exp)| *exp)
.map(|(key, _)| key)
{
self.cache.remove(&key);
}
}
pub fn capacity(&self) -> usize {
self.capacity
}
pub fn entry_count(&self) -> usize {
self.cache.len()
}
pub fn hit_rate(&self) -> f64 {
let hits = self.hits.load(Ordering::Relaxed);
let misses = self.misses.load(Ordering::Relaxed);
let total = hits + misses;
if total == 0 {
0.0
} else {
hits as f64 / total as f64
}
}
}
impl Default for DashMapMemoryBackend {
fn default() -> Self {
Self::new()
}
}
impl std::fmt::Debug for DashMapMemoryBackend {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("DashMapMemoryBackend")
.field("capacity", &self.capacity)
.field("entry_count", &self.cache.len())
.field("hit_rate", &self.hit_rate())
.finish()
}
}
#[async_trait]
impl CacheReader for DashMapMemoryBackend {
async fn get(&self, key: &str) -> Result<Option<Vec<u8>>> {
let now = Instant::now();
let result = self.cache.get(key).map(|entry_ref| {
let entry = entry_ref.value();
if let Some(expires_at) = entry.expires_at {
if expires_at <= now {
self.misses.fetch_add(1, Ordering::SeqCst);
return None;
}
}
self.hits.fetch_add(1, Ordering::SeqCst);
Some(entry.value.clone())
});
if result.is_none() {
self.misses.fetch_add(1, Ordering::SeqCst);
}
Ok(result.flatten())
}
async fn exists(&self, key: &str) -> Result<bool> {
let now = Instant::now();
if let Some(entry_ref) = self.cache.get(key) {
let entry = entry_ref.value();
if let Some(expires_at) = entry.expires_at {
if expires_at <= now {
return Ok(false);
}
}
Ok(true)
} else {
Ok(false)
}
}
async fn ttl(&self, key: &str) -> Result<Option<Duration>> {
let now = Instant::now();
if let Some(entry_ref) = self.cache.get(key) {
let entry = entry_ref.value();
if let Some(expires_at) = entry.expires_at {
if expires_at > now {
return Ok(Some(expires_at.duration_since(now)));
} else {
return Ok(None);
}
}
Ok(None)
} else {
Ok(None)
}
}
async fn len(&self) -> Result<u64> {
Ok(self.cache.len() as u64)
}
async fn is_empty(&self) -> Result<bool> {
Ok(self.cache.is_empty())
}
async fn capacity(&self) -> Result<u64> {
Ok(self.capacity as u64)
}
async fn stats(&self) -> Result<HashMap<String, String>> {
let mut stats = HashMap::new();
stats.insert("type".to_string(), "dashmap".to_string());
stats.insert("capacity".to_string(), self.capacity.to_string());
stats.insert("entry_count".to_string(), self.cache.len().to_string());
stats.insert("hits".to_string(), self.hits.load(Ordering::Relaxed).to_string());
stats.insert("misses".to_string(), self.misses.load(Ordering::Relaxed).to_string());
stats.insert("hit_rate".to_string(), format!("{:.4}", self.hit_rate()));
Ok(stats)
}
}
#[async_trait]
impl CacheWriter for DashMapMemoryBackend {
async fn set(&self, key: &str, value: Vec<u8>, ttl: Option<Duration>) -> Result<()> {
let now = Instant::now();
let expires_at = ttl.or(self.default_ttl).map(|duration| now + duration);
let entry = CacheEntry { value, expires_at };
self.cache.insert(key.to_string(), entry);
if self.cache.len() > self.capacity {
self.evict_if_full();
}
Ok(())
}
async fn delete(&self, key: &str) -> Result<()> {
self.cache.remove(key);
Ok(())
}
async fn clear(&self) -> Result<()> {
self.cache.clear();
self.hits.store(0, Ordering::Relaxed);
self.misses.store(0, Ordering::Relaxed);
Ok(())
}
async fn expire(&self, key: &str, ttl: Duration) -> Result<bool> {
let now = Instant::now();
let new_expires_at = now + ttl;
if let Some(mut entry_ref) = self.cache.get_mut(key) {
entry_ref.expires_at = Some(new_expires_at);
Ok(true)
} else {
Ok(false)
}
}
}
#[async_trait]
impl CacheConnector for DashMapMemoryBackend {
async fn health_check(&self) -> Result<()> {
Ok(())
}
async fn shutdown(&self) {
self.cache.clear();
}
fn backend_kind(&self) -> BackendKind {
BackendKind::DashMap
}
}
impl BackendScore for DashMapMemoryBackend {
fn score(&self) -> u8 {
Scores::DASHMAP
}
fn is_persistent(&self) -> bool {
false
}
fn backend_name(&self) -> &'static str {
"dashmap"
}
}
#[derive(Debug, Clone, Default)]
pub struct DashMapBackendBuilder {
capacity: usize,
default_ttl: Option<Duration>,
}
impl DashMapBackendBuilder {
pub fn capacity(mut self, capacity: usize) -> Self {
self.capacity = capacity;
self
}
pub fn default_ttl(mut self, ttl: Duration) -> Self {
self.default_ttl = Some(ttl);
self
}
pub fn build(self) -> DashMapMemoryBackend {
let capacity = if self.capacity > 0 {
self.capacity
} else {
10_000 };
DashMapMemoryBackend {
cache: Arc::new(DashMap::new()),
hits: Arc::new(AtomicUsize::new(0)),
misses: Arc::new(AtomicUsize::new(0)),
capacity,
default_ttl: self.default_ttl,
}
}
}
pub fn dashmap_memory() -> DashMapMemoryBackend {
DashMapMemoryBackend::new()
}
pub fn dashmap_memory_with_capacity(capacity: usize) -> DashMapMemoryBackend {
DashMapMemoryBackend::builder().capacity(capacity).build()
}
pub fn dashmap_memory_with_capacity_and_ttl(capacity: usize, ttl: Duration) -> DashMapMemoryBackend {
DashMapMemoryBackend::builder()
.capacity(capacity)
.default_ttl(ttl)
.build()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_dashmap_backend_builder() {
let backend = DashMapMemoryBackend::builder()
.capacity(1000)
.default_ttl(Duration::from_secs(3600))
.build();
assert_eq!(backend.capacity(), 1000);
}
#[test]
fn test_dashmap_backend_default() {
let backend = DashMapMemoryBackend::default();
assert!(backend.capacity() > 0);
}
#[tokio::test]
async fn test_dashmap_basic_operations() {
let backend = DashMapMemoryBackend::new();
backend.set("key1", b"value1".to_vec(), None).await.unwrap();
let value = backend.get("key1").await.unwrap();
assert_eq!(value, Some(b"value1".to_vec()));
assert!(backend.exists("key1").await.unwrap());
assert!(!backend.exists("key2").await.unwrap());
backend.delete("key1").await.unwrap();
assert!(!backend.exists("key1").await.unwrap());
backend.health_check().await.unwrap();
let stats = backend.stats().await.unwrap();
assert_eq!(stats.get("type"), Some(&"dashmap".to_string()));
assert_eq!(stats.get("capacity"), Some(&backend.capacity().to_string()));
}
#[tokio::test]
async fn test_dashmap_ttl() {
let backend = DashMapMemoryBackend::new();
backend
.set("key1", b"value1".to_vec(), Some(Duration::from_millis(100)))
.await
.unwrap();
assert!(backend.exists("key1").await.unwrap());
tokio::time::sleep(Duration::from_millis(150)).await;
assert!(!backend.exists("key1").await.unwrap());
}
#[test]
fn test_convenience_functions() {
let backend1 = dashmap_memory();
let backend2 = dashmap_memory_with_capacity(1000);
let backend3 = dashmap_memory_with_capacity_and_ttl(1000, Duration::from_secs(3600));
assert!(backend1.capacity() > 0);
assert_eq!(backend2.capacity(), 1000);
assert_eq!(backend3.capacity(), 1000);
}
}