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::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};
15/// Default number of page frames in the buffer pool.
16const DEFAULT_BUFFER_POOL_SIZE: usize = 1024;
17
18/// Auto-checkpoint when WAL file exceeds this size (8 MB).
19const WAL_AUTO_CHECKPOINT_BYTES: u64 = 8 * 1024 * 1024;
20
21/// The unified storage engine for MenteDB.
22///
23/// Coordinates page allocation, caching, and write-ahead logging to provide
24/// crash-safe, page-oriented storage for memory nodes.
25///
26/// Concurrency model (inspired by WAL-mode databases):
27/// - **Reads are lock-free**: `read_page` only touches the buffer pool and page
28///   manager — no file locks, no WAL access.
29/// - **Writes are fully serialized** via a blocking `flock(2)` on the WAL file.
30///   The entire write transaction (page allocation + WAL append + page write +
31///   fsync) executes under a single flock, ensuring correctness across multiple
32///   processes sharing the same data directory.
33/// - **State is refreshed from disk** under the flock: page count is re-read
34///   from the file header and LSN is re-read from the WAL tail, so no process
35///   can act on stale in-memory state.
36/// - **One process per database.** Open acquires an exclusive lock on a LOCK
37///   file in the data directory and holds it for the engine's lifetime. The
38///   per operation flock protects individual write transactions, but layers
39///   above the storage engine cache state across operations (buffer pool,
40///   page map, index snapshots), and a second concurrent process flushing
41///   those caches rolls the database back to its own open time snapshot.
42///   A second open therefore fails fast with a "locked" error instead of
43///   silently corrupting state; callers retry until the owner exits.
44pub struct StorageEngine {
45    page_manager: Mutex<PageManager>,
46    buffer_pool: BufferPool,
47    wal: Mutex<Wal>,
48    /// Held exclusively for the lifetime of this engine; released on close
49    /// or process exit. Guards against concurrent multi process opens.
50    process_lock: Mutex<Option<File>>,
51}
52
53impl StorageEngine {
54    /// Open (or create) a storage engine rooted at `path`.
55    ///
56    /// `path` must be a directory; it will be created if it does not exist.
57    /// After opening, any uncommitted WAL entries are replayed for crash recovery.
58    ///
59    /// # Example
60    ///
61    /// ```no_run
62    /// use mentedb_storage::StorageEngine;
63    ///
64    /// let engine = StorageEngine::open("/tmp/mentedb".as_ref())?;
65    /// // engine is ready — WAL recovery already ran if needed
66    /// # Ok::<(), mentedb_core::error::MenteError>(())
67    /// ```
68    pub fn open(path: &Path) -> MenteResult<Self> {
69        std::fs::create_dir_all(path)?;
70
71        // Exclusive process lock, held until close or process exit. A second
72        // process opening the same directory would interleave stale cached
73        // state with ours and roll the database back, so refuse it up front.
74        let lock_path = path.join("LOCK");
75        let lock_file = std::fs::OpenOptions::new()
76            .create(true)
77            .truncate(false)
78            .write(true)
79            .open(&lock_path)?;
80        fs2::FileExt::try_lock_exclusive(&lock_file).map_err(|_| {
81            MenteError::Storage(format!(
82                "database directory {} is locked by another process",
83                path.display()
84            ))
85        })?;
86
87        let page_manager = PageManager::open(path)?;
88        let buffer_pool = BufferPool::new(DEFAULT_BUFFER_POOL_SIZE);
89        let wal = Wal::open(path)?;
90
91        let engine = Self {
92            page_manager: Mutex::new(page_manager),
93            buffer_pool,
94            wal: Mutex::new(wal),
95            process_lock: Mutex::new(Some(lock_file)),
96        };
97
98        let recovered = engine.recover()?;
99        if recovered > 0 {
100            info!(recovered, ?path, "storage engine opened with WAL recovery");
101        } else {
102            info!(?path, "storage engine opened");
103        }
104
105        Ok(engine)
106    }
107
108    /// Replay WAL entries to recover writes that were not checkpointed.
109    ///
110    /// For each `PageWrite` entry the serialized data is written back to its page.
111    /// After replay the WAL is truncated. Returns the number of entries replayed.
112    pub fn recover(&self) -> MenteResult<usize> {
113        let mut wal = self.wal.lock();
114        wal.lock_exclusive()?;
115        let entries = wal.iterate()?;
116        let mut count = 0usize;
117        let mut pm = self.page_manager.lock();
118
119        // Refresh page count from disk — another process may have written pages.
120        pm.reload_header()?;
121
122        // Replay with last-op-wins per page: every PageWrite carries a full page
123        // image and PageFree discards the page, so only the final entry for each
124        // page matters. This is what makes free-then-reuse sequences safe: a
125        // PageFree followed by a later PageWrite for the same page must not
126        // leave the page on the free list.
127        let mut last_op: std::collections::HashMap<u64, &crate::wal::WalEntry> = Default::default();
128        let mut order: Vec<u64> = Vec::new();
129        for entry in &entries {
130            match entry.entry_type {
131                WalEntryType::PageWrite | WalEntryType::PageFree => {
132                    if !last_op.contains_key(&entry.page_id) {
133                        order.push(entry.page_id);
134                    }
135                    last_op.insert(entry.page_id, entry);
136                }
137                WalEntryType::Checkpoint | WalEntryType::Commit => {}
138            }
139        }
140
141        for page_id_raw in order {
142            let entry = last_op[&page_id_raw];
143            let page_id = PageId(entry.page_id);
144            match entry.entry_type {
145                WalEntryType::PageWrite => {
146                    while pm.page_count() <= entry.page_id {
147                        pm.allocate_page()?;
148                    }
149
150                    let mut page = pm.read_page(page_id)?;
151                    let copy_len = entry.data.len().min(PAGE_DATA_SIZE);
152                    page.data[..copy_len].copy_from_slice(&entry.data[..copy_len]);
153                    if copy_len < PAGE_DATA_SIZE {
154                        page.data[copy_len..].fill(0);
155                    }
156                    page.header.page_id = entry.page_id;
157                    page.header.lsn = entry.lsn;
158                    page.header.page_type = PageType::Data as u8;
159                    page.header.free_space = (PAGE_DATA_SIZE - copy_len) as u16;
160                    page.header.checksum = page.compute_checksum();
161
162                    pm.write_page(page_id, &page)?;
163                    count += 1;
164                }
165                WalEntryType::PageFree => {
166                    // Mark the page Free without touching the free list; the
167                    // list is rebuilt from page types after replay so it stays
168                    // consistent with WAL-derived page states.
169                    if entry.page_id < pm.page_count() {
170                        let mut page = Page::zeroed();
171                        page.header.page_id = entry.page_id;
172                        page.header.page_type = PageType::Free as u8;
173                        pm.write_page(page_id, &page)?;
174                        self.buffer_pool.invalidate(page_id);
175                        count += 1;
176                    }
177                }
178                WalEntryType::Checkpoint | WalEntryType::Commit => {}
179            }
180        }
181
182        if count > 0 {
183            // Header updates (free list head, page count) are not fsynced on
184            // every write, so after a crash the free list can disagree with the
185            // replayed page states. Rebuild it from page types: the WAL is the
186            // sole source of truth for which pages are Free vs Data.
187            pm.rebuild_free_list()?;
188            pm.sync()?;
189            let next_lsn = wal.next_lsn();
190            wal.truncate(next_lsn)?;
191            info!(count, "WAL recovery replayed entries");
192        }
193
194        wal.unlock()?;
195        Ok(count)
196    }
197
198    /// Gracefully shut down: flush dirty pages, sync files.
199    ///
200    /// # Example
201    ///
202    /// ```no_run
203    /// # use mentedb_storage::StorageEngine;
204    /// # let engine = StorageEngine::open("/tmp/mentedb".as_ref())?;
205    /// engine.close()?;
206    /// # Ok::<(), mentedb_core::error::MenteError>(())
207    /// ```
208    pub fn close(&self) -> MenteResult<()> {
209        let mut pm = self.page_manager.lock();
210        self.buffer_pool.flush_all(&mut pm)?;
211        pm.sync()?;
212        self.wal.lock().sync()?;
213        // Release the process lock so the directory can be reopened, by this
214        // process or another, without waiting for us to exit.
215        if let Some(lock_file) = self.process_lock.lock().take() {
216            let _ = fs2::FileExt::unlock(&lock_file);
217        }
218        info!("storage engine closed");
219        Ok(())
220    }
221
222    /// Release the process lock without flushing anything.
223    ///
224    /// Test support for simulating a process crash: a real crash releases
225    /// the OS file lock but persists nothing beyond what was already synced.
226    #[doc(hidden)]
227    pub fn release_process_lock(&self) {
228        if let Some(lock_file) = self.process_lock.lock().take() {
229            let _ = fs2::FileExt::unlock(&lock_file);
230        }
231    }
232
233    // ---- low-level page operations ----
234
235    /// Allocate a fresh page (for internal/test use).
236    ///
237    /// **WARNING**: In multi-process scenarios, prefer `store_memory` which
238    /// allocates under the WAL flock. This method does NOT acquire the flock.
239    pub fn allocate_page(&self) -> MenteResult<PageId> {
240        self.page_manager.lock().allocate_page()
241    }
242
243    /// Read a page through the buffer pool (lock-free — no WAL access).
244    pub fn read_page(&self, page_id: PageId) -> MenteResult<Box<Page>> {
245        self.buffer_pool
246            .fetch_page(page_id, &mut self.page_manager.lock())
247    }
248
249    /// Write data into an already-allocated page with WAL protection.
250    ///
251    /// Acquires the WAL flock for the duration of the write transaction.
252    /// For new pages, prefer `store_memory` which allocates + writes atomically.
253    pub fn write_page(&self, page_id: PageId, data: &[u8]) -> MenteResult<()> {
254        let lsn = {
255            let mut wal = self.wal.lock();
256            wal.lock_exclusive()?;
257            wal.reload_lsn()?;
258            let lsn = wal.append(WalEntryType::PageWrite, page_id.0, data)?;
259            wal.sync()?;
260            wal.unlock()?;
261            lsn
262        };
263
264        self.apply_page_write(page_id, data, lsn)
265    }
266
267    /// Apply a page write to the buffer pool and page manager (after WAL).
268    fn apply_page_write(&self, page_id: PageId, data: &[u8], lsn: u64) -> MenteResult<()> {
269        let mut pm = self.page_manager.lock();
270        let mut page = self.buffer_pool.fetch_page(page_id, &mut pm)?;
271        drop(pm);
272
273        let copy_len = data.len().min(PAGE_DATA_SIZE);
274        page.data[..copy_len].copy_from_slice(&data[..copy_len]);
275        if copy_len < PAGE_DATA_SIZE {
276            page.data[copy_len..].fill(0);
277        }
278        page.header.lsn = lsn;
279        page.header.page_type = PageType::Data as u8;
280        page.header.free_space = (PAGE_DATA_SIZE - copy_len) as u16;
281        page.header.checksum = page.compute_checksum();
282
283        if self.buffer_pool.update_page(page_id, &page).is_err() {
284            self.page_manager.lock().write_page(page_id, &page)?;
285        }
286        self.buffer_pool.unpin_page(page_id, true).ok();
287
288        Ok(())
289    }
290
291    // ---- high-level memory operations ----
292
293    /// Serialize and store a [`MemoryNode`] into a single page.
294    ///
295    /// The entire operation — page allocation, WAL append, page write — executes
296    /// under a single WAL flock, making it safe across multiple processes.
297    ///
298    /// # Example
299    ///
300    /// ```no_run
301    /// use mentedb_storage::StorageEngine;
302    /// use mentedb_core::{MemoryNode, memory::MemoryType, types::AgentId};
303    ///
304    /// let engine = StorageEngine::open("/tmp/mentedb".as_ref())?;
305    /// let node = MemoryNode::new(
306    ///     AgentId::new(),
307    ///     MemoryType::Semantic,
308    ///     "User likes dark mode".to_string(),
309    ///     vec![0.1, 0.2],
310    /// );
311    /// let page_id = engine.store_memory(&node)?;
312    /// # Ok::<(), mentedb_core::error::MenteError>(())
313    /// ```
314    pub fn store_memory(&self, node: &MemoryNode) -> MenteResult<PageId> {
315        let serialized =
316            serde_json::to_vec(node).map_err(|e| MenteError::Serialization(e.to_string()))?;
317
318        if serialized.len() + 4 > PAGE_DATA_SIZE {
319            return Err(MenteError::CapacityExceeded(format!(
320                "memory node serialized to {} bytes (max {})",
321                serialized.len(),
322                PAGE_DATA_SIZE - 4,
323            )));
324        }
325
326        let mut buf = Vec::with_capacity(4 + serialized.len());
327        buf.extend_from_slice(&(serialized.len() as u32).to_le_bytes());
328        buf.extend_from_slice(&serialized);
329
330        // Atomic write transaction: allocate + WAL + page write under one flock
331        let (page_id, lsn) = {
332            let mut wal = self.wal.lock();
333            let mut pm = self.page_manager.lock();
334
335            // Acquire flock and refresh state from disk
336            wal.lock_exclusive()?;
337            pm.reload_header()?;
338            wal.reload_lsn()?;
339
340            // Allocate page (using fresh page_count from disk)
341            let page_id = pm.allocate_page()?;
342
343            // WAL append + sync (WAL fsync guarantees durability;
344            // page data is written but not fsynced — checkpoint handles that)
345            let lsn = wal.append(WalEntryType::PageWrite, page_id.0, &buf)?;
346            wal.sync()?;
347
348            // Write page data to disk (no fsync — recoverable from WAL)
349            let mut page = Page::zeroed();
350            page.header.page_id = page_id.0;
351            let copy_len = buf.len().min(PAGE_DATA_SIZE);
352            page.data[..copy_len].copy_from_slice(&buf[..copy_len]);
353            page.header.lsn = lsn;
354            page.header.page_type = PageType::Data as u8;
355            page.header.free_space = (PAGE_DATA_SIZE - copy_len) as u16;
356            page.header.checksum = page.compute_checksum();
357            pm.write_page(page_id, &page)?;
358
359            // Release flock — other processes can now write
360            wal.unlock()?;
361
362            (page_id, lsn)
363        };
364
365        // Drop any stale cached copy (the page may have been reused from the
366        // free list); the next read loads the fresh data from disk.
367        let _ = lsn;
368        self.buffer_pool.invalidate(page_id);
369
370        // Auto-checkpoint when WAL exceeds threshold to prevent unbounded growth.
371        // This keeps reload_lsn() fast for subsequent writes.
372        if self.wal.lock().file_size() > WAL_AUTO_CHECKPOINT_BYTES
373            && let Err(e) = self.checkpoint()
374        {
375            tracing::warn!("auto-checkpoint failed: {e}");
376        }
377
378        info!(
379            page_id = page_id.0,
380            bytes = serialized.len(),
381            "stored memory node"
382        );
383        Ok(page_id)
384    }
385
386    /// Store multiple [`MemoryNode`]s in a single locked transaction.
387    ///
388    /// Acquires the WAL flock once, writes all nodes, then releases. This avoids
389    /// the per-write overhead of `reload_header` / `reload_lsn` for bulk inserts.
390    /// Auto-checkpoints after the batch if the WAL exceeds the threshold.
391    pub fn store_memory_batch(&self, nodes: &[MemoryNode]) -> MenteResult<Vec<PageId>> {
392        // Phase 1: serialize all nodes upfront (no locks held)
393        let mut bufs = Vec::with_capacity(nodes.len());
394        for node in nodes {
395            let serialized =
396                serde_json::to_vec(node).map_err(|e| MenteError::Serialization(e.to_string()))?;
397            if serialized.len() + 4 > PAGE_DATA_SIZE {
398                return Err(MenteError::CapacityExceeded(format!(
399                    "memory node serialized to {} bytes (max {})",
400                    serialized.len(),
401                    PAGE_DATA_SIZE - 4,
402                )));
403            }
404            let mut buf = Vec::with_capacity(4 + serialized.len());
405            buf.extend_from_slice(&(serialized.len() as u32).to_le_bytes());
406            buf.extend_from_slice(&serialized);
407            bufs.push(buf);
408        }
409
410        // Phase 2: single locked transaction for all writes
411        let page_ids = {
412            let mut wal = self.wal.lock();
413            let mut pm = self.page_manager.lock();
414
415            wal.lock_exclusive()?;
416            pm.reload_header()?;
417            wal.reload_lsn()?;
418
419            let mut ids = Vec::with_capacity(bufs.len());
420            for buf in &bufs {
421                let page_id = pm.allocate_page()?;
422                let lsn = wal.append(WalEntryType::PageWrite, page_id.0, buf)?;
423
424                let mut page = Page::zeroed();
425                page.header.page_id = page_id.0;
426                let copy_len = buf.len().min(PAGE_DATA_SIZE);
427                page.data[..copy_len].copy_from_slice(&buf[..copy_len]);
428                page.header.lsn = lsn;
429                page.header.page_type = PageType::Data as u8;
430                page.header.free_space = (PAGE_DATA_SIZE - copy_len) as u16;
431                page.header.checksum = page.compute_checksum();
432                pm.write_page(page_id, &page)?;
433
434                ids.push(page_id);
435            }
436
437            // WAL fsync only — page data is recoverable from WAL on crash.
438            // Checkpoint handles page file fsync.
439            wal.sync()?;
440            wal.unlock()?;
441
442            ids
443        };
444
445        // Drop stale cached copies for pages reused from the free list.
446        for page_id in &page_ids {
447            self.buffer_pool.invalidate(*page_id);
448        }
449
450        // Auto-checkpoint if WAL grew too large
451        if self.wal.lock().file_size() > WAL_AUTO_CHECKPOINT_BYTES
452            && let Err(e) = self.checkpoint()
453        {
454            tracing::warn!("auto-checkpoint failed: {e}");
455        }
456
457        info!(count = page_ids.len(), "stored memory batch");
458        Ok(page_ids)
459    }
460
461    /// Update a [`MemoryNode`] in place on its existing page.
462    ///
463    /// The write goes through the WAL-protected `write_page` path, so it is
464    /// crash-durable and keeps the buffer pool coherent. Unlike storing to a
465    /// fresh page, this never orphans the old copy.
466    pub fn update_memory(&self, page_id: PageId, node: &MemoryNode) -> MenteResult<()> {
467        let serialized =
468            serde_json::to_vec(node).map_err(|e| MenteError::Serialization(e.to_string()))?;
469
470        if serialized.len() + 4 > PAGE_DATA_SIZE {
471            return Err(MenteError::CapacityExceeded(format!(
472                "memory node serialized to {} bytes (max {})",
473                serialized.len(),
474                PAGE_DATA_SIZE - 4,
475            )));
476        }
477
478        let mut buf = Vec::with_capacity(4 + serialized.len());
479        buf.extend_from_slice(&(serialized.len() as u32).to_le_bytes());
480        buf.extend_from_slice(&serialized);
481
482        self.write_page(page_id, &buf)
483    }
484
485    /// Delete a memory by returning its page to the free list.
486    ///
487    /// The deletion is WAL-logged before the page is freed, so it survives a
488    /// crash: recovery replays the `PageFree` entry and the memory does not
489    /// resurrect on reopen. The freed page is reused by later allocations.
490    pub fn delete_memory(&self, page_id: PageId) -> MenteResult<()> {
491        {
492            let mut wal = self.wal.lock();
493            let mut pm = self.page_manager.lock();
494
495            wal.lock_exclusive()?;
496            pm.reload_header()?;
497            wal.reload_lsn()?;
498
499            // WAL fsync guarantees the deletion is durable before the page
500            // is touched; the free-list update itself is recoverable.
501            wal.append(WalEntryType::PageFree, page_id.0, &[])?;
502            wal.sync()?;
503
504            pm.free_page(page_id)?;
505            wal.unlock()?;
506        }
507
508        // A stale cached copy must never be served or flushed back.
509        self.buffer_pool.invalidate(page_id);
510
511        info!(page_id = page_id.0, "deleted memory node");
512        Ok(())
513    }
514
515    /// Load and deserialize a [`MemoryNode`] from the given page.
516    ///
517    /// # Example
518    ///
519    /// ```no_run
520    /// # use mentedb_storage::{StorageEngine, PageId};
521    /// # let engine = StorageEngine::open("/tmp/mentedb".as_ref())?;
522    /// let node = engine.load_memory(PageId(1))?;
523    /// println!("memory: {}", node.content);
524    /// # Ok::<(), mentedb_core::error::MenteError>(())
525    /// ```
526    pub fn load_memory(&self, page_id: PageId) -> MenteResult<MemoryNode> {
527        let page = self.read_page(page_id)?;
528        self.buffer_pool.unpin_page(page_id, false).ok();
529
530        if PageType::from(page.header.page_type) != PageType::Data {
531            return Err(MenteError::Storage(format!(
532                "page {} is not a data page",
533                page_id.0
534            )));
535        }
536
537        let len = u32::from_le_bytes(page.data[..4].try_into().unwrap()) as usize;
538        if len == 0 || len + 4 > PAGE_DATA_SIZE {
539            return Err(MenteError::Storage(format!(
540                "invalid memory node length prefix: {len}"
541            )));
542        }
543
544        serde_json::from_slice(&page.data[4..4 + len])
545            .map_err(|e| MenteError::Serialization(e.to_string()))
546    }
547
548    // ---- durability ----
549
550    /// Checkpoint: flush all dirty pages, sync to disk, and truncate the WAL.
551    ///
552    /// # Example
553    ///
554    /// ```no_run
555    /// # use mentedb_storage::StorageEngine;
556    /// # let engine = StorageEngine::open("/tmp/mentedb".as_ref())?;
557    /// // After a batch of writes, checkpoint to reclaim WAL space
558    /// engine.checkpoint()?;
559    /// # Ok::<(), mentedb_core::error::MenteError>(())
560    /// ```
561    pub fn checkpoint(&self) -> MenteResult<()> {
562        let mut wal = self.wal.lock();
563        let mut pm = self.page_manager.lock();
564
565        wal.lock_exclusive()?;
566        wal.reload_lsn()?;
567
568        self.buffer_pool.flush_all(&mut pm)?;
569        pm.sync()?;
570
571        let lsn = wal.append(WalEntryType::Checkpoint, 0, &[])?;
572        wal.sync()?;
573        wal.truncate(lsn)?;
574        wal.unlock()?;
575
576        info!(lsn, "checkpoint complete");
577        Ok(())
578    }
579
580    /// Scan all pages and return (MemoryId, PageId) pairs for every valid memory node.
581    ///
582    /// Refreshes the page count from disk before scanning so pages written by
583    /// other processes are included. Used to rebuild the page map on startup.
584    ///
585    /// # Example
586    ///
587    /// ```no_run
588    /// # use mentedb_storage::StorageEngine;
589    /// # let engine = StorageEngine::open("/tmp/mentedb".as_ref())?;
590    /// let memories = engine.scan_all_memories();
591    /// for (memory_id, page_id) in &memories {
592    ///     println!("{memory_id} -> page {}", page_id.0);
593    /// }
594    /// # Ok::<(), mentedb_core::error::MenteError>(())
595    /// ```
596    pub fn scan_all_memories(&self) -> Vec<(mentedb_core::types::MemoryId, PageId)> {
597        let mut pm = self.page_manager.lock();
598        // Refresh from disk to see pages written by other processes
599        let _ = pm.reload_header();
600        let count = pm.page_count();
601        drop(pm);
602
603        let mut results = Vec::new();
604        for i in 1..count {
605            let page_id = PageId(i);
606            if let Ok(node) = self.load_memory(page_id) {
607                results.push((node.id, page_id));
608            }
609        }
610        results
611    }
612}
613
614#[cfg(test)]
615mod tests {
616    use super::*;
617    use mentedb_core::memory::MemoryType;
618    use mentedb_core::types::AgentId;
619
620    fn setup() -> (tempfile::TempDir, StorageEngine) {
621        let dir = tempfile::tempdir().unwrap();
622        let engine = StorageEngine::open(dir.path()).unwrap();
623        (dir, engine)
624    }
625
626    #[test]
627    fn test_allocate_write_read() {
628        let (_dir, engine) = setup();
629
630        let pid = engine.allocate_page().unwrap();
631        engine.write_page(pid, b"hello storage engine").unwrap();
632
633        let page = engine.read_page(pid).unwrap();
634        assert_eq!(&page.data[..20], b"hello storage engine");
635        engine.buffer_pool.unpin_page(pid, false).ok();
636    }
637
638    #[test]
639    fn test_store_and_load_memory() {
640        let (_dir, engine) = setup();
641
642        let node = MemoryNode::new(
643            AgentId::new(),
644            MemoryType::Episodic,
645            "The user prefers Rust over Go".to_string(),
646            vec![0.1, 0.2, 0.3, 0.4],
647        );
648
649        let page_id = engine.store_memory(&node).unwrap();
650        let loaded = engine.load_memory(page_id).unwrap();
651
652        assert_eq!(node.id, loaded.id);
653        assert_eq!(node.content, loaded.content);
654        assert_eq!(node.embedding, loaded.embedding);
655        assert_eq!(node.memory_type, loaded.memory_type);
656    }
657
658    #[test]
659    fn test_checkpoint() {
660        let (_dir, engine) = setup();
661
662        let node = MemoryNode::new(
663            AgentId::new(),
664            MemoryType::Semantic,
665            "checkpoint test".to_string(),
666            vec![1.0, 2.0],
667        );
668
669        let pid = engine.store_memory(&node).unwrap();
670        engine.checkpoint().unwrap();
671
672        let loaded = engine.load_memory(pid).unwrap();
673        assert_eq!(loaded.content, "checkpoint test");
674    }
675
676    #[test]
677    fn test_close_and_reopen() {
678        let dir = tempfile::tempdir().unwrap();
679        let pid;
680        {
681            let engine = StorageEngine::open(dir.path()).unwrap();
682            let node = MemoryNode::new(
683                AgentId::new(),
684                MemoryType::Procedural,
685                "persist across close".to_string(),
686                vec![0.5],
687            );
688            pid = engine.store_memory(&node).unwrap();
689            engine.close().unwrap();
690        }
691        {
692            let engine = StorageEngine::open(dir.path()).unwrap();
693            let loaded = engine.load_memory(pid).unwrap();
694            assert_eq!(loaded.content, "persist across close");
695        }
696    }
697
698    #[test]
699    fn test_crash_recovery() {
700        let dir = tempfile::tempdir().unwrap();
701        let mut ids = Vec::new();
702        let mut contents = Vec::new();
703        {
704            let engine = StorageEngine::open(dir.path()).unwrap();
705            for i in 0..3 {
706                let content = format!("crash-recovery-{i}");
707                let node = MemoryNode::new(
708                    AgentId::new(),
709                    MemoryType::Episodic,
710                    content.clone(),
711                    vec![i as f32],
712                );
713                let pid = engine.store_memory(&node).unwrap();
714                ids.push(pid);
715                contents.push(content);
716            }
717            // Simulate crash: sync the WAL but do NOT call close/checkpoint.
718            engine.wal.lock().sync().unwrap();
719        }
720        {
721            let engine = StorageEngine::open(dir.path()).unwrap();
722            for (pid, expected) in ids.iter().zip(contents.iter()) {
723                let loaded = engine.load_memory(*pid).unwrap();
724                assert_eq!(&loaded.content, expected);
725            }
726        }
727    }
728
729    #[test]
730    fn test_recovery_idempotent() {
731        let dir = tempfile::tempdir().unwrap();
732        let pid;
733        let content = "idempotent-check".to_string();
734        {
735            let engine = StorageEngine::open(dir.path()).unwrap();
736            let node = MemoryNode::new(
737                AgentId::new(),
738                MemoryType::Semantic,
739                content.clone(),
740                vec![1.0, 2.0],
741            );
742            pid = engine.store_memory(&node).unwrap();
743            engine.checkpoint().unwrap();
744            engine.close().unwrap();
745        }
746        {
747            let engine = StorageEngine::open(dir.path()).unwrap();
748            let loaded = engine.load_memory(pid).unwrap();
749            assert_eq!(loaded.content, content);
750        }
751    }
752
753    #[test]
754    fn test_partial_write_recovery() {
755        let dir = tempfile::tempdir().unwrap();
756        let mut ids = Vec::new();
757        let mut contents = Vec::new();
758        {
759            let engine = StorageEngine::open(dir.path()).unwrap();
760            for i in 0..3 {
761                let content = format!("checkpointed-{i}");
762                let node = MemoryNode::new(
763                    AgentId::new(),
764                    MemoryType::Semantic,
765                    content.clone(),
766                    vec![i as f32],
767                );
768                let pid = engine.store_memory(&node).unwrap();
769                ids.push(pid);
770                contents.push(content);
771            }
772            engine.checkpoint().unwrap();
773
774            for i in 3..5 {
775                let content = format!("unckeckpointed-{i}");
776                let node = MemoryNode::new(
777                    AgentId::new(),
778                    MemoryType::Episodic,
779                    content.clone(),
780                    vec![i as f32],
781                );
782                let pid = engine.store_memory(&node).unwrap();
783                ids.push(pid);
784                contents.push(content);
785            }
786            // Simulate crash — sync WAL but don't close.
787            engine.wal.lock().sync().unwrap();
788        }
789        {
790            let engine = StorageEngine::open(dir.path()).unwrap();
791            for (pid, expected) in ids.iter().zip(contents.iter()) {
792                let loaded = engine.load_memory(*pid).unwrap();
793                assert_eq!(&loaded.content, expected);
794            }
795        }
796    }
797
798    #[test]
799    fn test_delete_memory_durable() {
800        let dir = tempfile::tempdir().unwrap();
801        let pid;
802        {
803            let engine = StorageEngine::open(dir.path()).unwrap();
804            let node = MemoryNode::new(
805                AgentId::new(),
806                MemoryType::Semantic,
807                "to be deleted".to_string(),
808                vec![1.0],
809            );
810            pid = engine.store_memory(&node).unwrap();
811            engine.delete_memory(pid).unwrap();
812            assert!(engine.load_memory(pid).is_err());
813            assert!(engine.scan_all_memories().is_empty());
814            engine.close().unwrap();
815        }
816        {
817            let engine = StorageEngine::open(dir.path()).unwrap();
818            assert!(
819                engine.load_memory(pid).is_err(),
820                "deleted memory must not resurrect on reopen"
821            );
822            assert!(engine.scan_all_memories().is_empty());
823        }
824    }
825
826    #[test]
827    fn test_delete_survives_crash() {
828        let dir = tempfile::tempdir().unwrap();
829        let pid;
830        {
831            let engine = StorageEngine::open(dir.path()).unwrap();
832            let node = MemoryNode::new(
833                AgentId::new(),
834                MemoryType::Semantic,
835                "crash delete".to_string(),
836                vec![1.0],
837            );
838            pid = engine.store_memory(&node).unwrap();
839            engine.delete_memory(pid).unwrap();
840            // Simulate crash: no close, no checkpoint.
841        }
842        {
843            let engine = StorageEngine::open(dir.path()).unwrap();
844            assert!(
845                engine.load_memory(pid).is_err(),
846                "deletion must survive a crash via WAL replay"
847            );
848            assert!(engine.scan_all_memories().is_empty());
849        }
850    }
851
852    #[test]
853    fn test_deleted_page_reused() {
854        let (_dir, engine) = setup();
855
856        let a = MemoryNode::new(AgentId::new(), MemoryType::Semantic, "a".into(), vec![1.0]);
857        let pid_a = engine.store_memory(&a).unwrap();
858        engine.delete_memory(pid_a).unwrap();
859
860        let b = MemoryNode::new(AgentId::new(), MemoryType::Semantic, "b".into(), vec![2.0]);
861        let pid_b = engine.store_memory(&b).unwrap();
862        assert_eq!(pid_a, pid_b, "freed page should be reused");
863
864        let loaded = engine.load_memory(pid_b).unwrap();
865        assert_eq!(loaded.content, "b");
866    }
867
868    #[test]
869    fn test_delete_reuse_crash_recovery() {
870        let dir = tempfile::tempdir().unwrap();
871        let pid;
872        let b_id;
873        {
874            let engine = StorageEngine::open(dir.path()).unwrap();
875            let a = MemoryNode::new(AgentId::new(), MemoryType::Semantic, "a".into(), vec![1.0]);
876            pid = engine.store_memory(&a).unwrap();
877            engine.delete_memory(pid).unwrap();
878            let b = MemoryNode::new(AgentId::new(), MemoryType::Semantic, "b".into(), vec![2.0]);
879            let pid_b = engine.store_memory(&b).unwrap();
880            assert_eq!(pid, pid_b);
881            b_id = b.id;
882            // Simulate crash: the WAL now holds PageFree(p) then PageWrite(p).
883        }
884        {
885            let engine = StorageEngine::open(dir.path()).unwrap();
886            let loaded = engine.load_memory(pid).unwrap();
887            assert_eq!(loaded.content, "b", "later write must win over the free");
888            assert_eq!(loaded.id, b_id);
889            // The page must NOT be on the free list: a fresh allocation must
890            // not clobber b.
891            let c = MemoryNode::new(AgentId::new(), MemoryType::Semantic, "c".into(), vec![3.0]);
892            let pid_c = engine.store_memory(&c).unwrap();
893            assert_ne!(pid_c, pid, "recovered free list must exclude reused page");
894            assert_eq!(engine.load_memory(pid).unwrap().content, "b");
895        }
896    }
897
898    #[test]
899    fn test_update_memory_in_place() {
900        let dir = tempfile::tempdir().unwrap();
901        let pid;
902        let id;
903        {
904            let engine = StorageEngine::open(dir.path()).unwrap();
905            let mut node = MemoryNode::new(
906                AgentId::new(),
907                MemoryType::Semantic,
908                "original".to_string(),
909                vec![1.0],
910            );
911            pid = engine.store_memory(&node).unwrap();
912            id = node.id;
913
914            node.content = "updated".to_string();
915            engine.update_memory(pid, &node).unwrap();
916
917            let loaded = engine.load_memory(pid).unwrap();
918            assert_eq!(loaded.content, "updated");
919            // No orphan copy: exactly one entry in a full scan.
920            let scanned = engine.scan_all_memories();
921            assert_eq!(scanned.len(), 1);
922            engine.close().unwrap();
923        }
924        {
925            let engine = StorageEngine::open(dir.path()).unwrap();
926            let loaded = engine.load_memory(pid).unwrap();
927            assert_eq!(loaded.content, "updated");
928            assert_eq!(loaded.id, id);
929            assert_eq!(engine.scan_all_memories().len(), 1);
930        }
931    }
932
933    #[test]
934    fn test_concurrent_open_is_rejected() {
935        let dir = tempfile::tempdir().unwrap();
936
937        // The per operation flock serializes individual write transactions,
938        // but layers above the storage engine cache state across operations,
939        // so a second concurrent engine rolls the database back to its own
940        // open time snapshot. The process lock must refuse it.
941        let engine1 = StorageEngine::open(dir.path()).unwrap();
942        let second = StorageEngine::open(dir.path());
943        assert!(second.is_err(), "second concurrent open must fail");
944        let msg = second.err().unwrap().to_string();
945        assert!(msg.contains("locked"), "error names the lock: {msg}");
946
947        // Close releases the lock; the directory can be opened again.
948        engine1.close().unwrap();
949        let engine2 = StorageEngine::open(dir.path()).unwrap();
950        engine2.close().unwrap();
951    }
952
953    #[test]
954    fn test_concurrent_writes_from_threads() {
955        use std::sync::Arc;
956        let dir = tempfile::tempdir().unwrap();
957        let engine = Arc::new(StorageEngine::open(dir.path()).unwrap());
958
959        let handles: Vec<_> = (0..10)
960            .map(|i| {
961                let eng = Arc::clone(&engine);
962                std::thread::spawn(move || {
963                    let node = MemoryNode::new(
964                        AgentId::new(),
965                        MemoryType::Episodic,
966                        format!("thread-{i}"),
967                        vec![i as f32],
968                    );
969                    eng.store_memory(&node).unwrap()
970                })
971            })
972            .collect();
973
974        let pids: Vec<PageId> = handles.into_iter().map(|h| h.join().unwrap()).collect();
975
976        // All 10 writes succeeded and are readable
977        for (i, pid) in pids.iter().enumerate() {
978            let loaded = engine.load_memory(*pid).unwrap();
979            assert_eq!(loaded.content, format!("thread-{i}"));
980        }
981    }
982}