1use std::os::fd::AsRawFd;
6use std::path::{Path, PathBuf};
7use std::sync::atomic::Ordering;
8
9use nodedb_mem::{ReservationToken, ScopedMemory};
10
11use super::format::{
12 FOOTER_SIZE, FORMAT_VERSION, HEADER_SIZE, MAGIC, VectorSegmentCodec, VectorSegmentDropPolicy,
13 observability, vec_pad,
14};
15use super::writer::write_segment;
16use crate::error::VectorError;
17
18#[derive(Debug)]
26pub struct MmapVectorSegment {
27 path: PathBuf,
28 _fd: std::fs::File,
29 base: *const u8,
30 mmap_size: usize,
31 dim: usize,
32 count: usize,
33 vec_offset: usize,
35 sid_offset: usize,
37 drop_policy: VectorSegmentDropPolicy,
38 madvise_state: Option<libc::c_int>,
39 _reservation: ReservationToken,
42}
43
44unsafe impl Send for MmapVectorSegment {}
49unsafe impl Sync for MmapVectorSegment {}
50
51impl MmapVectorSegment {
52 pub fn create(
57 dir: &Path,
58 name: &str,
59 dim: usize,
60 vectors: &[&[f32]],
61 memory: &ScopedMemory,
62 ) -> Result<Self, VectorError> {
63 write_segment(dir, name, dim, vectors, &[])?;
64 Self::open_with_policy(&dir.join(name), memory, VectorSegmentDropPolicy::default())
65 }
66
67 pub fn create_with_surrogates(
70 dir: &Path,
71 name: &str,
72 dim: usize,
73 vectors: &[&[f32]],
74 surrogate_ids: &[u64],
75 memory: &ScopedMemory,
76 ) -> Result<Self, VectorError> {
77 write_segment(dir, name, dim, vectors, surrogate_ids)?;
78 Self::open_with_policy(&dir.join(name), memory, VectorSegmentDropPolicy::default())
79 }
80
81 pub fn create_with_policy(
84 dir: &Path,
85 name: &str,
86 dim: usize,
87 vectors: &[&[f32]],
88 policy: VectorSegmentDropPolicy,
89 memory: &ScopedMemory,
90 ) -> Result<Self, VectorError> {
91 write_segment(dir, name, dim, vectors, &[])?;
92 Self::open_with_policy(&dir.join(name), memory, policy)
93 }
94
95 pub fn open(path: &Path, memory: &ScopedMemory) -> Result<Self, VectorError> {
103 Self::open_with_policy(path, memory, VectorSegmentDropPolicy::default())
104 }
105
106 pub fn open_with_policy(
109 path: &Path,
110 memory: &ScopedMemory,
111 policy: VectorSegmentDropPolicy,
112 ) -> Result<Self, VectorError> {
113 let fd = std::fs::OpenOptions::new().read(true).open(path)?;
114 let file_size = fd.metadata()?.len() as usize;
115
116 let reservation = memory.reserve(file_size)?;
117
118 let min_size = HEADER_SIZE + FOOTER_SIZE;
119 if file_size < min_size {
120 return Err(std::io::Error::new(
122 std::io::ErrorKind::InvalidData,
123 format!("segment file too small: {file_size} < {min_size} bytes"),
124 )
125 .into());
126 }
127
128 let base = unsafe {
129 libc::mmap(
130 std::ptr::null_mut(),
131 file_size,
132 libc::PROT_READ,
133 libc::MAP_PRIVATE,
134 fd.as_raw_fd(),
135 0,
136 )
137 };
138 if base == libc::MAP_FAILED {
139 return Err(std::io::Error::last_os_error().into());
141 }
142 let base = base as *const u8;
143
144 Self::validate_and_build(fd, base, file_size, path, policy, reservation)
145 .map_err(VectorError::from)
146 .inspect_err(|_| {
147 unsafe { libc::munmap(base as *mut libc::c_void, file_size) };
148 })
149 }
150
151 fn validate_and_build(
154 fd: std::fs::File,
155 base: *const u8,
156 file_size: usize,
157 path: &Path,
158 policy: VectorSegmentDropPolicy,
159 reservation: ReservationToken,
160 ) -> std::io::Result<Self> {
161 let header = unsafe { std::slice::from_raw_parts(base, HEADER_SIZE) };
163 if &header[0..4] != MAGIC.as_slice() {
164 return Err(std::io::Error::new(
165 std::io::ErrorKind::InvalidData,
166 "invalid NDVS magic bytes",
167 ));
168 }
169 let fv = u16::from_le_bytes([header[4], header[5]]);
170 if fv != FORMAT_VERSION {
171 return Err(std::io::Error::new(
172 std::io::ErrorKind::InvalidData,
173 format!("unsupported segment format version {fv}; expected {FORMAT_VERSION}"),
174 ));
175 }
176
177 let dim = u32::from_le_bytes([header[8], header[9], header[10], header[11]]) as usize;
178 let count = u64::from_le_bytes([
179 header[12], header[13], header[14], header[15], header[16], header[17], header[18],
180 header[19],
181 ]) as usize;
182 let compression_byte = header[21];
183
184 let codec = VectorSegmentCodec::from_u8(compression_byte)?;
187 match codec {
188 VectorSegmentCodec::None => { }
189 }
190
191 if dim == 0 && count > 0 {
192 return Err(std::io::Error::new(
193 std::io::ErrorKind::InvalidData,
194 "segment has dim=0 with nonzero count",
195 ));
196 }
197
198 let vec_bytes = dim
200 .checked_mul(count)
201 .and_then(|n| n.checked_mul(4))
202 .ok_or_else(|| {
203 std::io::Error::new(
204 std::io::ErrorKind::InvalidData,
205 format!("segment header overflow: dim={dim}, count={count}"),
206 )
207 })?;
208 let sid_bytes = count.checked_mul(8).ok_or_else(|| {
209 std::io::Error::new(
210 std::io::ErrorKind::InvalidData,
211 format!("surrogate block overflow: count={count}"),
212 )
213 })?;
214 let pad_bytes = vec_pad(vec_bytes);
215 let expected = HEADER_SIZE
216 .checked_add(vec_bytes)
217 .and_then(|n| n.checked_add(pad_bytes))
218 .and_then(|n| n.checked_add(sid_bytes))
219 .and_then(|n| n.checked_add(FOOTER_SIZE))
220 .ok_or_else(|| {
221 std::io::Error::new(
222 std::io::ErrorKind::InvalidData,
223 "total segment size overflow",
224 )
225 })?;
226 if file_size != expected {
227 return Err(std::io::Error::new(
228 std::io::ErrorKind::InvalidData,
229 format!("segment size mismatch: expected {expected} bytes, got {file_size}"),
230 ));
231 }
232
233 let footer_start = file_size - FOOTER_SIZE;
235 let footer = unsafe { std::slice::from_raw_parts(base.add(footer_start), FOOTER_SIZE) };
236
237 if &footer[42..46] != MAGIC.as_slice() {
239 return Err(std::io::Error::new(
240 std::io::ErrorKind::InvalidData,
241 "invalid NDVS trailing magic bytes",
242 ));
243 }
244
245 let footer_fv = u16::from_le_bytes([footer[0], footer[1]]);
246 if footer_fv != FORMAT_VERSION {
247 return Err(std::io::Error::new(
248 std::io::ErrorKind::InvalidData,
249 format!(
250 "unsupported segment footer version {footer_fv}; expected {FORMAT_VERSION}"
251 ),
252 ));
253 }
254 let stored_footer_size =
255 u32::from_le_bytes([footer[38], footer[39], footer[40], footer[41]]) as usize;
256 if stored_footer_size != FOOTER_SIZE {
257 return Err(std::io::Error::new(
258 std::io::ErrorKind::InvalidData,
259 format!("footer_size field {stored_footer_size} != {FOOTER_SIZE}"),
260 ));
261 }
262
263 let body = unsafe { std::slice::from_raw_parts(base, footer_start) };
265 let computed = crc32c::crc32c(body);
266 let stored = u32::from_le_bytes([footer[34], footer[35], footer[36], footer[37]]);
267 if computed != stored {
268 return Err(std::io::Error::new(
269 std::io::ErrorKind::InvalidData,
270 format!("CRC32C mismatch: stored {stored:#010x}, computed {computed:#010x}"),
271 ));
272 }
273
274 let vec_offset = HEADER_SIZE;
275 let sid_offset = HEADER_SIZE + vec_bytes + pad_bytes;
276
277 let mut madvise_state = None;
279 if vec_bytes + sid_bytes > 0 {
280 let rc =
281 unsafe { libc::madvise(base as *mut libc::c_void, file_size, libc::MADV_RANDOM) };
282 if rc == 0 {
283 madvise_state = Some(libc::MADV_RANDOM);
284 observability::RANDOM_COUNT.fetch_add(1, Ordering::Relaxed);
285 } else {
286 tracing::warn!(
287 path = %path.display(),
288 errno = std::io::Error::last_os_error().raw_os_error().unwrap_or(0),
289 "madvise(MADV_RANDOM) failed on vector segment; continuing with kernel default",
290 );
291 }
292 }
293
294 Ok(Self {
295 path: path.to_path_buf(),
296 _fd: fd,
297 base,
298 mmap_size: file_size,
299 dim,
300 count,
301 vec_offset,
302 sid_offset,
303 drop_policy: policy,
304 madvise_state,
305 _reservation: reservation,
306 })
307 }
308
309 pub fn madvise_state(&self) -> Option<libc::c_int> {
313 self.madvise_state
314 }
315
316 #[inline]
318 pub fn get_vector(&self, id: u32) -> Option<&[f32]> {
319 let idx = id as usize;
320 if idx >= self.count {
321 return None;
322 }
323 let byte_len = self.dim.checked_mul(4)?;
324 let offset = self.vec_offset.checked_add(idx.checked_mul(byte_len)?)?;
325 let end = offset.checked_add(byte_len)?;
326 if end > self.sid_offset {
327 return None;
328 }
329 unsafe {
330 let ptr = self.base.add(offset) as *const f32;
331 Some(std::slice::from_raw_parts(ptr, self.dim))
332 }
333 }
334
335 #[inline]
337 pub fn get_surrogate_id(&self, id: u32) -> Option<u64> {
338 let idx = id as usize;
339 if idx >= self.count {
340 return None;
341 }
342 let offset = self.sid_offset.checked_add(idx.checked_mul(8)?)?;
343 let end = offset.checked_add(8)?;
344 let sid_end = self.mmap_size - FOOTER_SIZE;
345 if end > sid_end {
346 return None;
347 }
348 let bytes = unsafe { std::slice::from_raw_parts(self.base.add(offset), 8) };
349 Some(u64::from_le_bytes(bytes.try_into().expect("invariant: from_raw_parts constructed with len=8, so try_into::<[u8;8]> always succeeds")))
350 }
351
352 #[inline]
356 pub fn all_vectors_flat(&self) -> &[f32] {
357 let float_count = self.dim * self.count;
358 unsafe {
359 let ptr = self.base.add(self.vec_offset) as *const f32;
360 std::slice::from_raw_parts(ptr, float_count)
361 }
362 }
363
364 #[inline]
368 pub fn all_surrogate_ids(&self) -> &[u64] {
369 unsafe {
370 let ptr = self.base.add(self.sid_offset) as *const u64;
371 std::slice::from_raw_parts(ptr, self.count)
372 }
373 }
374
375 pub fn prefetch(&self, id: u32) {
377 let idx = id as usize;
378 if idx >= self.count {
379 return;
380 }
381 let byte_len = match self.dim.checked_mul(4) {
382 Some(v) => v,
383 None => return,
384 };
385 let Some(idx_bytes) = idx.checked_mul(byte_len) else {
386 return;
387 };
388 let Some(offset) = self.vec_offset.checked_add(idx_bytes) else {
389 return;
390 };
391 if offset
392 .checked_add(byte_len)
393 .is_none_or(|e| e > self.sid_offset)
394 {
395 return;
396 }
397 let page_start = offset & !(4095);
398 let len = (byte_len + 4095) & !(4095);
399 unsafe {
400 libc::madvise(
401 self.base.add(page_start) as *mut libc::c_void,
402 len,
403 libc::MADV_WILLNEED,
404 );
405 }
406 }
407
408 pub fn prefetch_batch(&self, ids: &[u32]) {
410 for &id in ids {
411 self.prefetch(id);
412 }
413 }
414
415 pub fn dim(&self) -> usize {
416 self.dim
417 }
418
419 pub fn count(&self) -> usize {
420 self.count
421 }
422
423 pub fn path(&self) -> &Path {
424 &self.path
425 }
426
427 pub fn mmap_bytes(&self) -> usize {
428 self.mmap_size
429 }
430
431 pub fn file_size(&self) -> usize {
432 self.mmap_size
433 }
434}
435
436impl Drop for MmapVectorSegment {
437 fn drop(&mut self) {
438 if !self.base.is_null() && self.mmap_size > 0 {
439 if self.drop_policy.dontneed_on_drop() {
440 let data_bytes = self.mmap_size.saturating_sub(HEADER_SIZE + FOOTER_SIZE);
441 if data_bytes > 0 {
442 unsafe {
443 libc::madvise(
444 self.base as *mut libc::c_void,
445 self.mmap_size,
446 libc::MADV_DONTNEED,
447 );
448 }
449 observability::DONTNEED_COUNT.fetch_add(1, Ordering::Relaxed);
450 }
451 }
452 unsafe {
453 libc::munmap(self.base as *mut libc::c_void, self.mmap_size);
454 }
455 }
456 }
457}
458
459#[cfg(test)]
460mod tests {
461 use super::*;
462 use crate::test_support::test_memory;
463
464 fn io_err(err: VectorError) -> std::io::Error {
466 match err {
467 VectorError::SegmentIo(e) => e,
468 other => panic!("expected VectorError::SegmentIo, got: {other:?}"),
469 }
470 }
471
472 #[test]
473 fn create_and_read() {
474 let dir = tempfile::tempdir().unwrap();
475
476 let v0 = vec![1.0f32, 2.0, 3.0];
477 let v1 = vec![4.0f32, 5.0, 6.0];
478 let v2 = vec![7.0f32, 8.0, 9.0];
479 let surrogates = vec![10u64, 20, 30];
480
481 let seg = MmapVectorSegment::create_with_surrogates(
482 dir.path(),
483 "test.vseg",
484 3,
485 &[&v0, &v1, &v2],
486 &surrogates,
487 &test_memory(),
488 )
489 .unwrap();
490
491 assert_eq!(seg.dim(), 3);
492 assert_eq!(seg.count(), 3);
493 assert_eq!(seg.get_vector(0).unwrap(), &[1.0, 2.0, 3.0]);
494 assert_eq!(seg.get_vector(1).unwrap(), &[4.0, 5.0, 6.0]);
495 assert_eq!(seg.get_vector(2).unwrap(), &[7.0, 8.0, 9.0]);
496 assert!(seg.get_vector(3).is_none());
497 assert_eq!(seg.get_surrogate_id(0).unwrap(), 10);
498 assert_eq!(seg.get_surrogate_id(1).unwrap(), 20);
499 assert_eq!(seg.get_surrogate_id(2).unwrap(), 30);
500 assert!(seg.get_surrogate_id(3).is_none());
501 }
502
503 #[test]
504 fn flat_slices() {
505 let dir = tempfile::tempdir().unwrap();
506
507 let v0 = vec![1.0f32, 2.0, 3.0];
508 let v1 = vec![4.0f32, 5.0, 6.0];
509 let sids = vec![100u64, 200];
510
511 let seg = MmapVectorSegment::create_with_surrogates(
512 dir.path(),
513 "flat.vseg",
514 3,
515 &[&v0, &v1],
516 &sids,
517 &test_memory(),
518 )
519 .unwrap();
520
521 assert_eq!(seg.all_vectors_flat(), &[1.0, 2.0, 3.0, 4.0, 5.0, 6.0]);
522 assert_eq!(seg.all_surrogate_ids(), &[100u64, 200]);
523 }
524
525 #[test]
526 fn reopen_roundtrip() {
527 let dir = tempfile::tempdir().unwrap();
528 let path = dir.path().join("reopen.vseg");
529
530 let vectors: Vec<Vec<f32>> = (0..100)
531 .map(|i| vec![i as f32, (i as f32).sin(), (i as f32).cos()])
532 .collect();
533 let refs: Vec<&[f32]> = vectors.iter().map(|v| v.as_slice()).collect();
534 let sids: Vec<u64> = (0u64..100).collect();
535
536 MmapVectorSegment::create_with_surrogates(
537 dir.path(),
538 "reopen.vseg",
539 3,
540 &refs,
541 &sids,
542 &test_memory(),
543 )
544 .unwrap();
545
546 let seg = MmapVectorSegment::open(&path, &test_memory()).unwrap();
547 assert_eq!(seg.count(), 100);
548 for (i, v) in vectors.iter().enumerate() {
549 assert_eq!(seg.get_vector(i as u32).unwrap(), v.as_slice());
550 assert_eq!(seg.get_surrogate_id(i as u32).unwrap(), i as u64);
551 }
552 }
553
554 #[test]
555 fn no_surrogates_defaults_to_zero() {
556 let dir = tempfile::tempdir().unwrap();
557
558 let v = vec![1.0f32, 2.0];
559 let seg =
560 MmapVectorSegment::create(dir.path(), "nosid.vseg", 2, &[&v], &test_memory()).unwrap();
561 assert_eq!(seg.get_surrogate_id(0).unwrap(), 0);
562 }
563
564 #[test]
565 fn prefetch_does_not_crash() {
566 let dir = tempfile::tempdir().unwrap();
567
568 let v = vec![1.0f32; 768];
569 let seg =
570 MmapVectorSegment::create(dir.path(), "prefetch.vseg", 768, &[&v], &test_memory())
571 .unwrap();
572 seg.prefetch(0);
573 seg.prefetch(999);
574 }
575
576 #[test]
577 fn empty_segment() {
578 let dir = tempfile::tempdir().unwrap();
579
580 let seg =
581 MmapVectorSegment::create(dir.path(), "empty.vseg", 3, &[], &test_memory()).unwrap();
582 assert_eq!(seg.count(), 0);
583 assert!(seg.get_vector(0).is_none());
584 assert_eq!(seg.all_vectors_flat().len(), 0);
585 assert_eq!(seg.all_surrogate_ids().len(), 0);
586 }
587
588 #[test]
589 fn footer_golden_layout() {
590 let dir = tempfile::tempdir().unwrap();
591 let path = dir.path().join("golden.vseg");
592 let v = vec![1.0f32, 2.0, 3.0];
593 write_segment(dir.path(), "golden.vseg", 3, &[&v], &[42]).unwrap();
594 let data = std::fs::read(&path).unwrap();
595
596 let footer_start = data.len() - FOOTER_SIZE;
598 let footer = &data[footer_start..];
599
600 let fv = u16::from_le_bytes([footer[0], footer[1]]);
602 assert_eq!(fv, FORMAT_VERSION);
603
604 let body = &data[..footer_start];
606 let expected_crc = crc32c::crc32c(body);
607 let stored_crc = u32::from_le_bytes([footer[34], footer[35], footer[36], footer[37]]);
608 assert_eq!(stored_crc, expected_crc);
609
610 let fs = u32::from_le_bytes([footer[38], footer[39], footer[40], footer[41]]) as usize;
612 assert_eq!(fs, FOOTER_SIZE);
613
614 assert_eq!(&footer[42..46], b"NDVS");
616 }
617
618 #[test]
619 fn trailing_magic_corruption_rejected() {
620 let dir = tempfile::tempdir().unwrap();
621 let path = dir.path().join("trailmagic.vseg");
622 let v = vec![1.0f32, 2.0, 3.0];
623 write_segment(dir.path(), "trailmagic.vseg", 3, &[&v], &[42]).unwrap();
624
625 let mut data = std::fs::read(&path).unwrap();
626 let last = data.len();
628 data[last - 4] = 0xde;
629 data[last - 3] = 0xad;
630 data[last - 2] = 0xbe;
631 data[last - 1] = 0xef;
632 std::fs::write(&path, &data).unwrap();
633
634 let result = MmapVectorSegment::open(&path, &test_memory());
635 assert!(result.is_err(), "expected trailing magic error");
636 let err = result.unwrap_err();
637 let err_string = err.to_string();
638 assert_eq!(io_err(err).kind(), std::io::ErrorKind::InvalidData);
639 assert!(
640 err_string.contains("trailing magic"),
641 "expected trailing magic message, got: {err_string}"
642 );
643 }
644
645 #[test]
646 fn footer_version_mismatch_rejected() {
647 let dir = tempfile::tempdir().unwrap();
648 let path = dir.path().join("fvmismatch.vseg");
649 let v = vec![1.0f32, 2.0, 3.0];
650 write_segment(dir.path(), "fvmismatch.vseg", 3, &[&v], &[42]).unwrap();
651
652 let mut data = std::fs::read(&path).unwrap();
653 let footer_start = data.len() - FOOTER_SIZE;
655 let fv_bytes = 99u16.to_le_bytes();
656 data[footer_start] = fv_bytes[0];
657 data[footer_start + 1] = fv_bytes[1];
658 std::fs::write(&path, &data).unwrap();
659
660 let result = MmapVectorSegment::open(&path, &test_memory());
661 assert!(result.is_err(), "expected footer version mismatch error");
662 assert_eq!(
663 io_err(result.unwrap_err()).kind(),
664 std::io::ErrorKind::InvalidData
665 );
666 }
667
668 #[test]
669 fn crc_corruption_rejected() {
670 let dir = tempfile::tempdir().unwrap();
671 let path = dir.path().join("corrupt.vseg");
672
673 let v = vec![1.0f32, 2.0, 3.0];
674 write_segment(dir.path(), "corrupt.vseg", 3, &[&v], &[42]).unwrap();
675
676 let mut data = std::fs::read(&path).unwrap();
677 data[HEADER_SIZE] ^= 0xff;
678 std::fs::write(&path, &data).unwrap();
679
680 let result = MmapVectorSegment::open(&path, &test_memory());
681 assert!(result.is_err(), "expected CRC error");
682 assert_eq!(
683 io_err(result.unwrap_err()).kind(),
684 std::io::ErrorKind::InvalidData
685 );
686 }
687
688 #[test]
689 fn bad_magic_rejected() {
690 let dir = tempfile::tempdir().unwrap();
691 let path = dir.path().join("badmagic.vseg");
692
693 let v = vec![1.0f32, 2.0];
694 write_segment(dir.path(), "badmagic.vseg", 2, &[&v], &[]).unwrap();
695
696 let mut data = std::fs::read(&path).unwrap();
697 data[0] = b'X';
698 std::fs::write(&path, &data).unwrap();
699
700 let result = MmapVectorSegment::open(&path, &test_memory());
701 assert!(result.is_err());
702 assert_eq!(
703 io_err(result.unwrap_err()).kind(),
704 std::io::ErrorKind::InvalidData
705 );
706 }
707
708 #[test]
709 fn overflow_dim_count_rejected() {
710 let dir = tempfile::tempdir().unwrap();
711 let path = dir.path().join("overflow.vseg");
712
713 let dim: u32 = 0x40000001;
714 let count: u64 = 0x40000001;
715
716 let mut buf = Vec::new();
717 buf.extend_from_slice(&MAGIC);
718 buf.extend_from_slice(&FORMAT_VERSION.to_le_bytes());
719 buf.extend_from_slice(&0u16.to_le_bytes());
720 buf.extend_from_slice(&dim.to_le_bytes());
721 buf.extend_from_slice(&count.to_le_bytes());
722 buf.push(0u8); buf.push(0u8); buf.extend_from_slice(&[0u8; 6]); std::fs::write(&path, &buf).unwrap();
726
727 let result = MmapVectorSegment::open(&path, &test_memory());
728 assert!(
729 result.is_err(),
730 "expected Err for overflow-inducing dim/count"
731 );
732 }
733
734 #[test]
735 fn zero_dim_with_nonzero_count_rejected() {
736 let dir = tempfile::tempdir().unwrap();
737 let path = dir.path().join("zerodim.vseg");
738
739 let dim: u32 = 0;
740 let count: u64 = 1000;
741
742 let mut buf = Vec::new();
743 buf.extend_from_slice(&MAGIC);
744 buf.extend_from_slice(&FORMAT_VERSION.to_le_bytes());
745 buf.extend_from_slice(&0u16.to_le_bytes());
746 buf.extend_from_slice(&dim.to_le_bytes());
747 buf.extend_from_slice(&count.to_le_bytes());
748 buf.push(0u8);
749 buf.push(0u8);
750 buf.extend_from_slice(&[0u8; 6]);
751 buf.extend_from_slice(&[0u8; 64]);
752 std::fs::write(&path, &buf).unwrap();
753
754 let result = MmapVectorSegment::open(&path, &test_memory());
755 assert!(result.is_err(), "expected Err for dim=0 with nonzero count");
756 }
757}