use bincode::config::{BigEndian, Configuration};
use bincode::serde::{decode_from_slice, encode_to_vec};
use dashmap::DashMap;
use lazy_static::lazy_static;
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
use std::collections::HashMap;
use std::fs::{self, read_dir, File, OpenOptions};
use std::io::{ErrorKind, Read, Write};
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicU8, Ordering};
use std::sync::{Arc, Mutex, RwLock};
use std::thread::{sleep, JoinHandle};
use std::time::{Duration, SystemTime, UNIX_EPOCH};
fn now() -> u128 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("Time went backwards")
.as_millis()
}
#[derive(Serialize, Deserialize)]
struct PersistentCache {
entries: HashMap<String, (Vec<u8>, u128)>, }
#[derive(Clone)]
pub struct CacheConfig {
pub persistent: bool,
pub hash_prefix_length: usize,
pub dir_path: String,
pub cleanup_interval: Duration,
}
impl Default for CacheConfig {
fn default() -> Self {
Self {
persistent: true,
hash_prefix_length: 2,
dir_path: "cache_data".to_string(),
cleanup_interval: Duration::from_secs(10),
}
}
}
lazy_static! {
static ref ENTRIES: DashMap<String, (Vec<u8>,u128)> = DashMap::new();
static ref FILE_LOCKS: DashMap<String, Arc<Mutex<()>>> = DashMap::new();
static ref CACHE: RwLock<Option<Cache>> = RwLock::new(None);
static ref CACHESTATE: AtomicU8 = AtomicU8::new(0); static ref CLEANUP_THREAD_HANDLE: Mutex<Option<JoinHandle<()>>> = Mutex::new(None);
}
fn start_cleanup_thread(cache: Cache) -> JoinHandle<()> {
std::thread::spawn(move || {
loop {
if CACHESTATE.load(Ordering::SeqCst) != 1 {
CACHESTATE.store(0, Ordering::SeqCst);
break;
}
let now = now();
let expired_keys: Vec<String> = ENTRIES
.iter()
.filter(|entry| entry.1 <= now)
.map(|entry| entry.key().clone())
.collect();
for key in expired_keys {
let _ = cache.remove(&key);
}
sleep(cache.config.cleanup_interval);
}
})
}
#[derive(Clone)]
pub struct Cache {
config: CacheConfig,
}
impl Cache {
pub fn drop() {
CACHESTATE.store(2, Ordering::SeqCst);
if let Some(handle) = CLEANUP_THREAD_HANDLE.lock().unwrap().take() {
let _ = handle.join();
}
ENTRIES.clear();
FILE_LOCKS.clear();
let mut conf = CACHE.write().unwrap();
*conf = None;
}
pub fn instance() -> Result<Self, Box<dyn std::error::Error>> {
if let Some(cache) = CACHE.read().unwrap().as_ref() {
return Ok(cache.clone());
}
Err(Box::new(std::io::Error::new(
ErrorKind::Other,
"Cache::new not running",
)))
}
pub fn new(config: CacheConfig) -> Result<Self, Box<dyn std::error::Error>> {
if CACHESTATE.load(Ordering::SeqCst) != 0 {
return Err(Box::new(std::io::Error::new(
ErrorKind::Other,
"Cache is running",
)));
}
CACHESTATE.store(1, Ordering::SeqCst);
if config.persistent {
fs::create_dir_all(&config.dir_path)?;
}
let cache = Self { config };
if cache.config.persistent {
cache.load_persistent_data()?;
}
{
let mut conf = CACHE.write().unwrap();
*conf = Some(cache.clone());
}
let handle = start_cleanup_thread(cache.clone());
*CLEANUP_THREAD_HANDLE.lock().unwrap() = Some(handle);
Ok(cache)
}
fn config() -> Configuration<BigEndian> {
bincode::config::standard()
.with_big_endian()
.with_variable_int_encoding()
}
fn get_file_path(&self, key: &str) -> PathBuf {
let mut hasher = Sha256::new();
hasher.update(key.as_bytes());
let hash = hasher.finalize();
let prefix_len = self.config.hash_prefix_length.min(hash.len());
let prefix = hash[..prefix_len]
.iter()
.map(|b| format!("{:02x}", b).get(0..1).unwrap().to_string())
.collect::<String>();
Path::new(&self.config.dir_path).join(format!("cache_{}.bin", prefix))
}
fn load_persistent_data(&self) -> Result<(), Box<dyn std::error::Error>> {
for entry in read_dir(&self.config.dir_path)? {
let entry = entry?;
let path = entry.path();
if path.is_file() && path.extension().and_then(|s| s.to_str()) == Some("bin") {
let file_key = path.to_string_lossy().to_string();
let file_lock = FILE_LOCKS
.entry(file_key)
.or_insert_with(|| Arc::new(Mutex::new(())))
.clone();
let _guard = file_lock.lock().unwrap();
let mut file = File::open(&path)?;
let mut buffer = Vec::new();
file.read_to_end(&mut buffer)?;
let persistent_cache: HashMap<String, (Vec<u8>, u128)> =
decode_from_slice(&buffer, Self::config())?.0;
for (key, (value, expires_at)) in persistent_cache {
ENTRIES.insert(key.clone(), (value, expires_at));
}
}
}
Ok(())
}
pub fn set<V: Serialize>(
&self,
key: &str,
value: V,
ttl: Duration,
) -> Result<(), Box<dyn std::error::Error>> {
let serialized = encode_to_vec(&value, Self::config())?;
let expires_at = now() + ttl.as_millis();
ENTRIES.insert(key.to_string(), (serialized, expires_at));
if self.config.persistent {
self.persist_key(key)?;
}
Ok(())
}
fn persist_key(&self, key: &str) -> Result<(), Box<dyn std::error::Error>> {
let file_path = self.get_file_path(key);
let file_key = file_path.to_string_lossy().to_string();
let file_lock = FILE_LOCKS
.entry(file_key)
.or_insert_with(|| Arc::new(Mutex::new(())))
.clone();
let _guard = file_lock.lock().unwrap();
let mut persistent_entries = if file_path.exists() {
let mut file = File::open(&file_path)?;
let mut buffer = Vec::new();
file.read_to_end(&mut buffer)?;
let r: PersistentCache = decode_from_slice(&buffer, Self::config())?.0;
r.entries
} else {
HashMap::new()
};
if let Some(v) = ENTRIES.get(key) {
persistent_entries.insert(key.to_string(), v.clone());
} else {
persistent_entries.remove(key);
}
if persistent_entries.is_empty() {
if file_path.exists() {
fs::remove_file(file_path)?;
}
return Ok(());
}
let persistent_cache = PersistentCache {
entries: persistent_entries,
};
let serialized = encode_to_vec(&persistent_cache, Self::config())?;
let mut file = OpenOptions::new()
.create(true)
.write(true)
.truncate(true)
.open(&file_path)?;
file.write_all(&serialized)?;
Ok(())
}
pub fn get<V: for<'de> Deserialize<'de>>(&self, key: &str) -> Option<V> {
let now = now();
ENTRIES.get(key).and_then(|entry| {
if entry.1 > now {
decode_from_slice(&entry.0, Self::config())
.ok()
.map(|(v, _)| v)
} else {
None
}
})
}
pub fn expire(&self, key: &str) -> Option<Duration> {
let now = now();
ENTRIES.get(key).and_then(|entry| {
if entry.1 > now {
let remaining = entry.1 - now;
Some(Duration::from_millis(remaining as u64))
} else {
None
}
})
}
pub fn remove(&self, key: &str) -> Result<(), Box<dyn std::error::Error>> {
ENTRIES.remove(key);
if self.config.persistent {
self.persist_key(key)?;
}
Ok(())
}
pub fn clear(&self) -> Result<(), Box<dyn std::error::Error>> {
ENTRIES.clear();
FILE_LOCKS.clear();
if self.config.persistent {
for entry in read_dir(&self.config.dir_path)? {
let entry = entry?;
let path = entry.path();
if path.is_file() && path.extension().and_then(|s| s.to_str()) == Some("bin") {
fs::remove_file(path)?;
}
}
}
Ok(())
}
pub fn len(&self) -> usize {
ENTRIES.len()
}
pub fn is_empty(&self) -> bool {
ENTRIES.is_empty()
}
pub fn memory_usage(&self) -> usize {
let entries_size = ENTRIES
.iter()
.fold(0, |acc, entry| {
acc + size_of_val(entry.key())
+ size_of_val(&entry.value().0) + size_of::<u128>() + size_of::<String>() });
let file_locks_size = FILE_LOCKS
.iter()
.fold(0, |acc, entry| {
acc + size_of_val(entry.key())
+ size_of::<Arc<Mutex<()>>>() + size_of::<String>() });
entries_size+file_locks_size
}
}