use std::io::Write;
use std::sync::Arc;
use super::store_log::{LogRecord, VectorLog};
use super::*;
const A: &str = "01J9ZXW0PRJCTAAAAAAAAAAAAA";
const B: &str = "01J9ZXW0PRJCTBBBBBBBBBBBBB";
fn from(inst: &str) -> Provenance {
Provenance { instance_id: inst.into(), source_node: "node-a".into() }
}
fn vec_of(i: u64) -> [f32; DIMS] {
let mut v = [0.0; DIMS];
v[i as usize % DIMS] = 1.0;
v[(i as usize + 3) % DIMS] += 0.5;
v
}
fn vecs(range: std::ops::Range<u64>) -> Vec<IngestVector> {
range.map(|i| IngestVector { id: i, values: vec_of(i) }).collect()
}
fn rec(id: u64) -> LogRecord {
LogRecord { instance: "inst".into(), node: "n".into(), id, values: [id as f32; DIMS] }
}
fn dir_over(path: &std::path::Path) -> VectorDirectory {
VectorDirectory::new([A.to_string(), B.to_string()], true).with_persistence(path.to_path_buf())
}
#[test]
fn a_restarted_owner_has_the_vectors_it_had_and_keeps_dedup_and_namespaces() {
let tmp = tempfile::tempdir().unwrap();
{
let d = dir_over(tmp.path());
let a = d.store_for(Some(A)).unwrap();
assert_eq!(a.ingest(&from("inst-1"), &vecs(0..5), true).unwrap().accepted, 5);
d.store_for(Some(B)).unwrap().ingest(&from("inst-2"), &vecs(0..2), true).unwrap();
d.store_for(None).unwrap().ingest(&from("inst-3"), &vecs(0..1), true).unwrap();
}
let d = dir_over(tmp.path());
let a = d.store_for(Some(A)).unwrap();
assert_eq!(a.len(), 5);
assert_eq!(d.store_for(Some(B)).unwrap().len(), 2);
assert_eq!(d.store_for(None).unwrap().len(), 1);
let hit = &a.query(&vec_of(3), 1)[0];
assert_eq!((hit.instance_id.as_str(), hit.id), ("inst-1", 3));
let again = a.ingest(&from("inst-1"), &vecs(0..5), true).unwrap();
assert_eq!((again.accepted, again.deduped, again.total), (0, 5, 5));
assert_eq!(a.ingest(&from("inst-9"), &vecs(0..2), true).unwrap().accepted, 2);
drop(d);
let d = dir_over(tmp.path());
assert_eq!(d.store_for(Some(A)).unwrap().len(), 7);
}
#[test]
fn projects_are_separate_files_and_unowned_projects_get_no_store() {
let tmp = tempfile::tempdir().unwrap();
let d = dir_over(tmp.path());
d.store_for(Some(A)).unwrap().ingest(&from("i"), &vecs(0..1), true).unwrap();
assert!(tmp.path().join(format!("{A}.vec")).is_file());
assert!(!tmp.path().join(format!("{B}.vec")).exists(), "created on first use only");
assert!(d.store_for(Some("01J9ZXW0PRJCTCCCCCCCCCCCCC")).is_none());
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let mode = std::fs::metadata(tmp.path().join(format!("{A}.vec"))).unwrap().permissions().mode();
assert_eq!(mode & 0o077, 0, "owner-only");
}
}
#[test]
fn a_full_log_refuses_the_batch_and_writes_nothing() {
let tmp = tempfile::tempdir().unwrap();
let one = super::store_log::frame_len(&[LogRecord {
instance: "inst-1".into(),
node: "node-a".into(),
id: 0,
values: [0.0; DIMS],
}]);
let cap = 8 + one * 2;
let d = VectorDirectory::new([A.to_string()], false).with_persistence_capped(tmp.path().to_path_buf(), cap);
let s = d.store_for(Some(A)).unwrap();
s.ingest(&from("inst-1"), &vecs(0..1), true).unwrap();
s.ingest(&from("inst-1"), &vecs(1..2), true).unwrap();
let before = std::fs::metadata(tmp.path().join(format!("{A}.vec"))).unwrap().len();
assert!(matches!(s.ingest(&from("inst-1"), &vecs(2..3), true), Err(StoreError::Full(_))));
assert_eq!(s.len(), 2, "memory did not get ahead of the log");
assert_eq!(std::fs::metadata(tmp.path().join(format!("{A}.vec"))).unwrap().len(), before);
assert!(before <= cap);
}
#[test]
fn a_torn_tail_is_dropped_and_appends_resume() {
let tmp = tempfile::tempdir().unwrap();
let path = tmp.path().join("p.vec");
{
let (mut log, existing) = VectorLog::open(&path, 1 << 20).unwrap();
assert!(existing.is_empty());
log.append(&[rec(1), rec(2)]).unwrap();
log.append(&[rec(3)]).unwrap();
}
let good = std::fs::metadata(&path).unwrap().len();
std::fs::OpenOptions::new().append(true).open(&path).unwrap().write_all(&[9, 0, 0, 0, 1, 2, 3]).unwrap();
let (mut log, records) = VectorLog::open(&path, 1 << 20).unwrap();
assert_eq!(records, vec![rec(1), rec(2), rec(3)]);
assert_eq!(std::fs::metadata(&path).unwrap().len(), good, "tail truncated");
log.append(&[rec(4)]).unwrap();
drop(log);
assert_eq!(VectorLog::open(&path, 1 << 20).unwrap().1.len(), 4);
let mut bytes = std::fs::read(&path).unwrap();
let n = bytes.len();
bytes[n - 8] ^= 0xff;
std::fs::write(&path, &bytes).unwrap();
assert_eq!(VectorLog::open(&path, 1 << 20).unwrap().1, vec![rec(1), rec(2), rec(3)]);
}
#[test]
fn an_unreadable_log_is_moved_aside_not_overwritten() {
let tmp = tempfile::tempdir().unwrap();
std::fs::write(tmp.path().join(format!("{A}.vec")), b"not a vector log at all").unwrap();
let d = dir_over(tmp.path());
let s = d.store_for(Some(A)).unwrap();
assert!(s.is_empty());
let aside = |tmp: &std::path::Path| -> Vec<std::path::PathBuf> {
std::fs::read_dir(tmp)
.unwrap()
.flatten()
.map(|e| e.path())
.filter(|p| p.to_string_lossy().contains(".vec.unreadable."))
.collect()
};
let first = aside(tmp.path());
assert_eq!(first.len(), 1);
assert_eq!(std::fs::read(&first[0]).unwrap(), b"not a vector log at all");
s.ingest(&from("i"), &vecs(0..1), true).unwrap();
assert_eq!(dir_over(tmp.path()).store_for(Some(A)).unwrap().len(), 1);
drop(d);
std::fs::write(tmp.path().join(format!("{A}.vec")), b"garbage again").unwrap();
let d = dir_over(tmp.path());
assert!(d.store_for(Some(A)).unwrap().is_empty());
assert_eq!(aside(tmp.path()).len(), 2);
}
#[test]
fn a_memory_only_directory_still_forgets_on_restart() {
let d = VectorDirectory::new([A.to_string()], false);
d.store_for(Some(A)).unwrap().ingest(&from("i"), &vecs(0..3), true).unwrap();
let d2 = VectorDirectory::new([A.to_string()], false);
assert!(d2.store_for(Some(A)).unwrap().is_empty());
let _: Arc<dyn IngestStore> = d.store_for(Some(A)).unwrap();
}
#[test]
fn mid_file_corruption_keeps_a_copy_of_the_dropped_tail() {
let tmp = tempfile::tempdir().unwrap();
let path = tmp.path().join("p.vec");
{
let (mut log, _) = VectorLog::open(&path, 1 << 20).unwrap();
log.append(&[rec(1)]).unwrap();
log.append(&[rec(2)]).unwrap();
log.append(&[rec(3)]).unwrap();
}
let one_frame = super::store_log::frame_len(&[rec(1)]) as usize;
let mut bytes = std::fs::read(&path).unwrap();
bytes[8 + one_frame + 12] ^= 0xff;
let original_len = bytes.len();
std::fs::write(&path, &bytes).unwrap();
let (_, records) = VectorLog::open(&path, 1 << 20).unwrap();
assert_eq!(records, vec![rec(1)], "frames after the bad one are dropped from the index");
let copies: Vec<_> = std::fs::read_dir(tmp.path())
.unwrap()
.flatten()
.filter(|e| e.file_name().to_string_lossy().contains(".corrupt."))
.collect();
assert_eq!(copies.len(), 1);
assert_eq!(std::fs::read(copies[0].path()).unwrap().len(), original_len - 8 - one_frame);
assert_eq!(std::fs::metadata(&path).unwrap().len() as usize, 8 + one_frame);
}
#[test]
fn dedup_hits_do_not_count_against_the_cap() {
let tmp = tempfile::tempdir().unwrap();
let one = |n: usize| {
super::store_log::frame_len(&vec![
LogRecord { instance: "inst-1".into(), node: "node-a".into(), id: 0, values: [0.0; DIMS] };
n
])
};
let cap = 8 + one(3);
let d = VectorDirectory::new([A.to_string()], false).with_persistence_capped(tmp.path().to_path_buf(), cap);
let s = d.store_for(Some(A)).unwrap();
s.ingest(&from("inst-1"), &vecs(0..3), true).unwrap();
let again = s.ingest(&from("inst-1"), &vecs(0..3), true).unwrap();
assert_eq!((again.accepted, again.deduped), (0, 3));
assert!(matches!(s.ingest(&from("inst-1"), &vecs(3..4), true), Err(StoreError::Full(_))));
}
#[test]
fn the_log_dir_is_private_and_concurrent_first_use_opens_one_store() {
let tmp = tempfile::tempdir().unwrap();
let dir = tmp.path().join("cog-ingest-vectors");
let d = dir_over(&dir);
let stores: Vec<_> = (0..4)
.map(|_| {
let d = d.clone();
std::thread::spawn(move || d.store_for(Some(A)).unwrap())
})
.collect::<Vec<_>>()
.into_iter()
.map(|h| h.join().unwrap())
.collect();
for s in &stores[1..] {
assert!(Arc::ptr_eq(s, &stores[0]), "one store, one open log");
}
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
assert_eq!(std::fs::metadata(&dir).unwrap().permissions().mode() & 0o777, 0o700);
}
}
#[tokio::test(flavor = "multi_thread")]
async fn a_first_use_replay_and_an_append_run_on_a_multi_thread_runtime() {
let tmp = tempfile::tempdir().unwrap();
let d = dir_over(tmp.path());
let s = d.store_for(Some(A)).unwrap();
s.ingest(&from("i"), &vecs(0..2), true).unwrap();
drop(s);
let d2 = dir_over(tmp.path());
assert_eq!(d2.store_for(Some(A)).unwrap().len(), 2);
}
#[test]
fn a_large_dropped_tail_is_kept_even_when_it_reads_as_torn() {
let tmp = tempfile::tempdir().unwrap();
let path = tmp.path().join("p.vec");
{
let (mut log, _) = VectorLog::open(&path, 1 << 20).unwrap();
for i in 0..40 {
log.append(&[rec(i)]).unwrap();
}
}
let one = super::store_log::frame_len(&[rec(0)]) as usize;
let mut bytes = std::fs::read(&path).unwrap();
bytes[8 + one + 3] = 0x7f;
std::fs::write(&path, &bytes).unwrap();
let (_, records) = VectorLog::open(&path, 1 << 20).unwrap();
assert_eq!(records.len(), 1);
let kept: Vec<_> = std::fs::read_dir(tmp.path())
.unwrap()
.flatten()
.filter(|e| e.file_name().to_string_lossy().contains(".corrupt."))
.collect();
assert_eq!(kept.len(), 1, "a big dropped tail is copied aside");
assert_eq!(std::fs::read(kept[0].path()).unwrap().len(), bytes.len() - 8 - one);
}