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 buffer_stats(&self) -> crate::buffer::BufferStats {
264 self.buffer_pool.stats()
265 }
266
267 pub fn page_count(&self) -> u64 {
269 self.page_manager.lock().page_count()
270 }
271
272 pub fn write_page(&self, page_id: PageId, data: &[u8]) -> MenteResult<()> {
277 let lsn = {
278 let mut wal = self.wal.lock();
279 wal.lock_exclusive()?;
280 wal.reload_lsn()?;
281 let lsn = wal.append(WalEntryType::PageWrite, page_id.0, data)?;
282 wal.sync()?;
283 wal.unlock()?;
284 lsn
285 };
286
287 self.apply_page_write(page_id, data, lsn)
288 }
289
290 fn apply_page_write(&self, page_id: PageId, data: &[u8], lsn: u64) -> MenteResult<()> {
292 let mut pm = self.page_manager.lock();
293 let mut page = self.buffer_pool.fetch_page(page_id, &mut pm)?;
294 drop(pm);
295
296 let copy_len = data.len().min(PAGE_DATA_SIZE);
297 page.data[..copy_len].copy_from_slice(&data[..copy_len]);
298 if copy_len < PAGE_DATA_SIZE {
299 page.data[copy_len..].fill(0);
300 }
301 page.header.lsn = lsn;
302 page.header.page_type = PageType::Data as u8;
303 page.header.free_space = (PAGE_DATA_SIZE - copy_len) as u16;
304 page.header.checksum = page.compute_checksum();
305
306 if self.buffer_pool.update_page(page_id, &page).is_err() {
307 self.page_manager.lock().write_page(page_id, &page)?;
308 }
309 self.buffer_pool.unpin_page(page_id, true).ok();
310
311 Ok(())
312 }
313
314 pub fn store_memory(&self, node: &MemoryNode) -> MenteResult<PageId> {
338 let serialized =
339 serde_json::to_vec(node).map_err(|e| MenteError::Serialization(e.to_string()))?;
340
341 if serialized.len() + 4 > PAGE_DATA_SIZE {
342 return Err(MenteError::CapacityExceeded(format!(
343 "memory node serialized to {} bytes (max {})",
344 serialized.len(),
345 PAGE_DATA_SIZE - 4,
346 )));
347 }
348
349 let mut buf = Vec::with_capacity(4 + serialized.len());
350 buf.extend_from_slice(&(serialized.len() as u32).to_le_bytes());
351 buf.extend_from_slice(&serialized);
352
353 let (page_id, lsn) = {
355 let mut wal = self.wal.lock();
356 let mut pm = self.page_manager.lock();
357
358 wal.lock_exclusive()?;
360 pm.reload_header()?;
361 wal.reload_lsn()?;
362
363 let page_id = pm.allocate_page()?;
365
366 let lsn = wal.append(WalEntryType::PageWrite, page_id.0, &buf)?;
369 wal.sync()?;
370
371 let mut page = Page::zeroed();
373 page.header.page_id = page_id.0;
374 let copy_len = buf.len().min(PAGE_DATA_SIZE);
375 page.data[..copy_len].copy_from_slice(&buf[..copy_len]);
376 page.header.lsn = lsn;
377 page.header.page_type = PageType::Data as u8;
378 page.header.free_space = (PAGE_DATA_SIZE - copy_len) as u16;
379 page.header.checksum = page.compute_checksum();
380 pm.write_page(page_id, &page)?;
381
382 wal.unlock()?;
384
385 (page_id, lsn)
386 };
387
388 let _ = lsn;
391 self.buffer_pool.invalidate(page_id);
392
393 if self.wal.lock().file_size() > WAL_AUTO_CHECKPOINT_BYTES
396 && let Err(e) = self.checkpoint()
397 {
398 tracing::warn!("auto-checkpoint failed: {e}");
399 }
400
401 info!(
402 page_id = page_id.0,
403 bytes = serialized.len(),
404 "stored memory node"
405 );
406 Ok(page_id)
407 }
408
409 pub fn store_memory_batch(&self, nodes: &[MemoryNode]) -> MenteResult<Vec<PageId>> {
415 let mut bufs = Vec::with_capacity(nodes.len());
417 for node in nodes {
418 let serialized =
419 serde_json::to_vec(node).map_err(|e| MenteError::Serialization(e.to_string()))?;
420 if serialized.len() + 4 > PAGE_DATA_SIZE {
421 return Err(MenteError::CapacityExceeded(format!(
422 "memory node serialized to {} bytes (max {})",
423 serialized.len(),
424 PAGE_DATA_SIZE - 4,
425 )));
426 }
427 let mut buf = Vec::with_capacity(4 + serialized.len());
428 buf.extend_from_slice(&(serialized.len() as u32).to_le_bytes());
429 buf.extend_from_slice(&serialized);
430 bufs.push(buf);
431 }
432
433 let page_ids = {
435 let mut wal = self.wal.lock();
436 let mut pm = self.page_manager.lock();
437
438 wal.lock_exclusive()?;
439 pm.reload_header()?;
440 wal.reload_lsn()?;
441
442 let mut ids = Vec::with_capacity(bufs.len());
443 for buf in &bufs {
444 let page_id = pm.allocate_page()?;
445 let lsn = wal.append(WalEntryType::PageWrite, page_id.0, buf)?;
446
447 let mut page = Page::zeroed();
448 page.header.page_id = page_id.0;
449 let copy_len = buf.len().min(PAGE_DATA_SIZE);
450 page.data[..copy_len].copy_from_slice(&buf[..copy_len]);
451 page.header.lsn = lsn;
452 page.header.page_type = PageType::Data as u8;
453 page.header.free_space = (PAGE_DATA_SIZE - copy_len) as u16;
454 page.header.checksum = page.compute_checksum();
455 pm.write_page(page_id, &page)?;
456
457 ids.push(page_id);
458 }
459
460 wal.sync()?;
463 wal.unlock()?;
464
465 ids
466 };
467
468 for page_id in &page_ids {
470 self.buffer_pool.invalidate(*page_id);
471 }
472
473 if self.wal.lock().file_size() > WAL_AUTO_CHECKPOINT_BYTES
475 && let Err(e) = self.checkpoint()
476 {
477 tracing::warn!("auto-checkpoint failed: {e}");
478 }
479
480 info!(count = page_ids.len(), "stored memory batch");
481 Ok(page_ids)
482 }
483
484 pub fn update_memory(&self, page_id: PageId, node: &MemoryNode) -> MenteResult<()> {
490 let serialized =
491 serde_json::to_vec(node).map_err(|e| MenteError::Serialization(e.to_string()))?;
492
493 if serialized.len() + 4 > PAGE_DATA_SIZE {
494 return Err(MenteError::CapacityExceeded(format!(
495 "memory node serialized to {} bytes (max {})",
496 serialized.len(),
497 PAGE_DATA_SIZE - 4,
498 )));
499 }
500
501 let mut buf = Vec::with_capacity(4 + serialized.len());
502 buf.extend_from_slice(&(serialized.len() as u32).to_le_bytes());
503 buf.extend_from_slice(&serialized);
504
505 self.write_page(page_id, &buf)
506 }
507
508 pub fn delete_memory(&self, page_id: PageId) -> MenteResult<()> {
514 {
515 let mut wal = self.wal.lock();
516 let mut pm = self.page_manager.lock();
517
518 wal.lock_exclusive()?;
519 pm.reload_header()?;
520 wal.reload_lsn()?;
521
522 wal.append(WalEntryType::PageFree, page_id.0, &[])?;
525 wal.sync()?;
526
527 pm.free_page(page_id)?;
528 wal.unlock()?;
529 }
530
531 self.buffer_pool.invalidate(page_id);
533
534 info!(page_id = page_id.0, "deleted memory node");
535 Ok(())
536 }
537
538 pub fn load_memory(&self, page_id: PageId) -> MenteResult<MemoryNode> {
550 let page = self.read_page(page_id)?;
551 self.buffer_pool.unpin_page(page_id, false).ok();
552
553 if PageType::from(page.header.page_type) != PageType::Data {
554 return Err(MenteError::Storage(format!(
555 "page {} is not a data page",
556 page_id.0
557 )));
558 }
559
560 let len = u32::from_le_bytes(page.data[..4].try_into().unwrap()) as usize;
561 if len == 0 || len + 4 > PAGE_DATA_SIZE {
562 return Err(MenteError::Storage(format!(
563 "invalid memory node length prefix: {len}"
564 )));
565 }
566
567 serde_json::from_slice(&page.data[4..4 + len])
568 .map_err(|e| MenteError::Serialization(e.to_string()))
569 }
570
571 pub fn checkpoint(&self) -> MenteResult<()> {
585 let mut wal = self.wal.lock();
586 let mut pm = self.page_manager.lock();
587
588 wal.lock_exclusive()?;
589 wal.reload_lsn()?;
590
591 self.buffer_pool.flush_all(&mut pm)?;
592 pm.sync()?;
593
594 let lsn = wal.append(WalEntryType::Checkpoint, 0, &[])?;
595 wal.sync()?;
596 wal.truncate(lsn)?;
597 wal.unlock()?;
598
599 info!(lsn, "checkpoint complete");
600 Ok(())
601 }
602
603 pub fn scan_all_memories(&self) -> Vec<(mentedb_core::types::MemoryId, PageId)> {
620 let mut pm = self.page_manager.lock();
621 let _ = pm.reload_header();
623 let count = pm.page_count();
624 drop(pm);
625
626 let mut results = Vec::new();
627 for i in 1..count {
628 let page_id = PageId(i);
629 if let Ok(node) = self.load_memory(page_id) {
630 results.push((node.id, page_id));
631 }
632 }
633 results
634 }
635}
636
637#[cfg(test)]
638mod tests {
639 use super::*;
640 use mentedb_core::memory::MemoryType;
641 use mentedb_core::types::AgentId;
642
643 fn setup() -> (tempfile::TempDir, StorageEngine) {
644 let dir = tempfile::tempdir().unwrap();
645 let engine = StorageEngine::open(dir.path()).unwrap();
646 (dir, engine)
647 }
648
649 #[test]
650 fn test_allocate_write_read() {
651 let (_dir, engine) = setup();
652
653 let pid = engine.allocate_page().unwrap();
654 engine.write_page(pid, b"hello storage engine").unwrap();
655
656 let page = engine.read_page(pid).unwrap();
657 assert_eq!(&page.data[..20], b"hello storage engine");
658 engine.buffer_pool.unpin_page(pid, false).ok();
659 }
660
661 #[test]
662 fn test_store_and_load_memory() {
663 let (_dir, engine) = setup();
664
665 let node = MemoryNode::new(
666 AgentId::new(),
667 MemoryType::Episodic,
668 "The user prefers Rust over Go".to_string(),
669 vec![0.1, 0.2, 0.3, 0.4],
670 );
671
672 let page_id = engine.store_memory(&node).unwrap();
673 let loaded = engine.load_memory(page_id).unwrap();
674
675 assert_eq!(node.id, loaded.id);
676 assert_eq!(node.content, loaded.content);
677 assert_eq!(node.embedding, loaded.embedding);
678 assert_eq!(node.memory_type, loaded.memory_type);
679 }
680
681 #[test]
682 fn test_checkpoint() {
683 let (_dir, engine) = setup();
684
685 let node = MemoryNode::new(
686 AgentId::new(),
687 MemoryType::Semantic,
688 "checkpoint test".to_string(),
689 vec![1.0, 2.0],
690 );
691
692 let pid = engine.store_memory(&node).unwrap();
693 engine.checkpoint().unwrap();
694
695 let loaded = engine.load_memory(pid).unwrap();
696 assert_eq!(loaded.content, "checkpoint test");
697 }
698
699 #[test]
700 fn test_close_and_reopen() {
701 let dir = tempfile::tempdir().unwrap();
702 let pid;
703 {
704 let engine = StorageEngine::open(dir.path()).unwrap();
705 let node = MemoryNode::new(
706 AgentId::new(),
707 MemoryType::Procedural,
708 "persist across close".to_string(),
709 vec![0.5],
710 );
711 pid = engine.store_memory(&node).unwrap();
712 engine.close().unwrap();
713 }
714 {
715 let engine = StorageEngine::open(dir.path()).unwrap();
716 let loaded = engine.load_memory(pid).unwrap();
717 assert_eq!(loaded.content, "persist across close");
718 }
719 }
720
721 #[test]
722 fn test_crash_recovery() {
723 let dir = tempfile::tempdir().unwrap();
724 let mut ids = Vec::new();
725 let mut contents = Vec::new();
726 {
727 let engine = StorageEngine::open(dir.path()).unwrap();
728 for i in 0..3 {
729 let content = format!("crash-recovery-{i}");
730 let node = MemoryNode::new(
731 AgentId::new(),
732 MemoryType::Episodic,
733 content.clone(),
734 vec![i as f32],
735 );
736 let pid = engine.store_memory(&node).unwrap();
737 ids.push(pid);
738 contents.push(content);
739 }
740 engine.wal.lock().sync().unwrap();
742 }
743 {
744 let engine = StorageEngine::open(dir.path()).unwrap();
745 for (pid, expected) in ids.iter().zip(contents.iter()) {
746 let loaded = engine.load_memory(*pid).unwrap();
747 assert_eq!(&loaded.content, expected);
748 }
749 }
750 }
751
752 #[test]
753 fn test_recovery_idempotent() {
754 let dir = tempfile::tempdir().unwrap();
755 let pid;
756 let content = "idempotent-check".to_string();
757 {
758 let engine = StorageEngine::open(dir.path()).unwrap();
759 let node = MemoryNode::new(
760 AgentId::new(),
761 MemoryType::Semantic,
762 content.clone(),
763 vec![1.0, 2.0],
764 );
765 pid = engine.store_memory(&node).unwrap();
766 engine.checkpoint().unwrap();
767 engine.close().unwrap();
768 }
769 {
770 let engine = StorageEngine::open(dir.path()).unwrap();
771 let loaded = engine.load_memory(pid).unwrap();
772 assert_eq!(loaded.content, content);
773 }
774 }
775
776 #[test]
777 fn test_partial_write_recovery() {
778 let dir = tempfile::tempdir().unwrap();
779 let mut ids = Vec::new();
780 let mut contents = Vec::new();
781 {
782 let engine = StorageEngine::open(dir.path()).unwrap();
783 for i in 0..3 {
784 let content = format!("checkpointed-{i}");
785 let node = MemoryNode::new(
786 AgentId::new(),
787 MemoryType::Semantic,
788 content.clone(),
789 vec![i as f32],
790 );
791 let pid = engine.store_memory(&node).unwrap();
792 ids.push(pid);
793 contents.push(content);
794 }
795 engine.checkpoint().unwrap();
796
797 for i in 3..5 {
798 let content = format!("unckeckpointed-{i}");
799 let node = MemoryNode::new(
800 AgentId::new(),
801 MemoryType::Episodic,
802 content.clone(),
803 vec![i as f32],
804 );
805 let pid = engine.store_memory(&node).unwrap();
806 ids.push(pid);
807 contents.push(content);
808 }
809 engine.wal.lock().sync().unwrap();
811 }
812 {
813 let engine = StorageEngine::open(dir.path()).unwrap();
814 for (pid, expected) in ids.iter().zip(contents.iter()) {
815 let loaded = engine.load_memory(*pid).unwrap();
816 assert_eq!(&loaded.content, expected);
817 }
818 }
819 }
820
821 #[test]
822 fn test_delete_memory_durable() {
823 let dir = tempfile::tempdir().unwrap();
824 let pid;
825 {
826 let engine = StorageEngine::open(dir.path()).unwrap();
827 let node = MemoryNode::new(
828 AgentId::new(),
829 MemoryType::Semantic,
830 "to be deleted".to_string(),
831 vec![1.0],
832 );
833 pid = engine.store_memory(&node).unwrap();
834 engine.delete_memory(pid).unwrap();
835 assert!(engine.load_memory(pid).is_err());
836 assert!(engine.scan_all_memories().is_empty());
837 engine.close().unwrap();
838 }
839 {
840 let engine = StorageEngine::open(dir.path()).unwrap();
841 assert!(
842 engine.load_memory(pid).is_err(),
843 "deleted memory must not resurrect on reopen"
844 );
845 assert!(engine.scan_all_memories().is_empty());
846 }
847 }
848
849 #[test]
850 fn test_delete_survives_crash() {
851 let dir = tempfile::tempdir().unwrap();
852 let pid;
853 {
854 let engine = StorageEngine::open(dir.path()).unwrap();
855 let node = MemoryNode::new(
856 AgentId::new(),
857 MemoryType::Semantic,
858 "crash delete".to_string(),
859 vec![1.0],
860 );
861 pid = engine.store_memory(&node).unwrap();
862 engine.delete_memory(pid).unwrap();
863 }
865 {
866 let engine = StorageEngine::open(dir.path()).unwrap();
867 assert!(
868 engine.load_memory(pid).is_err(),
869 "deletion must survive a crash via WAL replay"
870 );
871 assert!(engine.scan_all_memories().is_empty());
872 }
873 }
874
875 #[test]
876 fn test_deleted_page_reused() {
877 let (_dir, engine) = setup();
878
879 let a = MemoryNode::new(AgentId::new(), MemoryType::Semantic, "a".into(), vec![1.0]);
880 let pid_a = engine.store_memory(&a).unwrap();
881 engine.delete_memory(pid_a).unwrap();
882
883 let b = MemoryNode::new(AgentId::new(), MemoryType::Semantic, "b".into(), vec![2.0]);
884 let pid_b = engine.store_memory(&b).unwrap();
885 assert_eq!(pid_a, pid_b, "freed page should be reused");
886
887 let loaded = engine.load_memory(pid_b).unwrap();
888 assert_eq!(loaded.content, "b");
889 }
890
891 #[test]
892 fn test_delete_reuse_crash_recovery() {
893 let dir = tempfile::tempdir().unwrap();
894 let pid;
895 let b_id;
896 {
897 let engine = StorageEngine::open(dir.path()).unwrap();
898 let a = MemoryNode::new(AgentId::new(), MemoryType::Semantic, "a".into(), vec![1.0]);
899 pid = engine.store_memory(&a).unwrap();
900 engine.delete_memory(pid).unwrap();
901 let b = MemoryNode::new(AgentId::new(), MemoryType::Semantic, "b".into(), vec![2.0]);
902 let pid_b = engine.store_memory(&b).unwrap();
903 assert_eq!(pid, pid_b);
904 b_id = b.id;
905 }
907 {
908 let engine = StorageEngine::open(dir.path()).unwrap();
909 let loaded = engine.load_memory(pid).unwrap();
910 assert_eq!(loaded.content, "b", "later write must win over the free");
911 assert_eq!(loaded.id, b_id);
912 let c = MemoryNode::new(AgentId::new(), MemoryType::Semantic, "c".into(), vec![3.0]);
915 let pid_c = engine.store_memory(&c).unwrap();
916 assert_ne!(pid_c, pid, "recovered free list must exclude reused page");
917 assert_eq!(engine.load_memory(pid).unwrap().content, "b");
918 }
919 }
920
921 #[test]
922 fn test_update_memory_in_place() {
923 let dir = tempfile::tempdir().unwrap();
924 let pid;
925 let id;
926 {
927 let engine = StorageEngine::open(dir.path()).unwrap();
928 let mut node = MemoryNode::new(
929 AgentId::new(),
930 MemoryType::Semantic,
931 "original".to_string(),
932 vec![1.0],
933 );
934 pid = engine.store_memory(&node).unwrap();
935 id = node.id;
936
937 node.content = "updated".to_string();
938 engine.update_memory(pid, &node).unwrap();
939
940 let loaded = engine.load_memory(pid).unwrap();
941 assert_eq!(loaded.content, "updated");
942 let scanned = engine.scan_all_memories();
944 assert_eq!(scanned.len(), 1);
945 engine.close().unwrap();
946 }
947 {
948 let engine = StorageEngine::open(dir.path()).unwrap();
949 let loaded = engine.load_memory(pid).unwrap();
950 assert_eq!(loaded.content, "updated");
951 assert_eq!(loaded.id, id);
952 assert_eq!(engine.scan_all_memories().len(), 1);
953 }
954 }
955
956 #[test]
957 fn test_concurrent_open_is_rejected() {
958 let dir = tempfile::tempdir().unwrap();
959
960 let engine1 = StorageEngine::open(dir.path()).unwrap();
965 let second = StorageEngine::open(dir.path());
966 assert!(second.is_err(), "second concurrent open must fail");
967 let msg = second.err().unwrap().to_string();
968 assert!(msg.contains("locked"), "error names the lock: {msg}");
969
970 engine1.close().unwrap();
972 let engine2 = StorageEngine::open(dir.path()).unwrap();
973 engine2.close().unwrap();
974 }
975
976 #[test]
977 fn test_concurrent_writes_from_threads() {
978 use std::sync::Arc;
979 let dir = tempfile::tempdir().unwrap();
980 let engine = Arc::new(StorageEngine::open(dir.path()).unwrap());
981
982 let handles: Vec<_> = (0..10)
983 .map(|i| {
984 let eng = Arc::clone(&engine);
985 std::thread::spawn(move || {
986 let node = MemoryNode::new(
987 AgentId::new(),
988 MemoryType::Episodic,
989 format!("thread-{i}"),
990 vec![i as f32],
991 );
992 eng.store_memory(&node).unwrap()
993 })
994 })
995 .collect();
996
997 let pids: Vec<PageId> = handles.into_iter().map(|h| h.join().unwrap()).collect();
998
999 for (i, pid) in pids.iter().enumerate() {
1001 let loaded = engine.load_memory(*pid).unwrap();
1002 assert_eq!(loaded.content, format!("thread-{i}"));
1003 }
1004 }
1005}