1use std::fs::File;
4use std::path::{Path, PathBuf};
5
6use mentedb_core::MemoryNode;
7use mentedb_core::error::{MenteError, MenteResult};
8
9use parking_lot::Mutex;
10use tracing::info;
11
12use crate::buffer::BufferPool;
13use crate::memory_map::{self, MapAnchor, MappedMemory, MemoryTable};
14use crate::page::{PAGE_DATA_SIZE, Page, PageId, PageManager, PageType};
15use crate::wal::{Wal, WalEntryType};
16const DEFAULT_BUFFER_POOL_SIZE: usize = 1024;
18
19const WAL_AUTO_CHECKPOINT_BYTES: u64 = 8 * 1024 * 1024;
21
22pub struct StorageEngine {
46 page_manager: Mutex<PageManager>,
47 buffer_pool: BufferPool,
48 wal: Mutex<Wal>,
49 process_lock: Mutex<Option<File>>,
52 dir: PathBuf,
54 map_anchor: Mutex<Option<MapAnchor>>,
59 memory_loads: std::sync::atomic::AtomicU64,
61}
62
63impl StorageEngine {
64 pub fn open(path: &Path) -> MenteResult<Self> {
79 std::fs::create_dir_all(path)?;
80
81 let lock_path = path.join("LOCK");
85 let lock_file = std::fs::OpenOptions::new()
86 .create(true)
87 .truncate(false)
88 .write(true)
89 .open(&lock_path)?;
90 match crate::lock::try_lock_exclusive(&lock_file) {
95 Ok(true) => {}
96 Ok(false) => {
97 return Err(MenteError::Storage(format!(
98 "database directory {} is locked by another process",
99 path.display()
100 )));
101 }
102 Err(e) => {
103 return Err(MenteError::Storage(format!(
104 "failed to lock database directory {}: {e}",
105 path.display()
106 )));
107 }
108 }
109
110 let page_manager = PageManager::open(path)?;
111 let buffer_pool = BufferPool::new(DEFAULT_BUFFER_POOL_SIZE);
112 let wal = Wal::open(path)?;
113
114 let engine = Self {
115 page_manager: Mutex::new(page_manager),
116 buffer_pool,
117 wal: Mutex::new(wal),
118 process_lock: Mutex::new(Some(lock_file)),
119 dir: path.to_path_buf(),
120 map_anchor: Mutex::new(None),
121 memory_loads: std::sync::atomic::AtomicU64::new(0),
122 };
123
124 let recovered = engine.recover()?;
125 if recovered > 0 {
126 info!(recovered, ?path, "storage engine opened with WAL recovery");
127 } else {
128 info!(?path, "storage engine opened");
129 }
130
131 Ok(engine)
132 }
133
134 pub fn recover(&self) -> MenteResult<usize> {
139 let mut wal = self.wal.lock();
140 wal.lock_exclusive()?;
141 let result = self.recover_locked(&mut wal);
142 let unlocked = wal.unlock();
143 let count = result?;
144 unlocked?;
145 Ok(count)
146 }
147
148 fn load_anchor(&self, entries: &[crate::wal::WalEntry], page_count: u64) -> Option<MapAnchor> {
152 let mut anchor = match memory_map::read_snapshot(&self.dir) {
153 Ok(Some(a)) => a,
154 Ok(None) => return None,
155 Err(e) => {
156 tracing::warn!(error = %e, "memory map snapshot unreadable, open will scan pages");
157 return None;
158 }
159 };
160 if !anchor.fits(page_count) {
161 tracing::warn!(
162 snapshot_pages = anchor.page_count,
163 page_count,
164 "memory map snapshot does not fit the page file, open will scan pages"
165 );
166 return None;
167 }
168 if anchor.advance(entries).is_none() {
169 info!("memory map snapshot predates the WAL, open will scan pages");
170 return None;
171 }
172 Some(anchor)
173 }
174
175 fn recover_locked(&self, wal: &mut Wal) -> MenteResult<usize> {
176 let entries = wal.iterate()?;
177 let mut count = 0usize;
178 let mut pm = self.page_manager.lock();
179
180 pm.reload_header()?;
182
183 let anchor = self.load_anchor(&entries, pm.page_count());
186
187 let mut last_op: std::collections::HashMap<u64, &crate::wal::WalEntry> = Default::default();
193 let mut order: Vec<u64> = Vec::new();
194 for entry in &entries {
195 match entry.entry_type {
196 WalEntryType::PageWrite | WalEntryType::PageFree => {
197 if !last_op.contains_key(&entry.page_id) {
198 order.push(entry.page_id);
199 }
200 last_op.insert(entry.page_id, entry);
201 }
202 WalEntryType::Checkpoint | WalEntryType::Commit => {}
203 }
204 }
205
206 for page_id_raw in order {
207 let entry = last_op[&page_id_raw];
208 let page_id = PageId(entry.page_id);
209 match entry.entry_type {
210 WalEntryType::PageWrite => {
211 while pm.page_count() <= entry.page_id {
212 pm.allocate_page()?;
213 }
214
215 let mut page = pm.read_page(page_id)?;
216 let copy_len = entry.data.len().min(PAGE_DATA_SIZE);
217 page.data[..copy_len].copy_from_slice(&entry.data[..copy_len]);
218 if copy_len < PAGE_DATA_SIZE {
219 page.data[copy_len..].fill(0);
220 }
221 page.header.page_id = entry.page_id;
222 page.header.lsn = entry.lsn;
223 page.header.page_type = PageType::Data as u8;
224 page.header.free_space = (PAGE_DATA_SIZE - copy_len) as u16;
225 page.header.checksum = page.compute_checksum();
226
227 pm.write_page(page_id, &page)?;
228 count += 1;
229 }
230 WalEntryType::PageFree => {
231 if entry.page_id < pm.page_count() {
235 let mut page = Page::zeroed();
236 page.header.page_id = entry.page_id;
237 page.header.page_type = PageType::Free as u8;
238 pm.write_page(page_id, &page)?;
239 self.buffer_pool.invalidate(page_id);
240 count += 1;
241 }
242 }
243 WalEntryType::Checkpoint | WalEntryType::Commit => {}
244 }
245 }
246
247 if count > 0 {
248 let rebuilt = match &anchor {
255 Some(a) => pm.rebuild_free_list_from(a.table.free_pages())?,
256 None => false,
257 };
258 if !rebuilt {
259 pm.rebuild_free_list()?;
260 }
261 pm.sync()?;
262 match anchor {
263 Some(anchor) => {
267 self.anchor_checkpoint(wal, &pm, anchor.table)?;
268 }
269 None => {
270 let next_lsn = wal.next_lsn();
271 wal.truncate(next_lsn)?;
272 }
273 }
274 info!(count, "WAL recovery replayed entries");
275 } else if let Some(anchor) = anchor {
276 *self.map_anchor.lock() = Some(anchor);
278 }
279
280 Ok(count)
281 }
282
283 fn anchor_checkpoint(
288 &self,
289 wal: &mut Wal,
290 pm: &PageManager,
291 table: MemoryTable,
292 ) -> MenteResult<u64> {
293 let token = memory_map::new_token();
294 let lsn = wal.append(WalEntryType::Checkpoint, 0, &token)?;
295 wal.sync()?;
296 let anchor = MapAnchor {
297 token,
298 page_count: pm.page_count(),
299 table,
300 pending: Default::default(),
301 };
302 let written = memory_map::write_snapshot(&self.dir, &anchor);
305 match written {
306 Ok(()) => *self.map_anchor.lock() = Some(anchor),
307 Err(e) => {
308 tracing::warn!(error = %e, "memory map snapshot write failed, next open will scan pages");
309 memory_map::remove_snapshot(&self.dir);
310 *self.map_anchor.lock() = None;
311 }
312 }
313 wal.truncate(lsn)?;
314 Ok(lsn)
315 }
316
317 pub fn close(&self) -> MenteResult<()> {
328 let mut pm = self.page_manager.lock();
329 self.buffer_pool.flush_all(&mut pm)?;
330 pm.sync()?;
331 self.wal.lock().sync()?;
332 if let Some(lock_file) = self.process_lock.lock().take() {
335 let _ = crate::lock::unlock(&lock_file);
336 }
337 info!("storage engine closed");
338 Ok(())
339 }
340
341 #[doc(hidden)]
346 pub fn release_process_lock(&self) {
347 if let Some(lock_file) = self.process_lock.lock().take() {
348 let _ = crate::lock::unlock(&lock_file);
349 }
350 }
351
352 pub fn allocate_page(&self) -> MenteResult<PageId> {
359 self.page_manager.lock().allocate_page()
360 }
361
362 pub fn read_page(&self, page_id: PageId) -> MenteResult<Box<Page>> {
364 self.buffer_pool
365 .fetch_page(page_id, &mut self.page_manager.lock())
366 }
367
368 pub fn buffer_stats(&self) -> crate::buffer::BufferStats {
370 self.buffer_pool.stats()
371 }
372
373 pub fn page_count(&self) -> u64 {
375 self.page_manager.lock().page_count()
376 }
377
378 pub fn write_page(&self, page_id: PageId, data: &[u8]) -> MenteResult<()> {
383 let id = memory_map::decode_memory(data).ok().map(|n| n.id);
384 self.write_page_as(page_id, data, id)
385 }
386
387 fn write_page_as(
389 &self,
390 page_id: PageId,
391 data: &[u8],
392 id: Option<mentedb_core::types::MemoryId>,
393 ) -> MenteResult<()> {
394 let lsn = {
395 let mut wal = self.wal.lock();
396 wal.lock_exclusive()?;
397 wal.reload_lsn()?;
398 let lsn = wal.append(WalEntryType::PageWrite, page_id.0, data)?;
399 wal.sync()?;
400 self.note_map_write(page_id, id, lsn);
401 wal.unlock()?;
402 lsn
403 };
404
405 self.apply_page_write(page_id, data, lsn)
406 }
407
408 fn apply_page_write(&self, page_id: PageId, data: &[u8], lsn: u64) -> MenteResult<()> {
410 let mut pm = self.page_manager.lock();
411 let mut page = self.buffer_pool.fetch_page(page_id, &mut pm)?;
412 drop(pm);
413
414 let copy_len = data.len().min(PAGE_DATA_SIZE);
415 page.data[..copy_len].copy_from_slice(&data[..copy_len]);
416 if copy_len < PAGE_DATA_SIZE {
417 page.data[copy_len..].fill(0);
418 }
419 page.header.lsn = lsn;
420 page.header.page_type = PageType::Data as u8;
421 page.header.free_space = (PAGE_DATA_SIZE - copy_len) as u16;
422 page.header.checksum = page.compute_checksum();
423
424 if self.buffer_pool.update_page(page_id, &page).is_err() {
425 self.page_manager.lock().write_page(page_id, &page)?;
426 }
427 self.buffer_pool.unpin_page(page_id, true).ok();
428
429 Ok(())
430 }
431
432 pub fn store_memory(&self, node: &MemoryNode) -> MenteResult<PageId> {
456 let serialized =
457 serde_json::to_vec(node).map_err(|e| MenteError::Serialization(e.to_string()))?;
458
459 if serialized.len() + 4 > PAGE_DATA_SIZE {
460 return Err(MenteError::CapacityExceeded(format!(
461 "memory node serialized to {} bytes (max {})",
462 serialized.len(),
463 PAGE_DATA_SIZE - 4,
464 )));
465 }
466
467 let mut buf = Vec::with_capacity(4 + serialized.len());
468 buf.extend_from_slice(&(serialized.len() as u32).to_le_bytes());
469 buf.extend_from_slice(&serialized);
470
471 let (page_id, lsn) = {
473 let mut wal = self.wal.lock();
474 let mut pm = self.page_manager.lock();
475
476 wal.lock_exclusive()?;
478 pm.reload_header()?;
479 wal.reload_lsn()?;
480
481 let page_id = pm.allocate_page()?;
483
484 let lsn = wal.append(WalEntryType::PageWrite, page_id.0, &buf)?;
487 wal.sync()?;
488 self.note_map_write(page_id, Some(node.id), lsn);
489
490 let mut page = Page::zeroed();
492 page.header.page_id = page_id.0;
493 let copy_len = buf.len().min(PAGE_DATA_SIZE);
494 page.data[..copy_len].copy_from_slice(&buf[..copy_len]);
495 page.header.lsn = lsn;
496 page.header.page_type = PageType::Data as u8;
497 page.header.free_space = (PAGE_DATA_SIZE - copy_len) as u16;
498 page.header.checksum = page.compute_checksum();
499 pm.write_page(page_id, &page)?;
500
501 wal.unlock()?;
503
504 (page_id, lsn)
505 };
506
507 let _ = lsn;
510 self.buffer_pool.invalidate(page_id);
511
512 if self.wal.lock().file_size() > WAL_AUTO_CHECKPOINT_BYTES
515 && let Err(e) = self.checkpoint()
516 {
517 tracing::warn!("auto-checkpoint failed: {e}");
518 }
519
520 info!(
521 page_id = page_id.0,
522 bytes = serialized.len(),
523 "stored memory node"
524 );
525 Ok(page_id)
526 }
527
528 pub fn store_memory_batch(&self, nodes: &[MemoryNode]) -> MenteResult<Vec<PageId>> {
534 let mut bufs = Vec::with_capacity(nodes.len());
536 for node in nodes {
537 let serialized =
538 serde_json::to_vec(node).map_err(|e| MenteError::Serialization(e.to_string()))?;
539 if serialized.len() + 4 > PAGE_DATA_SIZE {
540 return Err(MenteError::CapacityExceeded(format!(
541 "memory node serialized to {} bytes (max {})",
542 serialized.len(),
543 PAGE_DATA_SIZE - 4,
544 )));
545 }
546 let mut buf = Vec::with_capacity(4 + serialized.len());
547 buf.extend_from_slice(&(serialized.len() as u32).to_le_bytes());
548 buf.extend_from_slice(&serialized);
549 bufs.push(buf);
550 }
551
552 let page_ids = {
554 let mut wal = self.wal.lock();
555 let mut pm = self.page_manager.lock();
556
557 wal.lock_exclusive()?;
558 pm.reload_header()?;
559 wal.reload_lsn()?;
560
561 let mut ids = Vec::with_capacity(bufs.len());
562 for (buf, node) in bufs.iter().zip(nodes) {
563 let page_id = pm.allocate_page()?;
564 let lsn = wal.append(WalEntryType::PageWrite, page_id.0, buf)?;
565 self.note_map_write(page_id, Some(node.id), lsn);
566
567 let mut page = Page::zeroed();
568 page.header.page_id = page_id.0;
569 let copy_len = buf.len().min(PAGE_DATA_SIZE);
570 page.data[..copy_len].copy_from_slice(&buf[..copy_len]);
571 page.header.lsn = lsn;
572 page.header.page_type = PageType::Data as u8;
573 page.header.free_space = (PAGE_DATA_SIZE - copy_len) as u16;
574 page.header.checksum = page.compute_checksum();
575 pm.write_page(page_id, &page)?;
576
577 ids.push(page_id);
578 }
579
580 wal.sync()?;
583 wal.unlock()?;
584
585 ids
586 };
587
588 for page_id in &page_ids {
590 self.buffer_pool.invalidate(*page_id);
591 }
592
593 if self.wal.lock().file_size() > WAL_AUTO_CHECKPOINT_BYTES
595 && let Err(e) = self.checkpoint()
596 {
597 tracing::warn!("auto-checkpoint failed: {e}");
598 }
599
600 info!(count = page_ids.len(), "stored memory batch");
601 Ok(page_ids)
602 }
603
604 pub fn update_memory(&self, page_id: PageId, node: &MemoryNode) -> MenteResult<()> {
610 let serialized =
611 serde_json::to_vec(node).map_err(|e| MenteError::Serialization(e.to_string()))?;
612
613 if serialized.len() + 4 > PAGE_DATA_SIZE {
614 return Err(MenteError::CapacityExceeded(format!(
615 "memory node serialized to {} bytes (max {})",
616 serialized.len(),
617 PAGE_DATA_SIZE - 4,
618 )));
619 }
620
621 let mut buf = Vec::with_capacity(4 + serialized.len());
622 buf.extend_from_slice(&(serialized.len() as u32).to_le_bytes());
623 buf.extend_from_slice(&serialized);
624
625 self.write_page_as(page_id, &buf, Some(node.id))
626 }
627
628 pub fn delete_memory(&self, page_id: PageId) -> MenteResult<()> {
634 {
635 let mut wal = self.wal.lock();
636 let mut pm = self.page_manager.lock();
637
638 wal.lock_exclusive()?;
639 pm.reload_header()?;
640 wal.reload_lsn()?;
641
642 let lsn = wal.append(WalEntryType::PageFree, page_id.0, &[])?;
645 wal.sync()?;
646 self.note_map_write(page_id, None, lsn);
647
648 pm.free_page(page_id)?;
649 wal.unlock()?;
650 }
651
652 self.buffer_pool.invalidate(page_id);
654
655 info!(page_id = page_id.0, "deleted memory node");
656 Ok(())
657 }
658
659 pub fn load_memory(&self, page_id: PageId) -> MenteResult<MemoryNode> {
671 self.memory_loads
672 .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
673 let page = self.read_page(page_id)?;
674 self.buffer_pool.unpin_page(page_id, false).ok();
675
676 if PageType::from(page.header.page_type) != PageType::Data {
677 return Err(MenteError::Storage(format!(
678 "page {} is not a data page",
679 page_id.0
680 )));
681 }
682
683 memory_map::decode_memory(&page.data)
684 }
685
686 pub fn checkpoint(&self) -> MenteResult<()> {
700 let mut wal = self.wal.lock();
701 let mut pm = self.page_manager.lock();
702
703 wal.lock_exclusive()?;
704 let result = self.checkpoint_locked(&mut wal, &mut pm, None);
705 let unlocked = wal.unlock();
706 let lsn = result?;
707 unlocked?;
708
709 info!(lsn, "checkpoint complete");
710 Ok(())
711 }
712
713 fn checkpoint_locked(
719 &self,
720 wal: &mut Wal,
721 pm: &mut PageManager,
722 fresh: Option<MemoryTable>,
723 ) -> MenteResult<u64> {
724 wal.reload_lsn()?;
725
726 self.buffer_pool.flush_all(pm)?;
727 pm.sync()?;
728
729 let table = match fresh {
730 Some(t) => Some(t),
731 None => {
732 let current = self.map_anchor.lock().take();
733 match current {
734 Some(mut anchor) => {
735 let entries = wal.iterate()?;
736 match anchor.advance(&entries) {
737 Some(true) => Some(anchor.table),
738 Some(false) => {
739 let lsn = wal.append(WalEntryType::Checkpoint, 0, &anchor.token)?;
742 wal.sync()?;
743 *self.map_anchor.lock() = Some(anchor);
744 wal.truncate(lsn)?;
745 return Ok(lsn);
746 }
747 None => {
748 tracing::warn!(
749 "memory map anchor left the WAL, next open will scan pages"
750 );
751 None
752 }
753 }
754 }
755 None => None,
756 }
757 }
758 };
759
760 match table {
761 Some(table) => self.anchor_checkpoint(wal, pm, table),
762 None => {
763 let lsn = wal.append(WalEntryType::Checkpoint, 0, &memory_map::new_token())?;
766 wal.sync()?;
767 wal.truncate(lsn)?;
768 Ok(lsn)
769 }
770 }
771 }
772
773 pub fn memory_map(&self) -> MenteResult<Vec<MappedMemory>> {
782 let mut wal = self.wal.lock();
783 wal.lock_exclusive()?;
784 let result = self.memory_map_locked(&mut wal);
785 let unlocked = wal.unlock();
786 let map = result?;
787 unlocked?;
788 Ok(map)
789 }
790
791 fn memory_map_locked(&self, wal: &mut Wal) -> MenteResult<Vec<MappedMemory>> {
792 let entries = wal.iterate()?;
793 {
794 let mut slot = self.map_anchor.lock();
795 if let Some(anchor) = slot.as_ref() {
796 let mut current = anchor.clone();
797 if current.advance(&entries).is_some() {
798 return Ok(current.table.memories());
799 }
800 *slot = None;
801 }
802 }
803
804 let mut pm = self.page_manager.lock();
805 let table = self.scan_table(&mut pm)?;
806 let memories = table.memories();
807 info!(
808 memories = memories.len(),
809 "memory map rebuilt by full page scan"
810 );
811 self.checkpoint_locked(wal, &mut pm, Some(table))?;
812 Ok(memories)
813 }
814
815 fn scan_table(&self, pm: &mut PageManager) -> MenteResult<MemoryTable> {
817 pm.reload_header()?;
818 let count = pm.page_count();
819 let mut table = MemoryTable::default();
820 for i in 1..count {
821 let page_id = PageId(i);
822 let Ok(page) = self.buffer_pool.fetch_page(page_id, pm) else {
823 continue;
824 };
825 self.buffer_pool.unpin_page(page_id, false).ok();
826 match PageType::from(page.header.page_type) {
827 PageType::Data => {
828 if let Ok(node) = memory_map::decode_memory(&page.data) {
829 table.set_scanned(i, node.id);
830 }
831 }
832 PageType::Free => table.set_free(i),
833 _ => {}
834 }
835 }
836 Ok(table)
837 }
838
839 pub fn memory_stamp(&self, page: PageId) -> Option<(mentedb_core::types::MemoryId, u64)> {
843 self.map_anchor.lock().as_ref()?.current(page.0)
844 }
845
846 fn note_map_write(&self, page: PageId, id: Option<mentedb_core::types::MemoryId>, lsn: u64) {
850 if let Some(anchor) = self.map_anchor.lock().as_mut() {
851 anchor.note_write(page.0, id, lsn);
852 }
853 }
854
855 pub fn memory_loads(&self) -> u64 {
859 self.memory_loads.load(std::sync::atomic::Ordering::Relaxed)
860 }
861
862 pub fn page_reads(&self) -> u64 {
864 self.page_manager.lock().reads()
865 }
866
867 pub fn scan_all_memories(&self) -> Vec<(mentedb_core::types::MemoryId, PageId)> {
884 let mut pm = self.page_manager.lock();
885 let _ = pm.reload_header();
887 let count = pm.page_count();
888 drop(pm);
889
890 let mut results = Vec::new();
891 for i in 1..count {
892 let page_id = PageId(i);
893 if let Ok(node) = self.load_memory(page_id) {
894 results.push((node.id, page_id));
895 }
896 }
897 results
898 }
899}
900
901#[cfg(test)]
902mod tests {
903 use super::*;
904 use crate::memory_map::MappedMemory;
905 use mentedb_core::memory::MemoryType;
906 use mentedb_core::types::AgentId;
907
908 fn setup() -> (tempfile::TempDir, StorageEngine) {
909 let dir = tempfile::tempdir().unwrap();
910 let engine = StorageEngine::open(dir.path()).unwrap();
911 (dir, engine)
912 }
913
914 #[test]
915 fn test_allocate_write_read() {
916 let (_dir, engine) = setup();
917
918 let pid = engine.allocate_page().unwrap();
919 engine.write_page(pid, b"hello storage engine").unwrap();
920
921 let page = engine.read_page(pid).unwrap();
922 assert_eq!(&page.data[..20], b"hello storage engine");
923 engine.buffer_pool.unpin_page(pid, false).ok();
924 }
925
926 #[test]
927 fn test_store_and_load_memory() {
928 let (_dir, engine) = setup();
929
930 let node = MemoryNode::new(
931 AgentId::new(),
932 MemoryType::Episodic,
933 "The user prefers Rust over Go".to_string(),
934 vec![0.1, 0.2, 0.3, 0.4],
935 );
936
937 let page_id = engine.store_memory(&node).unwrap();
938 let loaded = engine.load_memory(page_id).unwrap();
939
940 assert_eq!(node.id, loaded.id);
941 assert_eq!(node.content, loaded.content);
942 assert_eq!(node.embedding, loaded.embedding);
943 assert_eq!(node.memory_type, loaded.memory_type);
944 }
945
946 #[test]
947 fn test_checkpoint() {
948 let (_dir, engine) = setup();
949
950 let node = MemoryNode::new(
951 AgentId::new(),
952 MemoryType::Semantic,
953 "checkpoint test".to_string(),
954 vec![1.0, 2.0],
955 );
956
957 let pid = engine.store_memory(&node).unwrap();
958 engine.checkpoint().unwrap();
959
960 let loaded = engine.load_memory(pid).unwrap();
961 assert_eq!(loaded.content, "checkpoint test");
962 }
963
964 #[test]
965 fn test_close_and_reopen() {
966 let dir = tempfile::tempdir().unwrap();
967 let pid;
968 {
969 let engine = StorageEngine::open(dir.path()).unwrap();
970 let node = MemoryNode::new(
971 AgentId::new(),
972 MemoryType::Procedural,
973 "persist across close".to_string(),
974 vec![0.5],
975 );
976 pid = engine.store_memory(&node).unwrap();
977 engine.close().unwrap();
978 }
979 {
980 let engine = StorageEngine::open(dir.path()).unwrap();
981 let loaded = engine.load_memory(pid).unwrap();
982 assert_eq!(loaded.content, "persist across close");
983 }
984 }
985
986 #[test]
987 fn test_crash_recovery() {
988 let dir = tempfile::tempdir().unwrap();
989 let mut ids = Vec::new();
990 let mut contents = Vec::new();
991 {
992 let engine = StorageEngine::open(dir.path()).unwrap();
993 for i in 0..3 {
994 let content = format!("crash-recovery-{i}");
995 let node = MemoryNode::new(
996 AgentId::new(),
997 MemoryType::Episodic,
998 content.clone(),
999 vec![i as f32],
1000 );
1001 let pid = engine.store_memory(&node).unwrap();
1002 ids.push(pid);
1003 contents.push(content);
1004 }
1005 engine.wal.lock().sync().unwrap();
1007 }
1008 {
1009 let engine = StorageEngine::open(dir.path()).unwrap();
1010 for (pid, expected) in ids.iter().zip(contents.iter()) {
1011 let loaded = engine.load_memory(*pid).unwrap();
1012 assert_eq!(&loaded.content, expected);
1013 }
1014 }
1015 }
1016
1017 #[test]
1018 fn test_recovery_idempotent() {
1019 let dir = tempfile::tempdir().unwrap();
1020 let pid;
1021 let content = "idempotent-check".to_string();
1022 {
1023 let engine = StorageEngine::open(dir.path()).unwrap();
1024 let node = MemoryNode::new(
1025 AgentId::new(),
1026 MemoryType::Semantic,
1027 content.clone(),
1028 vec![1.0, 2.0],
1029 );
1030 pid = engine.store_memory(&node).unwrap();
1031 engine.checkpoint().unwrap();
1032 engine.close().unwrap();
1033 }
1034 {
1035 let engine = StorageEngine::open(dir.path()).unwrap();
1036 let loaded = engine.load_memory(pid).unwrap();
1037 assert_eq!(loaded.content, content);
1038 }
1039 }
1040
1041 #[test]
1042 fn test_partial_write_recovery() {
1043 let dir = tempfile::tempdir().unwrap();
1044 let mut ids = Vec::new();
1045 let mut contents = Vec::new();
1046 {
1047 let engine = StorageEngine::open(dir.path()).unwrap();
1048 for i in 0..3 {
1049 let content = format!("checkpointed-{i}");
1050 let node = MemoryNode::new(
1051 AgentId::new(),
1052 MemoryType::Semantic,
1053 content.clone(),
1054 vec![i as f32],
1055 );
1056 let pid = engine.store_memory(&node).unwrap();
1057 ids.push(pid);
1058 contents.push(content);
1059 }
1060 engine.checkpoint().unwrap();
1061
1062 for i in 3..5 {
1063 let content = format!("unckeckpointed-{i}");
1064 let node = MemoryNode::new(
1065 AgentId::new(),
1066 MemoryType::Episodic,
1067 content.clone(),
1068 vec![i as f32],
1069 );
1070 let pid = engine.store_memory(&node).unwrap();
1071 ids.push(pid);
1072 contents.push(content);
1073 }
1074 engine.wal.lock().sync().unwrap();
1076 }
1077 {
1078 let engine = StorageEngine::open(dir.path()).unwrap();
1079 for (pid, expected) in ids.iter().zip(contents.iter()) {
1080 let loaded = engine.load_memory(*pid).unwrap();
1081 assert_eq!(&loaded.content, expected);
1082 }
1083 }
1084 }
1085
1086 #[test]
1087 fn test_delete_memory_durable() {
1088 let dir = tempfile::tempdir().unwrap();
1089 let pid;
1090 {
1091 let engine = StorageEngine::open(dir.path()).unwrap();
1092 let node = MemoryNode::new(
1093 AgentId::new(),
1094 MemoryType::Semantic,
1095 "to be deleted".to_string(),
1096 vec![1.0],
1097 );
1098 pid = engine.store_memory(&node).unwrap();
1099 engine.delete_memory(pid).unwrap();
1100 assert!(engine.load_memory(pid).is_err());
1101 assert!(engine.scan_all_memories().is_empty());
1102 engine.close().unwrap();
1103 }
1104 {
1105 let engine = StorageEngine::open(dir.path()).unwrap();
1106 assert!(
1107 engine.load_memory(pid).is_err(),
1108 "deleted memory must not resurrect on reopen"
1109 );
1110 assert!(engine.scan_all_memories().is_empty());
1111 }
1112 }
1113
1114 #[test]
1115 fn test_delete_survives_crash() {
1116 let dir = tempfile::tempdir().unwrap();
1117 let pid;
1118 {
1119 let engine = StorageEngine::open(dir.path()).unwrap();
1120 let node = MemoryNode::new(
1121 AgentId::new(),
1122 MemoryType::Semantic,
1123 "crash delete".to_string(),
1124 vec![1.0],
1125 );
1126 pid = engine.store_memory(&node).unwrap();
1127 engine.delete_memory(pid).unwrap();
1128 }
1130 {
1131 let engine = StorageEngine::open(dir.path()).unwrap();
1132 assert!(
1133 engine.load_memory(pid).is_err(),
1134 "deletion must survive a crash via WAL replay"
1135 );
1136 assert!(engine.scan_all_memories().is_empty());
1137 }
1138 }
1139
1140 #[test]
1141 fn test_deleted_page_reused() {
1142 let (_dir, engine) = setup();
1143
1144 let a = MemoryNode::new(AgentId::new(), MemoryType::Semantic, "a".into(), vec![1.0]);
1145 let pid_a = engine.store_memory(&a).unwrap();
1146 engine.delete_memory(pid_a).unwrap();
1147
1148 let b = MemoryNode::new(AgentId::new(), MemoryType::Semantic, "b".into(), vec![2.0]);
1149 let pid_b = engine.store_memory(&b).unwrap();
1150 assert_eq!(pid_a, pid_b, "freed page should be reused");
1151
1152 let loaded = engine.load_memory(pid_b).unwrap();
1153 assert_eq!(loaded.content, "b");
1154 }
1155
1156 #[test]
1157 fn test_delete_reuse_crash_recovery() {
1158 let dir = tempfile::tempdir().unwrap();
1159 let pid;
1160 let b_id;
1161 {
1162 let engine = StorageEngine::open(dir.path()).unwrap();
1163 let a = MemoryNode::new(AgentId::new(), MemoryType::Semantic, "a".into(), vec![1.0]);
1164 pid = engine.store_memory(&a).unwrap();
1165 engine.delete_memory(pid).unwrap();
1166 let b = MemoryNode::new(AgentId::new(), MemoryType::Semantic, "b".into(), vec![2.0]);
1167 let pid_b = engine.store_memory(&b).unwrap();
1168 assert_eq!(pid, pid_b);
1169 b_id = b.id;
1170 }
1172 {
1173 let engine = StorageEngine::open(dir.path()).unwrap();
1174 let loaded = engine.load_memory(pid).unwrap();
1175 assert_eq!(loaded.content, "b", "later write must win over the free");
1176 assert_eq!(loaded.id, b_id);
1177 let c = MemoryNode::new(AgentId::new(), MemoryType::Semantic, "c".into(), vec![3.0]);
1180 let pid_c = engine.store_memory(&c).unwrap();
1181 assert_ne!(pid_c, pid, "recovered free list must exclude reused page");
1182 assert_eq!(engine.load_memory(pid).unwrap().content, "b");
1183 }
1184 }
1185
1186 #[test]
1187 fn test_update_memory_in_place() {
1188 let dir = tempfile::tempdir().unwrap();
1189 let pid;
1190 let id;
1191 {
1192 let engine = StorageEngine::open(dir.path()).unwrap();
1193 let mut node = MemoryNode::new(
1194 AgentId::new(),
1195 MemoryType::Semantic,
1196 "original".to_string(),
1197 vec![1.0],
1198 );
1199 pid = engine.store_memory(&node).unwrap();
1200 id = node.id;
1201
1202 node.content = "updated".to_string();
1203 engine.update_memory(pid, &node).unwrap();
1204
1205 let loaded = engine.load_memory(pid).unwrap();
1206 assert_eq!(loaded.content, "updated");
1207 let scanned = engine.scan_all_memories();
1209 assert_eq!(scanned.len(), 1);
1210 engine.close().unwrap();
1211 }
1212 {
1213 let engine = StorageEngine::open(dir.path()).unwrap();
1214 let loaded = engine.load_memory(pid).unwrap();
1215 assert_eq!(loaded.content, "updated");
1216 assert_eq!(loaded.id, id);
1217 assert_eq!(engine.scan_all_memories().len(), 1);
1218 }
1219 }
1220
1221 #[test]
1222 fn test_concurrent_open_is_rejected() {
1223 let dir = tempfile::tempdir().unwrap();
1224
1225 let engine1 = StorageEngine::open(dir.path()).unwrap();
1230 let second = StorageEngine::open(dir.path());
1231 assert!(second.is_err(), "second concurrent open must fail");
1232 let msg = second.err().unwrap().to_string();
1233 assert!(msg.contains("locked"), "error names the lock: {msg}");
1234
1235 engine1.close().unwrap();
1237 let engine2 = StorageEngine::open(dir.path()).unwrap();
1238 engine2.close().unwrap();
1239 }
1240
1241 #[test]
1242 fn test_concurrent_writes_from_threads() {
1243 use std::sync::Arc;
1244 let dir = tempfile::tempdir().unwrap();
1245 let engine = Arc::new(StorageEngine::open(dir.path()).unwrap());
1246
1247 let handles: Vec<_> = (0..10)
1248 .map(|i| {
1249 let eng = Arc::clone(&engine);
1250 std::thread::spawn(move || {
1251 let node = MemoryNode::new(
1252 AgentId::new(),
1253 MemoryType::Episodic,
1254 format!("thread-{i}"),
1255 vec![i as f32],
1256 );
1257 eng.store_memory(&node).unwrap()
1258 })
1259 })
1260 .collect();
1261
1262 let pids: Vec<PageId> = handles.into_iter().map(|h| h.join().unwrap()).collect();
1263
1264 for (i, pid) in pids.iter().enumerate() {
1266 let loaded = engine.load_memory(*pid).unwrap();
1267 assert_eq!(loaded.content, format!("thread-{i}"));
1268 }
1269 }
1270
1271 fn node(content: &str) -> MemoryNode {
1274 MemoryNode::new(
1275 AgentId::new(),
1276 MemoryType::Semantic,
1277 content.to_string(),
1278 vec![1.0, 2.0],
1279 )
1280 }
1281
1282 fn ids(map: &[MappedMemory]) -> Vec<(mentedb_core::types::MemoryId, PageId)> {
1284 map.iter().map(|m| (m.id, m.page)).collect()
1285 }
1286
1287 fn scan_truth(dir: &Path) -> Vec<(mentedb_core::types::MemoryId, PageId)> {
1290 let engine = StorageEngine::open(dir).unwrap();
1291 let scanned = engine.scan_all_memories();
1292 engine.release_process_lock();
1293 scanned
1294 }
1295
1296 #[test]
1297 fn open_after_checkpoint_reads_no_pages() {
1298 let dir = tempfile::tempdir().unwrap();
1299 let n = 300;
1300 let mut stored = Vec::new();
1301 {
1302 let engine = StorageEngine::open(dir.path()).unwrap();
1303 for i in 0..n {
1304 let m = node(&format!("m{i}"));
1305 let pid = engine.store_memory(&m).unwrap();
1306 stored.push((m.id, pid));
1307 }
1308 assert_eq!(ids(&engine.memory_map().unwrap()), stored);
1310 engine.checkpoint().unwrap();
1311 engine.close().unwrap();
1312 }
1313 let engine = StorageEngine::open(dir.path()).unwrap();
1314 let map = engine.memory_map().unwrap();
1315 assert_eq!(ids(&map), stored);
1316 assert_eq!(
1317 engine.page_reads(),
1318 0,
1319 "open must serve the map from the snapshot, not by reading {n} pages"
1320 );
1321 }
1322
1323 #[test]
1324 fn open_replays_only_wal_pages_after_crash() {
1325 let dir = tempfile::tempdir().unwrap();
1326 let mut stored = Vec::new();
1327 {
1328 let engine = StorageEngine::open(dir.path()).unwrap();
1329 for i in 0..200 {
1330 let m = node(&format!("base{i}"));
1331 stored.push((m.id, engine.store_memory(&m).unwrap()));
1332 }
1333 engine.memory_map().unwrap();
1334 engine.checkpoint().unwrap();
1335 for i in 0..5 {
1336 let m = node(&format!("after{i}"));
1337 stored.push((m.id, engine.store_memory(&m).unwrap()));
1338 }
1339 engine.release_process_lock();
1341 }
1342 let engine = StorageEngine::open(dir.path()).unwrap();
1343 let map = engine.memory_map().unwrap();
1344 assert_eq!(ids(&map), stored, "uncheckpointed stores survive the crash");
1345 assert_eq!(
1346 engine.page_reads(),
1347 5,
1348 "replay reads the 5 WAL pages only, not the 200 checkpointed ones"
1349 );
1350 engine.release_process_lock();
1351 assert_eq!(scan_truth(dir.path()), stored);
1352 }
1353
1354 #[test]
1355 fn deletes_and_updates_after_checkpoint_are_replayed() {
1356 let dir = tempfile::tempdir().unwrap();
1357 let (keep, gone, updated);
1358 let updated_pid;
1359 {
1360 let engine = StorageEngine::open(dir.path()).unwrap();
1361 let k = node("keep");
1362 let g = node("gone");
1363 let mut u = node("old");
1364 let kp = engine.store_memory(&k).unwrap();
1365 let gp = engine.store_memory(&g).unwrap();
1366 updated_pid = engine.store_memory(&u).unwrap();
1367 engine.memory_map().unwrap();
1368 engine.checkpoint().unwrap();
1369 engine.delete_memory(gp).unwrap();
1370 u.content = "new".into();
1371 engine.update_memory(updated_pid, &u).unwrap();
1372 keep = (k.id, kp);
1373 gone = g.id;
1374 updated = u.id;
1375 engine.release_process_lock();
1376 }
1377 let engine = StorageEngine::open(dir.path()).unwrap();
1378 let map = engine.memory_map().unwrap();
1379 assert!(
1380 map.iter().all(|m| m.id != gone),
1381 "deleted memory stays deleted"
1382 );
1383 assert!(ids(&map).contains(&keep));
1384 assert!(ids(&map).contains(&(updated, updated_pid)));
1385 assert_eq!(engine.load_memory(updated_pid).unwrap().content, "new");
1386 assert_eq!(map.len(), 2);
1387 engine.release_process_lock();
1388 assert_eq!(scan_truth(dir.path()), ids(&map));
1389 }
1390
1391 #[test]
1392 fn free_list_reuse_after_checkpoint_maps_the_new_memory() {
1393 let dir = tempfile::tempdir().unwrap();
1394 let (a_id, b_id, pid);
1395 {
1396 let engine = StorageEngine::open(dir.path()).unwrap();
1397 let a = node("a");
1398 pid = engine.store_memory(&a).unwrap();
1399 engine.store_memory(&node("other")).unwrap();
1400 engine.memory_map().unwrap();
1401 engine.checkpoint().unwrap();
1402 engine.delete_memory(pid).unwrap();
1403 let b = node("b");
1404 assert_eq!(engine.store_memory(&b).unwrap(), pid, "freed page reused");
1405 a_id = a.id;
1406 b_id = b.id;
1407 engine.release_process_lock();
1408 }
1409 let engine = StorageEngine::open(dir.path()).unwrap();
1410 let map = engine.memory_map().unwrap();
1411 assert!(
1412 map.iter().all(|m| m.id != a_id),
1413 "no stale id for the reused page"
1414 );
1415 assert!(ids(&map).contains(&(b_id, pid)));
1416 let c = node("c");
1418 assert_ne!(engine.store_memory(&c).unwrap(), pid);
1419 assert_eq!(engine.load_memory(pid).unwrap().id, b_id);
1420 engine.release_process_lock();
1421 let truth = scan_truth(dir.path());
1422 assert!(truth.contains(&(b_id, pid)));
1423 assert!(truth.iter().all(|(id, _)| *id != a_id));
1424 }
1425
1426 #[test]
1427 fn free_list_rebuilt_from_map_after_crash() {
1428 let dir = tempfile::tempdir().unwrap();
1429 let mut freed = Vec::new();
1430 {
1431 let engine = StorageEngine::open(dir.path()).unwrap();
1432 let mut pids = Vec::new();
1433 for i in 0..50 {
1434 pids.push(engine.store_memory(&node(&format!("m{i}"))).unwrap());
1435 }
1436 engine.delete_memory(pids[3]).unwrap();
1437 freed.push(pids[3]);
1438 engine.memory_map().unwrap();
1439 engine.checkpoint().unwrap();
1440 engine.delete_memory(pids[7]).unwrap();
1441 freed.push(pids[7]);
1442 engine.release_process_lock();
1443 }
1444 let engine = StorageEngine::open(dir.path()).unwrap();
1445 assert!(engine.page_reads() <= 3, "reads {}", engine.page_reads());
1448 let a = engine.store_memory(&node("x")).unwrap();
1449 let b = engine.store_memory(&node("y")).unwrap();
1450 let mut got = vec![a, b];
1451 got.sort_by_key(|p| p.0);
1452 assert_eq!(got, freed, "both freed pages are reusable after recovery");
1453 let c = engine.store_memory(&node("z")).unwrap();
1454 assert_eq!(c, PageId(51), "then allocation grows the file");
1455 }
1456
1457 #[test]
1458 fn missing_corrupt_or_stale_snapshot_falls_back_to_scan() {
1459 let dir = tempfile::tempdir().unwrap();
1460 let mut stored = Vec::new();
1461 {
1462 let engine = StorageEngine::open(dir.path()).unwrap();
1463 for i in 0..40 {
1464 let m = node(&format!("m{i}"));
1465 stored.push((m.id, engine.store_memory(&m).unwrap()));
1466 }
1467 engine.memory_map().unwrap();
1468 engine.checkpoint().unwrap();
1469 engine.close().unwrap();
1470 }
1471 let snap = dir.path().join(memory_map::MEMORY_MAP_FILE);
1472 let good = std::fs::read(&snap).unwrap();
1473
1474 let check = |label: &str| {
1475 let engine = StorageEngine::open(dir.path()).unwrap();
1476 let map = engine.memory_map().unwrap();
1477 assert_eq!(ids(&map), stored, "{label}: same result as a scan");
1478 assert!(
1479 engine.page_reads() >= 40,
1480 "{label}: must have scanned, read {}",
1481 engine.page_reads()
1482 );
1483 engine.close().unwrap();
1484 let engine = StorageEngine::open(dir.path()).unwrap();
1486 assert_eq!(ids(&engine.memory_map().unwrap()), stored);
1487 assert_eq!(engine.page_reads(), 0, "{label}: next open is fast again");
1488 engine.close().unwrap();
1489 };
1490
1491 std::fs::remove_file(&snap).unwrap();
1492 check("missing");
1493
1494 let mut bad = std::fs::read(&snap).unwrap();
1495 let mid = bad.len() / 2;
1496 bad[mid] ^= 0x55;
1497 std::fs::write(&snap, &bad).unwrap();
1498 check("corrupt");
1499
1500 std::fs::write(&snap, &good).unwrap();
1503 check("stale");
1504 }
1505
1506 #[test]
1507 fn stale_snapshot_with_unchanged_token_reuse_is_never_matched() {
1508 let dir = tempfile::tempdir().unwrap();
1511 let (a, b);
1512 {
1513 let engine = StorageEngine::open(dir.path()).unwrap();
1514 let na = node("a");
1515 let nb = node("b");
1516 a = engine.store_memory(&na).unwrap();
1517 b = (nb.id, engine.store_memory(&nb).unwrap());
1518 engine.memory_map().unwrap();
1519 engine.checkpoint().unwrap();
1520 *engine.map_anchor.lock() = None;
1523 engine.delete_memory(a).unwrap();
1524 engine.checkpoint().unwrap();
1525 engine.close().unwrap();
1526 }
1527 let engine = StorageEngine::open(dir.path()).unwrap();
1528 assert_eq!(ids(&engine.memory_map().unwrap()), vec![b]);
1529 }
1530
1531 #[test]
1532 fn handoff_between_handles_sees_writes_and_checkpoints() {
1533 let dir = tempfile::tempdir().unwrap();
1534 let mut stored = Vec::new();
1535 {
1537 let e1 = StorageEngine::open(dir.path()).unwrap();
1538 for i in 0..20 {
1539 let m = node(&format!("h1-{i}"));
1540 stored.push((m.id, e1.store_memory(&m).unwrap()));
1541 }
1542 e1.memory_map().unwrap();
1543 e1.checkpoint().unwrap();
1544 e1.close().unwrap();
1545 }
1546 {
1549 let e2 = StorageEngine::open(dir.path()).unwrap();
1550 e2.memory_map().unwrap();
1551 for i in 0..10 {
1552 let m = node(&format!("h2-{i}"));
1553 stored.push((m.id, e2.store_memory(&m).unwrap()));
1554 }
1555 e2.checkpoint().unwrap();
1556 e2.delete_memory(stored[0].1).unwrap();
1557 stored.remove(0);
1558 let m = node("h2-last");
1559 stored.push((m.id, e2.store_memory(&m).unwrap()));
1560 e2.release_process_lock();
1561 }
1562 let e3 = StorageEngine::open(dir.path()).unwrap();
1564 let mut got = ids(&e3.memory_map().unwrap());
1565 got.sort_by_key(|(_, p)| p.0);
1566 stored.sort_by_key(|(_, p)| p.0);
1567 assert_eq!(got, stored);
1568 assert!(e3.page_reads() < 10, "reads {}", e3.page_reads());
1569 e3.release_process_lock();
1570 assert_eq!(scan_truth(dir.path()), stored);
1571 }
1572
1573 #[test]
1574 fn live_stamps_survive_checkpoint_and_crash_replay() {
1575 let dir = tempfile::tempdir().unwrap();
1576 let (b_page, b_live);
1577 {
1578 let engine = StorageEngine::open(dir.path()).unwrap();
1579 engine.store_memory(&node("a")).unwrap();
1580 engine.memory_map().unwrap();
1581 engine.checkpoint().unwrap();
1582 let c = engine.store_memory(&node("c")).unwrap();
1584 let c_live = engine.memory_stamp(c).unwrap();
1585 engine.checkpoint().unwrap();
1586 assert_eq!(
1587 engine.memory_stamp(c).unwrap(),
1588 c_live,
1589 "checkpoint keeps it"
1590 );
1591 b_page = engine.store_memory(&node("b")).unwrap();
1592 b_live = engine.memory_stamp(b_page).unwrap();
1593 engine.release_process_lock();
1594 }
1595 let engine = StorageEngine::open(dir.path()).unwrap();
1596 let map = engine.memory_map().unwrap();
1597 let b = map.iter().find(|m| m.page == b_page).unwrap();
1598 assert_eq!(
1599 (b.id, b.stamp),
1600 b_live,
1601 "WAL replay reproduces the live stamp"
1602 );
1603 }
1604
1605 #[test]
1606 fn unchanged_checkpoints_keep_the_snapshot_and_stamps() {
1607 let dir = tempfile::tempdir().unwrap();
1608 let engine = StorageEngine::open(dir.path()).unwrap();
1609 let pid = engine.store_memory(&node("a")).unwrap();
1610 engine.memory_map().unwrap();
1611 engine.checkpoint().unwrap();
1612 let snap = dir.path().join(memory_map::MEMORY_MAP_FILE);
1613 let before = std::fs::metadata(&snap).unwrap().modified().unwrap();
1614 let stamp = engine.memory_stamp(pid).unwrap();
1615 std::thread::sleep(std::time::Duration::from_millis(20));
1616 engine.checkpoint().unwrap();
1617 engine.checkpoint().unwrap();
1618 assert_eq!(
1619 std::fs::metadata(&snap).unwrap().modified().unwrap(),
1620 before
1621 );
1622 assert_eq!(engine.memory_stamp(pid).unwrap(), stamp);
1623 let mut n = engine.load_memory(pid).unwrap();
1625 n.content = "b".into();
1626 engine.update_memory(pid, &n).unwrap();
1627 engine.checkpoint().unwrap();
1628 assert_ne!(engine.memory_stamp(pid).unwrap().1, stamp.1);
1629 engine.close().unwrap();
1630 let engine = StorageEngine::open(dir.path()).unwrap();
1631 let map = engine.memory_map().unwrap();
1632 assert_eq!(map[0].stamp, engine.memory_stamp(pid).unwrap().1);
1633 }
1634}