use std::sync::Arc;
use std::time::{Duration, SystemTime, UNIX_EPOCH};
use async_trait::async_trait;
use common::{NamespaceId, Result, Vector, VectorId};
use crate::compaction::{CompactionConfig, CompactionManager};
use crate::traits::VectorStorage;
fn now_secs() -> u64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0)
}
pub struct SegmentedStorage {
manager: Arc<CompactionManager>,
}
impl SegmentedStorage {
pub fn new(config: CompactionConfig) -> Self {
let manager = Arc::new(CompactionManager::new(config.clone()));
if config.auto_compact {
if let Ok(handle) = tokio::runtime::Handle::try_current() {
let mgr = manager.clone();
let interval = config.compaction_interval_secs.max(1);
handle.spawn(async move {
let mut ticker = tokio::time::interval(Duration::from_secs(interval));
ticker.tick().await; loop {
ticker.tick().await;
let results = mgr.compact_all();
if !results.is_empty() {
let merged: usize = results.values().map(|v| v.len()).sum();
tracing::debug!(
namespaces = results.len(),
compactions = merged,
"segmented storage auto-compaction merged segments"
);
}
}
});
tracing::info!(
interval_secs = interval,
"Segmented storage background compaction started"
);
}
}
Self { manager }
}
pub fn with_defaults() -> Self {
Self::new(CompactionConfig::default())
}
pub fn manager(&self) -> &Arc<CompactionManager> {
&self.manager
}
pub fn compact_all(&self) -> usize {
self.manager.compact_all().values().map(|v| v.len()).sum()
}
}
impl Default for SegmentedStorage {
fn default() -> Self {
Self::with_defaults()
}
}
#[async_trait]
impl VectorStorage for SegmentedStorage {
async fn upsert(&self, namespace: &NamespaceId, vectors: Vec<Vector>) -> Result<usize> {
let count = vectors.len();
for vector in vectors {
self.manager.add(namespace, vector);
}
Ok(count)
}
async fn get(&self, namespace: &NamespaceId, ids: &[VectorId]) -> Result<Vec<Vector>> {
let now = now_secs();
Ok(ids
.iter()
.filter_map(|id| self.manager.get(namespace, id))
.filter(|v| !v.is_expired_at(now))
.collect())
}
async fn get_all(&self, namespace: &NamespaceId) -> Result<Vec<Vector>> {
let now = now_secs();
Ok(self
.manager
.get_all(namespace)
.into_iter()
.filter(|v| !v.is_expired_at(now))
.collect())
}
async fn delete(&self, namespace: &NamespaceId, ids: &[VectorId]) -> Result<usize> {
let mut removed = 0;
for id in ids {
if self.manager.delete(namespace, id) {
removed += 1;
}
}
Ok(removed)
}
async fn namespace_exists(&self, namespace: &NamespaceId) -> Result<bool> {
Ok(self
.manager
.list_namespaces()
.iter()
.any(|n| n == namespace))
}
async fn ensure_namespace(&self, namespace: &NamespaceId) -> Result<()> {
let _ = self.manager.namespace(namespace);
Ok(())
}
async fn count(&self, namespace: &NamespaceId) -> Result<usize> {
let now = now_secs();
Ok(self
.manager
.get_all(namespace)
.into_iter()
.filter(|v| !v.is_expired_at(now))
.count())
}
async fn dimension(&self, namespace: &NamespaceId) -> Result<Option<usize>> {
let now = now_secs();
Ok(self
.manager
.get_all(namespace)
.into_iter()
.find(|v| !v.is_expired_at(now))
.map(|v| v.values.len()))
}
async fn list_namespaces(&self) -> Result<Vec<NamespaceId>> {
Ok(self.manager.list_namespaces())
}
async fn delete_namespace(&self, namespace: &NamespaceId) -> Result<bool> {
Ok(self.manager.delete_namespace(namespace))
}
async fn cleanup_expired(&self, namespace: &NamespaceId) -> Result<usize> {
let now = now_secs();
let expired: Vec<VectorId> = self
.manager
.get_all(namespace)
.into_iter()
.filter(|v| v.is_expired_at(now))
.map(|v| v.id)
.collect();
let mut removed = 0;
for id in &expired {
if self.manager.delete(namespace, id) {
removed += 1;
}
}
Ok(removed)
}
async fn cleanup_all_expired(&self) -> Result<usize> {
let mut total = 0;
for namespace in self.manager.list_namespaces() {
total += self.cleanup_expired(&namespace).await?;
}
Ok(total)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn tv(id: &str) -> Vector {
Vector {
id: id.to_string(),
values: vec![0.1, 0.2, 0.3],
metadata: None,
ttl_seconds: None,
expires_at: None,
}
}
fn store() -> SegmentedStorage {
SegmentedStorage::new(CompactionConfig {
auto_compact: false,
..Default::default()
})
}
#[tokio::test]
async fn upsert_get_delete_roundtrip() {
let s = store();
let ns = "n".to_string();
s.ensure_namespace(&ns).await.unwrap();
assert_eq!(s.upsert(&ns, vec![tv("a"), tv("b")]).await.unwrap(), 2);
assert_eq!(s.count(&ns).await.unwrap(), 2);
assert_eq!(s.dimension(&ns).await.unwrap(), Some(3));
let got = s.get(&ns, &["a".to_string()]).await.unwrap();
assert_eq!(got.len(), 1);
assert_eq!(got[0].id, "a");
assert_eq!(s.delete(&ns, &["a".to_string()]).await.unwrap(), 1);
assert!(s.get(&ns, &["a".to_string()]).await.unwrap().is_empty());
assert_eq!(s.count(&ns).await.unwrap(), 1);
let all: Vec<String> = s
.get_all(&ns)
.await
.unwrap()
.into_iter()
.map(|v| v.id)
.collect();
assert_eq!(all, vec!["b".to_string()]);
}
#[tokio::test]
async fn expired_vectors_are_filtered_and_reclaimed() {
let s = store();
let ns = "n".to_string();
let mut expired = tv("old");
expired.expires_at = Some(1); s.upsert(&ns, vec![expired, tv("fresh")]).await.unwrap();
assert_eq!(s.count(&ns).await.unwrap(), 1);
let ids: Vec<String> = s
.get_all(&ns)
.await
.unwrap()
.into_iter()
.map(|v| v.id)
.collect();
assert_eq!(ids, vec!["fresh".to_string()]);
assert_eq!(s.cleanup_expired(&ns).await.unwrap(), 1);
}
#[tokio::test]
async fn namespace_lifecycle() {
let s = store();
let ns = "n".to_string();
s.upsert(&ns, vec![tv("a")]).await.unwrap();
assert!(s.namespace_exists(&ns).await.unwrap());
assert_eq!(s.list_namespaces().await.unwrap(), vec![ns.clone()]);
assert!(s.delete_namespace(&ns).await.unwrap());
assert!(!s.namespace_exists(&ns).await.unwrap());
}
}