use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use std::time::{Duration, Instant};
use parking_lot::Mutex;
use super::{
FilesystemBackend, GetResult, Inner, NoopBackend, ResponseStore, StoreBackend, StoredEntry,
StoredKind,
};
impl ResponseStore {
pub fn from_env() -> Result<Arc<Self>, String> {
let max_entries = crate::env_config::parse_min(
"METRALE_STORE_MAX_ENTRIES",
std::env::var("METRALE_STORE_MAX_ENTRIES").ok().as_deref(),
1_usize,
"how many stored responses to keep before evicting the coldest",
)?
.unwrap_or(10_000);
let ttl_secs = crate::env_config::parse_min(
"METRALE_STORE_TTL_SECONDS",
std::env::var("METRALE_STORE_TTL_SECONDS").ok().as_deref(),
1_u64,
"how long a stored response stays retrievable, in seconds",
)?
.unwrap_or(86_400_u64);
let ttl = Duration::from_secs(ttl_secs);
let (backend, persistent, persist_dir): (Box<dyn StoreBackend>, bool, Option<PathBuf>) =
match std::env::var("METRALE_STORE_DIR")
.ok()
.filter(|s| !s.is_empty())
{
Some(dir) => {
let p = PathBuf::from(&dir);
match FilesystemBackend::new(&p) {
Ok(fb) => (Box::new(fb), true, Some(p)),
Err(e) => {
tracing::warn!(
"response_store: falling back to in-memory (METRALE_STORE_DIR={dir} init failed: {e})"
);
(Box::new(NoopBackend), false, None)
}
}
}
None => (Box::new(NoopBackend), false, None),
};
let replayed = backend.replay(ttl);
let mut map = HashMap::with_capacity(max_entries.min(1024).max(replayed.len()));
let mut order = std::collections::VecDeque::with_capacity(map.capacity());
for e in replayed {
order.push_back(e.id.clone());
map.insert(e.id.clone(), e);
}
Ok(Arc::new(Self {
inner: Mutex::new(Inner { map, order }),
ttl,
max_entries,
backend,
persistent,
persist_dir,
}))
}
#[cfg(test)]
pub fn with_config(max_entries: usize, ttl: Duration) -> Arc<Self> {
Arc::new(Self {
inner: Mutex::new(Inner {
map: HashMap::new(),
order: std::collections::VecDeque::new(),
}),
ttl,
max_entries,
backend: Box::new(NoopBackend),
persistent: false,
persist_dir: None,
})
}
#[cfg(test)]
pub fn with_filesystem(
max_entries: usize,
ttl: Duration,
dir: &Path,
) -> std::io::Result<Arc<Self>> {
let fb = FilesystemBackend::new(dir)?;
let replayed = fb.replay(ttl);
let mut map = HashMap::new();
let mut order = std::collections::VecDeque::new();
for e in replayed {
order.push_back(e.id.clone());
map.insert(e.id.clone(), e);
}
Ok(Arc::new(Self {
inner: Mutex::new(Inner { map, order }),
ttl,
max_entries,
backend: Box::new(fb),
persistent: true,
persist_dir: Some(dir.to_path_buf()),
}))
}
pub fn insert(&self, entry: StoredEntry) {
self.backend.persist(&entry);
let mut inner = self.inner.lock();
if inner.map.contains_key(&entry.id) {
inner.order.retain(|k| k != &entry.id);
}
let id = entry.id.clone();
inner.map.insert(id.clone(), entry);
inner.order.push_back(id);
while inner.map.len() > self.max_entries {
if let Some(oldest) = inner.order.pop_front() {
inner.map.remove(&oldest);
self.backend.forget(&oldest);
} else {
break;
}
}
}
pub fn get(&self, id: &str, kind: StoredKind) -> Option<GetResult> {
let mut inner = self.inner.lock();
let expired = match inner.map.get(id) {
Some(e) if e.kind != kind => return None,
Some(e) => e.last_access.elapsed() > self.ttl,
None => return None,
};
if expired {
inner.map.remove(id);
inner.order.retain(|k| k != id);
self.backend.forget(id);
return None;
}
inner.order.retain(|k| k != id);
inner.order.push_back(id.to_string());
let entry = inner.map.get_mut(id).expect("entry present");
entry.last_access = Instant::now();
Some(GetResult {
model: entry.model.clone(),
created_at: entry.created_at,
messages: entry.messages.clone(),
body: entry.body.clone(),
})
}
pub fn delete(&self, id: &str, kind: StoredKind) -> bool {
let mut inner = self.inner.lock();
let matches = inner.map.get(id).map(|e| e.kind == kind).unwrap_or(false);
if !matches {
return false;
}
inner.map.remove(id);
inner.order.retain(|k| k != id);
self.backend.forget(id);
true
}
pub fn len(&self) -> usize {
self.inner.lock().map.len()
}
pub fn max_entries(&self) -> usize {
self.max_entries
}
pub fn ttl(&self) -> Duration {
self.ttl
}
pub fn is_persistent(&self) -> bool {
self.persistent
}
pub fn persist_dir(&self) -> Option<&Path> {
self.persist_dir.as_deref()
}
}