Skip to main content

mentedb_storage/
engine.rs

1//! Storage Engine: facade that ties the page manager, WAL, and buffer pool together.
2
3use 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};
14/// Default number of page frames in the buffer pool.
15const DEFAULT_BUFFER_POOL_SIZE: usize = 1024;
16
17/// Auto-checkpoint when WAL file exceeds this size (8 MB).
18const WAL_AUTO_CHECKPOINT_BYTES: u64 = 8 * 1024 * 1024;
19
20/// The unified storage engine for MenteDB.
21///
22/// Coordinates page allocation, caching, and write-ahead logging to provide
23/// crash-safe, page-oriented storage for memory nodes.
24///
25/// Concurrency model (inspired by WAL-mode databases):
26/// - **Reads are lock-free**: `read_page` only touches the buffer pool and page
27///   manager — no file locks, no WAL access.
28/// - **Writes are fully serialized** via a blocking `flock(2)` on the WAL file.
29///   The entire write transaction (page allocation + WAL append + page write +
30///   fsync) executes under a single flock, ensuring correctness across multiple
31///   processes sharing the same data directory.
32/// - **State is refreshed from disk** under the flock: page count is re-read
33///   from the file header and LSN is re-read from the WAL tail, so no process
34///   can act on stale in-memory state.
35/// - **No DB-level lock on open.** Multiple processes can open the same database
36///   simultaneously.
37pub struct StorageEngine {
38    page_manager: Mutex<PageManager>,
39    buffer_pool: BufferPool,
40    wal: Mutex<Wal>,
41}
42
43impl StorageEngine {
44    /// Open (or create) a storage engine rooted at `path`.
45    ///
46    /// `path` must be a directory; it will be created if it does not exist.
47    /// After opening, any uncommitted WAL entries are replayed for crash recovery.
48    ///
49    /// # Example
50    ///
51    /// ```no_run
52    /// use mentedb_storage::StorageEngine;
53    ///
54    /// let engine = StorageEngine::open("/tmp/mentedb".as_ref())?;
55    /// // engine is ready — WAL recovery already ran if needed
56    /// # Ok::<(), mentedb_core::error::MenteError>(())
57    /// ```
58    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    /// Replay WAL entries to recover writes that were not checkpointed.
82    ///
83    /// For each `PageWrite` entry the serialized data is written back to its page.
84    /// After replay the WAL is truncated. Returns the number of entries replayed.
85    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        // Refresh page count from disk — another process may have written pages.
93        pm.reload_header()?;
94
95        // Replay with last-op-wins per page: every PageWrite carries a full page
96        // image and PageFree discards the page, so only the final entry for each
97        // page matters. This is what makes free-then-reuse sequences safe: a
98        // PageFree followed by a later PageWrite for the same page must not
99        // leave the page on the free list.
100        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                    // Mark the page Free without touching the free list; the
140                    // list is rebuilt from page types after replay so it stays
141                    // consistent with WAL-derived page states.
142                    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            // Header updates (free list head, page count) are not fsynced on
157            // every write, so after a crash the free list can disagree with the
158            // replayed page states. Rebuild it from page types: the WAL is the
159            // sole source of truth for which pages are Free vs Data.
160            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    /// Gracefully shut down: flush dirty pages, sync files.
172    ///
173    /// # Example
174    ///
175    /// ```no_run
176    /// # use mentedb_storage::StorageEngine;
177    /// # let engine = StorageEngine::open("/tmp/mentedb".as_ref())?;
178    /// engine.close()?;
179    /// # Ok::<(), mentedb_core::error::MenteError>(())
180    /// ```
181    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    // ---- low-level page operations ----
191
192    /// Allocate a fresh page (for internal/test use).
193    ///
194    /// **WARNING**: In multi-process scenarios, prefer `store_memory` which
195    /// allocates under the WAL flock. This method does NOT acquire the flock.
196    pub fn allocate_page(&self) -> MenteResult<PageId> {
197        self.page_manager.lock().allocate_page()
198    }
199
200    /// Read a page through the buffer pool (lock-free — no WAL access).
201    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    /// Write data into an already-allocated page with WAL protection.
207    ///
208    /// Acquires the WAL flock for the duration of the write transaction.
209    /// For new pages, prefer `store_memory` which allocates + writes atomically.
210    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    /// Apply a page write to the buffer pool and page manager (after WAL).
225    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    // ---- high-level memory operations ----
249
250    /// Serialize and store a [`MemoryNode`] into a single page.
251    ///
252    /// The entire operation — page allocation, WAL append, page write — executes
253    /// under a single WAL flock, making it safe across multiple processes.
254    ///
255    /// # Example
256    ///
257    /// ```no_run
258    /// use mentedb_storage::StorageEngine;
259    /// use mentedb_core::{MemoryNode, memory::MemoryType, types::AgentId};
260    ///
261    /// let engine = StorageEngine::open("/tmp/mentedb".as_ref())?;
262    /// let node = MemoryNode::new(
263    ///     AgentId::new(),
264    ///     MemoryType::Semantic,
265    ///     "User likes dark mode".to_string(),
266    ///     vec![0.1, 0.2],
267    /// );
268    /// let page_id = engine.store_memory(&node)?;
269    /// # Ok::<(), mentedb_core::error::MenteError>(())
270    /// ```
271    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        // Atomic write transaction: allocate + WAL + page write under one flock
288        let (page_id, lsn) = {
289            let mut wal = self.wal.lock();
290            let mut pm = self.page_manager.lock();
291
292            // Acquire flock and refresh state from disk
293            wal.lock_exclusive()?;
294            pm.reload_header()?;
295            wal.reload_lsn()?;
296
297            // Allocate page (using fresh page_count from disk)
298            let page_id = pm.allocate_page()?;
299
300            // WAL append + sync (WAL fsync guarantees durability;
301            // page data is written but not fsynced — checkpoint handles that)
302            let lsn = wal.append(WalEntryType::PageWrite, page_id.0, &buf)?;
303            wal.sync()?;
304
305            // Write page data to disk (no fsync — recoverable from WAL)
306            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            // Release flock — other processes can now write
317            wal.unlock()?;
318
319            (page_id, lsn)
320        };
321
322        // Drop any stale cached copy (the page may have been reused from the
323        // free list); the next read loads the fresh data from disk.
324        let _ = lsn;
325        self.buffer_pool.invalidate(page_id);
326
327        // Auto-checkpoint when WAL exceeds threshold to prevent unbounded growth.
328        // This keeps reload_lsn() fast for subsequent writes.
329        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    /// Store multiple [`MemoryNode`]s in a single locked transaction.
344    ///
345    /// Acquires the WAL flock once, writes all nodes, then releases. This avoids
346    /// the per-write overhead of `reload_header` / `reload_lsn` for bulk inserts.
347    /// Auto-checkpoints after the batch if the WAL exceeds the threshold.
348    pub fn store_memory_batch(&self, nodes: &[MemoryNode]) -> MenteResult<Vec<PageId>> {
349        // Phase 1: serialize all nodes upfront (no locks held)
350        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        // Phase 2: single locked transaction for all writes
368        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 fsync only — page data is recoverable from WAL on crash.
395            // Checkpoint handles page file fsync.
396            wal.sync()?;
397            wal.unlock()?;
398
399            ids
400        };
401
402        // Drop stale cached copies for pages reused from the free list.
403        for page_id in &page_ids {
404            self.buffer_pool.invalidate(*page_id);
405        }
406
407        // Auto-checkpoint if WAL grew too large
408        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    /// Update a [`MemoryNode`] in place on its existing page.
419    ///
420    /// The write goes through the WAL-protected `write_page` path, so it is
421    /// crash-durable and keeps the buffer pool coherent. Unlike storing to a
422    /// fresh page, this never orphans the old copy.
423    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    /// Delete a memory by returning its page to the free list.
443    ///
444    /// The deletion is WAL-logged before the page is freed, so it survives a
445    /// crash: recovery replays the `PageFree` entry and the memory does not
446    /// resurrect on reopen. The freed page is reused by later allocations.
447    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 fsync guarantees the deletion is durable before the page
457            // is touched; the free-list update itself is recoverable.
458            wal.append(WalEntryType::PageFree, page_id.0, &[])?;
459            wal.sync()?;
460
461            pm.free_page(page_id)?;
462            wal.unlock()?;
463        }
464
465        // A stale cached copy must never be served or flushed back.
466        self.buffer_pool.invalidate(page_id);
467
468        info!(page_id = page_id.0, "deleted memory node");
469        Ok(())
470    }
471
472    /// Load and deserialize a [`MemoryNode`] from the given page.
473    ///
474    /// # Example
475    ///
476    /// ```no_run
477    /// # use mentedb_storage::{StorageEngine, PageId};
478    /// # let engine = StorageEngine::open("/tmp/mentedb".as_ref())?;
479    /// let node = engine.load_memory(PageId(1))?;
480    /// println!("memory: {}", node.content);
481    /// # Ok::<(), mentedb_core::error::MenteError>(())
482    /// ```
483    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    // ---- durability ----
506
507    /// Checkpoint: flush all dirty pages, sync to disk, and truncate the WAL.
508    ///
509    /// # Example
510    ///
511    /// ```no_run
512    /// # use mentedb_storage::StorageEngine;
513    /// # let engine = StorageEngine::open("/tmp/mentedb".as_ref())?;
514    /// // After a batch of writes, checkpoint to reclaim WAL space
515    /// engine.checkpoint()?;
516    /// # Ok::<(), mentedb_core::error::MenteError>(())
517    /// ```
518    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    /// Scan all pages and return (MemoryId, PageId) pairs for every valid memory node.
538    ///
539    /// Refreshes the page count from disk before scanning so pages written by
540    /// other processes are included. Used to rebuild the page map on startup.
541    ///
542    /// # Example
543    ///
544    /// ```no_run
545    /// # use mentedb_storage::StorageEngine;
546    /// # let engine = StorageEngine::open("/tmp/mentedb".as_ref())?;
547    /// let memories = engine.scan_all_memories();
548    /// for (memory_id, page_id) in &memories {
549    ///     println!("{memory_id} -> page {}", page_id.0);
550    /// }
551    /// # Ok::<(), mentedb_core::error::MenteError>(())
552    /// ```
553    pub fn scan_all_memories(&self) -> Vec<(mentedb_core::types::MemoryId, PageId)> {
554        let mut pm = self.page_manager.lock();
555        // Refresh from disk to see pages written by other processes
556        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            // Simulate crash: sync the WAL but do NOT call close/checkpoint.
675            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            // Simulate crash — sync WAL but don't close.
744            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            // Simulate crash: no close, no checkpoint.
798        }
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            // Simulate crash: the WAL now holds PageFree(p) then PageWrite(p).
840        }
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            // The page must NOT be on the free list: a fresh allocation must
847            // not clobber b.
848            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            // No orphan copy: exactly one entry in a full scan.
877            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        // Two engines open the same directory simultaneously — should succeed
895        // now that we no longer hold an exclusive DB-level flock.
896        let engine1 = StorageEngine::open(dir.path()).unwrap();
897        let engine2 = StorageEngine::open(dir.path()).unwrap();
898
899        // Both can write (serialized by WAL file lock)
900        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        // Each engine can read what it wrote
917        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        // All 10 writes succeeded and are readable
948        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}