use std::collections::{HashMap, HashSet};
use std::sync::{Arc, Mutex};
use super::store::{IngestStore, StoreDirectory, VectorBackendStore};
use super::store_log::{OpenError, run_blocking};
#[derive(Clone)]
pub struct VectorDirectory {
projects: HashSet<String>,
fallback: bool,
stores: Arc<Mutex<HashMap<Option<String>, Slot>>>,
persist: Option<(std::path::PathBuf, u64)>,
}
type Slot = Arc<Mutex<Option<Arc<VectorBackendStore>>>>;
const FALLBACK_LOG: &str = "_controller.vec";
impl VectorDirectory {
pub fn new(projects: impl IntoIterator<Item = String>, fallback: bool) -> Self {
Self {
projects: projects.into_iter().collect(),
fallback,
stores: Arc::default(),
persist: None,
}
}
pub fn with_persistence(self, dir: std::path::PathBuf) -> Self {
self.with_persistence_capped(dir, super::store_log::DEFAULT_MAX_LOG_BYTES)
}
pub fn with_persistence_capped(mut self, dir: std::path::PathBuf, max_log_bytes: u64) -> Self {
self.persist = Some((dir, max_log_bytes));
self
}
pub fn view(&self, projects: impl IntoIterator<Item = String>, fallback: bool) -> Self {
Self {
projects: projects.into_iter().collect(),
fallback,
stores: self.stores.clone(),
persist: self.persist.clone(),
}
}
fn open_store(&self, project_id: Option<&str>) -> Option<Arc<VectorBackendStore>> {
let backend = || {
Arc::new(crate::vector_hnsw::HnswBackend::new(
crate::hnsw_service::HnswServiceConfig::default(),
))
};
let Some((dir, cap)) = &self.persist else {
return Some(Arc::new(VectorBackendStore::new(backend())));
};
let file = match project_id {
Some(p) => format!("{p}.vec"),
None => FALLBACK_LOG.to_string(),
};
let path = dir.join(&file);
match VectorBackendStore::persistent(backend(), &path, *cap) {
Ok(s) => Some(Arc::new(s)),
Err(OpenError::Io(e)) => {
tracing::warn!(error = %e, path = %path.display(), "cog ingest vector log could not be opened");
None
}
Err(OpenError::Corrupt(e)) => {
let ts = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_nanos())
.unwrap_or_default();
let aside = dir.join(format!("{file}.unreadable.{ts}"));
tracing::warn!(error = %e, path = %path.display(), aside = %aside.display(), "cog ingest vector log unusable; moving it aside");
std::fs::rename(&path, &aside).ok()?;
VectorBackendStore::persistent(backend(), &path, *cap).ok().map(Arc::new)
}
}
}
}
impl StoreDirectory for VectorDirectory {
fn store_for(&self, project_id: Option<&str>) -> Option<Arc<dyn IngestStore>> {
match project_id {
Some(p) if !self.projects.contains(p) => return None,
None if !self.fallback => return None,
_ => {}
}
let slot = {
let mut g = self.stores.lock().ok()?;
g.entry(project_id.map(String::from)).or_default().clone()
};
let mut cell = slot.lock().ok()?;
if cell.is_none() {
*cell = run_blocking(|| self.open_store(project_id));
}
cell.clone().map(|s| s as Arc<dyn IngestStore>)
}
}