1use std::collections::HashSet;
32use std::fs::{self, File, OpenOptions};
33use std::io::{Read, Seek, SeekFrom, Write};
34use std::path::{Path, PathBuf};
35use std::sync::{Arc, Mutex};
36
37use anyhow::{bail, Context, Result};
38use blake2::{Blake2b512, Digest};
39use dashmap::DashMap;
40
41const DEFAULT_MAX_SEGMENT_BYTES: u64 = 256 * 1024 * 1024;
43
44const IDX_MAGIC: &[u8; 4] = b"NIX1";
46const IDX_ENTRY_BYTES: usize = 32 + 8 + 4;
48const IDX_HEADER_BYTES: usize = 4 + 8;
50const IDX_CHECKSUM_BYTES: usize = 32;
51
52#[derive(Clone, Copy, Debug)]
54struct SegmentLocation {
55 segment_id: u32,
56 offset: u64,
58 len: u32,
59}
60
61#[derive(Clone, Copy, Debug, Default)]
63pub struct CompactStats {
64 pub live_objects: usize,
66 pub dropped_objects: usize,
68 pub bytes_reclaimed: u64,
70 pub segments_after: usize,
72}
73
74fn blake2b_raw(data: &[u8]) -> [u8; 32] {
76 let mut h = Blake2b512::new();
77 h.update(data);
78 let out = h.finalize();
79 let mut a = [0u8; 32];
80 a.copy_from_slice(&out[..32]);
81 a
82}
83
84fn blake2b(data: &[u8]) -> String {
87 hex::encode(blake2b_raw(data))
88}
89
90#[cfg(unix)]
93fn read_at(f: &File, buf: &mut [u8], offset: u64) -> std::io::Result<()> {
94 use std::os::unix::fs::FileExt;
95 f.read_exact_at(buf, offset)
96}
97
98#[cfg(windows)]
105fn read_at(f: &File, buf: &mut [u8], offset: u64) -> std::io::Result<()> {
106 use std::os::windows::fs::FileExt;
107 let mut done = 0usize;
108 while done < buf.len() {
109 let n = f.seek_read(&mut buf[done..], offset + done as u64)?;
110 if n == 0 {
111 return Err(std::io::Error::new(
112 std::io::ErrorKind::UnexpectedEof,
113 "eof mid-record in segment read",
114 ));
115 }
116 done += n;
117 }
118 Ok(())
119}
120
121struct Active {
123 id: u32,
124 file: File,
125 offset: u64,
127}
128
129pub struct SegmentStore {
131 dir: PathBuf,
132 index: DashMap<String, SegmentLocation>,
133 active: Mutex<Active>,
134 max_segment_bytes: u64,
135 fast_fsync: bool,
139 read_handles: DashMap<u32, Arc<File>>,
150}
151
152fn durable_sync(file: &File, fast: bool) -> std::io::Result<()> {
167 #[cfg(target_os = "macos")]
168 if fast {
169 use std::os::unix::io::AsRawFd;
170 let rc = unsafe { libc::fsync(file.as_raw_fd()) };
172 return if rc == 0 { Ok(()) } else { Err(std::io::Error::last_os_error()) };
173 }
174 #[cfg(not(target_os = "macos"))]
175 let _ = fast;
176 file.sync_all()
177}
178
179impl SegmentStore {
180 fn seg_path(dir: &Path, id: u32) -> PathBuf {
181 dir.join(format!("seg-{:06}.dat", id))
182 }
183 fn idx_path(dir: &Path, id: u32) -> PathBuf {
184 dir.join(format!("seg-{:06}.idx", id))
185 }
186
187 pub fn open(objects_root: &Path) -> Result<Self> {
189 Self::open_with_max(objects_root, DEFAULT_MAX_SEGMENT_BYTES)
190 }
191
192 pub fn open_with_max(objects_root: &Path, max_segment_bytes: u64) -> Result<Self> {
194 let dir = objects_root.join("segments");
195 fs::create_dir_all(&dir).context("create objects/segments dir")?;
196
197 let mut ids: Vec<u32> = Vec::new();
199 for entry in fs::read_dir(&dir).context("read segments dir")? {
200 let entry = entry?;
201 let name = entry.file_name().to_string_lossy().to_string();
202 if let Some(rest) = name.strip_prefix("seg-") {
203 if let Some(num) = rest.strip_suffix(".dat") {
204 if let Ok(id) = num.parse::<u32>() {
205 ids.push(id);
206 }
207 }
208 }
209 }
210 ids.sort_unstable();
211
212 let index: DashMap<String, SegmentLocation> = DashMap::new();
213 let mut active_id: u32 = 0;
214 let mut active_end: u64 = 0;
215
216 for (pos, &id) in ids.iter().enumerate() {
217 let is_last = pos + 1 == ids.len();
218 if is_last {
219 let (valid_end, entries) = Self::scan_segment(&dir, id)?;
222 for (h, o, l) in entries {
223 index.insert(h, SegmentLocation { segment_id: id, offset: o, len: l });
224 }
225 let path = Self::seg_path(&dir, id);
226 let file_len = fs::metadata(&path)?.len();
227 if valid_end < file_len {
228 let f = OpenOptions::new().write(true).open(&path)?;
229 f.set_len(valid_end)?;
230 }
231 active_id = id;
232 active_end = valid_end;
233 } else {
234 match Self::load_idx(&dir, id) {
237 Ok(Some(entries)) => {
238 for (h, o, l) in entries {
239 index.insert(h, SegmentLocation { segment_id: id, offset: o, len: l });
240 }
241 }
242 _ => {
243 let (_ve, entries) = Self::scan_segment(&dir, id)?;
244 for (h, o, l) in &entries {
245 index.insert(h.clone(), SegmentLocation { segment_id: id, offset: *o, len: *l });
246 }
247 let _ = Self::write_idx(&dir, id, &entries); }
249 }
250 }
251 }
252
253 let active_path = Self::seg_path(&dir, active_id);
255 let mut file = OpenOptions::new()
256 .create(true)
257 .read(true)
258 .write(true)
259 .open(&active_path)
260 .with_context(|| format!("open active segment {:?}", active_path))?;
261 file.seek(SeekFrom::Start(active_end))?;
262
263 let fast_fsync = std::env::var("NEDB_FAST_FSYNC")
264 .map(|v| {
265 let v = v.trim();
266 v == "1" || v.eq_ignore_ascii_case("true")
267 || v.eq_ignore_ascii_case("on")
268 || v.eq_ignore_ascii_case("yes")
269 })
270 .unwrap_or(false);
271
272 Ok(Self {
273 dir,
274 index,
275 active: Mutex::new(Active { id: active_id, file, offset: active_end }),
276 max_segment_bytes,
277 fast_fsync,
278 read_handles: DashMap::new(),
279 })
280 }
281
282 fn scan_segment(dir: &Path, id: u32) -> Result<(u64, Vec<(String, u64, u32)>)> {
285 let path = Self::seg_path(dir, id);
286 let mut f = match File::open(&path) {
287 Ok(f) => f,
288 Err(_) => return Ok((0, Vec::new())),
289 };
290 let file_len = f.metadata()?.len();
291 let mut pos: u64 = 0;
292 let mut entries: Vec<(String, u64, u32)> = Vec::new();
293 loop {
294 if pos + 4 > file_len {
295 break; }
297 f.seek(SeekFrom::Start(pos))?;
298 let mut len_buf = [0u8; 4];
299 if f.read_exact(&mut len_buf).is_err() {
300 break;
301 }
302 let len = u32::from_le_bytes(len_buf);
303 let content_off = pos + 4;
304 if content_off + (len as u64) > file_len {
305 break; }
307 let mut content = vec![0u8; len as usize];
308 if f.read_exact(&mut content).is_err() {
309 break;
310 }
311 entries.push((blake2b(&content), content_off, len));
312 pos = content_off + len as u64;
313 }
314 Ok((pos, entries))
315 }
316
317 fn read_handle(&self, id: u32) -> Result<Arc<File>> {
321 if let Some(h) = self.read_handles.get(&id) {
322 return Ok(Arc::clone(h.value()));
323 }
324 let path = Self::seg_path(&self.dir, id);
325 let f = Arc::new(File::open(&path).with_context(|| format!("open segment {:?}", path))?);
326 Ok(Arc::clone(self.read_handles.entry(id).or_insert(f).value()))
327 }
328
329 fn read_content(&self, loc: &SegmentLocation, expect_hash: &str) -> Result<Vec<u8>> {
336 let f = self.read_handle(loc.segment_id)?;
337 let mut content = vec![0u8; loc.len as usize];
338 read_at(&f, &mut content, loc.offset)
339 .with_context(|| format!("read record from segment {}", loc.segment_id))?;
340 let actual = blake2b(&content);
341 if actual != expect_hash {
342 bail!("segment object {} tampered: recomputed {}", expect_hash, actual);
343 }
344 Ok(content)
345 }
346
347 fn write_idx(dir: &Path, id: u32, entries: &[(String, u64, u32)]) -> Result<()> {
352 let mut body: Vec<u8> = Vec::with_capacity(IDX_HEADER_BYTES + entries.len() * IDX_ENTRY_BYTES);
353 body.extend_from_slice(IDX_MAGIC);
354 body.extend_from_slice(&(entries.len() as u64).to_le_bytes());
355 for (hash, off, len) in entries {
356 let raw = hex::decode(hash).map_err(|_| anyhow::anyhow!("bad hash hex in idx write"))?;
357 if raw.len() != 32 {
358 bail!("idx write: hash not 32 bytes");
359 }
360 body.extend_from_slice(&raw);
361 body.extend_from_slice(&off.to_le_bytes());
362 body.extend_from_slice(&len.to_le_bytes());
363 }
364 let checksum = blake2b_raw(&body);
365 body.extend_from_slice(&checksum);
366
367 let path = Self::idx_path(dir, id);
368 let tmp = path.with_extension("idx.tmp");
369 fs::write(&tmp, &body)?;
370 fs::rename(&tmp, &path)?;
371 Ok(())
372 }
373
374 fn load_idx(dir: &Path, id: u32) -> Result<Option<Vec<(String, u64, u32)>>> {
377 let path = Self::idx_path(dir, id);
378 let data = match fs::read(&path) {
379 Ok(d) => d,
380 Err(_) => return Ok(None),
381 };
382 if data.len() < IDX_HEADER_BYTES + IDX_CHECKSUM_BYTES {
383 return Ok(None);
384 }
385 if &data[0..4] != IDX_MAGIC {
386 return Ok(None);
387 }
388 let count = u64::from_le_bytes(data[4..12].try_into().unwrap()) as usize;
389 let expected = IDX_HEADER_BYTES + count * IDX_ENTRY_BYTES + IDX_CHECKSUM_BYTES;
390 if data.len() != expected {
391 return Ok(None);
392 }
393 let body = &data[..data.len() - IDX_CHECKSUM_BYTES];
394 let stored: [u8; 32] = match data[data.len() - IDX_CHECKSUM_BYTES..].try_into() {
395 Ok(a) => a,
396 Err(_) => return Ok(None),
397 };
398 if blake2b_raw(body) != stored {
399 return Ok(None); }
401 let mut entries = Vec::with_capacity(count);
402 let mut p = IDX_HEADER_BYTES;
403 for _ in 0..count {
404 let hash = hex::encode(&data[p..p + 32]);
405 let off = u64::from_le_bytes(data[p + 32..p + 40].try_into().unwrap());
406 let len = u32::from_le_bytes(data[p + 40..p + 44].try_into().unwrap());
407 entries.push((hash, off, len));
408 p += IDX_ENTRY_BYTES;
409 }
410 Ok(Some(entries))
411 }
412
413 fn entries_for_segment(&self, id: u32) -> Vec<(String, u64, u32)> {
415 self.index
416 .iter()
417 .filter(|e| e.value().segment_id == id)
418 .map(|e| (e.key().clone(), e.value().offset, e.value().len))
419 .collect()
420 }
421
422 pub fn contains(&self, hash: &str) -> bool {
426 self.index.contains_key(hash)
427 }
428
429 pub fn put(&self, hash: &str, content: &[u8]) -> Result<()> {
432 if self.index.contains_key(hash) {
433 return Ok(());
434 }
435 let len = content.len() as u32;
436 let record_size = 4u64 + content.len() as u64;
437
438 let mut active = self.active.lock().unwrap();
439 if self.index.contains_key(hash) {
440 return Ok(());
441 }
442
443 if active.offset > 0 && active.offset + record_size > self.max_segment_bytes {
445 let _ = active.file.flush();
446 let _ = durable_sync(&active.file, self.fast_fsync);
447 let sealed_id = active.id;
449 let entries = self.entries_for_segment(sealed_id);
450 let _ = Self::write_idx(&self.dir, sealed_id, &entries);
451 let next_id = sealed_id + 1;
452 let path = Self::seg_path(&self.dir, next_id);
453 let file = OpenOptions::new()
454 .create(true)
455 .read(true)
456 .write(true)
457 .open(&path)
458 .with_context(|| format!("open new segment {:?}", path))?;
459 *active = Active { id: next_id, file, offset: 0 };
460 }
461
462 let content_off = active.offset + 4;
463 let mut rec = Vec::with_capacity(4 + content.len());
464 rec.extend_from_slice(&len.to_le_bytes());
465 rec.extend_from_slice(content);
466 active.file.write_all(&rec)?;
467
468 let seg_id = active.id;
469 active.offset += record_size;
470 self.index.insert(
471 hash.to_string(),
472 SegmentLocation { segment_id: seg_id, offset: content_off, len },
473 );
474 Ok(())
475 }
476
477 pub fn get(&self, hash: &str) -> Result<Option<Vec<u8>>> {
480 let loc = match self.index.get(hash) {
481 Some(entry) => *entry.value(),
482 None => return Ok(None),
483 };
484 Ok(Some(self.read_content(&loc, hash)?))
485 }
486
487 pub fn all_hashes(&self) -> Vec<String> {
489 self.index.iter().map(|e| e.key().clone()).collect()
490 }
491
492 pub fn sync(&self) -> Result<()> {
494 let mut active = self.active.lock().unwrap();
495 let _ = active.file.flush();
496 durable_sync(&active.file, self.fast_fsync).context("fsync active segment")?;
497 Ok(())
498 }
499
500 pub fn compact(&self, live: &HashSet<String>) -> Result<CompactStats> {
519 let mut active = self.active.lock().unwrap();
520
521 let total_before = self.index.len();
522 let old_max = active.id;
523 let new_base = old_max + 1;
524
525 let to_copy: Vec<(String, SegmentLocation)> = self
527 .index
528 .iter()
529 .filter(|e| live.contains(e.key()))
530 .map(|e| (e.key().clone(), *e.value()))
531 .collect();
532
533 let new_index: DashMap<String, SegmentLocation> = DashMap::new();
535 let mut cur_id = new_base;
536 let mut cur_path = Self::seg_path(&self.dir, cur_id);
537 let mut cur_file = OpenOptions::new()
538 .create(true)
539 .truncate(true)
540 .read(true)
541 .write(true)
542 .open(&cur_path)
543 .with_context(|| format!("open compaction segment {:?}", cur_path))?;
544 let mut cur_off: u64 = 0;
545
546 for (hash, loc) in &to_copy {
547 let content = self.read_content(loc, hash)?;
548 let len = content.len() as u32;
549 let record_size = 4u64 + content.len() as u64;
550
551 if cur_off > 0 && cur_off + record_size > self.max_segment_bytes {
552 let _ = cur_file.flush();
553 durable_sync(&cur_file, self.fast_fsync).context("fsync sealed compaction segment")?;
554 let entries: Vec<(String, u64, u32)> = new_index
555 .iter()
556 .filter(|e| e.value().segment_id == cur_id)
557 .map(|e| (e.key().clone(), e.value().offset, e.value().len))
558 .collect();
559 let _ = Self::write_idx(&self.dir, cur_id, &entries);
560 cur_id += 1;
561 cur_path = Self::seg_path(&self.dir, cur_id);
562 cur_file = OpenOptions::new()
563 .create(true)
564 .truncate(true)
565 .read(true)
566 .write(true)
567 .open(&cur_path)
568 .with_context(|| format!("open compaction segment {:?}", cur_path))?;
569 cur_off = 0;
570 }
571
572 let content_off = cur_off + 4;
573 let mut rec = Vec::with_capacity(4 + content.len());
574 rec.extend_from_slice(&len.to_le_bytes());
575 rec.extend_from_slice(&content);
576 cur_file.write_all(&rec)?;
577 new_index.insert(hash.clone(), SegmentLocation { segment_id: cur_id, offset: content_off, len });
578 cur_off += record_size;
579 }
580 let _ = cur_file.flush();
581 durable_sync(&cur_file, self.fast_fsync).context("fsync active compaction segment")?;
582
583 let live_objects = to_copy.len();
585
586 self.index.clear();
588 for e in new_index.iter() {
589 self.index.insert(e.key().clone(), *e.value());
590 }
591 *active = Active { id: cur_id, file: cur_file, offset: cur_off };
592
593 self.read_handles.clear();
601
602 let mut bytes_reclaimed: u64 = 0;
605 if let Ok(rd) = fs::read_dir(&self.dir) {
606 for entry in rd.flatten() {
607 let name = entry.file_name().to_string_lossy().to_string();
608 let id_of = name
609 .strip_prefix("seg-")
610 .and_then(|r| r.strip_suffix(".dat").or_else(|| r.strip_suffix(".idx")))
611 .and_then(|n| n.parse::<u32>().ok());
612 if let Some(id) = id_of {
613 if id < new_base {
614 if name.ends_with(".dat") {
615 if let Ok(m) = entry.metadata() {
616 bytes_reclaimed += m.len();
617 }
618 }
619 let _ = fs::remove_file(entry.path());
620 }
621 }
622 }
623 }
624
625 let segments_after = (cur_id - new_base + 1) as usize;
626 Ok(CompactStats {
627 live_objects,
628 dropped_objects: total_before.saturating_sub(live_objects),
629 bytes_reclaimed,
630 segments_after,
631 })
632 }
633}
634
635#[cfg(test)]
636mod tests {
637 use super::*;
638 use tempfile::tempdir;
639
640 fn put_get_hash(s: &SegmentStore, content: &[u8]) -> String {
641 let h = blake2b(content);
642 s.put(&h, content).unwrap();
643 h
644 }
645
646 #[test]
647 fn put_get_roundtrip() {
648 let dir = tempdir().unwrap();
649 let s = SegmentStore::open(dir.path()).unwrap();
650 let h = put_get_hash(&s, b"hello nedb v3");
651 assert_eq!(s.get(&h).unwrap().unwrap(), b"hello nedb v3");
652 assert!(s.contains(&h));
653 assert!(s.get(&"0".repeat(64)).unwrap().is_none());
654 }
655
656 #[test]
657 fn idempotent_put() {
658 let dir = tempdir().unwrap();
659 let s = SegmentStore::open(dir.path()).unwrap();
660 let h1 = put_get_hash(&s, b"dup");
661 let h2 = put_get_hash(&s, b"dup");
662 assert_eq!(h1, h2);
663 assert_eq!(s.all_hashes().len(), 1);
664 }
665
666 #[test]
667 fn index_rebuilt_on_reopen() {
668 let dir = tempdir().unwrap();
669 let h = {
670 let s = SegmentStore::open(dir.path()).unwrap();
671 let h = put_get_hash(&s, b"persisted");
672 s.sync().unwrap();
673 h
674 };
675 let s2 = SegmentStore::open(dir.path()).unwrap();
676 assert_eq!(s2.get(&h).unwrap().unwrap(), b"persisted");
677 }
678
679 #[test]
680 fn rollover_writes_idx_and_reopen_uses_it() {
681 let dir = tempdir().unwrap();
682 let s = SegmentStore::open_with_max(dir.path(), 32).unwrap();
683 let mut hashes = Vec::new();
684 for i in 0..8u32 {
685 hashes.push(put_get_hash(&s, format!("record-{}", i).as_bytes()));
686 }
687 s.sync().unwrap();
688 let idx_files = fs::read_dir(dir.path().join("segments"))
690 .unwrap()
691 .flatten()
692 .filter(|e| e.file_name().to_string_lossy().ends_with(".idx"))
693 .count();
694 assert!(idx_files >= 1, "expected at least one sealed .idx");
695 let s2 = SegmentStore::open(dir.path()).unwrap();
697 for h in &hashes {
698 assert!(s2.get(h).unwrap().is_some());
699 }
700 }
701
702 #[test]
703 fn corrupt_idx_falls_back_to_scan() {
704 let dir = tempdir().unwrap();
705 let mut hashes = Vec::new();
706 {
707 let s = SegmentStore::open_with_max(dir.path(), 32).unwrap();
708 for i in 0..6u32 {
709 hashes.push(put_get_hash(&s, format!("rec-{}", i).as_bytes()));
710 }
711 s.sync().unwrap();
712 }
713 for e in fs::read_dir(dir.path().join("segments")).unwrap().flatten() {
715 if e.file_name().to_string_lossy().ends_with(".idx") {
716 fs::write(e.path(), b"garbage").unwrap();
717 }
718 }
719 let s2 = SegmentStore::open(dir.path()).unwrap();
720 for h in &hashes {
721 assert!(s2.get(h).unwrap().is_some(), "scan fallback must recover the object");
722 }
723 }
724
725 #[test]
726 fn torn_tail_is_truncated_on_open() {
727 let dir = tempdir().unwrap();
728 let good = {
729 let s = SegmentStore::open(dir.path()).unwrap();
730 let h = put_get_hash(&s, b"good record");
731 s.sync().unwrap();
732 h
733 };
734 let seg = dir.path().join("segments").join("seg-000000.dat");
735 {
736 let mut f = OpenOptions::new().append(true).open(&seg).unwrap();
737 f.write_all(&9999u32.to_le_bytes()).unwrap();
738 f.write_all(b"short").unwrap();
739 }
740 let s2 = SegmentStore::open(dir.path()).unwrap();
741 assert_eq!(s2.get(&good).unwrap().unwrap(), b"good record");
742 let h2 = put_get_hash(&s2, b"after recovery");
743 assert!(s2.get(&h2).unwrap().is_some());
744 }
745
746 #[test]
747 fn tamper_detected_on_read() {
748 let dir = tempdir().unwrap();
749 let h = {
750 let s = SegmentStore::open(dir.path()).unwrap();
751 let h = put_get_hash(&s, b"authentic");
752 s.sync().unwrap();
753 h
754 };
755 let seg = dir.path().join("segments").join("seg-000000.dat");
756 let mut bytes = fs::read(&seg).unwrap();
757 let n = bytes.len();
758 bytes[n - 1] ^= 0xff;
759 fs::write(&seg, bytes).unwrap();
760 let s2 = SegmentStore::open(dir.path()).unwrap();
761 match s2.get(&h) {
762 Ok(None) => {}
763 Err(_) => {}
764 Ok(Some(_)) => panic!("tampered content must not verify under original hash"),
765 }
766 }
767
768 #[test]
769 fn compaction_keeps_live_drops_dead() {
770 let dir = tempdir().unwrap();
771 let s = SegmentStore::open(dir.path()).unwrap();
772 let keep = put_get_hash(&s, b"keep me");
773 let _drop1 = put_get_hash(&s, b"drop me 1");
774 let _drop2 = put_get_hash(&s, b"drop me 2");
775 s.sync().unwrap();
776 assert_eq!(s.all_hashes().len(), 3);
777
778 let mut live = HashSet::new();
779 live.insert(keep.clone());
780 let stats = s.compact(&live).unwrap();
781 assert_eq!(stats.live_objects, 1);
782 assert_eq!(stats.dropped_objects, 2);
783
784 assert_eq!(s.get(&keep).unwrap().unwrap(), b"keep me");
786 assert_eq!(s.all_hashes().len(), 1);
787
788 let s2 = SegmentStore::open(dir.path()).unwrap();
790 assert_eq!(s2.get(&keep).unwrap().unwrap(), b"keep me");
791 assert!(s2.get(&_drop1).unwrap().is_none());
792
793 let after = put_get_hash(&s, b"post-compaction");
795 assert!(s.get(&after).unwrap().is_some());
796 }
797
798 #[test]
799 fn compaction_reclaims_and_writes_still_read() {
800 let dir = tempdir().unwrap();
801 let s = SegmentStore::open_with_max(dir.path(), 64).unwrap();
802 let mut all = Vec::new();
803 for i in 0..20u32 {
804 all.push(put_get_hash(&s, format!("obj-{:03}", i).as_bytes()));
805 }
806 s.sync().unwrap();
807 let mut live = HashSet::new();
809 for (i, h) in all.iter().enumerate() {
810 if i % 2 == 0 {
811 live.insert(h.clone());
812 }
813 }
814 let stats = s.compact(&live).unwrap();
815 assert_eq!(stats.live_objects, 10);
816 assert_eq!(stats.dropped_objects, 10);
817 for (i, h) in all.iter().enumerate() {
818 let got = s.get(h).unwrap();
819 if i % 2 == 0 {
820 assert!(got.is_some(), "live object {} must survive", i);
821 } else {
822 assert!(got.is_none(), "dead object {} must be pruned", i);
823 }
824 }
825 }
826
827 #[test]
833 fn concurrent_reads_share_cached_handles() {
834 let dir = tempdir().unwrap();
835 let s = Arc::new(SegmentStore::open_with_max(dir.path(), 256).unwrap());
836 let mut hashes = Vec::new();
837 for i in 0..64u32 {
838 hashes.push(put_get_hash(&s, format!("concurrent-record-{:04}", i).as_bytes()));
839 }
840 s.sync().unwrap();
841
842 let hashes = Arc::new(hashes);
843 let mut joins = vec![];
844 for t in 0..4 {
845 let s2 = Arc::clone(&s);
846 let hs = Arc::clone(&hashes);
847 joins.push(std::thread::spawn(move || {
848 for pass in 0..2 {
851 for (i, h) in hs.iter().enumerate() {
852 let got = s2.get(h).unwrap()
853 .unwrap_or_else(|| panic!("thread {} pass {} record {}: missing", t, pass, i));
854 assert_eq!(got, format!("concurrent-record-{:04}", i).as_bytes(),
855 "thread {} pass {} record {}: wrong bytes", t, pass, i);
856 }
857 }
858 }));
859 }
860 for j in joins { j.join().unwrap(); }
861
862 let live: HashSet<String> = hashes.iter().cloned().collect();
864 let stats = s.compact(&live).unwrap();
865 assert_eq!(stats.live_objects, 64);
866 for (i, h) in hashes.iter().enumerate() {
867 assert_eq!(s.get(h).unwrap().unwrap(),
868 format!("concurrent-record-{:04}", i).as_bytes(),
869 "record {} must read correctly through fresh post-compact handles", i);
870 }
871 }
872}