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 match crate::lock::try_lock_exclusive(&lock_file) {
85 Ok(true) => {}
86 Ok(false) => {
87 return Err(MenteError::Storage(format!(
88 "database directory {} is locked by another process",
89 path.display()
90 )));
91 }
92 Err(e) => {
93 return Err(MenteError::Storage(format!(
94 "failed to lock database directory {}: {e}",
95 path.display()
96 )));
97 }
98 }
99
100 let page_manager = PageManager::open(path)?;
101 let buffer_pool = BufferPool::new(DEFAULT_BUFFER_POOL_SIZE);
102 let wal = Wal::open(path)?;
103
104 let engine = Self {
105 page_manager: Mutex::new(page_manager),
106 buffer_pool,
107 wal: Mutex::new(wal),
108 process_lock: Mutex::new(Some(lock_file)),
109 };
110
111 let recovered = engine.recover()?;
112 if recovered > 0 {
113 info!(recovered, ?path, "storage engine opened with WAL recovery");
114 } else {
115 info!(?path, "storage engine opened");
116 }
117
118 Ok(engine)
119 }
120
121 pub fn recover(&self) -> MenteResult<usize> {
126 let mut wal = self.wal.lock();
127 wal.lock_exclusive()?;
128 let entries = wal.iterate()?;
129 let mut count = 0usize;
130 let mut pm = self.page_manager.lock();
131
132 pm.reload_header()?;
134
135 let mut last_op: std::collections::HashMap<u64, &crate::wal::WalEntry> = Default::default();
141 let mut order: Vec<u64> = Vec::new();
142 for entry in &entries {
143 match entry.entry_type {
144 WalEntryType::PageWrite | WalEntryType::PageFree => {
145 if !last_op.contains_key(&entry.page_id) {
146 order.push(entry.page_id);
147 }
148 last_op.insert(entry.page_id, entry);
149 }
150 WalEntryType::Checkpoint | WalEntryType::Commit => {}
151 }
152 }
153
154 for page_id_raw in order {
155 let entry = last_op[&page_id_raw];
156 let page_id = PageId(entry.page_id);
157 match entry.entry_type {
158 WalEntryType::PageWrite => {
159 while pm.page_count() <= entry.page_id {
160 pm.allocate_page()?;
161 }
162
163 let mut page = pm.read_page(page_id)?;
164 let copy_len = entry.data.len().min(PAGE_DATA_SIZE);
165 page.data[..copy_len].copy_from_slice(&entry.data[..copy_len]);
166 if copy_len < PAGE_DATA_SIZE {
167 page.data[copy_len..].fill(0);
168 }
169 page.header.page_id = entry.page_id;
170 page.header.lsn = entry.lsn;
171 page.header.page_type = PageType::Data as u8;
172 page.header.free_space = (PAGE_DATA_SIZE - copy_len) as u16;
173 page.header.checksum = page.compute_checksum();
174
175 pm.write_page(page_id, &page)?;
176 count += 1;
177 }
178 WalEntryType::PageFree => {
179 if entry.page_id < pm.page_count() {
183 let mut page = Page::zeroed();
184 page.header.page_id = entry.page_id;
185 page.header.page_type = PageType::Free as u8;
186 pm.write_page(page_id, &page)?;
187 self.buffer_pool.invalidate(page_id);
188 count += 1;
189 }
190 }
191 WalEntryType::Checkpoint | WalEntryType::Commit => {}
192 }
193 }
194
195 if count > 0 {
196 pm.rebuild_free_list()?;
201 pm.sync()?;
202 let next_lsn = wal.next_lsn();
203 wal.truncate(next_lsn)?;
204 info!(count, "WAL recovery replayed entries");
205 }
206
207 wal.unlock()?;
208 Ok(count)
209 }
210
211 pub fn close(&self) -> MenteResult<()> {
222 let mut pm = self.page_manager.lock();
223 self.buffer_pool.flush_all(&mut pm)?;
224 pm.sync()?;
225 self.wal.lock().sync()?;
226 if let Some(lock_file) = self.process_lock.lock().take() {
229 let _ = crate::lock::unlock(&lock_file);
230 }
231 info!("storage engine closed");
232 Ok(())
233 }
234
235 #[doc(hidden)]
240 pub fn release_process_lock(&self) {
241 if let Some(lock_file) = self.process_lock.lock().take() {
242 let _ = crate::lock::unlock(&lock_file);
243 }
244 }
245
246 pub fn allocate_page(&self) -> MenteResult<PageId> {
253 self.page_manager.lock().allocate_page()
254 }
255
256 pub fn read_page(&self, page_id: PageId) -> MenteResult<Box<Page>> {
258 self.buffer_pool
259 .fetch_page(page_id, &mut self.page_manager.lock())
260 }
261
262 pub fn write_page(&self, page_id: PageId, data: &[u8]) -> MenteResult<()> {
267 let lsn = {
268 let mut wal = self.wal.lock();
269 wal.lock_exclusive()?;
270 wal.reload_lsn()?;
271 let lsn = wal.append(WalEntryType::PageWrite, page_id.0, data)?;
272 wal.sync()?;
273 wal.unlock()?;
274 lsn
275 };
276
277 self.apply_page_write(page_id, data, lsn)
278 }
279
280 fn apply_page_write(&self, page_id: PageId, data: &[u8], lsn: u64) -> MenteResult<()> {
282 let mut pm = self.page_manager.lock();
283 let mut page = self.buffer_pool.fetch_page(page_id, &mut pm)?;
284 drop(pm);
285
286 let copy_len = data.len().min(PAGE_DATA_SIZE);
287 page.data[..copy_len].copy_from_slice(&data[..copy_len]);
288 if copy_len < PAGE_DATA_SIZE {
289 page.data[copy_len..].fill(0);
290 }
291 page.header.lsn = lsn;
292 page.header.page_type = PageType::Data as u8;
293 page.header.free_space = (PAGE_DATA_SIZE - copy_len) as u16;
294 page.header.checksum = page.compute_checksum();
295
296 if self.buffer_pool.update_page(page_id, &page).is_err() {
297 self.page_manager.lock().write_page(page_id, &page)?;
298 }
299 self.buffer_pool.unpin_page(page_id, true).ok();
300
301 Ok(())
302 }
303
304 pub fn store_memory(&self, node: &MemoryNode) -> MenteResult<PageId> {
328 let serialized =
329 serde_json::to_vec(node).map_err(|e| MenteError::Serialization(e.to_string()))?;
330
331 if serialized.len() + 4 > PAGE_DATA_SIZE {
332 return Err(MenteError::CapacityExceeded(format!(
333 "memory node serialized to {} bytes (max {})",
334 serialized.len(),
335 PAGE_DATA_SIZE - 4,
336 )));
337 }
338
339 let mut buf = Vec::with_capacity(4 + serialized.len());
340 buf.extend_from_slice(&(serialized.len() as u32).to_le_bytes());
341 buf.extend_from_slice(&serialized);
342
343 let (page_id, lsn) = {
345 let mut wal = self.wal.lock();
346 let mut pm = self.page_manager.lock();
347
348 wal.lock_exclusive()?;
350 pm.reload_header()?;
351 wal.reload_lsn()?;
352
353 let page_id = pm.allocate_page()?;
355
356 let lsn = wal.append(WalEntryType::PageWrite, page_id.0, &buf)?;
359 wal.sync()?;
360
361 let mut page = Page::zeroed();
363 page.header.page_id = page_id.0;
364 let copy_len = buf.len().min(PAGE_DATA_SIZE);
365 page.data[..copy_len].copy_from_slice(&buf[..copy_len]);
366 page.header.lsn = lsn;
367 page.header.page_type = PageType::Data as u8;
368 page.header.free_space = (PAGE_DATA_SIZE - copy_len) as u16;
369 page.header.checksum = page.compute_checksum();
370 pm.write_page(page_id, &page)?;
371
372 wal.unlock()?;
374
375 (page_id, lsn)
376 };
377
378 let _ = lsn;
381 self.buffer_pool.invalidate(page_id);
382
383 if self.wal.lock().file_size() > WAL_AUTO_CHECKPOINT_BYTES
386 && let Err(e) = self.checkpoint()
387 {
388 tracing::warn!("auto-checkpoint failed: {e}");
389 }
390
391 info!(
392 page_id = page_id.0,
393 bytes = serialized.len(),
394 "stored memory node"
395 );
396 Ok(page_id)
397 }
398
399 pub fn store_memory_batch(&self, nodes: &[MemoryNode]) -> MenteResult<Vec<PageId>> {
405 let mut bufs = Vec::with_capacity(nodes.len());
407 for node in nodes {
408 let serialized =
409 serde_json::to_vec(node).map_err(|e| MenteError::Serialization(e.to_string()))?;
410 if serialized.len() + 4 > PAGE_DATA_SIZE {
411 return Err(MenteError::CapacityExceeded(format!(
412 "memory node serialized to {} bytes (max {})",
413 serialized.len(),
414 PAGE_DATA_SIZE - 4,
415 )));
416 }
417 let mut buf = Vec::with_capacity(4 + serialized.len());
418 buf.extend_from_slice(&(serialized.len() as u32).to_le_bytes());
419 buf.extend_from_slice(&serialized);
420 bufs.push(buf);
421 }
422
423 let page_ids = {
425 let mut wal = self.wal.lock();
426 let mut pm = self.page_manager.lock();
427
428 wal.lock_exclusive()?;
429 pm.reload_header()?;
430 wal.reload_lsn()?;
431
432 let mut ids = Vec::with_capacity(bufs.len());
433 for buf in &bufs {
434 let page_id = pm.allocate_page()?;
435 let lsn = wal.append(WalEntryType::PageWrite, page_id.0, buf)?;
436
437 let mut page = Page::zeroed();
438 page.header.page_id = page_id.0;
439 let copy_len = buf.len().min(PAGE_DATA_SIZE);
440 page.data[..copy_len].copy_from_slice(&buf[..copy_len]);
441 page.header.lsn = lsn;
442 page.header.page_type = PageType::Data as u8;
443 page.header.free_space = (PAGE_DATA_SIZE - copy_len) as u16;
444 page.header.checksum = page.compute_checksum();
445 pm.write_page(page_id, &page)?;
446
447 ids.push(page_id);
448 }
449
450 wal.sync()?;
453 wal.unlock()?;
454
455 ids
456 };
457
458 for page_id in &page_ids {
460 self.buffer_pool.invalidate(*page_id);
461 }
462
463 if self.wal.lock().file_size() > WAL_AUTO_CHECKPOINT_BYTES
465 && let Err(e) = self.checkpoint()
466 {
467 tracing::warn!("auto-checkpoint failed: {e}");
468 }
469
470 info!(count = page_ids.len(), "stored memory batch");
471 Ok(page_ids)
472 }
473
474 pub fn update_memory(&self, page_id: PageId, node: &MemoryNode) -> MenteResult<()> {
480 let serialized =
481 serde_json::to_vec(node).map_err(|e| MenteError::Serialization(e.to_string()))?;
482
483 if serialized.len() + 4 > PAGE_DATA_SIZE {
484 return Err(MenteError::CapacityExceeded(format!(
485 "memory node serialized to {} bytes (max {})",
486 serialized.len(),
487 PAGE_DATA_SIZE - 4,
488 )));
489 }
490
491 let mut buf = Vec::with_capacity(4 + serialized.len());
492 buf.extend_from_slice(&(serialized.len() as u32).to_le_bytes());
493 buf.extend_from_slice(&serialized);
494
495 self.write_page(page_id, &buf)
496 }
497
498 pub fn delete_memory(&self, page_id: PageId) -> MenteResult<()> {
504 {
505 let mut wal = self.wal.lock();
506 let mut pm = self.page_manager.lock();
507
508 wal.lock_exclusive()?;
509 pm.reload_header()?;
510 wal.reload_lsn()?;
511
512 wal.append(WalEntryType::PageFree, page_id.0, &[])?;
515 wal.sync()?;
516
517 pm.free_page(page_id)?;
518 wal.unlock()?;
519 }
520
521 self.buffer_pool.invalidate(page_id);
523
524 info!(page_id = page_id.0, "deleted memory node");
525 Ok(())
526 }
527
528 pub fn load_memory(&self, page_id: PageId) -> MenteResult<MemoryNode> {
540 let page = self.read_page(page_id)?;
541 self.buffer_pool.unpin_page(page_id, false).ok();
542
543 if PageType::from(page.header.page_type) != PageType::Data {
544 return Err(MenteError::Storage(format!(
545 "page {} is not a data page",
546 page_id.0
547 )));
548 }
549
550 let len = u32::from_le_bytes(page.data[..4].try_into().unwrap()) as usize;
551 if len == 0 || len + 4 > PAGE_DATA_SIZE {
552 return Err(MenteError::Storage(format!(
553 "invalid memory node length prefix: {len}"
554 )));
555 }
556
557 serde_json::from_slice(&page.data[4..4 + len])
558 .map_err(|e| MenteError::Serialization(e.to_string()))
559 }
560
561 pub fn checkpoint(&self) -> MenteResult<()> {
575 let mut wal = self.wal.lock();
576 let mut pm = self.page_manager.lock();
577
578 wal.lock_exclusive()?;
579 wal.reload_lsn()?;
580
581 self.buffer_pool.flush_all(&mut pm)?;
582 pm.sync()?;
583
584 let lsn = wal.append(WalEntryType::Checkpoint, 0, &[])?;
585 wal.sync()?;
586 wal.truncate(lsn)?;
587 wal.unlock()?;
588
589 info!(lsn, "checkpoint complete");
590 Ok(())
591 }
592
593 pub fn scan_all_memories(&self) -> Vec<(mentedb_core::types::MemoryId, PageId)> {
610 let mut pm = self.page_manager.lock();
611 let _ = pm.reload_header();
613 let count = pm.page_count();
614 drop(pm);
615
616 let mut results = Vec::new();
617 for i in 1..count {
618 let page_id = PageId(i);
619 if let Ok(node) = self.load_memory(page_id) {
620 results.push((node.id, page_id));
621 }
622 }
623 results
624 }
625}
626
627#[cfg(test)]
628mod tests {
629 use super::*;
630 use mentedb_core::memory::MemoryType;
631 use mentedb_core::types::AgentId;
632
633 fn setup() -> (tempfile::TempDir, StorageEngine) {
634 let dir = tempfile::tempdir().unwrap();
635 let engine = StorageEngine::open(dir.path()).unwrap();
636 (dir, engine)
637 }
638
639 #[test]
640 fn test_allocate_write_read() {
641 let (_dir, engine) = setup();
642
643 let pid = engine.allocate_page().unwrap();
644 engine.write_page(pid, b"hello storage engine").unwrap();
645
646 let page = engine.read_page(pid).unwrap();
647 assert_eq!(&page.data[..20], b"hello storage engine");
648 engine.buffer_pool.unpin_page(pid, false).ok();
649 }
650
651 #[test]
652 fn test_store_and_load_memory() {
653 let (_dir, engine) = setup();
654
655 let node = MemoryNode::new(
656 AgentId::new(),
657 MemoryType::Episodic,
658 "The user prefers Rust over Go".to_string(),
659 vec![0.1, 0.2, 0.3, 0.4],
660 );
661
662 let page_id = engine.store_memory(&node).unwrap();
663 let loaded = engine.load_memory(page_id).unwrap();
664
665 assert_eq!(node.id, loaded.id);
666 assert_eq!(node.content, loaded.content);
667 assert_eq!(node.embedding, loaded.embedding);
668 assert_eq!(node.memory_type, loaded.memory_type);
669 }
670
671 #[test]
672 fn test_checkpoint() {
673 let (_dir, engine) = setup();
674
675 let node = MemoryNode::new(
676 AgentId::new(),
677 MemoryType::Semantic,
678 "checkpoint test".to_string(),
679 vec![1.0, 2.0],
680 );
681
682 let pid = engine.store_memory(&node).unwrap();
683 engine.checkpoint().unwrap();
684
685 let loaded = engine.load_memory(pid).unwrap();
686 assert_eq!(loaded.content, "checkpoint test");
687 }
688
689 #[test]
690 fn test_close_and_reopen() {
691 let dir = tempfile::tempdir().unwrap();
692 let pid;
693 {
694 let engine = StorageEngine::open(dir.path()).unwrap();
695 let node = MemoryNode::new(
696 AgentId::new(),
697 MemoryType::Procedural,
698 "persist across close".to_string(),
699 vec![0.5],
700 );
701 pid = engine.store_memory(&node).unwrap();
702 engine.close().unwrap();
703 }
704 {
705 let engine = StorageEngine::open(dir.path()).unwrap();
706 let loaded = engine.load_memory(pid).unwrap();
707 assert_eq!(loaded.content, "persist across close");
708 }
709 }
710
711 #[test]
712 fn test_crash_recovery() {
713 let dir = tempfile::tempdir().unwrap();
714 let mut ids = Vec::new();
715 let mut contents = Vec::new();
716 {
717 let engine = StorageEngine::open(dir.path()).unwrap();
718 for i in 0..3 {
719 let content = format!("crash-recovery-{i}");
720 let node = MemoryNode::new(
721 AgentId::new(),
722 MemoryType::Episodic,
723 content.clone(),
724 vec![i as f32],
725 );
726 let pid = engine.store_memory(&node).unwrap();
727 ids.push(pid);
728 contents.push(content);
729 }
730 engine.wal.lock().sync().unwrap();
732 }
733 {
734 let engine = StorageEngine::open(dir.path()).unwrap();
735 for (pid, expected) in ids.iter().zip(contents.iter()) {
736 let loaded = engine.load_memory(*pid).unwrap();
737 assert_eq!(&loaded.content, expected);
738 }
739 }
740 }
741
742 #[test]
743 fn test_recovery_idempotent() {
744 let dir = tempfile::tempdir().unwrap();
745 let pid;
746 let content = "idempotent-check".to_string();
747 {
748 let engine = StorageEngine::open(dir.path()).unwrap();
749 let node = MemoryNode::new(
750 AgentId::new(),
751 MemoryType::Semantic,
752 content.clone(),
753 vec![1.0, 2.0],
754 );
755 pid = engine.store_memory(&node).unwrap();
756 engine.checkpoint().unwrap();
757 engine.close().unwrap();
758 }
759 {
760 let engine = StorageEngine::open(dir.path()).unwrap();
761 let loaded = engine.load_memory(pid).unwrap();
762 assert_eq!(loaded.content, content);
763 }
764 }
765
766 #[test]
767 fn test_partial_write_recovery() {
768 let dir = tempfile::tempdir().unwrap();
769 let mut ids = Vec::new();
770 let mut contents = Vec::new();
771 {
772 let engine = StorageEngine::open(dir.path()).unwrap();
773 for i in 0..3 {
774 let content = format!("checkpointed-{i}");
775 let node = MemoryNode::new(
776 AgentId::new(),
777 MemoryType::Semantic,
778 content.clone(),
779 vec![i as f32],
780 );
781 let pid = engine.store_memory(&node).unwrap();
782 ids.push(pid);
783 contents.push(content);
784 }
785 engine.checkpoint().unwrap();
786
787 for i in 3..5 {
788 let content = format!("unckeckpointed-{i}");
789 let node = MemoryNode::new(
790 AgentId::new(),
791 MemoryType::Episodic,
792 content.clone(),
793 vec![i as f32],
794 );
795 let pid = engine.store_memory(&node).unwrap();
796 ids.push(pid);
797 contents.push(content);
798 }
799 engine.wal.lock().sync().unwrap();
801 }
802 {
803 let engine = StorageEngine::open(dir.path()).unwrap();
804 for (pid, expected) in ids.iter().zip(contents.iter()) {
805 let loaded = engine.load_memory(*pid).unwrap();
806 assert_eq!(&loaded.content, expected);
807 }
808 }
809 }
810
811 #[test]
812 fn test_delete_memory_durable() {
813 let dir = tempfile::tempdir().unwrap();
814 let pid;
815 {
816 let engine = StorageEngine::open(dir.path()).unwrap();
817 let node = MemoryNode::new(
818 AgentId::new(),
819 MemoryType::Semantic,
820 "to be deleted".to_string(),
821 vec![1.0],
822 );
823 pid = engine.store_memory(&node).unwrap();
824 engine.delete_memory(pid).unwrap();
825 assert!(engine.load_memory(pid).is_err());
826 assert!(engine.scan_all_memories().is_empty());
827 engine.close().unwrap();
828 }
829 {
830 let engine = StorageEngine::open(dir.path()).unwrap();
831 assert!(
832 engine.load_memory(pid).is_err(),
833 "deleted memory must not resurrect on reopen"
834 );
835 assert!(engine.scan_all_memories().is_empty());
836 }
837 }
838
839 #[test]
840 fn test_delete_survives_crash() {
841 let dir = tempfile::tempdir().unwrap();
842 let pid;
843 {
844 let engine = StorageEngine::open(dir.path()).unwrap();
845 let node = MemoryNode::new(
846 AgentId::new(),
847 MemoryType::Semantic,
848 "crash delete".to_string(),
849 vec![1.0],
850 );
851 pid = engine.store_memory(&node).unwrap();
852 engine.delete_memory(pid).unwrap();
853 }
855 {
856 let engine = StorageEngine::open(dir.path()).unwrap();
857 assert!(
858 engine.load_memory(pid).is_err(),
859 "deletion must survive a crash via WAL replay"
860 );
861 assert!(engine.scan_all_memories().is_empty());
862 }
863 }
864
865 #[test]
866 fn test_deleted_page_reused() {
867 let (_dir, engine) = setup();
868
869 let a = MemoryNode::new(AgentId::new(), MemoryType::Semantic, "a".into(), vec![1.0]);
870 let pid_a = engine.store_memory(&a).unwrap();
871 engine.delete_memory(pid_a).unwrap();
872
873 let b = MemoryNode::new(AgentId::new(), MemoryType::Semantic, "b".into(), vec![2.0]);
874 let pid_b = engine.store_memory(&b).unwrap();
875 assert_eq!(pid_a, pid_b, "freed page should be reused");
876
877 let loaded = engine.load_memory(pid_b).unwrap();
878 assert_eq!(loaded.content, "b");
879 }
880
881 #[test]
882 fn test_delete_reuse_crash_recovery() {
883 let dir = tempfile::tempdir().unwrap();
884 let pid;
885 let b_id;
886 {
887 let engine = StorageEngine::open(dir.path()).unwrap();
888 let a = MemoryNode::new(AgentId::new(), MemoryType::Semantic, "a".into(), vec![1.0]);
889 pid = engine.store_memory(&a).unwrap();
890 engine.delete_memory(pid).unwrap();
891 let b = MemoryNode::new(AgentId::new(), MemoryType::Semantic, "b".into(), vec![2.0]);
892 let pid_b = engine.store_memory(&b).unwrap();
893 assert_eq!(pid, pid_b);
894 b_id = b.id;
895 }
897 {
898 let engine = StorageEngine::open(dir.path()).unwrap();
899 let loaded = engine.load_memory(pid).unwrap();
900 assert_eq!(loaded.content, "b", "later write must win over the free");
901 assert_eq!(loaded.id, b_id);
902 let c = MemoryNode::new(AgentId::new(), MemoryType::Semantic, "c".into(), vec![3.0]);
905 let pid_c = engine.store_memory(&c).unwrap();
906 assert_ne!(pid_c, pid, "recovered free list must exclude reused page");
907 assert_eq!(engine.load_memory(pid).unwrap().content, "b");
908 }
909 }
910
911 #[test]
912 fn test_update_memory_in_place() {
913 let dir = tempfile::tempdir().unwrap();
914 let pid;
915 let id;
916 {
917 let engine = StorageEngine::open(dir.path()).unwrap();
918 let mut node = MemoryNode::new(
919 AgentId::new(),
920 MemoryType::Semantic,
921 "original".to_string(),
922 vec![1.0],
923 );
924 pid = engine.store_memory(&node).unwrap();
925 id = node.id;
926
927 node.content = "updated".to_string();
928 engine.update_memory(pid, &node).unwrap();
929
930 let loaded = engine.load_memory(pid).unwrap();
931 assert_eq!(loaded.content, "updated");
932 let scanned = engine.scan_all_memories();
934 assert_eq!(scanned.len(), 1);
935 engine.close().unwrap();
936 }
937 {
938 let engine = StorageEngine::open(dir.path()).unwrap();
939 let loaded = engine.load_memory(pid).unwrap();
940 assert_eq!(loaded.content, "updated");
941 assert_eq!(loaded.id, id);
942 assert_eq!(engine.scan_all_memories().len(), 1);
943 }
944 }
945
946 #[test]
947 fn test_concurrent_open_is_rejected() {
948 let dir = tempfile::tempdir().unwrap();
949
950 let engine1 = StorageEngine::open(dir.path()).unwrap();
955 let second = StorageEngine::open(dir.path());
956 assert!(second.is_err(), "second concurrent open must fail");
957 let msg = second.err().unwrap().to_string();
958 assert!(msg.contains("locked"), "error names the lock: {msg}");
959
960 engine1.close().unwrap();
962 let engine2 = StorageEngine::open(dir.path()).unwrap();
963 engine2.close().unwrap();
964 }
965
966 #[test]
967 fn test_concurrent_writes_from_threads() {
968 use std::sync::Arc;
969 let dir = tempfile::tempdir().unwrap();
970 let engine = Arc::new(StorageEngine::open(dir.path()).unwrap());
971
972 let handles: Vec<_> = (0..10)
973 .map(|i| {
974 let eng = Arc::clone(&engine);
975 std::thread::spawn(move || {
976 let node = MemoryNode::new(
977 AgentId::new(),
978 MemoryType::Episodic,
979 format!("thread-{i}"),
980 vec![i as f32],
981 );
982 eng.store_memory(&node).unwrap()
983 })
984 })
985 .collect();
986
987 let pids: Vec<PageId> = handles.into_iter().map(|h| h.join().unwrap()).collect();
988
989 for (i, pid) in pids.iter().enumerate() {
991 let loaded = engine.load_memory(*pid).unwrap();
992 assert_eq!(loaded.content, format!("thread-{i}"));
993 }
994 }
995}