use std::collections::{HashMap, VecDeque};
use std::io::{BufRead, Read, Write};
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicU64, AtomicUsize, Ordering};
use std::sync::Mutex;
use std::time::{Duration, SystemTime, UNIX_EPOCH};
use crate::response::Response;
const MAGIC: &str = "RWSCACHE1";
const SUFFIX: &str = ".entry";
const MAX_KEY: usize = 16 * 1024;
#[derive(Debug, Clone)]
struct Meta {
file: PathBuf,
size: u64,
stored_ms: u64,
ttl: Duration,
}
impl Meta {
fn age(&self) -> Duration {
Duration::from_millis(now_ms().saturating_sub(self.stored_ms))
}
fn fresh(&self) -> bool {
self.age() < self.ttl
}
}
#[derive(Default)]
struct Index {
entries: HashMap<String, Meta>,
order: VecDeque<String>,
total: u64,
}
impl Index {
fn remove(&mut self, key: &str) -> Option<Meta> {
let meta = self.entries.remove(key)?;
self.order.retain(|k| k != key);
self.total = self.total.saturating_sub(meta.size);
Some(meta)
}
}
pub(crate) struct DiskStore {
dir: PathBuf,
max_bytes: u64,
index: Mutex<Index>,
count: AtomicUsize,
seq: AtomicU64,
}
fn now_ms() -> u64 {
SystemTime::now().duration_since(UNIX_EPOCH).map(|d| d.as_millis() as u64).unwrap_or(0)
}
fn file_name(key: &str) -> String {
let hash = crate::core::sha256::sha256(key.as_bytes());
let hex: String = hash[..16].iter().map(|b| format!("{b:02x}")).collect();
format!("{hex}{SUFFIX}")
}
pub(crate) fn parse_entry_header(bytes: &[u8]) -> Option<(u64, Duration, String, usize)> {
let line_end = memchr::memchr(b'\n', &bytes[..bytes.len().min(128)])?;
let line = std::str::from_utf8(&bytes[..line_end]).ok()?;
let mut parts = line.split(' ');
if parts.next()? != MAGIC {
return None;
}
let stored_ms: u64 = parts.next()?.parse().ok()?;
let ttl: u64 = parts.next()?.parse().ok()?;
let key_len: usize = parts.next()?.parse().ok()?;
if parts.next().is_some() || key_len > MAX_KEY {
return None;
}
let key_start = line_end + 1;
let key_end = key_start.checked_add(key_len)?;
if bytes.get(key_end) != Some(&b'\n') {
return None;
}
let key = std::str::from_utf8(&bytes[key_start..key_end]).ok()?.to_string();
Some((stored_ms, Duration::from_secs(ttl), key, key_end + 1))
}
impl DiskStore {
pub(crate) fn open(dir: impl Into<PathBuf>, max_bytes: u64) -> std::io::Result<DiskStore> {
let dir = dir.into();
std::fs::create_dir_all(&dir)?;
let mut found: Vec<(String, Meta)> = Vec::new();
for entry in std::fs::read_dir(&dir)? {
let Ok(entry) = entry else { continue };
let path = entry.path();
let name = entry.file_name().to_string_lossy().to_string();
if name.contains(".tmp-") {
let _ = std::fs::remove_file(&path);
continue;
}
if !name.ends_with(SUFFIX) {
continue;
}
match read_header(&path) {
Some((stored_ms, ttl, key, size)) => {
let meta = Meta { file: path.clone(), size, stored_ms, ttl };
if meta.fresh() {
found.push((key, meta));
} else {
let _ = std::fs::remove_file(&path);
}
}
None => {
let _ = std::fs::remove_file(&path);
}
}
}
found.sort_by_key(|(_, m)| m.stored_ms);
let mut index = Index::default();
for (key, meta) in found {
index.total += meta.size;
index.order.push_back(key.clone());
index.entries.insert(key, meta);
}
let store = DiskStore { dir, max_bytes, count: AtomicUsize::new(index.entries.len()), index: Mutex::new(index), seq: AtomicU64::new(0) };
let evicted = store.evict_over_limit();
remove_files(evicted);
Ok(store)
}
fn lock(&self) -> std::sync::MutexGuard<'_, Index> {
self.index.lock().unwrap_or_else(|e| e.into_inner())
}
pub(crate) fn is_empty(&self) -> bool {
self.count.load(Ordering::Relaxed) == 0
}
pub(crate) fn len(&self) -> usize {
self.count.load(Ordering::Relaxed)
}
pub(crate) fn bytes(&self) -> u64 {
self.lock().total
}
pub(crate) fn get(&self, key: &str) -> Option<(Response, Duration)> {
let meta = {
let mut index = self.lock();
let meta = index.entries.get(key)?.clone();
if !meta.fresh() {
index.remove(key);
self.count.store(index.entries.len(), Ordering::Relaxed);
drop(index);
let _ = std::fs::remove_file(&meta.file);
return None;
}
meta
};
match read_entry(&meta.file, key) {
Some(response) => Some((response, meta.age())),
None => {
let mut index = self.lock();
if index.entries.get(key).is_some_and(|m| m.stored_ms == meta.stored_ms) {
index.remove(key);
self.count.store(index.entries.len(), Ordering::Relaxed);
}
drop(index);
let _ = std::fs::remove_file(&meta.file);
None
}
}
}
pub(crate) fn put(&self, key: &str, response: &Response, ttl: Duration) {
if key.len() > MAX_KEY {
return;
}
let stored_ms = now_ms();
let mut bytes = format!("{MAGIC} {stored_ms} {} {}\n{key}\n", ttl.as_secs(), key.len()).into_bytes();
bytes.extend_from_slice(&response.clone().generate());
let size = bytes.len() as u64;
if size > self.max_bytes {
return;
}
let file = self.dir.join(file_name(key));
let tmp = self.dir.join(format!("{}.tmp-{}-{}", file_name(key), std::process::id(), self.seq.fetch_add(1, Ordering::Relaxed)));
let written = std::fs::File::create(&tmp).and_then(|mut f| f.write_all(&bytes)).and_then(|_| std::fs::rename(&tmp, &file));
if let Err(e) = written {
let _ = std::fs::remove_file(&tmp);
eprintln!("cache: can't write {}: {e}", file.display());
return;
}
let evicted = {
let mut index = self.lock();
index.remove(key);
index.total += size;
index.order.push_back(key.to_string());
index.entries.insert(key.to_string(), Meta { file, size, stored_ms, ttl });
drop(index);
self.evict_over_limit()
};
remove_files(evicted);
}
fn evict_over_limit(&self) -> Vec<PathBuf> {
let mut index = self.lock();
let mut files = Vec::new();
let stale: Vec<String> = index.entries.iter().filter(|(_, m)| !m.fresh()).map(|(k, _)| k.clone()).collect();
for key in stale {
if let Some(m) = index.remove(&key) {
files.push(m.file);
}
}
while index.total > self.max_bytes {
let Some(oldest) = index.order.front().cloned() else { break };
if let Some(m) = index.remove(&oldest) {
files.push(m.file);
}
}
self.count.store(index.entries.len(), Ordering::Relaxed);
files
}
pub(crate) fn remove_where(&self, matches: impl Fn(&str) -> bool) -> usize {
let files: Vec<PathBuf> = {
let mut index = self.lock();
let keys: Vec<String> = index.entries.keys().filter(|k| matches(k)).cloned().collect();
let files = keys.iter().filter_map(|k| index.remove(k)).map(|m| m.file).collect();
self.count.store(index.entries.len(), Ordering::Relaxed);
files
};
let n = files.len();
remove_files(files);
n
}
}
fn remove_files(files: Vec<PathBuf>) {
for file in files {
let _ = std::fs::remove_file(file);
}
}
fn read_header(path: &Path) -> Option<(u64, Duration, String, u64)> {
let file = std::fs::File::open(path).ok()?;
let size = file.metadata().ok()?.len();
let mut reader = std::io::BufReader::new(file);
let mut line = Vec::new();
reader.by_ref().take(128).read_until(b'\n', &mut line).ok()?;
let key_len: usize = std::str::from_utf8(&line).ok()?.trim_end().rsplit(' ').next()?.parse().ok()?;
if key_len > MAX_KEY {
return None;
}
let mut rest = vec![0; key_len + 1];
reader.read_exact(&mut rest).ok()?;
line.extend_from_slice(&rest);
let (stored_ms, ttl, key, _) = parse_entry_header(&line)?;
Some((stored_ms, ttl, key, size))
}
fn read_entry(path: &Path, key: &str) -> Option<Response> {
let bytes = std::fs::read(path).ok()?;
let (_, _, stored_key, offset) = parse_entry_header(&bytes)?;
if stored_key != key {
return None;
}
Response::parse_upstream(&bytes[offset..], crate::proxy::REGENERATED_OR_HOP_BY_HOP).ok()
}