use std::collections::{HashMap, HashSet};
use std::sync::Arc;
use khive_runtime::{KhiveRuntime, Namespace, NamespaceToken, RuntimeError};
use khive_storage::types::{SqlStatement, SqlValue};
use khive_vamana::{
corpus_content_hash, read_commit_fingerprint, CorpusFingerprint, VamanaConfig, VamanaIndex,
VamanaSnapshot,
};
use tokio::sync::RwLock;
use uuid::Uuid;
pub(crate) struct AnnBridge {
index: VamanaIndex,
id_map: Vec<Uuid>,
generation: u64,
}
#[derive(Clone, Debug, Eq, PartialEq, Hash)]
pub(crate) struct AnnKey {
namespace: String,
model: String,
}
impl AnnKey {
pub(crate) fn new(namespace: &str, model: &str) -> Self {
Self {
namespace: namespace.to_owned(),
model: model.to_owned(),
}
}
}
pub(crate) struct AnnState {
indexes: RwLock<HashMap<AnnKey, AnnBridge>>,
warming: std::sync::Mutex<HashSet<AnnKey>>,
generations: std::sync::Mutex<HashMap<String, u64>>,
unavailable: std::sync::Mutex<HashMap<AnnKey, u64>>,
}
pub(crate) type SharedAnn = Arc<AnnState>;
pub(crate) fn new_shared() -> SharedAnn {
Arc::new(AnnState {
indexes: RwLock::new(HashMap::new()),
warming: std::sync::Mutex::new(HashSet::new()),
generations: std::sync::Mutex::new(HashMap::new()),
unavailable: std::sync::Mutex::new(HashMap::new()),
})
}
fn generations_guard(
m: &std::sync::Mutex<HashMap<String, u64>>,
) -> std::sync::MutexGuard<'_, HashMap<String, u64>> {
m.lock().unwrap_or_else(std::sync::PoisonError::into_inner)
}
fn bump_generation(ann: &SharedAnn, namespace: &str) -> u64 {
let mut gens = generations_guard(&ann.generations);
let slot = gens.entry(namespace.to_owned()).or_insert(0);
*slot += 1;
*slot
}
pub(crate) fn current_generation(ann: &SharedAnn, namespace: &str) -> u64 {
generations_guard(&ann.generations)
.get(namespace)
.copied()
.unwrap_or(0)
}
pub(crate) async fn install_if_fresher(ann: &SharedAnn, key: &AnnKey, candidate: AnnBridge) {
let mut idxs = ann.indexes.write().await;
let ns_generation = current_generation(ann, &key.namespace);
if candidate.generation < ns_generation {
tracing::debug!(
key = ?key,
candidate_generation = candidate.generation,
namespace_generation = ns_generation,
"knowledge ANN install skipped: candidate predates namespace's current generation"
);
return;
}
match idxs.get(key) {
Some(existing) if existing.generation >= candidate.generation => {
tracing::debug!(
key = ?key,
existing_generation = existing.generation,
candidate_generation = candidate.generation,
"knowledge ANN install skipped: cached entry already >= this build's generation"
);
}
_ => {
idxs.insert(key.clone(), candidate);
unavailable_guard(&ann.unavailable).remove(key);
}
}
}
fn warming_guard(
m: &std::sync::Mutex<HashSet<AnnKey>>,
) -> std::sync::MutexGuard<'_, HashSet<AnnKey>> {
m.lock().unwrap_or_else(std::sync::PoisonError::into_inner)
}
fn unavailable_guard(
m: &std::sync::Mutex<HashMap<AnnKey, u64>>,
) -> std::sync::MutexGuard<'_, HashMap<AnnKey, u64>> {
m.lock().unwrap_or_else(std::sync::PoisonError::into_inner)
}
fn mark_unavailable(ann: &SharedAnn, key: &AnnKey, generation: u64) {
unavailable_guard(&ann.unavailable).insert(key.clone(), generation);
}
fn is_terminally_unavailable(ann: &SharedAnn, key: &AnnKey) -> bool {
let current = current_generation(ann, &key.namespace);
let mut guard = unavailable_guard(&ann.unavailable);
match guard.get(key) {
Some(&marked_generation) if marked_generation >= current => true,
Some(_) => {
guard.remove(key);
false
}
None => false,
}
}
#[cfg(test)]
pub(crate) async fn insert_ann_if_absent(ann: &SharedAnn, key: AnnKey, bridge: AnnBridge) -> bool {
use std::collections::hash_map::Entry;
let mut guard = ann.indexes.write().await;
match guard.entry(key) {
Entry::Occupied(_) => false,
Entry::Vacant(e) => {
e.insert(bridge);
true
}
}
}
pub(crate) async fn clear_namespace(ann: &SharedAnn, namespace: &str) {
{
let mut idxs = ann.indexes.write().await;
idxs.retain(|k, _| k.namespace != namespace);
bump_generation(ann, namespace);
}
warming_guard(&ann.warming).retain(|k| k.namespace != namespace);
}
pub(crate) async fn search_loaded(
ann: &SharedAnn,
key: &AnnKey,
query: &[f32],
k: usize,
) -> Option<Vec<(Uuid, f32)>> {
let guard = ann.indexes.read().await;
guard.get(key).map(|bridge| bridge.search(query, k))
}
pub(crate) fn is_warming_not_loaded(ann: &SharedAnn, key: &AnnKey) -> bool {
let in_warming = warming_guard(&ann.warming).contains(key);
if !in_warming {
return false;
}
match ann.indexes.try_read() {
Ok(guard) => !guard.contains_key(key),
Err(_) => true,
}
}
pub(crate) async fn wait_for_ann(
ann: &SharedAnn,
key: &AnnKey,
timeout_ms: u64,
poll_ms: u64,
) -> bool {
let deadline = std::time::Instant::now() + std::time::Duration::from_millis(timeout_ms);
loop {
if ann.indexes.read().await.contains_key(key) {
return true;
}
if is_terminally_unavailable(ann, key) {
return false;
}
if std::time::Instant::now() >= deadline {
return false;
}
tokio::time::sleep(std::time::Duration::from_millis(poll_ms)).await;
}
}
pub(crate) const ANN_WARM_WAIT_TIMEOUT_MS: u64 = 5_000;
pub(crate) const ANN_WARM_WAIT_POLL_MS: u64 = 50;
static ANN_WARM_WAIT_TIMEOUT_OVERRIDE_MS: std::sync::atomic::AtomicU64 =
std::sync::atomic::AtomicU64::new(0);
pub(crate) fn warm_wait_timeout_ms() -> u64 {
let o = ANN_WARM_WAIT_TIMEOUT_OVERRIDE_MS.load(std::sync::atomic::Ordering::Relaxed);
if o > 0 {
o
} else {
ANN_WARM_WAIT_TIMEOUT_MS
}
}
#[cfg(test)]
pub(crate) fn set_warm_wait_timeout_override_ms(ms: u64) {
ANN_WARM_WAIT_TIMEOUT_OVERRIDE_MS.store(ms, std::sync::atomic::Ordering::Relaxed);
}
impl AnnBridge {
pub fn build(mut vectors: Vec<f32>, dim: usize, id_map: Vec<Uuid>) -> Result<Self, String> {
if dim == 0 {
return Err("dimension must be > 0".into());
}
if vectors.is_empty() || id_map.is_empty() {
return Err("no vectors to build ANN index from".into());
}
let n = vectors.len() / dim;
if n != id_map.len() {
return Err(format!(
"id_map length {} != vector count {}",
id_map.len(),
n
));
}
for row in vectors.chunks_exact_mut(dim) {
l2_normalize(row);
}
let cfg = VamanaConfig::with_dimensions(dim);
let index = VamanaIndex::build(&vectors, cfg).map_err(|e| format!("{e}"))?;
Ok(Self {
index,
id_map,
generation: 0,
})
}
pub(crate) fn with_generation(mut self, generation: u64) -> Self {
self.generation = generation;
self
}
pub fn search(&self, query: &[f32], k: usize) -> Vec<(Uuid, f32)> {
let mut q = query.to_vec();
l2_normalize(&mut q);
match self.index.search(&q, k) {
Ok(results) => results
.into_iter()
.filter_map(|(idx, dist)| {
self.id_map.get(idx as usize).map(|uuid| {
let cosine = 1.0 - dist / 2.0;
(*uuid, cosine.max(0.0))
})
})
.collect(),
Err(e) => {
tracing::warn!(error = %e, "vamana ANN search failed");
Vec::new()
}
}
}
pub fn num_vectors(&self) -> usize {
self.index.num_vectors()
}
pub fn from_vamana_snapshot(snapshot: VamanaSnapshot) -> Result<Self, String> {
let id_map: Vec<Uuid> = snapshot
.external_ids
.iter()
.map(|s| Uuid::parse_str(s).map_err(|e| format!("bad UUID {s}: {e}")))
.collect::<Result<_, _>>()?;
let index =
VamanaIndex::from_snapshot(&snapshot).map_err(|e| format!("snapshot restore: {e}"))?;
Ok(Self {
index,
id_map,
generation: 0,
})
}
#[allow(dead_code)]
pub fn save_atomic(&self, dir: &std::path::Path) -> Result<(), String> {
let count = self.id_map.len();
if count != self.index.num_vectors() {
return Err(format!(
"id_map length {count} != index.num_vectors() {}",
self.index.num_vectors()
));
}
self.index
.save_atomic(dir)
.map_err(|e| format!("VamanaIndex::save_atomic: {e}"))?;
let fp = read_commit_fingerprint(dir)
.map_err(|e| format!("read_commit_fingerprint after save: {e}"))?
.ok_or_else(|| {
"save_atomic succeeded but read_commit_fingerprint returned None \
(unexpected v1 or torn commit)"
.to_string()
})?;
write_external_ids_sidecar(dir, &fp.content_hash, &self.id_map)
}
#[allow(dead_code)]
pub fn load(dir: &std::path::Path) -> Result<Self, String> {
let fp = read_commit_fingerprint(dir)
.map_err(|e| format!("read_commit_fingerprint: {e}"))?
.ok_or_else(|| {
"no v2 commit fingerprint: segment dir is absent, v1, or has a torn commit"
.to_string()
})?;
let index = VamanaIndex::load(dir).map_err(|e| format!("VamanaIndex::load: {e}"))?;
let (sidecar_hash, id_map) = read_external_ids_sidecar(dir)?;
if sidecar_hash != fp.content_hash {
return Err(
"external_ids.bin content_hash mismatch: torn segment/sidecar pair".to_string(),
);
}
if id_map.len() != index.num_vectors() {
return Err(format!(
"external_ids.bin count {} != index.num_vectors() {}",
id_map.len(),
index.num_vectors()
));
}
Ok(Self {
index,
id_map,
generation: 0,
})
}
}
fn l2_normalize(v: &mut [f32]) {
let norm: f32 = v.iter().map(|x| x * x).sum::<f32>().sqrt();
if norm > 1e-8 {
for x in v.iter_mut() {
*x /= norm;
}
}
}
#[allow(dead_code)]
const SIDECAR_MAGIC: &[u8; 8] = b"KHVANIDS";
#[allow(dead_code)]
fn write_external_ids_sidecar(
dir: &std::path::Path,
content_hash: &[u8; 32],
ids: &[Uuid],
) -> Result<(), String> {
use std::io::Write as _;
let tmp_path = dir.join("external_ids.bin.tmp");
let final_path = dir.join("external_ids.bin");
let count = ids.len() as u64;
let mut buf: Vec<u8> = Vec::with_capacity(8 + 32 + 8 + ids.len() * 16);
buf.extend_from_slice(SIDECAR_MAGIC);
buf.extend_from_slice(content_hash);
buf.extend_from_slice(&count.to_le_bytes());
for id in ids {
buf.extend_from_slice(id.as_bytes());
}
let mut f = std::fs::File::create(&tmp_path)
.map_err(|e| format!("create external_ids.bin.tmp: {e}"))?;
f.write_all(&buf)
.map_err(|e| format!("write external_ids.bin.tmp: {e}"))?;
f.sync_all()
.map_err(|e| format!("sync external_ids.bin.tmp: {e}"))?;
drop(f);
std::fs::rename(&tmp_path, &final_path)
.map_err(|e| format!("rename external_ids.bin.tmp -> external_ids.bin: {e}"))
}
#[allow(dead_code)]
fn read_external_ids_sidecar(dir: &std::path::Path) -> Result<([u8; 32], Vec<Uuid>), String> {
let bytes = std::fs::read(dir.join("external_ids.bin"))
.map_err(|e| format!("read external_ids.bin: {e}"))?;
if bytes.len() < 48 {
return Err(format!(
"external_ids.bin too short: {} bytes (need at least 48)",
bytes.len()
));
}
let magic = &bytes[0..8];
if magic != SIDECAR_MAGIC {
return Err(format!(
"external_ids.bin bad magic: got {:?}, expected {:?}",
magic, SIDECAR_MAGIC
));
}
let mut content_hash = [0u8; 32];
content_hash.copy_from_slice(&bytes[8..40]);
let count = u64::from_le_bytes(bytes[40..48].try_into().unwrap()) as usize;
let expected_len = 48 + count * 16;
if bytes.len() != expected_len {
return Err(format!(
"external_ids.bin length mismatch: got {} bytes, expected {} for {count} UUIDs",
bytes.len(),
expected_len
));
}
let mut ids = Vec::with_capacity(count);
for i in 0..count {
let start = 48 + i * 16;
let raw: [u8; 16] = bytes[start..start + 16].try_into().unwrap();
ids.push(Uuid::from_bytes(raw));
}
Ok((content_hash, ids))
}
pub(crate) fn snapshot_key(namespace: &str, model: &str) -> String {
format!("{namespace}::vamana::{model}")
}
fn ann_segment_dir(rt: &KhiveRuntime, ns: &str, model: &str) -> Option<std::path::PathBuf> {
let data_dir = rt.backend_data_dir()?;
let key = snapshot_key(ns, model);
let hex: String = key.bytes().map(|b| format!("{b:02x}")).collect();
Some(data_dir.join("ann").join(hex))
}
fn decode_ann_dir_name(name: &str) -> Option<(String, String)> {
let raw = name.as_bytes();
if !raw.len().is_multiple_of(2) {
return None;
}
let mut bytes = Vec::with_capacity(raw.len() / 2);
for pair in raw.chunks_exact(2) {
let hi = (pair[0] as char).to_digit(16)?;
let lo = (pair[1] as char).to_digit(16)?;
bytes.push((hi * 16 + lo) as u8);
}
let key = String::from_utf8(bytes).ok()?;
let (ns, model) = key.split_once("::vamana::")?;
if ns.is_empty() || model.is_empty() {
return None;
}
Some((ns.to_string(), model.to_string()))
}
pub(crate) fn sanitize_model_key(s: &str) -> String {
s.chars()
.map(|c| if c.is_ascii_alphanumeric() { c } else { '_' })
.collect()
}
pub(crate) fn persist_ann_v2(
rt: &KhiveRuntime,
ns: &str,
model: &str,
bridge: &AnnBridge,
) -> Result<(), String> {
match ann_segment_dir(rt, ns, model) {
Some(dir) => bridge.save_atomic(&dir),
None => Ok(()), }
}
async fn try_load_snapshot(
rt: &KhiveRuntime,
namespace: &str,
model: &str,
) -> Option<VamanaSnapshot> {
let key = snapshot_key(namespace, model);
let sql = rt.sql();
let mut reader = sql.reader().await.ok()?;
let rows = reader
.query_all(SqlStatement {
sql: "SELECT snapshot FROM retrieval_snapshots \
WHERE namespace = ?1 AND index_type = ?2"
.into(),
params: vec![SqlValue::Text(key), SqlValue::Text("vamana".into())],
label: None,
})
.await
.ok()?;
let row = rows.into_iter().next()?;
let blob = match row.get("snapshot")? {
SqlValue::Blob(b) => b.clone(),
_ => return None,
};
serde_json::from_slice::<VamanaSnapshot>(&blob).ok()
}
pub(crate) async fn compute_fingerprint(
rt: &KhiveRuntime,
token: &NamespaceToken,
model: &str,
) -> Option<CorpusFingerprint> {
let store = rt.vectors_for_model(token, model).ok()?;
let info = store.info().await.ok()?;
Some(CorpusFingerprint {
vector_count: info.entry_count,
dimensions: info.dimensions as u32,
})
}
async fn scan_corpus_raw(
rt: &KhiveRuntime,
token: &NamespaceToken,
model: &str,
) -> Result<Option<(Vec<f32>, Vec<Uuid>)>, RuntimeError> {
let store = rt
.vectors_for_model(token, model)
.map_err(|e| RuntimeError::Internal(e.to_string()))?;
let info = store
.info()
.await
.map_err(|e| RuntimeError::Internal(e.to_string()))?;
let count = info.entry_count;
let dims = info.dimensions;
if count == 0 || dims == 0 {
return Ok(None);
}
let ns = token.namespace().as_str().to_owned();
let model_key = sanitize_model_key(model);
let table_name = format!("vec_{model_key}");
let model_str = model.to_owned();
let sql = rt.sql();
let mut reader = sql
.reader()
.await
.map_err(|e| RuntimeError::Internal(e.to_string()))?;
let rows = reader
.query_all(SqlStatement {
sql: format!(
"SELECT subject_id, embedding FROM {table_name} \
WHERE namespace = ?1 AND embedding_model = ?2 \
AND field = 'knowledge.atom' \
ORDER BY subject_id"
),
params: vec![SqlValue::Text(ns), SqlValue::Text(model_str)],
label: Some("vamana_corpus_scan".into()),
})
.await
.map_err(|e| RuntimeError::Internal(e.to_string()))?;
if rows.is_empty() {
return Ok(None);
}
let mut id_map: Vec<Uuid> = Vec::with_capacity(rows.len());
let mut flat: Vec<f32> = Vec::with_capacity(rows.len() * dims);
for row in &rows {
let id_str = match row.get("subject_id") {
Some(SqlValue::Text(s)) => s.as_str(),
_ => continue,
};
let uuid = match Uuid::parse_str(id_str) {
Ok(id) => id,
Err(_) => continue,
};
let bytes = match row.get("embedding") {
Some(SqlValue::Blob(b)) => b.as_slice(),
_ => continue,
};
if bytes.len() != dims * 4 {
continue;
}
let vec: Vec<f32> = bytes
.chunks_exact(4)
.map(|c| f32::from_le_bytes([c[0], c[1], c[2], c[3]]))
.collect();
id_map.push(uuid);
flat.extend_from_slice(&vec);
}
if id_map.is_empty() {
return Ok(None);
}
Ok(Some((flat, id_map)))
}
pub(crate) async fn load_and_build_from_vector_store(
rt: &KhiveRuntime,
token: &NamespaceToken,
model: &str,
) -> Result<Option<AnnBridge>, RuntimeError> {
let Some((flat, id_map)) = scan_corpus_raw(rt, token, model).await? else {
return Ok(None);
};
let dims = flat.len() / id_map.len();
AnnBridge::build(flat, dims, id_map)
.map(Some)
.map_err(RuntimeError::Internal)
}
async fn live_content_hash(
rt: &KhiveRuntime,
token: &NamespaceToken,
model: &str,
) -> Option<[u8; 32]> {
let (mut flat, id_map) = scan_corpus_raw(rt, token, model).await.ok()??;
let dims = flat.len() / id_map.len();
for row in flat.chunks_exact_mut(dims) {
l2_normalize(row);
}
Some(corpus_content_hash(&flat))
}
fn escape_like(input: &str) -> String {
let mut out = String::with_capacity(input.len());
for c in input.chars() {
if matches!(c, '\\' | '%' | '_') {
out.push('\\');
}
out.push(c);
}
out
}
pub(crate) async fn invalidate_snapshot(rt: &KhiveRuntime, namespace: &str) {
let pattern = format!("{}::vamana::%", escape_like(namespace));
let sql = rt.sql();
let mut w = match sql.writer().await {
Ok(w) => w,
Err(e) => {
tracing::warn!(error = %e, "failed to open writer for Vamana snapshot invalidation");
return;
}
};
match w
.execute(SqlStatement {
sql: "DELETE FROM retrieval_snapshots WHERE namespace LIKE ?1 ESCAPE '\\'".into(),
params: vec![SqlValue::Text(pattern)],
label: Some("invalidate_vamana_snapshot".into()),
})
.await
{
Ok(_) => {}
Err(e) if e.to_string().contains("no such table") => {}
Err(e) => {
tracing::warn!(error = %e, "failed to invalidate Vamana snapshot");
}
}
}
pub(crate) async fn warm_known_snapshots(rt: &KhiveRuntime, ann: &SharedAnn) {
let rows = {
let sql = rt.sql();
match sql.reader().await {
Ok(mut reader) => reader
.query_all(SqlStatement {
sql:
"SELECT DISTINCT namespace FROM retrieval_snapshots WHERE namespace LIKE ?1"
.into(),
params: vec![SqlValue::Text("%::vamana::%".into())],
label: None,
})
.await
.unwrap_or_default(),
Err(_) => Vec::new(),
}
};
for row in &rows {
let ns_key = match row.get("namespace") {
Some(SqlValue::Text(s)) => s.as_str(),
_ => continue,
};
let Some((ns_str, model)) = ns_key.split_once("::vamana::") else {
continue;
};
if ns_str.is_empty() || model.is_empty() {
continue;
}
let ns = match Namespace::parse(ns_str) {
Ok(n) => n,
Err(_) => continue,
};
let token = match rt.authorize(ns) {
Ok(t) => t,
Err(_) => continue,
};
let key = AnnKey::new(ns_str, model);
{
let mut warming = warming_guard(&ann.warming);
if warming.contains(&key) {
continue; }
warming.insert(key.clone());
}
ensure_ann_for_model(rt, &token, ann, model).await;
let loaded = ann.indexes.read().await.contains_key(&key);
if !loaded {
warming_guard(&ann.warming).remove(&key);
}
}
let ann_root = match rt.backend_data_dir() {
Some(d) => d.join("ann"),
None => return,
};
let read_dir = match std::fs::read_dir(&ann_root) {
Ok(rd) => rd,
Err(_) => return, };
for entry in read_dir.flatten() {
let name = entry.file_name();
let hex = name.to_string_lossy();
let Some((ns_str, model)) = decode_ann_dir_name(hex.as_ref()) else {
continue;
};
let ns = match Namespace::parse(&ns_str) {
Ok(n) => n,
Err(_) => continue,
};
let token = match rt.authorize(ns) {
Ok(t) => t,
Err(_) => continue,
};
let key = AnnKey::new(&ns_str, &model);
if ann.indexes.read().await.contains_key(&key) {
continue;
}
ensure_ann_for_model(rt, &token, ann, &model).await;
}
}
pub(crate) fn ensure_ann_background(rt: &KhiveRuntime, token: &NamespaceToken, ann: &SharedAnn) {
let model = rt.default_embedder_name().to_string();
if model.is_empty() {
return;
}
let ns = token.namespace().as_str().to_owned();
let key = AnnKey::new(&ns, &model);
{
let mut warming = warming_guard(&ann.warming);
if warming.contains(&key) {
return; }
warming.insert(key.clone());
}
let rt = rt.clone();
let ann = ann.clone();
let token_ns = token.namespace().clone();
tokio::spawn(async move {
if let Ok(token) = rt.authorize(token_ns) {
ensure_ann_for_model(&rt, &token, &ann, &model).await;
}
let loaded = ann.indexes.read().await.contains_key(&key);
if !loaded {
warming_guard(&ann.warming).remove(&key);
}
});
}
pub(crate) async fn ensure_ann_for_model(
rt: &KhiveRuntime,
token: &NamespaceToken,
ann: &SharedAnn,
model: &str,
) {
if model.is_empty() {
return;
}
let ns = token.namespace().as_str().to_owned();
let key = AnnKey::new(&ns, model);
let target_generation = current_generation(ann, &ns);
if let Some(loaded_generation) = ann
.indexes
.read()
.await
.get(&key)
.map(|bridge| bridge.generation)
{
if loaded_generation >= target_generation {
return;
}
tracing::debug!(
namespace = %ns,
model = %model,
loaded_generation,
target_generation,
"knowledge ANN fast path skipped: cached entry generation stale; rebuilding"
);
}
if let Some(seg_dir) = ann_segment_dir(rt, &ns, model) {
match read_commit_fingerprint(&seg_dir) {
Ok(Some(persisted)) => {
let current_fp = compute_fingerprint(rt, token, model).await;
let count_dims_ok = current_fp.is_some_and(|fp| {
fp.vector_count == persisted.vector_count
&& fp.dimensions as u64 == persisted.dimensions
});
if count_dims_ok {
if let Some(live_hash) = live_content_hash(rt, token, model).await {
if live_hash == persisted.content_hash {
match AnnBridge::load(&seg_dir) {
Ok(bridge) => {
install_if_fresher(
ann,
&key,
bridge.with_generation(target_generation),
)
.await;
return;
}
Err(e) => {
tracing::warn!(
error = %e,
dir = %seg_dir.display(),
"v2 segment load failed; falling through to rebuild"
);
}
}
} else {
tracing::info!(
namespace = %ns,
model = %model,
"stale v2 segment (content-hash mismatch); rebuilding"
);
}
}
} else {
tracing::info!(
namespace = %ns,
model = %model,
"stale v2 segment (count/dims mismatch); rebuilding"
);
}
}
Ok(None) => {
}
Err(e) => {
tracing::warn!(
error = %e,
dir = %seg_dir.display(),
"error reading v2 segment fingerprint; falling through"
);
}
}
}
if let Some(snapshot) = try_load_snapshot(rt, &ns, model).await {
let current_fp = compute_fingerprint(rt, token, model).await;
if let Some(fp) = current_fp {
if snapshot.fingerprint == fp {
match AnnBridge::from_vamana_snapshot(snapshot) {
Ok(bridge) => {
install_if_fresher(ann, &key, bridge.with_generation(target_generation))
.await;
return;
}
Err(e) => {
tracing::warn!(error = %e, "corrupt Vamana v1 snapshot; rebuilding");
}
}
} else {
tracing::info!(
namespace = %ns,
model = %model,
"stale Vamana v1 snapshot (fingerprint mismatch); rebuilding"
);
}
}
}
match load_and_build_from_vector_store(rt, token, model).await {
Ok(Some(bridge)) => {
if let Err(e) = persist_ann_v2(rt, &ns, model, &bridge) {
tracing::error!(error = %e, "failed to persist v2 Vamana segment after rebuild");
}
install_if_fresher(ann, &key, bridge.with_generation(target_generation)).await;
}
Ok(None) => {
mark_unavailable(ann, &key, target_generation);
}
Err(e) => {
tracing::warn!(error = %e, "failed to rebuild Vamana ANN index");
}
}
}
#[cfg(test)]
pub(crate) fn simulate_warming_in_flight(ann: &SharedAnn, key: AnnKey) {
warming_guard(&ann.warming).insert(key);
}
#[cfg(test)]
mod tests {
use super::*;
use khive_runtime::KhiveRuntime;
use khive_storage::types::{SqlStatement, SqlValue};
#[tokio::test]
async fn test_invalidate_snapshot_removes_vamana_rows() {
let rt = KhiveRuntime::memory().expect("in-memory runtime");
let sql = rt.sql();
let mut w = sql.writer().await.expect("writer");
w.execute_script(
"CREATE TABLE IF NOT EXISTS retrieval_snapshots (\
namespace TEXT NOT NULL, index_type TEXT NOT NULL, \
snapshot BLOB NOT NULL, created_at INTEGER NOT NULL, \
PRIMARY KEY (namespace, index_type));"
.into(),
)
.await
.expect("create table");
for (ns, idx_type) in &[
("local::vamana::model-a", "vamana"),
("local::vamana::model-b", "vamana"),
("local::hnsw::model-a", "hnsw"),
] {
w.execute(SqlStatement {
sql: "INSERT INTO retrieval_snapshots (namespace, index_type, snapshot, created_at) VALUES (?1, ?2, ?3, 0)".into(),
params: vec![
SqlValue::Text(ns.to_string()),
SqlValue::Text(idx_type.to_string()),
SqlValue::Blob(b"{}".to_vec()),
],
label: None,
})
.await
.expect("insert");
}
drop(w);
invalidate_snapshot(&rt, "local").await;
let mut r = sql.reader().await.expect("reader");
let rows = r
.query_all(SqlStatement {
sql: "SELECT namespace FROM retrieval_snapshots ORDER BY namespace".into(),
params: vec![],
label: None,
})
.await
.expect("query");
let remaining: Vec<String> = rows
.iter()
.filter_map(|row| match row.get("namespace") {
Some(SqlValue::Text(s)) => Some(s.clone()),
_ => None,
})
.collect();
assert!(
remaining.contains(&"local::hnsw::model-a".to_string()),
"HNSW rows must survive: {remaining:?}"
);
assert!(
!remaining.contains(&"local::vamana::model-a".to_string()),
"vamana model-a must be deleted: {remaining:?}"
);
assert!(
!remaining.contains(&"local::vamana::model-b".to_string()),
"vamana model-b must be deleted: {remaining:?}"
);
}
#[tokio::test]
async fn test_invalidate_snapshot_does_not_cross_underscore_namespace() {
let rt = KhiveRuntime::memory().expect("in-memory runtime");
let sql = rt.sql();
let mut w = sql.writer().await.expect("writer");
w.execute_script(
"CREATE TABLE IF NOT EXISTS retrieval_snapshots (\
namespace TEXT NOT NULL, index_type TEXT NOT NULL, \
snapshot BLOB NOT NULL, created_at INTEGER NOT NULL, \
PRIMARY KEY (namespace, index_type));"
.into(),
)
.await
.expect("create table");
for ns in &["a_b::vamana::model-a", "aXb::vamana::model-a"] {
w.execute(SqlStatement {
sql: "INSERT INTO retrieval_snapshots (namespace, index_type, snapshot, created_at) VALUES (?1, ?2, ?3, 0)".into(),
params: vec![
SqlValue::Text(ns.to_string()),
SqlValue::Text("vamana".to_string()),
SqlValue::Blob(b"{}".to_vec()),
],
label: None,
})
.await
.expect("insert");
}
drop(w);
invalidate_snapshot(&rt, "a_b").await;
let mut r = sql.reader().await.expect("reader");
let rows = r
.query_all(SqlStatement {
sql: "SELECT namespace FROM retrieval_snapshots ORDER BY namespace".into(),
params: vec![],
label: None,
})
.await
.expect("query");
let remaining: Vec<String> = rows
.iter()
.filter_map(|row| match row.get("namespace") {
Some(SqlValue::Text(s)) => Some(s.clone()),
_ => None,
})
.collect();
assert!(
remaining.contains(&"aXb::vamana::model-a".to_string()),
"unrelated namespace 'aXb' must survive invalidating 'a_b': {remaining:?}"
);
assert!(
!remaining.contains(&"a_b::vamana::model-a".to_string()),
"'a_b' own snapshot must still be deleted: {remaining:?}"
);
}
#[tokio::test]
async fn test_invalidate_snapshot_tolerates_missing_table() {
let rt = KhiveRuntime::memory().expect("in-memory runtime");
invalidate_snapshot(&rt, "local").await;
}
#[tokio::test]
async fn test_invalidate_clears_in_memory_ann() {
let ann = new_shared();
let dim = 4;
let vectors = vec![1.0f32, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0];
let ids = vec![Uuid::new_v4(), Uuid::new_v4()];
let bridge = AnnBridge::build(vectors, dim, ids).expect("build");
let key = AnnKey::new("local", "test-model");
assert!(
insert_ann_if_absent(&ann, key.clone(), bridge).await,
"insert must succeed on empty cache"
);
assert!(
ann.indexes.read().await.contains_key(&key),
"pre-condition: ANN loaded"
);
clear_namespace(&ann, "local").await;
assert!(
!ann.indexes.read().await.contains_key(&key),
"clearing SharedAnn must remove the bridge"
);
}
#[tokio::test]
async fn shared_ann_is_keyed_by_namespace_and_model() {
let ann = new_shared();
let model = "test-model";
let id_a = Uuid::new_v4();
let id_b = Uuid::new_v4();
let bridge_a = AnnBridge::build(vec![1.0, 0.0, 0.0, 0.0], 4, vec![id_a])
.expect("build namespace A bridge");
let bridge_b = AnnBridge::build(vec![0.0, 1.0, 0.0, 0.0], 4, vec![id_b])
.expect("build namespace B bridge");
assert!(insert_ann_if_absent(&ann, AnnKey::new("ns:a", model), bridge_a).await);
assert!(insert_ann_if_absent(&ann, AnnKey::new("ns:b", model), bridge_b).await);
let hits_b = search_loaded(&ann, &AnnKey::new("ns:b", model), &[1.0, 0.0, 0.0, 0.0], 1)
.await
.expect("namespace B bridge exists");
assert_eq!(hits_b.len(), 1);
assert_eq!(
hits_b[0].0, id_b,
"namespace B query must not return namespace A neighbour"
);
}
#[tokio::test]
async fn install_if_fresher_rejects_late_stale_build() {
let ann = new_shared();
let key = AnnKey::new("local", "test-model");
let fresh = AnnBridge::build(vec![1.0, 0.0, 0.0, 0.0], 4, vec![Uuid::new_v4()])
.expect("build fresh bridge")
.with_generation(2);
let stale = AnnBridge::build(vec![0.0, 1.0, 0.0, 0.0], 4, vec![Uuid::new_v4()])
.expect("build stale bridge")
.with_generation(1);
install_if_fresher(&ann, &key, fresh).await;
install_if_fresher(&ann, &key, stale).await;
let installed_generation = ann
.indexes
.read()
.await
.get(&key)
.expect("entry present")
.generation;
assert_eq!(
installed_generation, 2,
"stale build (generation 1) must not replace fresher installed entry (generation 2)"
);
}
#[tokio::test]
async fn install_if_fresher_accepts_forward_progress() {
let ann = new_shared();
let key = AnnKey::new("local", "test-model");
let old = AnnBridge::build(vec![1.0, 0.0, 0.0, 0.0], 4, vec![Uuid::new_v4()])
.expect("build old bridge")
.with_generation(1);
let newer = AnnBridge::build(vec![0.0, 1.0, 0.0, 0.0], 4, vec![Uuid::new_v4()])
.expect("build newer bridge")
.with_generation(2);
install_if_fresher(&ann, &key, old).await;
install_if_fresher(&ann, &key, newer).await;
let installed_generation = ann
.indexes
.read()
.await
.get(&key)
.expect("entry present")
.generation;
assert_eq!(
installed_generation, 2,
"newer build must replace an older installed entry"
);
}
#[tokio::test]
async fn install_if_fresher_ties_keep_incumbent() {
let ann = new_shared();
let key = AnnKey::new("local", "test-model");
let first = AnnBridge::build(vec![1.0, 0.0, 0.0, 0.0], 4, vec![Uuid::new_v4()])
.expect("build first bridge")
.with_generation(1);
let second_id = Uuid::new_v4();
let second = AnnBridge::build(vec![0.0, 1.0, 0.0, 0.0], 4, vec![second_id])
.expect("build second bridge")
.with_generation(1);
install_if_fresher(&ann, &key, first).await;
install_if_fresher(&ann, &key, second).await;
let hits = search_loaded(&ann, &key, &[0.0, 1.0, 0.0, 0.0], 1)
.await
.expect("entry present");
assert_ne!(
hits.first().map(|(id, _)| *id),
Some(second_id),
"equal-generation candidate must not replace the incumbent"
);
}
#[tokio::test]
async fn install_if_fresher_installs_into_empty_slot() {
let ann = new_shared();
let key = AnnKey::new("local", "test-model");
let bridge = AnnBridge::build(vec![1.0, 0.0, 0.0, 0.0], 4, vec![Uuid::new_v4()])
.expect("build bridge")
.with_generation(0);
install_if_fresher(&ann, &key, bridge).await;
assert!(
ann.indexes.read().await.contains_key(&key),
"first successful build must always install into an empty slot"
);
}
#[tokio::test]
async fn clear_namespace_bumps_generation_scoped_to_namespace() {
let ann = new_shared();
assert_eq!(current_generation(&ann, "ns:a"), 0);
assert_eq!(current_generation(&ann, "ns:b"), 0);
clear_namespace(&ann, "ns:a").await;
assert_eq!(
current_generation(&ann, "ns:a"),
1,
"clear_namespace must bump the invalidated namespace's generation"
);
assert_eq!(
current_generation(&ann, "ns:b"),
0,
"clear_namespace must not affect a different namespace's generation"
);
}
#[tokio::test]
async fn stale_build_installs_before_invalidation_race_is_rejected_after() {
let ann = new_shared();
let key = AnnKey::new("local", "test-model");
let build_a_generation = current_generation(&ann, "local");
assert_eq!(build_a_generation, 0);
clear_namespace(&ann, "local").await;
assert_eq!(current_generation(&ann, "local"), 1);
let build_b_generation = current_generation(&ann, "local");
let build_b_id = Uuid::new_v4();
let build_b_bridge = AnnBridge::build(vec![0.0, 1.0, 0.0, 0.0], 4, vec![build_b_id])
.expect("build fresh bridge")
.with_generation(build_b_generation);
install_if_fresher(&ann, &key, build_b_bridge).await;
assert!(
ann.indexes.read().await.contains_key(&key),
"build B (post-invalidation generation) must install"
);
let build_a_bridge = AnnBridge::build(vec![1.0, 0.0, 0.0, 0.0], 4, vec![Uuid::new_v4()])
.expect("build stale bridge")
.with_generation(build_a_generation);
install_if_fresher(&ann, &key, build_a_bridge).await;
let hits = search_loaded(&ann, &key, &[0.0, 1.0, 0.0, 0.0], 1)
.await
.expect("entry present");
assert_eq!(
hits.first().map(|(id, _)| *id),
Some(build_b_id),
"build A's late, stale install must not clobber build B's fresher result"
);
}
#[tokio::test]
async fn stale_build_rejected_installing_into_still_empty_post_invalidation_slot() {
let ann = new_shared();
let key = AnnKey::new("local", "test-model");
let build_a_generation = current_generation(&ann, "local");
assert_eq!(build_a_generation, 0);
clear_namespace(&ann, "local").await;
assert_eq!(current_generation(&ann, "local"), 1);
assert!(
!ann.indexes.read().await.contains_key(&key),
"precondition: slot must still be empty after clear_namespace"
);
let build_a_bridge = AnnBridge::build(vec![1.0, 0.0, 0.0, 0.0], 4, vec![Uuid::new_v4()])
.expect("build stale bridge")
.with_generation(build_a_generation);
install_if_fresher(&ann, &key, build_a_bridge).await;
assert!(
!ann.indexes.read().await.contains_key(&key),
"stale pre-invalidation build must not install into the emptied slot, \
even with no incumbent to compare against"
);
assert!(
search_loaded(&ann, &key, &[1.0, 0.0, 0.0, 0.0], 1)
.await
.is_none(),
"the fast path must not serve a stale index that was correctly rejected at install"
);
}
#[test]
fn is_warming_false_when_neither_warming_nor_loaded() {
let ann = new_shared();
let key = AnnKey::new("local", "test-model");
assert!(
!is_warming_not_loaded(&ann, &key),
"key absent from both sets must return false"
);
}
#[test]
fn is_warming_true_when_in_warming_but_not_indexes() {
let ann = new_shared();
let key = AnnKey::new("local", "test-model");
simulate_warming_in_flight(&ann, key.clone());
assert!(
is_warming_not_loaded(&ann, &key),
"key in warming but not indexes must return true"
);
}
#[tokio::test]
async fn is_warming_false_when_both_warming_and_loaded() {
let ann = new_shared();
let key = AnnKey::new("local", "test-model");
simulate_warming_in_flight(&ann, key.clone());
let bridge =
AnnBridge::build(vec![1.0f32, 0.0, 0.0, 0.0], 4, vec![Uuid::new_v4()]).expect("build");
insert_ann_if_absent(&ann, key.clone(), bridge).await;
assert!(
!is_warming_not_loaded(&ann, &key),
"key in both warming and indexes must return false (warm is done)"
);
}
#[tokio::test]
async fn wait_for_ann_returns_true_immediately_when_already_loaded() {
let ann = new_shared();
let key = AnnKey::new("local", "test-model");
let bridge =
AnnBridge::build(vec![1.0f32, 0.0, 0.0, 0.0], 4, vec![Uuid::new_v4()]).expect("build");
insert_ann_if_absent(&ann, key.clone(), bridge).await;
let ready = wait_for_ann(&ann, &key, 100, 10).await;
assert!(ready, "must return true when index is already in the map");
}
#[tokio::test]
async fn wait_for_ann_returns_false_on_timeout_when_never_loaded() {
let ann = new_shared();
let key = AnnKey::new("local", "test-model");
let ready = wait_for_ann(&ann, &key, 60, 10).await;
assert!(
!ready,
"must return false when index never appears within timeout"
);
}
#[tokio::test]
async fn wait_for_ann_returns_true_when_index_appears_mid_poll() {
let ann = new_shared();
let key = AnnKey::new("local", "test-model");
let ann2 = ann.clone();
let key2 = key.clone();
tokio::spawn(async move {
tokio::time::sleep(std::time::Duration::from_millis(40)).await;
let bridge = AnnBridge::build(vec![1.0f32, 0.0, 0.0, 0.0], 4, vec![Uuid::new_v4()])
.expect("build");
insert_ann_if_absent(&ann2, key2, bridge).await;
});
let ready = wait_for_ann(&ann, &key, 500, 10).await;
assert!(ready, "must return true when index appears before timeout");
}
#[tokio::test]
async fn wait_for_ann_returns_false_immediately_when_marked_unavailable() {
let ann = new_shared();
let key = AnnKey::new("local", "test-model");
mark_unavailable(&ann, &key, current_generation(&ann, "local"));
let start = std::time::Instant::now();
let ready = wait_for_ann(&ann, &key, 5_000, 50).await;
let elapsed = start.elapsed();
assert!(
!ready,
"must return false for a key marked unavailable at the current generation"
);
assert!(
elapsed < std::time::Duration::from_millis(200),
"terminal unavailable outcome must short-circuit, not poll out the timeout: {elapsed:?}"
);
}
#[tokio::test]
async fn wait_for_ann_resumes_polling_when_unavailable_marker_is_stale() {
let ann = new_shared();
let key = AnnKey::new("local", "test-model");
mark_unavailable(&ann, &key, 0);
clear_namespace(&ann, "local").await;
assert_eq!(current_generation(&ann, "local"), 1);
let ann2 = ann.clone();
let key2 = key.clone();
tokio::spawn(async move {
tokio::time::sleep(std::time::Duration::from_millis(40)).await;
let bridge = AnnBridge::build(vec![1.0f32, 0.0, 0.0, 0.0], 4, vec![Uuid::new_v4()])
.expect("build")
.with_generation(1);
install_if_fresher(&ann2, &key2, bridge).await;
});
let ready = wait_for_ann(&ann, &key, 500, 10).await;
assert!(
ready,
"a stale unavailable marker must not block polling; the index installed \
mid-poll must still be observed"
);
}
#[tokio::test]
async fn install_if_fresher_clears_unavailable_marker_on_successful_install() {
let ann = new_shared();
let key = AnnKey::new("local", "test-model");
mark_unavailable(&ann, &key, 0);
let bridge = AnnBridge::build(vec![1.0f32, 0.0, 0.0, 0.0], 4, vec![Uuid::new_v4()])
.expect("build")
.with_generation(0);
install_if_fresher(&ann, &key, bridge).await;
assert!(
!unavailable_guard(&ann.unavailable).contains_key(&key),
"a successful install must clear the unavailable marker for its key"
);
}
#[tokio::test]
async fn install_if_fresher_stale_reject_does_not_clear_unavailable_marker() {
let ann = new_shared();
let key = AnnKey::new("local", "test-model");
clear_namespace(&ann, "local").await;
mark_unavailable(&ann, &key, current_generation(&ann, "local"));
let stale = AnnBridge::build(vec![1.0f32, 0.0, 0.0, 0.0], 4, vec![Uuid::new_v4()])
.expect("build")
.with_generation(0);
install_if_fresher(&ann, &key, stale).await;
assert!(
!ann.indexes.read().await.contains_key(&key),
"stale candidate must not install"
);
assert!(
unavailable_guard(&ann.unavailable).contains_key(&key),
"a rejected (non-installed) candidate must not clear the unavailable marker"
);
}
#[test]
fn warming_guard_recovers_from_poison() {
let ann = new_shared();
let key = AnnKey::new("poison-ns", "poison-model");
let ann2 = ann.clone();
let join_result = std::thread::spawn(move || {
let _guard = ann2.warming.lock().expect("pre-poison lock");
panic!("deliberate poison");
})
.join();
assert!(join_result.is_err(), "poison thread must have panicked");
assert!(
ann.warming.is_poisoned(),
"mutex must be poisoned before recovery"
);
let guard = warming_guard(&ann.warming);
assert!(
!guard.contains(&key),
"recovered guard must report key absent (HashSet is empty after poison)"
);
drop(guard);
assert!(
!is_warming_not_loaded(&ann, &key),
"is_warming_not_loaded must not panic on poisoned Mutex"
);
}
#[tokio::test]
async fn warm_path_key_in_warming_set_before_and_after_successful_load() {
let ann = new_shared();
let key = AnnKey::new("local", "warm-unify-model");
{
let mut warming = warming_guard(&ann.warming);
warming.insert(key.clone());
}
assert!(
is_warming_not_loaded(&ann, &key),
"key in warming but not indexes must report warming in flight"
);
let bridge = AnnBridge::build(vec![1.0f32, 0.0, 0.0, 0.0], 4, vec![Uuid::new_v4()])
.expect("build bridge for warm-path test");
insert_ann_if_absent(&ann, key.clone(), bridge).await;
assert!(
!is_warming_not_loaded(&ann, &key),
"key in both warming and indexes must not report warming in flight"
);
assert!(
ann.indexes.read().await.contains_key(&key),
"index must be present after successful build"
);
}
#[tokio::test]
async fn warm_path_failed_load_removes_key_from_warming_set() {
let ann = new_shared();
let key = AnnKey::new("local", "warm-unify-fail-model");
warming_guard(&ann.warming).insert(key.clone());
assert!(
is_warming_not_loaded(&ann, &key),
"pre-condition: key must show as warming in flight"
);
let loaded = ann.indexes.read().await.contains_key(&key);
if !loaded {
warming_guard(&ann.warming).remove(&key);
}
assert!(
!is_warming_not_loaded(&ann, &key),
"after failed-load cleanup, warming must not show in-flight"
);
assert!(
!ann.indexes.read().await.contains_key(&key),
"index must remain absent after failed load"
);
}
fn build_test_bridge(dim: usize, n: usize) -> (AnnBridge, Vec<Uuid>) {
let ids: Vec<Uuid> = (0..n).map(|_| Uuid::new_v4()).collect();
let mut vectors: Vec<f32> = Vec::with_capacity(n * dim);
for i in 0..n {
for d in 0..dim {
vectors.push(if d == i % dim { 1.0 } else { 0.0 });
}
}
let bridge = AnnBridge::build(vectors, dim, ids.clone()).expect("build test bridge");
(bridge, ids)
}
#[test]
fn ann_bridge_save_atomic_load_round_trip() {
let dir = tempfile::TempDir::new().expect("tempdir");
let dim = 4;
let (bridge, ids) = build_test_bridge(dim, 4);
let first_id = ids[0];
bridge.save_atomic(dir.path()).expect("save_atomic");
let loaded = AnnBridge::load(dir.path()).expect("load");
assert_eq!(
loaded.num_vectors(),
bridge.num_vectors(),
"loaded vector count must match saved"
);
let query = vec![1.0f32, 0.0, 0.0, 0.0];
let hits = loaded.search(&query, 1);
assert_eq!(hits.len(), 1, "must return 1 hit");
assert_eq!(hits[0].0, first_id, "top hit must be the first UUID");
}
#[test]
fn ann_bridge_load_missing_sidecar_err() {
let dir = tempfile::TempDir::new().expect("tempdir");
let (bridge, _) = build_test_bridge(4, 2);
bridge.save_atomic(dir.path()).expect("save_atomic");
std::fs::remove_file(dir.path().join("external_ids.bin")).expect("remove sidecar");
let result = AnnBridge::load(dir.path());
assert!(
result.is_err(),
"load must fail when external_ids.bin is missing"
);
}
#[test]
fn ann_bridge_load_torn_pair_err() {
let dir = tempfile::TempDir::new().expect("tempdir");
let dim = 4;
let (bridge_a, _) = build_test_bridge(dim, 2);
bridge_a.save_atomic(dir.path()).expect("save_atomic A");
let (bridge_b, _) = build_test_bridge(dim, 3);
bridge_b
.index
.save_atomic(dir.path())
.expect("save_atomic B segments");
let result = AnnBridge::load(dir.path());
assert!(
result.is_err(),
"load must fail when segment content_hash != sidecar content_hash (torn pair)"
);
let err = result.err().expect("already asserted is_err");
assert!(
err.contains("content_hash mismatch") || err.contains("torn"),
"error message must mention hash mismatch or torn pair, got: {err}"
);
}
#[test]
fn ann_bridge_load_count_mismatch_err() {
let dir = tempfile::TempDir::new().expect("tempdir");
let (bridge, _) = build_test_bridge(4, 2);
bridge.save_atomic(dir.path()).expect("save_atomic");
let sidecar_bytes =
std::fs::read(dir.path().join("external_ids.bin")).expect("read sidecar");
let mut new_sidecar: Vec<u8> = Vec::with_capacity(48 + 99 * 16);
new_sidecar.extend_from_slice(b"KHVANIDS");
new_sidecar.extend_from_slice(&sidecar_bytes[8..40]); new_sidecar.extend_from_slice(&99u64.to_le_bytes()); new_sidecar.extend(std::iter::repeat_n(0u8, 99 * 16)); std::fs::write(dir.path().join("external_ids.bin"), &new_sidecar)
.expect("write patched sidecar");
let result = AnnBridge::load(dir.path());
assert!(
result.is_err(),
"load must fail when sidecar count != index.num_vectors()"
);
}
#[test]
fn ann_bridge_load_bad_magic_err() {
let dir = tempfile::TempDir::new().expect("tempdir");
let (bridge, _) = build_test_bridge(4, 2);
bridge.save_atomic(dir.path()).expect("save_atomic");
let mut sidecar_bytes =
std::fs::read(dir.path().join("external_ids.bin")).expect("read sidecar");
sidecar_bytes[0..8].copy_from_slice(b"WRONGMAG");
std::fs::write(dir.path().join("external_ids.bin"), &sidecar_bytes)
.expect("write bad-magic sidecar");
let result = AnnBridge::load(dir.path());
assert!(
result.is_err(),
"load must fail when external_ids.bin has wrong magic"
);
let err = result.err().expect("already asserted is_err");
assert!(
err.contains("magic"),
"error must mention magic mismatch, got: {err}"
);
}
use async_trait::async_trait;
use khive_runtime::{AllowAllGate, BackendId, EmbedderProvider, RuntimeConfig};
use lattice_embed::{EmbedError, EmbeddingModel, EmbeddingService};
use tempfile::TempDir;
const WARM_TEST_MODEL: &str = "ann-test-model";
const WARM_DIMS: usize = 4;
struct ConstVecService;
#[async_trait]
impl EmbeddingService for ConstVecService {
async fn embed(
&self,
texts: &[String],
_model: EmbeddingModel,
) -> std::result::Result<Vec<Vec<f32>>, EmbedError> {
Ok(texts.iter().map(|_| vec![1.0_f32; WARM_DIMS]).collect())
}
fn supports_model(&self, _: EmbeddingModel) -> bool {
true
}
fn name(&self) -> &'static str {
"const-vec"
}
}
struct TestEmbedderProvider;
#[async_trait]
impl EmbedderProvider for TestEmbedderProvider {
fn name(&self) -> &str {
WARM_TEST_MODEL
}
fn dimensions(&self) -> usize {
WARM_DIMS
}
async fn build(&self) -> khive_runtime::RuntimeResult<Arc<dyn EmbeddingService>> {
Ok(Arc::new(ConstVecService))
}
}
fn file_rt_with_embedder(db_path: std::path::PathBuf) -> KhiveRuntime {
let rt = KhiveRuntime::new(RuntimeConfig {
git_write: Default::default(),
db_path: Some(db_path),
default_namespace: Namespace::local(),
embedding_model: None,
additional_embedding_models: vec![],
gate: Arc::new(AllowAllGate),
packs: vec!["kg".to_string(), "knowledge".to_string()],
backend_id: BackendId::main(),
brain_profile: None,
visible_namespaces: vec![],
allowed_outbound_namespaces: vec![],
actor_id: None,
})
.expect("file-backed runtime");
rt.register_embedder(TestEmbedderProvider);
rt
}
async fn seed_warm_corpus(rt: &KhiveRuntime, token: &NamespaceToken, n: usize) {
let _store = rt
.vectors_for_model(token, WARM_TEST_MODEL)
.expect("vec store");
let model_key = sanitize_model_key(WARM_TEST_MODEL);
let table = format!("vec_{model_key}");
let ns = token.namespace().as_str().to_owned();
let sql = rt.sql();
let mut w = sql.writer().await.expect("writer");
for i in 0..n {
let id = Uuid::new_v4();
let mut v = [0.0_f32; WARM_DIMS];
v[i % WARM_DIMS] = 1.0;
let bytes: Vec<u8> = v.iter().flat_map(|f| f.to_le_bytes()).collect();
w.execute(SqlStatement {
sql: format!(
"INSERT INTO {table} \
(subject_id, namespace, kind, field, embedding_model, embedding) \
VALUES (?1, ?2, ?3, ?4, ?5, ?6)"
),
params: vec![
SqlValue::Text(id.to_string()),
SqlValue::Text(ns.clone()),
SqlValue::Text("concept".to_string()),
SqlValue::Text("knowledge.atom".to_string()),
SqlValue::Text(WARM_TEST_MODEL.to_string()),
SqlValue::Blob(bytes),
],
label: None,
})
.await
.expect("insert corpus row");
}
}
#[tokio::test]
async fn ann_segment_dir_encode_decode_round_trip() {
let dir = TempDir::new().expect("tempdir");
let rt = file_rt_with_embedder(dir.path().join("test.db"));
let seg_dir = ann_segment_dir(&rt, "local", WARM_TEST_MODEL)
.expect("file backend must return Some(seg_dir)");
let dir_name = seg_dir
.file_name()
.expect("seg_dir must have a basename")
.to_string_lossy()
.into_owned();
let (decoded_ns, decoded_model) =
decode_ann_dir_name(&dir_name).expect("decode must succeed for a valid encode");
assert_eq!(decoded_ns, "local");
assert_eq!(decoded_model, WARM_TEST_MODEL);
let parent = seg_dir.parent().expect("seg_dir must have a parent");
assert_eq!(
parent.file_name().unwrap().to_string_lossy(),
"ann",
"seg_dir parent must be named 'ann'"
);
}
#[tokio::test]
async fn ensure_ann_no_data_dir_does_not_panic() {
let rt = KhiveRuntime::memory().expect("in-memory runtime");
let ann = new_shared();
let token = rt.authorize(Namespace::local()).expect("authorize");
ensure_ann_for_model(&rt, &token, &ann, WARM_TEST_MODEL).await;
let key = AnnKey::new("local", WARM_TEST_MODEL);
assert!(
!ann.indexes.read().await.contains_key(&key),
"no index should be loaded when corpus is empty and model is unknown"
);
}
#[tokio::test]
async fn ensure_ann_round_trip_hot() {
let dir = TempDir::new().expect("tempdir");
let rt = file_rt_with_embedder(dir.path().join("test.db"));
let token = rt.authorize(Namespace::local()).expect("authorize");
seed_warm_corpus(&rt, &token, 4).await;
let ann = new_shared();
ensure_ann_for_model(&rt, &token, &ann, WARM_TEST_MODEL).await;
let key = AnnKey::new("local", WARM_TEST_MODEL);
assert!(
ann.indexes.read().await.contains_key(&key),
"first call must build the ANN index"
);
let seg_dir = ann_segment_dir(&rt, "local", WARM_TEST_MODEL)
.expect("file backend must have a seg_dir");
assert!(
seg_dir.join("metadata.bin").exists(),
"first call must persist v2 segments (metadata.bin missing)"
);
let persisted = read_commit_fingerprint(&seg_dir)
.expect("read_commit_fingerprint must not err")
.expect("metadata.bin must carry a v2 fingerprint");
let live = live_content_hash(&rt, &token, WARM_TEST_MODEL)
.await
.expect("live_content_hash must return Some for a seeded corpus");
assert_eq!(
live, persisted.content_hash,
"live_content_hash must equal persisted content_hash (normalize-once invariant)"
);
use std::os::unix::fs::MetadataExt;
let meta_path = seg_dir.join("metadata.bin");
let ino_before = std::fs::metadata(&meta_path)
.expect("metadata.bin must exist after first build")
.ino();
let ann2 = new_shared();
ensure_ann_for_model(&rt, &token, &ann2, WARM_TEST_MODEL).await;
assert!(
ann2.indexes.read().await.contains_key(&key),
"second call must restore the ANN index from v2 segments"
);
let ino_after = std::fs::metadata(&meta_path)
.expect("metadata.bin must still exist")
.ino();
assert_eq!(
ino_before, ino_after,
"second call must NOT rewrite metadata.bin — proves the v2 Hot load path, not a rebuild"
);
}
#[tokio::test]
async fn ensure_ann_stale_rebuild() {
let dir = TempDir::new().expect("tempdir");
let rt = file_rt_with_embedder(dir.path().join("test.db"));
let token = rt.authorize(Namespace::local()).expect("authorize");
seed_warm_corpus(&rt, &token, 4).await;
let ann = new_shared();
ensure_ann_for_model(&rt, &token, &ann, WARM_TEST_MODEL).await;
let key = AnnKey::new("local", WARM_TEST_MODEL);
assert!(ann.indexes.read().await.contains_key(&key), "initial build");
seed_warm_corpus(&rt, &token, 1).await;
let seg_dir = ann_segment_dir(&rt, "local", WARM_TEST_MODEL)
.expect("file backend must have a seg_dir");
let persisted_before = read_commit_fingerprint(&seg_dir)
.expect("read_commit_fingerprint must not err")
.expect("v2 fingerprint must be present after initial build");
let live_after_mutation = live_content_hash(&rt, &token, WARM_TEST_MODEL)
.await
.expect("live_content_hash must return Some");
assert_ne!(
live_after_mutation, persisted_before.content_hash,
"mutation must make live hash differ from persisted (stale-branch pre-condition)"
);
let ann2 = new_shared();
ensure_ann_for_model(&rt, &token, &ann2, WARM_TEST_MODEL).await;
assert!(
ann2.indexes.read().await.contains_key(&key),
"must rebuild the index after corpus mutation (stale v2 segment rejected)"
);
let persisted_after = read_commit_fingerprint(&seg_dir)
.expect("read_commit_fingerprint after rebuild must not err")
.expect("v2 fingerprint must be present after rebuild");
let live_final = live_content_hash(&rt, &token, WARM_TEST_MODEL)
.await
.expect("live_content_hash must return Some after rebuild");
assert_eq!(
live_final, persisted_after.content_hash,
"rebuild must re-persist v2 matching the mutated corpus"
);
assert_eq!(
persisted_after.vector_count, 5,
"re-persisted segment must reflect the 5-row corpus (4 initial + 1 mutation)"
);
}
#[tokio::test]
async fn ensure_ann_fast_path_ignores_generation_stale_cached_entry() {
let dir = TempDir::new().expect("tempdir");
let rt = file_rt_with_embedder(dir.path().join("test.db"));
let token = rt.authorize(Namespace::local()).expect("authorize");
seed_warm_corpus(&rt, &token, 4).await;
let ann = new_shared();
ensure_ann_for_model(&rt, &token, &ann, WARM_TEST_MODEL).await;
let key = AnnKey::new("local", WARM_TEST_MODEL);
assert!(
ann.indexes.read().await.contains_key(&key),
"setup: first call must build and install the index at generation 0"
);
bump_generation(&ann, "local");
assert_eq!(current_generation(&ann, "local"), 1);
ensure_ann_for_model(&rt, &token, &ann, WARM_TEST_MODEL).await;
assert_eq!(
ann.indexes
.read()
.await
.get(&key)
.expect("entry present")
.generation,
1,
"a present-but-generation-stale entry must not short-circuit via the fast \
path; the reloaded/rebuilt entry must be re-stamped with the namespace's \
new current generation"
);
}
#[tokio::test]
async fn warm_known_snapshots_v2_only_no_legacy_table() {
let dir = TempDir::new().expect("tempdir");
let rt = file_rt_with_embedder(dir.path().join("test.db"));
let token = rt.authorize(Namespace::local()).expect("authorize");
seed_warm_corpus(&rt, &token, 4).await;
let ann = new_shared();
ensure_ann_for_model(&rt, &token, &ann, WARM_TEST_MODEL).await;
let key = AnnKey::new("local", WARM_TEST_MODEL);
assert!(
ann.indexes.read().await.contains_key(&key),
"setup: first ensure must persist v2 segments"
);
{
let sql = rt.sql();
let mut w = sql.writer().await.expect("writer");
w.execute(SqlStatement {
sql: "DROP TABLE IF EXISTS retrieval_snapshots".into(),
params: vec![],
label: None,
})
.await
.expect("drop retrieval_snapshots");
}
let ann_fresh = new_shared();
warm_known_snapshots(&rt, &ann_fresh).await;
assert!(
ann_fresh.indexes.read().await.contains_key(&key),
"warm_known_snapshots must warm v2 segments when retrieval_snapshots is absent \
(regression: a v1 query error must not abort the v2 filesystem pass)"
);
}
#[tokio::test]
async fn ensure_ann_for_model_empty_corpus_marks_unavailable_and_wait_short_circuits() {
let dir = TempDir::new().expect("tempdir");
let rt = file_rt_with_embedder(dir.path().join("test.db"));
let token = rt.authorize(Namespace::local()).expect("authorize");
let ann = new_shared();
ensure_ann_for_model(&rt, &token, &ann, WARM_TEST_MODEL).await;
let key = AnnKey::new("local", WARM_TEST_MODEL);
assert!(
!ann.indexes.read().await.contains_key(&key),
"empty corpus must not install an index"
);
let start = std::time::Instant::now();
let ready = wait_for_ann(&ann, &key, 5_000, 50).await;
let elapsed = start.elapsed();
assert!(!ready, "empty corpus must never become ready");
assert!(
elapsed < std::time::Duration::from_millis(200),
"the terminal unavailable outcome must short-circuit the 5s warm-wait: {elapsed:?}"
);
}
#[tokio::test]
async fn ensure_ann_for_model_rebuild_error_does_not_mark_unavailable() {
let dir = TempDir::new().expect("tempdir");
let rt = file_rt_with_embedder(dir.path().join("test.db"));
let token = rt.authorize(Namespace::local()).expect("authorize");
seed_warm_corpus(&rt, &token, 3).await;
let model_key = sanitize_model_key(WARM_TEST_MODEL);
let table = format!("vec_{model_key}");
let sql = rt.sql();
let mut w = sql.writer().await.expect("writer");
w.execute(SqlStatement {
sql: format!("DROP TABLE {table}"),
params: vec![],
label: None,
})
.await
.expect("drop corpus table");
w.execute(SqlStatement {
sql: format!("CREATE VIEW {table} AS SELECT * FROM missing_corpus_table"),
params: vec![],
label: None,
})
.await
.expect("create broken view");
drop(w);
let ann = new_shared();
ensure_ann_for_model(&rt, &token, &ann, WARM_TEST_MODEL).await;
let key = AnnKey::new("local", WARM_TEST_MODEL);
assert!(
!ann.indexes.read().await.contains_key(&key),
"a failed rebuild must not install an index"
);
assert!(
!unavailable_guard(&ann.unavailable).contains_key(&key),
"a rebuild ERROR must not mark the key unavailable — only a completed \
empty-corpus scan may; a marker here would short-circuit wait_for_ann \
while the same-generation retry is in flight"
);
let ann2 = ann.clone();
let key2 = key.clone();
tokio::spawn(async move {
tokio::time::sleep(std::time::Duration::from_millis(40)).await;
let bridge = AnnBridge::build(vec![1.0f32, 0.0, 0.0, 0.0], 4, vec![Uuid::new_v4()])
.expect("build")
.with_generation(0);
install_if_fresher(&ann2, &key2, bridge).await;
});
let ready = wait_for_ann(&ann, &key, 500, 10).await;
assert!(
ready,
"after a rebuild error the wait must keep polling and observe the \
retry's install, not short-circuit false"
);
}
#[tokio::test]
async fn ensure_ann_for_model_store_open_failure_does_not_mark_unavailable() {
let dir = TempDir::new().expect("tempdir");
let rt = file_rt_with_embedder(dir.path().join("test.db"));
let token = rt.authorize(Namespace::local()).expect("authorize");
let model = "model-with-no-registered-embedder";
assert!(
rt.vectors_for_model(&token, model).is_err(),
"precondition: opening the vector store for an unregistered model must fail"
);
let ann = new_shared();
ensure_ann_for_model(&rt, &token, &ann, model).await;
let key = AnnKey::new("local", model);
assert!(
!unavailable_guard(&ann.unavailable).contains_key(&key),
"a store-opening failure must not mark the key unavailable"
);
let ann2 = ann.clone();
let key2 = key.clone();
tokio::spawn(async move {
tokio::time::sleep(std::time::Duration::from_millis(40)).await;
let bridge = AnnBridge::build(vec![1.0f32, 0.0, 0.0, 0.0], 4, vec![Uuid::new_v4()])
.expect("build")
.with_generation(0);
install_if_fresher(&ann2, &key2, bridge).await;
});
let ready = wait_for_ann(&ann, &key, 500, 10).await;
assert!(
ready,
"after a store-opening failure the wait must keep polling and observe \
the retry's install, not short-circuit false"
);
}
}