1use std::path::Path;
4
5use mentedb_core::MemoryNode;
6use mentedb_core::error::{MenteError, MenteResult};
7
8use parking_lot::Mutex;
9use tracing::info;
10
11use crate::buffer::BufferPool;
12use crate::page::{PAGE_DATA_SIZE, Page, PageId, PageManager, PageType};
13use crate::wal::{Wal, WalEntryType};
14const DEFAULT_BUFFER_POOL_SIZE: usize = 1024;
16
17const WAL_AUTO_CHECKPOINT_BYTES: u64 = 8 * 1024 * 1024;
19
20pub struct StorageEngine {
38 page_manager: Mutex<PageManager>,
39 buffer_pool: BufferPool,
40 wal: Mutex<Wal>,
41}
42
43impl StorageEngine {
44 pub fn open(path: &Path) -> MenteResult<Self> {
59 std::fs::create_dir_all(path)?;
60
61 let page_manager = PageManager::open(path)?;
62 let buffer_pool = BufferPool::new(DEFAULT_BUFFER_POOL_SIZE);
63 let wal = Wal::open(path)?;
64
65 let engine = Self {
66 page_manager: Mutex::new(page_manager),
67 buffer_pool,
68 wal: Mutex::new(wal),
69 };
70
71 let recovered = engine.recover()?;
72 if recovered > 0 {
73 info!(recovered, ?path, "storage engine opened with WAL recovery");
74 } else {
75 info!(?path, "storage engine opened");
76 }
77
78 Ok(engine)
79 }
80
81 pub fn recover(&self) -> MenteResult<usize> {
86 let mut wal = self.wal.lock();
87 wal.lock_exclusive()?;
88 let entries = wal.iterate()?;
89 let mut count = 0usize;
90 let mut pm = self.page_manager.lock();
91
92 pm.reload_header()?;
94
95 let mut last_op: std::collections::HashMap<u64, &crate::wal::WalEntry> = Default::default();
101 let mut order: Vec<u64> = Vec::new();
102 for entry in &entries {
103 match entry.entry_type {
104 WalEntryType::PageWrite | WalEntryType::PageFree => {
105 if !last_op.contains_key(&entry.page_id) {
106 order.push(entry.page_id);
107 }
108 last_op.insert(entry.page_id, entry);
109 }
110 WalEntryType::Checkpoint | WalEntryType::Commit => {}
111 }
112 }
113
114 for page_id_raw in order {
115 let entry = last_op[&page_id_raw];
116 let page_id = PageId(entry.page_id);
117 match entry.entry_type {
118 WalEntryType::PageWrite => {
119 while pm.page_count() <= entry.page_id {
120 pm.allocate_page()?;
121 }
122
123 let mut page = pm.read_page(page_id)?;
124 let copy_len = entry.data.len().min(PAGE_DATA_SIZE);
125 page.data[..copy_len].copy_from_slice(&entry.data[..copy_len]);
126 if copy_len < PAGE_DATA_SIZE {
127 page.data[copy_len..].fill(0);
128 }
129 page.header.page_id = entry.page_id;
130 page.header.lsn = entry.lsn;
131 page.header.page_type = PageType::Data as u8;
132 page.header.free_space = (PAGE_DATA_SIZE - copy_len) as u16;
133 page.header.checksum = page.compute_checksum();
134
135 pm.write_page(page_id, &page)?;
136 count += 1;
137 }
138 WalEntryType::PageFree => {
139 if entry.page_id < pm.page_count() {
143 let mut page = Page::zeroed();
144 page.header.page_id = entry.page_id;
145 page.header.page_type = PageType::Free as u8;
146 pm.write_page(page_id, &page)?;
147 self.buffer_pool.invalidate(page_id);
148 count += 1;
149 }
150 }
151 WalEntryType::Checkpoint | WalEntryType::Commit => {}
152 }
153 }
154
155 if count > 0 {
156 pm.rebuild_free_list()?;
161 pm.sync()?;
162 let next_lsn = wal.next_lsn();
163 wal.truncate(next_lsn)?;
164 info!(count, "WAL recovery replayed entries");
165 }
166
167 wal.unlock()?;
168 Ok(count)
169 }
170
171 pub fn close(&self) -> MenteResult<()> {
182 let mut pm = self.page_manager.lock();
183 self.buffer_pool.flush_all(&mut pm)?;
184 pm.sync()?;
185 self.wal.lock().sync()?;
186 info!("storage engine closed");
187 Ok(())
188 }
189
190 pub fn allocate_page(&self) -> MenteResult<PageId> {
197 self.page_manager.lock().allocate_page()
198 }
199
200 pub fn read_page(&self, page_id: PageId) -> MenteResult<Box<Page>> {
202 self.buffer_pool
203 .fetch_page(page_id, &mut self.page_manager.lock())
204 }
205
206 pub fn write_page(&self, page_id: PageId, data: &[u8]) -> MenteResult<()> {
211 let lsn = {
212 let mut wal = self.wal.lock();
213 wal.lock_exclusive()?;
214 wal.reload_lsn()?;
215 let lsn = wal.append(WalEntryType::PageWrite, page_id.0, data)?;
216 wal.sync()?;
217 wal.unlock()?;
218 lsn
219 };
220
221 self.apply_page_write(page_id, data, lsn)
222 }
223
224 fn apply_page_write(&self, page_id: PageId, data: &[u8], lsn: u64) -> MenteResult<()> {
226 let mut pm = self.page_manager.lock();
227 let mut page = self.buffer_pool.fetch_page(page_id, &mut pm)?;
228 drop(pm);
229
230 let copy_len = data.len().min(PAGE_DATA_SIZE);
231 page.data[..copy_len].copy_from_slice(&data[..copy_len]);
232 if copy_len < PAGE_DATA_SIZE {
233 page.data[copy_len..].fill(0);
234 }
235 page.header.lsn = lsn;
236 page.header.page_type = PageType::Data as u8;
237 page.header.free_space = (PAGE_DATA_SIZE - copy_len) as u16;
238 page.header.checksum = page.compute_checksum();
239
240 if self.buffer_pool.update_page(page_id, &page).is_err() {
241 self.page_manager.lock().write_page(page_id, &page)?;
242 }
243 self.buffer_pool.unpin_page(page_id, true).ok();
244
245 Ok(())
246 }
247
248 pub fn store_memory(&self, node: &MemoryNode) -> MenteResult<PageId> {
272 let serialized =
273 serde_json::to_vec(node).map_err(|e| MenteError::Serialization(e.to_string()))?;
274
275 if serialized.len() + 4 > PAGE_DATA_SIZE {
276 return Err(MenteError::CapacityExceeded(format!(
277 "memory node serialized to {} bytes (max {})",
278 serialized.len(),
279 PAGE_DATA_SIZE - 4,
280 )));
281 }
282
283 let mut buf = Vec::with_capacity(4 + serialized.len());
284 buf.extend_from_slice(&(serialized.len() as u32).to_le_bytes());
285 buf.extend_from_slice(&serialized);
286
287 let (page_id, lsn) = {
289 let mut wal = self.wal.lock();
290 let mut pm = self.page_manager.lock();
291
292 wal.lock_exclusive()?;
294 pm.reload_header()?;
295 wal.reload_lsn()?;
296
297 let page_id = pm.allocate_page()?;
299
300 let lsn = wal.append(WalEntryType::PageWrite, page_id.0, &buf)?;
303 wal.sync()?;
304
305 let mut page = Page::zeroed();
307 page.header.page_id = page_id.0;
308 let copy_len = buf.len().min(PAGE_DATA_SIZE);
309 page.data[..copy_len].copy_from_slice(&buf[..copy_len]);
310 page.header.lsn = lsn;
311 page.header.page_type = PageType::Data as u8;
312 page.header.free_space = (PAGE_DATA_SIZE - copy_len) as u16;
313 page.header.checksum = page.compute_checksum();
314 pm.write_page(page_id, &page)?;
315
316 wal.unlock()?;
318
319 (page_id, lsn)
320 };
321
322 let _ = lsn;
325 self.buffer_pool.invalidate(page_id);
326
327 if self.wal.lock().file_size() > WAL_AUTO_CHECKPOINT_BYTES
330 && let Err(e) = self.checkpoint()
331 {
332 tracing::warn!("auto-checkpoint failed: {e}");
333 }
334
335 info!(
336 page_id = page_id.0,
337 bytes = serialized.len(),
338 "stored memory node"
339 );
340 Ok(page_id)
341 }
342
343 pub fn store_memory_batch(&self, nodes: &[MemoryNode]) -> MenteResult<Vec<PageId>> {
349 let mut bufs = Vec::with_capacity(nodes.len());
351 for node in nodes {
352 let serialized =
353 serde_json::to_vec(node).map_err(|e| MenteError::Serialization(e.to_string()))?;
354 if serialized.len() + 4 > PAGE_DATA_SIZE {
355 return Err(MenteError::CapacityExceeded(format!(
356 "memory node serialized to {} bytes (max {})",
357 serialized.len(),
358 PAGE_DATA_SIZE - 4,
359 )));
360 }
361 let mut buf = Vec::with_capacity(4 + serialized.len());
362 buf.extend_from_slice(&(serialized.len() as u32).to_le_bytes());
363 buf.extend_from_slice(&serialized);
364 bufs.push(buf);
365 }
366
367 let page_ids = {
369 let mut wal = self.wal.lock();
370 let mut pm = self.page_manager.lock();
371
372 wal.lock_exclusive()?;
373 pm.reload_header()?;
374 wal.reload_lsn()?;
375
376 let mut ids = Vec::with_capacity(bufs.len());
377 for buf in &bufs {
378 let page_id = pm.allocate_page()?;
379 let lsn = wal.append(WalEntryType::PageWrite, page_id.0, buf)?;
380
381 let mut page = Page::zeroed();
382 page.header.page_id = page_id.0;
383 let copy_len = buf.len().min(PAGE_DATA_SIZE);
384 page.data[..copy_len].copy_from_slice(&buf[..copy_len]);
385 page.header.lsn = lsn;
386 page.header.page_type = PageType::Data as u8;
387 page.header.free_space = (PAGE_DATA_SIZE - copy_len) as u16;
388 page.header.checksum = page.compute_checksum();
389 pm.write_page(page_id, &page)?;
390
391 ids.push(page_id);
392 }
393
394 wal.sync()?;
397 wal.unlock()?;
398
399 ids
400 };
401
402 for page_id in &page_ids {
404 self.buffer_pool.invalidate(*page_id);
405 }
406
407 if self.wal.lock().file_size() > WAL_AUTO_CHECKPOINT_BYTES
409 && let Err(e) = self.checkpoint()
410 {
411 tracing::warn!("auto-checkpoint failed: {e}");
412 }
413
414 info!(count = page_ids.len(), "stored memory batch");
415 Ok(page_ids)
416 }
417
418 pub fn update_memory(&self, page_id: PageId, node: &MemoryNode) -> MenteResult<()> {
424 let serialized =
425 serde_json::to_vec(node).map_err(|e| MenteError::Serialization(e.to_string()))?;
426
427 if serialized.len() + 4 > PAGE_DATA_SIZE {
428 return Err(MenteError::CapacityExceeded(format!(
429 "memory node serialized to {} bytes (max {})",
430 serialized.len(),
431 PAGE_DATA_SIZE - 4,
432 )));
433 }
434
435 let mut buf = Vec::with_capacity(4 + serialized.len());
436 buf.extend_from_slice(&(serialized.len() as u32).to_le_bytes());
437 buf.extend_from_slice(&serialized);
438
439 self.write_page(page_id, &buf)
440 }
441
442 pub fn delete_memory(&self, page_id: PageId) -> MenteResult<()> {
448 {
449 let mut wal = self.wal.lock();
450 let mut pm = self.page_manager.lock();
451
452 wal.lock_exclusive()?;
453 pm.reload_header()?;
454 wal.reload_lsn()?;
455
456 wal.append(WalEntryType::PageFree, page_id.0, &[])?;
459 wal.sync()?;
460
461 pm.free_page(page_id)?;
462 wal.unlock()?;
463 }
464
465 self.buffer_pool.invalidate(page_id);
467
468 info!(page_id = page_id.0, "deleted memory node");
469 Ok(())
470 }
471
472 pub fn load_memory(&self, page_id: PageId) -> MenteResult<MemoryNode> {
484 let page = self.read_page(page_id)?;
485 self.buffer_pool.unpin_page(page_id, false).ok();
486
487 if PageType::from(page.header.page_type) != PageType::Data {
488 return Err(MenteError::Storage(format!(
489 "page {} is not a data page",
490 page_id.0
491 )));
492 }
493
494 let len = u32::from_le_bytes(page.data[..4].try_into().unwrap()) as usize;
495 if len == 0 || len + 4 > PAGE_DATA_SIZE {
496 return Err(MenteError::Storage(format!(
497 "invalid memory node length prefix: {len}"
498 )));
499 }
500
501 serde_json::from_slice(&page.data[4..4 + len])
502 .map_err(|e| MenteError::Serialization(e.to_string()))
503 }
504
505 pub fn checkpoint(&self) -> MenteResult<()> {
519 let mut wal = self.wal.lock();
520 let mut pm = self.page_manager.lock();
521
522 wal.lock_exclusive()?;
523 wal.reload_lsn()?;
524
525 self.buffer_pool.flush_all(&mut pm)?;
526 pm.sync()?;
527
528 let lsn = wal.append(WalEntryType::Checkpoint, 0, &[])?;
529 wal.sync()?;
530 wal.truncate(lsn)?;
531 wal.unlock()?;
532
533 info!(lsn, "checkpoint complete");
534 Ok(())
535 }
536
537 pub fn scan_all_memories(&self) -> Vec<(mentedb_core::types::MemoryId, PageId)> {
554 let mut pm = self.page_manager.lock();
555 let _ = pm.reload_header();
557 let count = pm.page_count();
558 drop(pm);
559
560 let mut results = Vec::new();
561 for i in 1..count {
562 let page_id = PageId(i);
563 if let Ok(node) = self.load_memory(page_id) {
564 results.push((node.id, page_id));
565 }
566 }
567 results
568 }
569}
570
571#[cfg(test)]
572mod tests {
573 use super::*;
574 use mentedb_core::memory::MemoryType;
575 use mentedb_core::types::AgentId;
576
577 fn setup() -> (tempfile::TempDir, StorageEngine) {
578 let dir = tempfile::tempdir().unwrap();
579 let engine = StorageEngine::open(dir.path()).unwrap();
580 (dir, engine)
581 }
582
583 #[test]
584 fn test_allocate_write_read() {
585 let (_dir, engine) = setup();
586
587 let pid = engine.allocate_page().unwrap();
588 engine.write_page(pid, b"hello storage engine").unwrap();
589
590 let page = engine.read_page(pid).unwrap();
591 assert_eq!(&page.data[..20], b"hello storage engine");
592 engine.buffer_pool.unpin_page(pid, false).ok();
593 }
594
595 #[test]
596 fn test_store_and_load_memory() {
597 let (_dir, engine) = setup();
598
599 let node = MemoryNode::new(
600 AgentId::new(),
601 MemoryType::Episodic,
602 "The user prefers Rust over Go".to_string(),
603 vec![0.1, 0.2, 0.3, 0.4],
604 );
605
606 let page_id = engine.store_memory(&node).unwrap();
607 let loaded = engine.load_memory(page_id).unwrap();
608
609 assert_eq!(node.id, loaded.id);
610 assert_eq!(node.content, loaded.content);
611 assert_eq!(node.embedding, loaded.embedding);
612 assert_eq!(node.memory_type, loaded.memory_type);
613 }
614
615 #[test]
616 fn test_checkpoint() {
617 let (_dir, engine) = setup();
618
619 let node = MemoryNode::new(
620 AgentId::new(),
621 MemoryType::Semantic,
622 "checkpoint test".to_string(),
623 vec![1.0, 2.0],
624 );
625
626 let pid = engine.store_memory(&node).unwrap();
627 engine.checkpoint().unwrap();
628
629 let loaded = engine.load_memory(pid).unwrap();
630 assert_eq!(loaded.content, "checkpoint test");
631 }
632
633 #[test]
634 fn test_close_and_reopen() {
635 let dir = tempfile::tempdir().unwrap();
636 let pid;
637 {
638 let engine = StorageEngine::open(dir.path()).unwrap();
639 let node = MemoryNode::new(
640 AgentId::new(),
641 MemoryType::Procedural,
642 "persist across close".to_string(),
643 vec![0.5],
644 );
645 pid = engine.store_memory(&node).unwrap();
646 engine.close().unwrap();
647 }
648 {
649 let engine = StorageEngine::open(dir.path()).unwrap();
650 let loaded = engine.load_memory(pid).unwrap();
651 assert_eq!(loaded.content, "persist across close");
652 }
653 }
654
655 #[test]
656 fn test_crash_recovery() {
657 let dir = tempfile::tempdir().unwrap();
658 let mut ids = Vec::new();
659 let mut contents = Vec::new();
660 {
661 let engine = StorageEngine::open(dir.path()).unwrap();
662 for i in 0..3 {
663 let content = format!("crash-recovery-{i}");
664 let node = MemoryNode::new(
665 AgentId::new(),
666 MemoryType::Episodic,
667 content.clone(),
668 vec![i as f32],
669 );
670 let pid = engine.store_memory(&node).unwrap();
671 ids.push(pid);
672 contents.push(content);
673 }
674 engine.wal.lock().sync().unwrap();
676 }
677 {
678 let engine = StorageEngine::open(dir.path()).unwrap();
679 for (pid, expected) in ids.iter().zip(contents.iter()) {
680 let loaded = engine.load_memory(*pid).unwrap();
681 assert_eq!(&loaded.content, expected);
682 }
683 }
684 }
685
686 #[test]
687 fn test_recovery_idempotent() {
688 let dir = tempfile::tempdir().unwrap();
689 let pid;
690 let content = "idempotent-check".to_string();
691 {
692 let engine = StorageEngine::open(dir.path()).unwrap();
693 let node = MemoryNode::new(
694 AgentId::new(),
695 MemoryType::Semantic,
696 content.clone(),
697 vec![1.0, 2.0],
698 );
699 pid = engine.store_memory(&node).unwrap();
700 engine.checkpoint().unwrap();
701 engine.close().unwrap();
702 }
703 {
704 let engine = StorageEngine::open(dir.path()).unwrap();
705 let loaded = engine.load_memory(pid).unwrap();
706 assert_eq!(loaded.content, content);
707 }
708 }
709
710 #[test]
711 fn test_partial_write_recovery() {
712 let dir = tempfile::tempdir().unwrap();
713 let mut ids = Vec::new();
714 let mut contents = Vec::new();
715 {
716 let engine = StorageEngine::open(dir.path()).unwrap();
717 for i in 0..3 {
718 let content = format!("checkpointed-{i}");
719 let node = MemoryNode::new(
720 AgentId::new(),
721 MemoryType::Semantic,
722 content.clone(),
723 vec![i as f32],
724 );
725 let pid = engine.store_memory(&node).unwrap();
726 ids.push(pid);
727 contents.push(content);
728 }
729 engine.checkpoint().unwrap();
730
731 for i in 3..5 {
732 let content = format!("unckeckpointed-{i}");
733 let node = MemoryNode::new(
734 AgentId::new(),
735 MemoryType::Episodic,
736 content.clone(),
737 vec![i as f32],
738 );
739 let pid = engine.store_memory(&node).unwrap();
740 ids.push(pid);
741 contents.push(content);
742 }
743 engine.wal.lock().sync().unwrap();
745 }
746 {
747 let engine = StorageEngine::open(dir.path()).unwrap();
748 for (pid, expected) in ids.iter().zip(contents.iter()) {
749 let loaded = engine.load_memory(*pid).unwrap();
750 assert_eq!(&loaded.content, expected);
751 }
752 }
753 }
754
755 #[test]
756 fn test_delete_memory_durable() {
757 let dir = tempfile::tempdir().unwrap();
758 let pid;
759 {
760 let engine = StorageEngine::open(dir.path()).unwrap();
761 let node = MemoryNode::new(
762 AgentId::new(),
763 MemoryType::Semantic,
764 "to be deleted".to_string(),
765 vec![1.0],
766 );
767 pid = engine.store_memory(&node).unwrap();
768 engine.delete_memory(pid).unwrap();
769 assert!(engine.load_memory(pid).is_err());
770 assert!(engine.scan_all_memories().is_empty());
771 engine.close().unwrap();
772 }
773 {
774 let engine = StorageEngine::open(dir.path()).unwrap();
775 assert!(
776 engine.load_memory(pid).is_err(),
777 "deleted memory must not resurrect on reopen"
778 );
779 assert!(engine.scan_all_memories().is_empty());
780 }
781 }
782
783 #[test]
784 fn test_delete_survives_crash() {
785 let dir = tempfile::tempdir().unwrap();
786 let pid;
787 {
788 let engine = StorageEngine::open(dir.path()).unwrap();
789 let node = MemoryNode::new(
790 AgentId::new(),
791 MemoryType::Semantic,
792 "crash delete".to_string(),
793 vec![1.0],
794 );
795 pid = engine.store_memory(&node).unwrap();
796 engine.delete_memory(pid).unwrap();
797 }
799 {
800 let engine = StorageEngine::open(dir.path()).unwrap();
801 assert!(
802 engine.load_memory(pid).is_err(),
803 "deletion must survive a crash via WAL replay"
804 );
805 assert!(engine.scan_all_memories().is_empty());
806 }
807 }
808
809 #[test]
810 fn test_deleted_page_reused() {
811 let (_dir, engine) = setup();
812
813 let a = MemoryNode::new(AgentId::new(), MemoryType::Semantic, "a".into(), vec![1.0]);
814 let pid_a = engine.store_memory(&a).unwrap();
815 engine.delete_memory(pid_a).unwrap();
816
817 let b = MemoryNode::new(AgentId::new(), MemoryType::Semantic, "b".into(), vec![2.0]);
818 let pid_b = engine.store_memory(&b).unwrap();
819 assert_eq!(pid_a, pid_b, "freed page should be reused");
820
821 let loaded = engine.load_memory(pid_b).unwrap();
822 assert_eq!(loaded.content, "b");
823 }
824
825 #[test]
826 fn test_delete_reuse_crash_recovery() {
827 let dir = tempfile::tempdir().unwrap();
828 let pid;
829 let b_id;
830 {
831 let engine = StorageEngine::open(dir.path()).unwrap();
832 let a = MemoryNode::new(AgentId::new(), MemoryType::Semantic, "a".into(), vec![1.0]);
833 pid = engine.store_memory(&a).unwrap();
834 engine.delete_memory(pid).unwrap();
835 let b = MemoryNode::new(AgentId::new(), MemoryType::Semantic, "b".into(), vec![2.0]);
836 let pid_b = engine.store_memory(&b).unwrap();
837 assert_eq!(pid, pid_b);
838 b_id = b.id;
839 }
841 {
842 let engine = StorageEngine::open(dir.path()).unwrap();
843 let loaded = engine.load_memory(pid).unwrap();
844 assert_eq!(loaded.content, "b", "later write must win over the free");
845 assert_eq!(loaded.id, b_id);
846 let c = MemoryNode::new(AgentId::new(), MemoryType::Semantic, "c".into(), vec![3.0]);
849 let pid_c = engine.store_memory(&c).unwrap();
850 assert_ne!(pid_c, pid, "recovered free list must exclude reused page");
851 assert_eq!(engine.load_memory(pid).unwrap().content, "b");
852 }
853 }
854
855 #[test]
856 fn test_update_memory_in_place() {
857 let dir = tempfile::tempdir().unwrap();
858 let pid;
859 let id;
860 {
861 let engine = StorageEngine::open(dir.path()).unwrap();
862 let mut node = MemoryNode::new(
863 AgentId::new(),
864 MemoryType::Semantic,
865 "original".to_string(),
866 vec![1.0],
867 );
868 pid = engine.store_memory(&node).unwrap();
869 id = node.id;
870
871 node.content = "updated".to_string();
872 engine.update_memory(pid, &node).unwrap();
873
874 let loaded = engine.load_memory(pid).unwrap();
875 assert_eq!(loaded.content, "updated");
876 let scanned = engine.scan_all_memories();
878 assert_eq!(scanned.len(), 1);
879 engine.close().unwrap();
880 }
881 {
882 let engine = StorageEngine::open(dir.path()).unwrap();
883 let loaded = engine.load_memory(pid).unwrap();
884 assert_eq!(loaded.content, "updated");
885 assert_eq!(loaded.id, id);
886 assert_eq!(engine.scan_all_memories().len(), 1);
887 }
888 }
889
890 #[test]
891 fn test_concurrent_open_no_lock_conflict() {
892 let dir = tempfile::tempdir().unwrap();
893
894 let engine1 = StorageEngine::open(dir.path()).unwrap();
897 let engine2 = StorageEngine::open(dir.path()).unwrap();
898
899 let node1 = MemoryNode::new(
901 AgentId::new(),
902 MemoryType::Episodic,
903 "from engine 1".to_string(),
904 vec![1.0],
905 );
906 let node2 = MemoryNode::new(
907 AgentId::new(),
908 MemoryType::Episodic,
909 "from engine 2".to_string(),
910 vec![2.0],
911 );
912
913 let pid1 = engine1.store_memory(&node1).unwrap();
914 let pid2 = engine2.store_memory(&node2).unwrap();
915
916 let loaded1 = engine1.load_memory(pid1).unwrap();
918 assert_eq!(loaded1.content, "from engine 1");
919
920 let loaded2 = engine2.load_memory(pid2).unwrap();
921 assert_eq!(loaded2.content, "from engine 2");
922 }
923
924 #[test]
925 fn test_concurrent_writes_from_threads() {
926 use std::sync::Arc;
927 let dir = tempfile::tempdir().unwrap();
928 let engine = Arc::new(StorageEngine::open(dir.path()).unwrap());
929
930 let handles: Vec<_> = (0..10)
931 .map(|i| {
932 let eng = Arc::clone(&engine);
933 std::thread::spawn(move || {
934 let node = MemoryNode::new(
935 AgentId::new(),
936 MemoryType::Episodic,
937 format!("thread-{i}"),
938 vec![i as f32],
939 );
940 eng.store_memory(&node).unwrap()
941 })
942 })
943 .collect();
944
945 let pids: Vec<PageId> = handles.into_iter().map(|h| h.join().unwrap()).collect();
946
947 for (i, pid) in pids.iter().enumerate() {
949 let loaded = engine.load_memory(*pid).unwrap();
950 assert_eq!(loaded.content, format!("thread-{i}"));
951 }
952 }
953}