1use std::fs::File;
4use std::path::Path;
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::page::{PAGE_DATA_SIZE, Page, PageId, PageManager, PageType};
14use crate::wal::{Wal, WalEntryType};
15const DEFAULT_BUFFER_POOL_SIZE: usize = 1024;
17
18const WAL_AUTO_CHECKPOINT_BYTES: u64 = 8 * 1024 * 1024;
20
21pub struct StorageEngine {
45 page_manager: Mutex<PageManager>,
46 buffer_pool: BufferPool,
47 wal: Mutex<Wal>,
48 process_lock: Mutex<Option<File>>,
51}
52
53impl StorageEngine {
54 pub fn open(path: &Path) -> MenteResult<Self> {
69 std::fs::create_dir_all(path)?;
70
71 let lock_path = path.join("LOCK");
75 let lock_file = std::fs::OpenOptions::new()
76 .create(true)
77 .truncate(false)
78 .write(true)
79 .open(&lock_path)?;
80 fs2::FileExt::try_lock_exclusive(&lock_file).map_err(|_| {
81 MenteError::Storage(format!(
82 "database directory {} is locked by another process",
83 path.display()
84 ))
85 })?;
86
87 let page_manager = PageManager::open(path)?;
88 let buffer_pool = BufferPool::new(DEFAULT_BUFFER_POOL_SIZE);
89 let wal = Wal::open(path)?;
90
91 let engine = Self {
92 page_manager: Mutex::new(page_manager),
93 buffer_pool,
94 wal: Mutex::new(wal),
95 process_lock: Mutex::new(Some(lock_file)),
96 };
97
98 let recovered = engine.recover()?;
99 if recovered > 0 {
100 info!(recovered, ?path, "storage engine opened with WAL recovery");
101 } else {
102 info!(?path, "storage engine opened");
103 }
104
105 Ok(engine)
106 }
107
108 pub fn recover(&self) -> MenteResult<usize> {
113 let mut wal = self.wal.lock();
114 wal.lock_exclusive()?;
115 let entries = wal.iterate()?;
116 let mut count = 0usize;
117 let mut pm = self.page_manager.lock();
118
119 pm.reload_header()?;
121
122 let mut last_op: std::collections::HashMap<u64, &crate::wal::WalEntry> = Default::default();
128 let mut order: Vec<u64> = Vec::new();
129 for entry in &entries {
130 match entry.entry_type {
131 WalEntryType::PageWrite | WalEntryType::PageFree => {
132 if !last_op.contains_key(&entry.page_id) {
133 order.push(entry.page_id);
134 }
135 last_op.insert(entry.page_id, entry);
136 }
137 WalEntryType::Checkpoint | WalEntryType::Commit => {}
138 }
139 }
140
141 for page_id_raw in order {
142 let entry = last_op[&page_id_raw];
143 let page_id = PageId(entry.page_id);
144 match entry.entry_type {
145 WalEntryType::PageWrite => {
146 while pm.page_count() <= entry.page_id {
147 pm.allocate_page()?;
148 }
149
150 let mut page = pm.read_page(page_id)?;
151 let copy_len = entry.data.len().min(PAGE_DATA_SIZE);
152 page.data[..copy_len].copy_from_slice(&entry.data[..copy_len]);
153 if copy_len < PAGE_DATA_SIZE {
154 page.data[copy_len..].fill(0);
155 }
156 page.header.page_id = entry.page_id;
157 page.header.lsn = entry.lsn;
158 page.header.page_type = PageType::Data as u8;
159 page.header.free_space = (PAGE_DATA_SIZE - copy_len) as u16;
160 page.header.checksum = page.compute_checksum();
161
162 pm.write_page(page_id, &page)?;
163 count += 1;
164 }
165 WalEntryType::PageFree => {
166 if entry.page_id < pm.page_count() {
170 let mut page = Page::zeroed();
171 page.header.page_id = entry.page_id;
172 page.header.page_type = PageType::Free as u8;
173 pm.write_page(page_id, &page)?;
174 self.buffer_pool.invalidate(page_id);
175 count += 1;
176 }
177 }
178 WalEntryType::Checkpoint | WalEntryType::Commit => {}
179 }
180 }
181
182 if count > 0 {
183 pm.rebuild_free_list()?;
188 pm.sync()?;
189 let next_lsn = wal.next_lsn();
190 wal.truncate(next_lsn)?;
191 info!(count, "WAL recovery replayed entries");
192 }
193
194 wal.unlock()?;
195 Ok(count)
196 }
197
198 pub fn close(&self) -> MenteResult<()> {
209 let mut pm = self.page_manager.lock();
210 self.buffer_pool.flush_all(&mut pm)?;
211 pm.sync()?;
212 self.wal.lock().sync()?;
213 if let Some(lock_file) = self.process_lock.lock().take() {
216 let _ = fs2::FileExt::unlock(&lock_file);
217 }
218 info!("storage engine closed");
219 Ok(())
220 }
221
222 #[doc(hidden)]
227 pub fn release_process_lock(&self) {
228 if let Some(lock_file) = self.process_lock.lock().take() {
229 let _ = fs2::FileExt::unlock(&lock_file);
230 }
231 }
232
233 pub fn allocate_page(&self) -> MenteResult<PageId> {
240 self.page_manager.lock().allocate_page()
241 }
242
243 pub fn read_page(&self, page_id: PageId) -> MenteResult<Box<Page>> {
245 self.buffer_pool
246 .fetch_page(page_id, &mut self.page_manager.lock())
247 }
248
249 pub fn write_page(&self, page_id: PageId, data: &[u8]) -> MenteResult<()> {
254 let lsn = {
255 let mut wal = self.wal.lock();
256 wal.lock_exclusive()?;
257 wal.reload_lsn()?;
258 let lsn = wal.append(WalEntryType::PageWrite, page_id.0, data)?;
259 wal.sync()?;
260 wal.unlock()?;
261 lsn
262 };
263
264 self.apply_page_write(page_id, data, lsn)
265 }
266
267 fn apply_page_write(&self, page_id: PageId, data: &[u8], lsn: u64) -> MenteResult<()> {
269 let mut pm = self.page_manager.lock();
270 let mut page = self.buffer_pool.fetch_page(page_id, &mut pm)?;
271 drop(pm);
272
273 let copy_len = data.len().min(PAGE_DATA_SIZE);
274 page.data[..copy_len].copy_from_slice(&data[..copy_len]);
275 if copy_len < PAGE_DATA_SIZE {
276 page.data[copy_len..].fill(0);
277 }
278 page.header.lsn = lsn;
279 page.header.page_type = PageType::Data as u8;
280 page.header.free_space = (PAGE_DATA_SIZE - copy_len) as u16;
281 page.header.checksum = page.compute_checksum();
282
283 if self.buffer_pool.update_page(page_id, &page).is_err() {
284 self.page_manager.lock().write_page(page_id, &page)?;
285 }
286 self.buffer_pool.unpin_page(page_id, true).ok();
287
288 Ok(())
289 }
290
291 pub fn store_memory(&self, node: &MemoryNode) -> MenteResult<PageId> {
315 let serialized =
316 serde_json::to_vec(node).map_err(|e| MenteError::Serialization(e.to_string()))?;
317
318 if serialized.len() + 4 > PAGE_DATA_SIZE {
319 return Err(MenteError::CapacityExceeded(format!(
320 "memory node serialized to {} bytes (max {})",
321 serialized.len(),
322 PAGE_DATA_SIZE - 4,
323 )));
324 }
325
326 let mut buf = Vec::with_capacity(4 + serialized.len());
327 buf.extend_from_slice(&(serialized.len() as u32).to_le_bytes());
328 buf.extend_from_slice(&serialized);
329
330 let (page_id, lsn) = {
332 let mut wal = self.wal.lock();
333 let mut pm = self.page_manager.lock();
334
335 wal.lock_exclusive()?;
337 pm.reload_header()?;
338 wal.reload_lsn()?;
339
340 let page_id = pm.allocate_page()?;
342
343 let lsn = wal.append(WalEntryType::PageWrite, page_id.0, &buf)?;
346 wal.sync()?;
347
348 let mut page = Page::zeroed();
350 page.header.page_id = page_id.0;
351 let copy_len = buf.len().min(PAGE_DATA_SIZE);
352 page.data[..copy_len].copy_from_slice(&buf[..copy_len]);
353 page.header.lsn = lsn;
354 page.header.page_type = PageType::Data as u8;
355 page.header.free_space = (PAGE_DATA_SIZE - copy_len) as u16;
356 page.header.checksum = page.compute_checksum();
357 pm.write_page(page_id, &page)?;
358
359 wal.unlock()?;
361
362 (page_id, lsn)
363 };
364
365 let _ = lsn;
368 self.buffer_pool.invalidate(page_id);
369
370 if self.wal.lock().file_size() > WAL_AUTO_CHECKPOINT_BYTES
373 && let Err(e) = self.checkpoint()
374 {
375 tracing::warn!("auto-checkpoint failed: {e}");
376 }
377
378 info!(
379 page_id = page_id.0,
380 bytes = serialized.len(),
381 "stored memory node"
382 );
383 Ok(page_id)
384 }
385
386 pub fn store_memory_batch(&self, nodes: &[MemoryNode]) -> MenteResult<Vec<PageId>> {
392 let mut bufs = Vec::with_capacity(nodes.len());
394 for node in nodes {
395 let serialized =
396 serde_json::to_vec(node).map_err(|e| MenteError::Serialization(e.to_string()))?;
397 if serialized.len() + 4 > PAGE_DATA_SIZE {
398 return Err(MenteError::CapacityExceeded(format!(
399 "memory node serialized to {} bytes (max {})",
400 serialized.len(),
401 PAGE_DATA_SIZE - 4,
402 )));
403 }
404 let mut buf = Vec::with_capacity(4 + serialized.len());
405 buf.extend_from_slice(&(serialized.len() as u32).to_le_bytes());
406 buf.extend_from_slice(&serialized);
407 bufs.push(buf);
408 }
409
410 let page_ids = {
412 let mut wal = self.wal.lock();
413 let mut pm = self.page_manager.lock();
414
415 wal.lock_exclusive()?;
416 pm.reload_header()?;
417 wal.reload_lsn()?;
418
419 let mut ids = Vec::with_capacity(bufs.len());
420 for buf in &bufs {
421 let page_id = pm.allocate_page()?;
422 let lsn = wal.append(WalEntryType::PageWrite, page_id.0, buf)?;
423
424 let mut page = Page::zeroed();
425 page.header.page_id = page_id.0;
426 let copy_len = buf.len().min(PAGE_DATA_SIZE);
427 page.data[..copy_len].copy_from_slice(&buf[..copy_len]);
428 page.header.lsn = lsn;
429 page.header.page_type = PageType::Data as u8;
430 page.header.free_space = (PAGE_DATA_SIZE - copy_len) as u16;
431 page.header.checksum = page.compute_checksum();
432 pm.write_page(page_id, &page)?;
433
434 ids.push(page_id);
435 }
436
437 wal.sync()?;
440 wal.unlock()?;
441
442 ids
443 };
444
445 for page_id in &page_ids {
447 self.buffer_pool.invalidate(*page_id);
448 }
449
450 if self.wal.lock().file_size() > WAL_AUTO_CHECKPOINT_BYTES
452 && let Err(e) = self.checkpoint()
453 {
454 tracing::warn!("auto-checkpoint failed: {e}");
455 }
456
457 info!(count = page_ids.len(), "stored memory batch");
458 Ok(page_ids)
459 }
460
461 pub fn update_memory(&self, page_id: PageId, node: &MemoryNode) -> MenteResult<()> {
467 let serialized =
468 serde_json::to_vec(node).map_err(|e| MenteError::Serialization(e.to_string()))?;
469
470 if serialized.len() + 4 > PAGE_DATA_SIZE {
471 return Err(MenteError::CapacityExceeded(format!(
472 "memory node serialized to {} bytes (max {})",
473 serialized.len(),
474 PAGE_DATA_SIZE - 4,
475 )));
476 }
477
478 let mut buf = Vec::with_capacity(4 + serialized.len());
479 buf.extend_from_slice(&(serialized.len() as u32).to_le_bytes());
480 buf.extend_from_slice(&serialized);
481
482 self.write_page(page_id, &buf)
483 }
484
485 pub fn delete_memory(&self, page_id: PageId) -> MenteResult<()> {
491 {
492 let mut wal = self.wal.lock();
493 let mut pm = self.page_manager.lock();
494
495 wal.lock_exclusive()?;
496 pm.reload_header()?;
497 wal.reload_lsn()?;
498
499 wal.append(WalEntryType::PageFree, page_id.0, &[])?;
502 wal.sync()?;
503
504 pm.free_page(page_id)?;
505 wal.unlock()?;
506 }
507
508 self.buffer_pool.invalidate(page_id);
510
511 info!(page_id = page_id.0, "deleted memory node");
512 Ok(())
513 }
514
515 pub fn load_memory(&self, page_id: PageId) -> MenteResult<MemoryNode> {
527 let page = self.read_page(page_id)?;
528 self.buffer_pool.unpin_page(page_id, false).ok();
529
530 if PageType::from(page.header.page_type) != PageType::Data {
531 return Err(MenteError::Storage(format!(
532 "page {} is not a data page",
533 page_id.0
534 )));
535 }
536
537 let len = u32::from_le_bytes(page.data[..4].try_into().unwrap()) as usize;
538 if len == 0 || len + 4 > PAGE_DATA_SIZE {
539 return Err(MenteError::Storage(format!(
540 "invalid memory node length prefix: {len}"
541 )));
542 }
543
544 serde_json::from_slice(&page.data[4..4 + len])
545 .map_err(|e| MenteError::Serialization(e.to_string()))
546 }
547
548 pub fn checkpoint(&self) -> MenteResult<()> {
562 let mut wal = self.wal.lock();
563 let mut pm = self.page_manager.lock();
564
565 wal.lock_exclusive()?;
566 wal.reload_lsn()?;
567
568 self.buffer_pool.flush_all(&mut pm)?;
569 pm.sync()?;
570
571 let lsn = wal.append(WalEntryType::Checkpoint, 0, &[])?;
572 wal.sync()?;
573 wal.truncate(lsn)?;
574 wal.unlock()?;
575
576 info!(lsn, "checkpoint complete");
577 Ok(())
578 }
579
580 pub fn scan_all_memories(&self) -> Vec<(mentedb_core::types::MemoryId, PageId)> {
597 let mut pm = self.page_manager.lock();
598 let _ = pm.reload_header();
600 let count = pm.page_count();
601 drop(pm);
602
603 let mut results = Vec::new();
604 for i in 1..count {
605 let page_id = PageId(i);
606 if let Ok(node) = self.load_memory(page_id) {
607 results.push((node.id, page_id));
608 }
609 }
610 results
611 }
612}
613
614#[cfg(test)]
615mod tests {
616 use super::*;
617 use mentedb_core::memory::MemoryType;
618 use mentedb_core::types::AgentId;
619
620 fn setup() -> (tempfile::TempDir, StorageEngine) {
621 let dir = tempfile::tempdir().unwrap();
622 let engine = StorageEngine::open(dir.path()).unwrap();
623 (dir, engine)
624 }
625
626 #[test]
627 fn test_allocate_write_read() {
628 let (_dir, engine) = setup();
629
630 let pid = engine.allocate_page().unwrap();
631 engine.write_page(pid, b"hello storage engine").unwrap();
632
633 let page = engine.read_page(pid).unwrap();
634 assert_eq!(&page.data[..20], b"hello storage engine");
635 engine.buffer_pool.unpin_page(pid, false).ok();
636 }
637
638 #[test]
639 fn test_store_and_load_memory() {
640 let (_dir, engine) = setup();
641
642 let node = MemoryNode::new(
643 AgentId::new(),
644 MemoryType::Episodic,
645 "The user prefers Rust over Go".to_string(),
646 vec![0.1, 0.2, 0.3, 0.4],
647 );
648
649 let page_id = engine.store_memory(&node).unwrap();
650 let loaded = engine.load_memory(page_id).unwrap();
651
652 assert_eq!(node.id, loaded.id);
653 assert_eq!(node.content, loaded.content);
654 assert_eq!(node.embedding, loaded.embedding);
655 assert_eq!(node.memory_type, loaded.memory_type);
656 }
657
658 #[test]
659 fn test_checkpoint() {
660 let (_dir, engine) = setup();
661
662 let node = MemoryNode::new(
663 AgentId::new(),
664 MemoryType::Semantic,
665 "checkpoint test".to_string(),
666 vec![1.0, 2.0],
667 );
668
669 let pid = engine.store_memory(&node).unwrap();
670 engine.checkpoint().unwrap();
671
672 let loaded = engine.load_memory(pid).unwrap();
673 assert_eq!(loaded.content, "checkpoint test");
674 }
675
676 #[test]
677 fn test_close_and_reopen() {
678 let dir = tempfile::tempdir().unwrap();
679 let pid;
680 {
681 let engine = StorageEngine::open(dir.path()).unwrap();
682 let node = MemoryNode::new(
683 AgentId::new(),
684 MemoryType::Procedural,
685 "persist across close".to_string(),
686 vec![0.5],
687 );
688 pid = engine.store_memory(&node).unwrap();
689 engine.close().unwrap();
690 }
691 {
692 let engine = StorageEngine::open(dir.path()).unwrap();
693 let loaded = engine.load_memory(pid).unwrap();
694 assert_eq!(loaded.content, "persist across close");
695 }
696 }
697
698 #[test]
699 fn test_crash_recovery() {
700 let dir = tempfile::tempdir().unwrap();
701 let mut ids = Vec::new();
702 let mut contents = Vec::new();
703 {
704 let engine = StorageEngine::open(dir.path()).unwrap();
705 for i in 0..3 {
706 let content = format!("crash-recovery-{i}");
707 let node = MemoryNode::new(
708 AgentId::new(),
709 MemoryType::Episodic,
710 content.clone(),
711 vec![i as f32],
712 );
713 let pid = engine.store_memory(&node).unwrap();
714 ids.push(pid);
715 contents.push(content);
716 }
717 engine.wal.lock().sync().unwrap();
719 }
720 {
721 let engine = StorageEngine::open(dir.path()).unwrap();
722 for (pid, expected) in ids.iter().zip(contents.iter()) {
723 let loaded = engine.load_memory(*pid).unwrap();
724 assert_eq!(&loaded.content, expected);
725 }
726 }
727 }
728
729 #[test]
730 fn test_recovery_idempotent() {
731 let dir = tempfile::tempdir().unwrap();
732 let pid;
733 let content = "idempotent-check".to_string();
734 {
735 let engine = StorageEngine::open(dir.path()).unwrap();
736 let node = MemoryNode::new(
737 AgentId::new(),
738 MemoryType::Semantic,
739 content.clone(),
740 vec![1.0, 2.0],
741 );
742 pid = engine.store_memory(&node).unwrap();
743 engine.checkpoint().unwrap();
744 engine.close().unwrap();
745 }
746 {
747 let engine = StorageEngine::open(dir.path()).unwrap();
748 let loaded = engine.load_memory(pid).unwrap();
749 assert_eq!(loaded.content, content);
750 }
751 }
752
753 #[test]
754 fn test_partial_write_recovery() {
755 let dir = tempfile::tempdir().unwrap();
756 let mut ids = Vec::new();
757 let mut contents = Vec::new();
758 {
759 let engine = StorageEngine::open(dir.path()).unwrap();
760 for i in 0..3 {
761 let content = format!("checkpointed-{i}");
762 let node = MemoryNode::new(
763 AgentId::new(),
764 MemoryType::Semantic,
765 content.clone(),
766 vec![i as f32],
767 );
768 let pid = engine.store_memory(&node).unwrap();
769 ids.push(pid);
770 contents.push(content);
771 }
772 engine.checkpoint().unwrap();
773
774 for i in 3..5 {
775 let content = format!("unckeckpointed-{i}");
776 let node = MemoryNode::new(
777 AgentId::new(),
778 MemoryType::Episodic,
779 content.clone(),
780 vec![i as f32],
781 );
782 let pid = engine.store_memory(&node).unwrap();
783 ids.push(pid);
784 contents.push(content);
785 }
786 engine.wal.lock().sync().unwrap();
788 }
789 {
790 let engine = StorageEngine::open(dir.path()).unwrap();
791 for (pid, expected) in ids.iter().zip(contents.iter()) {
792 let loaded = engine.load_memory(*pid).unwrap();
793 assert_eq!(&loaded.content, expected);
794 }
795 }
796 }
797
798 #[test]
799 fn test_delete_memory_durable() {
800 let dir = tempfile::tempdir().unwrap();
801 let pid;
802 {
803 let engine = StorageEngine::open(dir.path()).unwrap();
804 let node = MemoryNode::new(
805 AgentId::new(),
806 MemoryType::Semantic,
807 "to be deleted".to_string(),
808 vec![1.0],
809 );
810 pid = engine.store_memory(&node).unwrap();
811 engine.delete_memory(pid).unwrap();
812 assert!(engine.load_memory(pid).is_err());
813 assert!(engine.scan_all_memories().is_empty());
814 engine.close().unwrap();
815 }
816 {
817 let engine = StorageEngine::open(dir.path()).unwrap();
818 assert!(
819 engine.load_memory(pid).is_err(),
820 "deleted memory must not resurrect on reopen"
821 );
822 assert!(engine.scan_all_memories().is_empty());
823 }
824 }
825
826 #[test]
827 fn test_delete_survives_crash() {
828 let dir = tempfile::tempdir().unwrap();
829 let pid;
830 {
831 let engine = StorageEngine::open(dir.path()).unwrap();
832 let node = MemoryNode::new(
833 AgentId::new(),
834 MemoryType::Semantic,
835 "crash delete".to_string(),
836 vec![1.0],
837 );
838 pid = engine.store_memory(&node).unwrap();
839 engine.delete_memory(pid).unwrap();
840 }
842 {
843 let engine = StorageEngine::open(dir.path()).unwrap();
844 assert!(
845 engine.load_memory(pid).is_err(),
846 "deletion must survive a crash via WAL replay"
847 );
848 assert!(engine.scan_all_memories().is_empty());
849 }
850 }
851
852 #[test]
853 fn test_deleted_page_reused() {
854 let (_dir, engine) = setup();
855
856 let a = MemoryNode::new(AgentId::new(), MemoryType::Semantic, "a".into(), vec![1.0]);
857 let pid_a = engine.store_memory(&a).unwrap();
858 engine.delete_memory(pid_a).unwrap();
859
860 let b = MemoryNode::new(AgentId::new(), MemoryType::Semantic, "b".into(), vec![2.0]);
861 let pid_b = engine.store_memory(&b).unwrap();
862 assert_eq!(pid_a, pid_b, "freed page should be reused");
863
864 let loaded = engine.load_memory(pid_b).unwrap();
865 assert_eq!(loaded.content, "b");
866 }
867
868 #[test]
869 fn test_delete_reuse_crash_recovery() {
870 let dir = tempfile::tempdir().unwrap();
871 let pid;
872 let b_id;
873 {
874 let engine = StorageEngine::open(dir.path()).unwrap();
875 let a = MemoryNode::new(AgentId::new(), MemoryType::Semantic, "a".into(), vec![1.0]);
876 pid = engine.store_memory(&a).unwrap();
877 engine.delete_memory(pid).unwrap();
878 let b = MemoryNode::new(AgentId::new(), MemoryType::Semantic, "b".into(), vec![2.0]);
879 let pid_b = engine.store_memory(&b).unwrap();
880 assert_eq!(pid, pid_b);
881 b_id = b.id;
882 }
884 {
885 let engine = StorageEngine::open(dir.path()).unwrap();
886 let loaded = engine.load_memory(pid).unwrap();
887 assert_eq!(loaded.content, "b", "later write must win over the free");
888 assert_eq!(loaded.id, b_id);
889 let c = MemoryNode::new(AgentId::new(), MemoryType::Semantic, "c".into(), vec![3.0]);
892 let pid_c = engine.store_memory(&c).unwrap();
893 assert_ne!(pid_c, pid, "recovered free list must exclude reused page");
894 assert_eq!(engine.load_memory(pid).unwrap().content, "b");
895 }
896 }
897
898 #[test]
899 fn test_update_memory_in_place() {
900 let dir = tempfile::tempdir().unwrap();
901 let pid;
902 let id;
903 {
904 let engine = StorageEngine::open(dir.path()).unwrap();
905 let mut node = MemoryNode::new(
906 AgentId::new(),
907 MemoryType::Semantic,
908 "original".to_string(),
909 vec![1.0],
910 );
911 pid = engine.store_memory(&node).unwrap();
912 id = node.id;
913
914 node.content = "updated".to_string();
915 engine.update_memory(pid, &node).unwrap();
916
917 let loaded = engine.load_memory(pid).unwrap();
918 assert_eq!(loaded.content, "updated");
919 let scanned = engine.scan_all_memories();
921 assert_eq!(scanned.len(), 1);
922 engine.close().unwrap();
923 }
924 {
925 let engine = StorageEngine::open(dir.path()).unwrap();
926 let loaded = engine.load_memory(pid).unwrap();
927 assert_eq!(loaded.content, "updated");
928 assert_eq!(loaded.id, id);
929 assert_eq!(engine.scan_all_memories().len(), 1);
930 }
931 }
932
933 #[test]
934 fn test_concurrent_open_is_rejected() {
935 let dir = tempfile::tempdir().unwrap();
936
937 let engine1 = StorageEngine::open(dir.path()).unwrap();
942 let second = StorageEngine::open(dir.path());
943 assert!(second.is_err(), "second concurrent open must fail");
944 let msg = second.err().unwrap().to_string();
945 assert!(msg.contains("locked"), "error names the lock: {msg}");
946
947 engine1.close().unwrap();
949 let engine2 = StorageEngine::open(dir.path()).unwrap();
950 engine2.close().unwrap();
951 }
952
953 #[test]
954 fn test_concurrent_writes_from_threads() {
955 use std::sync::Arc;
956 let dir = tempfile::tempdir().unwrap();
957 let engine = Arc::new(StorageEngine::open(dir.path()).unwrap());
958
959 let handles: Vec<_> = (0..10)
960 .map(|i| {
961 let eng = Arc::clone(&engine);
962 std::thread::spawn(move || {
963 let node = MemoryNode::new(
964 AgentId::new(),
965 MemoryType::Episodic,
966 format!("thread-{i}"),
967 vec![i as f32],
968 );
969 eng.store_memory(&node).unwrap()
970 })
971 })
972 .collect();
973
974 let pids: Vec<PageId> = handles.into_iter().map(|h| h.join().unwrap()).collect();
975
976 for (i, pid) in pids.iter().enumerate() {
978 let loaded = engine.load_memory(*pid).unwrap();
979 assert_eq!(loaded.content, format!("thread-{i}"));
980 }
981 }
982}