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, PathBuf};
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::memory_map::{self, MapAnchor, MappedMemory, MemoryTable};
14use crate::page::{PAGE_DATA_SIZE, Page, PageId, PageManager, PageType};
15use crate::wal::{Wal, WalEntryType};
16/// Default number of page frames in the buffer pool.
17const DEFAULT_BUFFER_POOL_SIZE: usize = 1024;
18
19/// Auto-checkpoint when WAL file exceeds this size (8 MB).
20const WAL_AUTO_CHECKPOINT_BYTES: u64 = 8 * 1024 * 1024;
21
22/// The unified storage engine for MenteDB.
23///
24/// Coordinates page allocation, caching, and write-ahead logging to provide
25/// crash-safe, page-oriented storage for memory nodes.
26///
27/// Concurrency model (inspired by WAL-mode databases):
28/// - **Reads are lock-free**: `read_page` only touches the buffer pool and page
29///   manager — no file locks, no WAL access.
30/// - **Writes are fully serialized** via a blocking `flock(2)` on the WAL file.
31///   The entire write transaction (page allocation + WAL append + page write +
32///   fsync) executes under a single flock, ensuring correctness across multiple
33///   processes sharing the same data directory.
34/// - **State is refreshed from disk** under the flock: page count is re-read
35///   from the file header and LSN is re-read from the WAL tail, so no process
36///   can act on stale in-memory state.
37/// - **One process per database.** Open acquires an exclusive lock on a LOCK
38///   file in the data directory and holds it for the engine's lifetime. The
39///   per operation flock protects individual write transactions, but layers
40///   above the storage engine cache state across operations (buffer pool,
41///   page map, index snapshots), and a second concurrent process flushing
42///   those caches rolls the database back to its own open time snapshot.
43///   A second open therefore fails fast with a "locked" error instead of
44///   silently corrupting state; callers retry until the owner exits.
45pub struct StorageEngine {
46    page_manager: Mutex<PageManager>,
47    buffer_pool: BufferPool,
48    wal: Mutex<Wal>,
49    /// Held exclusively for the lifetime of this engine; released on close
50    /// or process exit. Guards against concurrent multi process opens.
51    process_lock: Mutex<Option<File>>,
52    /// Database directory (holds the memory map snapshot).
53    dir: PathBuf,
54    /// The memory map as of the checkpoint entry carrying its token, when
55    /// known. None until a valid snapshot is loaded or a full scan builds one;
56    /// every checkpoint rolls it forward and persists it. Lock order: after
57    /// the WAL and page manager mutexes.
58    map_anchor: Mutex<Option<MapAnchor>>,
59    /// Memory loads served since open, cached or not (metrics and tests).
60    memory_loads: std::sync::atomic::AtomicU64,
61}
62
63impl StorageEngine {
64    /// Open (or create) a storage engine rooted at `path`.
65    ///
66    /// `path` must be a directory; it will be created if it does not exist.
67    /// After opening, any uncommitted WAL entries are replayed for crash recovery.
68    ///
69    /// # Example
70    ///
71    /// ```no_run
72    /// use mentedb_storage::StorageEngine;
73    ///
74    /// let engine = StorageEngine::open("/tmp/mentedb".as_ref())?;
75    /// // engine is ready — WAL recovery already ran if needed
76    /// # Ok::<(), mentedb_core::error::MenteError>(())
77    /// ```
78    pub fn open(path: &Path) -> MenteResult<Self> {
79        std::fs::create_dir_all(path)?;
80
81        // Exclusive process lock, held until close or process exit. A second
82        // process opening the same directory would interleave stale cached
83        // state with ours and roll the database back, so refuse it up front.
84        let lock_path = path.join("LOCK");
85        let lock_file = std::fs::OpenOptions::new()
86            .create(true)
87            .truncate(false)
88            .write(true)
89            .open(&lock_path)?;
90        // Cross-host lock: fcntl OFD lock on Linux (enforced across hosts by the
91        // NFSv4 lock manager), flock elsewhere. This makes single-writer safe
92        // when the directory lives on a network filesystem mounted by more than
93        // one host.
94        match crate::lock::try_lock_exclusive(&lock_file) {
95            Ok(true) => {}
96            Ok(false) => {
97                return Err(MenteError::Storage(format!(
98                    "database directory {} is locked by another process",
99                    path.display()
100                )));
101            }
102            Err(e) => {
103                return Err(MenteError::Storage(format!(
104                    "failed to lock database directory {}: {e}",
105                    path.display()
106                )));
107            }
108        }
109
110        let page_manager = PageManager::open(path)?;
111        let buffer_pool = BufferPool::new(DEFAULT_BUFFER_POOL_SIZE);
112        let wal = Wal::open(path)?;
113
114        let engine = Self {
115            page_manager: Mutex::new(page_manager),
116            buffer_pool,
117            wal: Mutex::new(wal),
118            process_lock: Mutex::new(Some(lock_file)),
119            dir: path.to_path_buf(),
120            map_anchor: Mutex::new(None),
121            memory_loads: std::sync::atomic::AtomicU64::new(0),
122        };
123
124        let recovered = engine.recover()?;
125        if recovered > 0 {
126            info!(recovered, ?path, "storage engine opened with WAL recovery");
127        } else {
128            info!(?path, "storage engine opened");
129        }
130
131        Ok(engine)
132    }
133
134    /// Replay WAL entries to recover writes that were not checkpointed.
135    ///
136    /// For each `PageWrite` entry the serialized data is written back to its page.
137    /// After replay the WAL is truncated. Returns the number of entries replayed.
138    pub fn recover(&self) -> MenteResult<usize> {
139        let mut wal = self.wal.lock();
140        wal.lock_exclusive()?;
141        let result = self.recover_locked(&mut wal);
142        let unlocked = wal.unlock();
143        let count = result?;
144        unlocked?;
145        Ok(count)
146    }
147
148    /// Load the persisted memory map and roll it forward over `entries`, or
149    /// None when it is missing, unreadable, or does not belong to this WAL
150    /// and page file. Never an error: the fallback is a full scan.
151    fn load_anchor(&self, entries: &[crate::wal::WalEntry], page_count: u64) -> Option<MapAnchor> {
152        let mut anchor = match memory_map::read_snapshot(&self.dir) {
153            Ok(Some(a)) => a,
154            Ok(None) => return None,
155            Err(e) => {
156                tracing::warn!(error = %e, "memory map snapshot unreadable, open will scan pages");
157                return None;
158            }
159        };
160        if !anchor.fits(page_count) {
161            tracing::warn!(
162                snapshot_pages = anchor.page_count,
163                page_count,
164                "memory map snapshot does not fit the page file, open will scan pages"
165            );
166            return None;
167        }
168        if anchor.advance(entries).is_none() {
169            info!("memory map snapshot predates the WAL, open will scan pages");
170            return None;
171        }
172        Some(anchor)
173    }
174
175    fn recover_locked(&self, wal: &mut Wal) -> MenteResult<usize> {
176        let entries = wal.iterate()?;
177        let mut count = 0usize;
178        let mut pm = self.page_manager.lock();
179
180        // Refresh page count from disk — another process may have written pages.
181        pm.reload_header()?;
182
183        // Roll the persisted memory map forward before replay may truncate
184        // the WAL entries it is anchored on.
185        let anchor = self.load_anchor(&entries, pm.page_count());
186
187        // Replay with last-op-wins per page: every PageWrite carries a full page
188        // image and PageFree discards the page, so only the final entry for each
189        // page matters. This is what makes free-then-reuse sequences safe: a
190        // PageFree followed by a later PageWrite for the same page must not
191        // leave the page on the free list.
192        let mut last_op: std::collections::HashMap<u64, &crate::wal::WalEntry> = Default::default();
193        let mut order: Vec<u64> = Vec::new();
194        for entry in &entries {
195            match entry.entry_type {
196                WalEntryType::PageWrite | WalEntryType::PageFree => {
197                    if !last_op.contains_key(&entry.page_id) {
198                        order.push(entry.page_id);
199                    }
200                    last_op.insert(entry.page_id, entry);
201                }
202                WalEntryType::Checkpoint | WalEntryType::Commit => {}
203            }
204        }
205
206        for page_id_raw in order {
207            let entry = last_op[&page_id_raw];
208            let page_id = PageId(entry.page_id);
209            match entry.entry_type {
210                WalEntryType::PageWrite => {
211                    while pm.page_count() <= entry.page_id {
212                        pm.allocate_page()?;
213                    }
214
215                    let mut page = pm.read_page(page_id)?;
216                    let copy_len = entry.data.len().min(PAGE_DATA_SIZE);
217                    page.data[..copy_len].copy_from_slice(&entry.data[..copy_len]);
218                    if copy_len < PAGE_DATA_SIZE {
219                        page.data[copy_len..].fill(0);
220                    }
221                    page.header.page_id = entry.page_id;
222                    page.header.lsn = entry.lsn;
223                    page.header.page_type = PageType::Data as u8;
224                    page.header.free_space = (PAGE_DATA_SIZE - copy_len) as u16;
225                    page.header.checksum = page.compute_checksum();
226
227                    pm.write_page(page_id, &page)?;
228                    count += 1;
229                }
230                WalEntryType::PageFree => {
231                    // Mark the page Free without touching the free list; the
232                    // list is rebuilt from page types after replay so it stays
233                    // consistent with WAL-derived page states.
234                    if entry.page_id < pm.page_count() {
235                        let mut page = Page::zeroed();
236                        page.header.page_id = entry.page_id;
237                        page.header.page_type = PageType::Free as u8;
238                        pm.write_page(page_id, &page)?;
239                        self.buffer_pool.invalidate(page_id);
240                        count += 1;
241                    }
242                }
243                WalEntryType::Checkpoint | WalEntryType::Commit => {}
244            }
245        }
246
247        if count > 0 {
248            // Header updates (free list head, page count) are not fsynced on
249            // every write, so after a crash the free list can disagree with the
250            // replayed page states. Rebuild it from page types: the WAL is the
251            // sole source of truth for which pages are Free vs Data. With a
252            // memory map the free pages are known, so only those are read
253            // (and verified); otherwise every page is scanned.
254            let rebuilt = match &anchor {
255                Some(a) => pm.rebuild_free_list_from(a.table.free_pages())?,
256                None => false,
257            };
258            if !rebuilt {
259                pm.rebuild_free_list()?;
260            }
261            pm.sync()?;
262            match anchor {
263                // The replayed pages are now durable, which makes this a
264                // checkpoint: anchor the rolled forward map on a fresh entry
265                // before truncating the entries it was derived from.
266                Some(anchor) => {
267                    self.anchor_checkpoint(wal, &pm, anchor.table)?;
268                }
269                None => {
270                    let next_lsn = wal.next_lsn();
271                    wal.truncate(next_lsn)?;
272                }
273            }
274            info!(count, "WAL recovery replayed entries");
275        } else if let Some(anchor) = anchor {
276            // Nothing to replay, so no entry after the anchor changed the map.
277            *self.map_anchor.lock() = Some(anchor);
278        }
279
280        Ok(count)
281    }
282
283    /// Append a checkpoint entry with a fresh token, persist `table` as the map
284    /// as of that entry, and truncate the WAL to it. The page file must already
285    /// be durable. When the snapshot cannot be written the anchor is dropped
286    /// (the next open scans) but the checkpoint still completes.
287    fn anchor_checkpoint(
288        &self,
289        wal: &mut Wal,
290        pm: &PageManager,
291        table: MemoryTable,
292    ) -> MenteResult<u64> {
293        let token = memory_map::new_token();
294        let lsn = wal.append(WalEntryType::Checkpoint, 0, &token)?;
295        wal.sync()?;
296        let anchor = MapAnchor {
297            token,
298            page_count: pm.page_count(),
299            table,
300            pending: Default::default(),
301        };
302        // The file is written before the slot is locked, so stamp lookups on
303        // the recall path never wait on snapshot I/O.
304        let written = memory_map::write_snapshot(&self.dir, &anchor);
305        match written {
306            Ok(()) => *self.map_anchor.lock() = Some(anchor),
307            Err(e) => {
308                tracing::warn!(error = %e, "memory map snapshot write failed, next open will scan pages");
309                memory_map::remove_snapshot(&self.dir);
310                *self.map_anchor.lock() = None;
311            }
312        }
313        wal.truncate(lsn)?;
314        Ok(lsn)
315    }
316
317    /// Gracefully shut down: flush dirty pages, sync files.
318    ///
319    /// # Example
320    ///
321    /// ```no_run
322    /// # use mentedb_storage::StorageEngine;
323    /// # let engine = StorageEngine::open("/tmp/mentedb".as_ref())?;
324    /// engine.close()?;
325    /// # Ok::<(), mentedb_core::error::MenteError>(())
326    /// ```
327    pub fn close(&self) -> MenteResult<()> {
328        let mut pm = self.page_manager.lock();
329        self.buffer_pool.flush_all(&mut pm)?;
330        pm.sync()?;
331        self.wal.lock().sync()?;
332        // Release the process lock so the directory can be reopened, by this
333        // process or another, without waiting for us to exit.
334        if let Some(lock_file) = self.process_lock.lock().take() {
335            let _ = crate::lock::unlock(&lock_file);
336        }
337        info!("storage engine closed");
338        Ok(())
339    }
340
341    /// Release the process lock without flushing anything.
342    ///
343    /// Test support for simulating a process crash: a real crash releases
344    /// the OS file lock but persists nothing beyond what was already synced.
345    #[doc(hidden)]
346    pub fn release_process_lock(&self) {
347        if let Some(lock_file) = self.process_lock.lock().take() {
348            let _ = crate::lock::unlock(&lock_file);
349        }
350    }
351
352    // ---- low-level page operations ----
353
354    /// Allocate a fresh page (for internal/test use).
355    ///
356    /// **WARNING**: In multi-process scenarios, prefer `store_memory` which
357    /// allocates under the WAL flock. This method does NOT acquire the flock.
358    pub fn allocate_page(&self) -> MenteResult<PageId> {
359        self.page_manager.lock().allocate_page()
360    }
361
362    /// Read a page through the buffer pool (lock-free — no WAL access).
363    pub fn read_page(&self, page_id: PageId) -> MenteResult<Box<Page>> {
364        self.buffer_pool
365            .fetch_page(page_id, &mut self.page_manager.lock())
366    }
367
368    /// Buffer-pool cache activity, for metrics (hit ratio, evictions, residency).
369    pub fn buffer_stats(&self) -> crate::buffer::BufferStats {
370        self.buffer_pool.stats()
371    }
372
373    /// Total pages in the store; multiply by `PAGE_SIZE` for on-disk bytes.
374    pub fn page_count(&self) -> u64 {
375        self.page_manager.lock().page_count()
376    }
377
378    /// Write data into an already-allocated page with WAL protection.
379    ///
380    /// Acquires the WAL flock for the duration of the write transaction.
381    /// For new pages, prefer `store_memory` which allocates + writes atomically.
382    pub fn write_page(&self, page_id: PageId, data: &[u8]) -> MenteResult<()> {
383        let id = memory_map::decode_memory(data).ok().map(|n| n.id);
384        self.write_page_as(page_id, data, id)
385    }
386
387    /// `write_page` for data known to decode as memory `id` (or as none).
388    fn write_page_as(
389        &self,
390        page_id: PageId,
391        data: &[u8],
392        id: Option<mentedb_core::types::MemoryId>,
393    ) -> MenteResult<()> {
394        let lsn = {
395            let mut wal = self.wal.lock();
396            wal.lock_exclusive()?;
397            wal.reload_lsn()?;
398            let lsn = wal.append(WalEntryType::PageWrite, page_id.0, data)?;
399            wal.sync()?;
400            self.note_map_write(page_id, id, lsn);
401            wal.unlock()?;
402            lsn
403        };
404
405        self.apply_page_write(page_id, data, lsn)
406    }
407
408    /// Apply a page write to the buffer pool and page manager (after WAL).
409    fn apply_page_write(&self, page_id: PageId, data: &[u8], lsn: u64) -> MenteResult<()> {
410        let mut pm = self.page_manager.lock();
411        let mut page = self.buffer_pool.fetch_page(page_id, &mut pm)?;
412        drop(pm);
413
414        let copy_len = data.len().min(PAGE_DATA_SIZE);
415        page.data[..copy_len].copy_from_slice(&data[..copy_len]);
416        if copy_len < PAGE_DATA_SIZE {
417            page.data[copy_len..].fill(0);
418        }
419        page.header.lsn = lsn;
420        page.header.page_type = PageType::Data as u8;
421        page.header.free_space = (PAGE_DATA_SIZE - copy_len) as u16;
422        page.header.checksum = page.compute_checksum();
423
424        if self.buffer_pool.update_page(page_id, &page).is_err() {
425            self.page_manager.lock().write_page(page_id, &page)?;
426        }
427        self.buffer_pool.unpin_page(page_id, true).ok();
428
429        Ok(())
430    }
431
432    // ---- high-level memory operations ----
433
434    /// Serialize and store a [`MemoryNode`] into a single page.
435    ///
436    /// The entire operation — page allocation, WAL append, page write — executes
437    /// under a single WAL flock, making it safe across multiple processes.
438    ///
439    /// # Example
440    ///
441    /// ```no_run
442    /// use mentedb_storage::StorageEngine;
443    /// use mentedb_core::{MemoryNode, memory::MemoryType, types::AgentId};
444    ///
445    /// let engine = StorageEngine::open("/tmp/mentedb".as_ref())?;
446    /// let node = MemoryNode::new(
447    ///     AgentId::new(),
448    ///     MemoryType::Semantic,
449    ///     "User likes dark mode".to_string(),
450    ///     vec![0.1, 0.2],
451    /// );
452    /// let page_id = engine.store_memory(&node)?;
453    /// # Ok::<(), mentedb_core::error::MenteError>(())
454    /// ```
455    pub fn store_memory(&self, node: &MemoryNode) -> MenteResult<PageId> {
456        let serialized =
457            serde_json::to_vec(node).map_err(|e| MenteError::Serialization(e.to_string()))?;
458
459        if serialized.len() + 4 > PAGE_DATA_SIZE {
460            return Err(MenteError::CapacityExceeded(format!(
461                "memory node serialized to {} bytes (max {})",
462                serialized.len(),
463                PAGE_DATA_SIZE - 4,
464            )));
465        }
466
467        let mut buf = Vec::with_capacity(4 + serialized.len());
468        buf.extend_from_slice(&(serialized.len() as u32).to_le_bytes());
469        buf.extend_from_slice(&serialized);
470
471        // Atomic write transaction: allocate + WAL + page write under one flock
472        let (page_id, lsn) = {
473            let mut wal = self.wal.lock();
474            let mut pm = self.page_manager.lock();
475
476            // Acquire flock and refresh state from disk
477            wal.lock_exclusive()?;
478            pm.reload_header()?;
479            wal.reload_lsn()?;
480
481            // Allocate page (using fresh page_count from disk)
482            let page_id = pm.allocate_page()?;
483
484            // WAL append + sync (WAL fsync guarantees durability;
485            // page data is written but not fsynced — checkpoint handles that)
486            let lsn = wal.append(WalEntryType::PageWrite, page_id.0, &buf)?;
487            wal.sync()?;
488            self.note_map_write(page_id, Some(node.id), lsn);
489
490            // Write page data to disk (no fsync — recoverable from WAL)
491            let mut page = Page::zeroed();
492            page.header.page_id = page_id.0;
493            let copy_len = buf.len().min(PAGE_DATA_SIZE);
494            page.data[..copy_len].copy_from_slice(&buf[..copy_len]);
495            page.header.lsn = lsn;
496            page.header.page_type = PageType::Data as u8;
497            page.header.free_space = (PAGE_DATA_SIZE - copy_len) as u16;
498            page.header.checksum = page.compute_checksum();
499            pm.write_page(page_id, &page)?;
500
501            // Release flock — other processes can now write
502            wal.unlock()?;
503
504            (page_id, lsn)
505        };
506
507        // Drop any stale cached copy (the page may have been reused from the
508        // free list); the next read loads the fresh data from disk.
509        let _ = lsn;
510        self.buffer_pool.invalidate(page_id);
511
512        // Auto-checkpoint when WAL exceeds threshold to prevent unbounded growth.
513        // This keeps reload_lsn() fast for subsequent writes.
514        if self.wal.lock().file_size() > WAL_AUTO_CHECKPOINT_BYTES
515            && let Err(e) = self.checkpoint()
516        {
517            tracing::warn!("auto-checkpoint failed: {e}");
518        }
519
520        info!(
521            page_id = page_id.0,
522            bytes = serialized.len(),
523            "stored memory node"
524        );
525        Ok(page_id)
526    }
527
528    /// Store multiple [`MemoryNode`]s in a single locked transaction.
529    ///
530    /// Acquires the WAL flock once, writes all nodes, then releases. This avoids
531    /// the per-write overhead of `reload_header` / `reload_lsn` for bulk inserts.
532    /// Auto-checkpoints after the batch if the WAL exceeds the threshold.
533    pub fn store_memory_batch(&self, nodes: &[MemoryNode]) -> MenteResult<Vec<PageId>> {
534        // Phase 1: serialize all nodes upfront (no locks held)
535        let mut bufs = Vec::with_capacity(nodes.len());
536        for node in nodes {
537            let serialized =
538                serde_json::to_vec(node).map_err(|e| MenteError::Serialization(e.to_string()))?;
539            if serialized.len() + 4 > PAGE_DATA_SIZE {
540                return Err(MenteError::CapacityExceeded(format!(
541                    "memory node serialized to {} bytes (max {})",
542                    serialized.len(),
543                    PAGE_DATA_SIZE - 4,
544                )));
545            }
546            let mut buf = Vec::with_capacity(4 + serialized.len());
547            buf.extend_from_slice(&(serialized.len() as u32).to_le_bytes());
548            buf.extend_from_slice(&serialized);
549            bufs.push(buf);
550        }
551
552        // Phase 2: single locked transaction for all writes
553        let page_ids = {
554            let mut wal = self.wal.lock();
555            let mut pm = self.page_manager.lock();
556
557            wal.lock_exclusive()?;
558            pm.reload_header()?;
559            wal.reload_lsn()?;
560
561            let mut ids = Vec::with_capacity(bufs.len());
562            for (buf, node) in bufs.iter().zip(nodes) {
563                let page_id = pm.allocate_page()?;
564                let lsn = wal.append(WalEntryType::PageWrite, page_id.0, buf)?;
565                self.note_map_write(page_id, Some(node.id), lsn);
566
567                let mut page = Page::zeroed();
568                page.header.page_id = page_id.0;
569                let copy_len = buf.len().min(PAGE_DATA_SIZE);
570                page.data[..copy_len].copy_from_slice(&buf[..copy_len]);
571                page.header.lsn = lsn;
572                page.header.page_type = PageType::Data as u8;
573                page.header.free_space = (PAGE_DATA_SIZE - copy_len) as u16;
574                page.header.checksum = page.compute_checksum();
575                pm.write_page(page_id, &page)?;
576
577                ids.push(page_id);
578            }
579
580            // WAL fsync only — page data is recoverable from WAL on crash.
581            // Checkpoint handles page file fsync.
582            wal.sync()?;
583            wal.unlock()?;
584
585            ids
586        };
587
588        // Drop stale cached copies for pages reused from the free list.
589        for page_id in &page_ids {
590            self.buffer_pool.invalidate(*page_id);
591        }
592
593        // Auto-checkpoint if WAL grew too large
594        if self.wal.lock().file_size() > WAL_AUTO_CHECKPOINT_BYTES
595            && let Err(e) = self.checkpoint()
596        {
597            tracing::warn!("auto-checkpoint failed: {e}");
598        }
599
600        info!(count = page_ids.len(), "stored memory batch");
601        Ok(page_ids)
602    }
603
604    /// Update a [`MemoryNode`] in place on its existing page.
605    ///
606    /// The write goes through the WAL-protected `write_page` path, so it is
607    /// crash-durable and keeps the buffer pool coherent. Unlike storing to a
608    /// fresh page, this never orphans the old copy.
609    pub fn update_memory(&self, page_id: PageId, node: &MemoryNode) -> MenteResult<()> {
610        let serialized =
611            serde_json::to_vec(node).map_err(|e| MenteError::Serialization(e.to_string()))?;
612
613        if serialized.len() + 4 > PAGE_DATA_SIZE {
614            return Err(MenteError::CapacityExceeded(format!(
615                "memory node serialized to {} bytes (max {})",
616                serialized.len(),
617                PAGE_DATA_SIZE - 4,
618            )));
619        }
620
621        let mut buf = Vec::with_capacity(4 + serialized.len());
622        buf.extend_from_slice(&(serialized.len() as u32).to_le_bytes());
623        buf.extend_from_slice(&serialized);
624
625        self.write_page_as(page_id, &buf, Some(node.id))
626    }
627
628    /// Delete a memory by returning its page to the free list.
629    ///
630    /// The deletion is WAL-logged before the page is freed, so it survives a
631    /// crash: recovery replays the `PageFree` entry and the memory does not
632    /// resurrect on reopen. The freed page is reused by later allocations.
633    pub fn delete_memory(&self, page_id: PageId) -> MenteResult<()> {
634        {
635            let mut wal = self.wal.lock();
636            let mut pm = self.page_manager.lock();
637
638            wal.lock_exclusive()?;
639            pm.reload_header()?;
640            wal.reload_lsn()?;
641
642            // WAL fsync guarantees the deletion is durable before the page
643            // is touched; the free-list update itself is recoverable.
644            let lsn = wal.append(WalEntryType::PageFree, page_id.0, &[])?;
645            wal.sync()?;
646            self.note_map_write(page_id, None, lsn);
647
648            pm.free_page(page_id)?;
649            wal.unlock()?;
650        }
651
652        // A stale cached copy must never be served or flushed back.
653        self.buffer_pool.invalidate(page_id);
654
655        info!(page_id = page_id.0, "deleted memory node");
656        Ok(())
657    }
658
659    /// Load and deserialize a [`MemoryNode`] from the given page.
660    ///
661    /// # Example
662    ///
663    /// ```no_run
664    /// # use mentedb_storage::{StorageEngine, PageId};
665    /// # let engine = StorageEngine::open("/tmp/mentedb".as_ref())?;
666    /// let node = engine.load_memory(PageId(1))?;
667    /// println!("memory: {}", node.content);
668    /// # Ok::<(), mentedb_core::error::MenteError>(())
669    /// ```
670    pub fn load_memory(&self, page_id: PageId) -> MenteResult<MemoryNode> {
671        self.memory_loads
672            .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
673        let page = self.read_page(page_id)?;
674        self.buffer_pool.unpin_page(page_id, false).ok();
675
676        if PageType::from(page.header.page_type) != PageType::Data {
677            return Err(MenteError::Storage(format!(
678                "page {} is not a data page",
679                page_id.0
680            )));
681        }
682
683        memory_map::decode_memory(&page.data)
684    }
685
686    // ---- durability ----
687
688    /// Checkpoint: flush all dirty pages, sync to disk, and truncate the WAL.
689    ///
690    /// # Example
691    ///
692    /// ```no_run
693    /// # use mentedb_storage::StorageEngine;
694    /// # let engine = StorageEngine::open("/tmp/mentedb".as_ref())?;
695    /// // After a batch of writes, checkpoint to reclaim WAL space
696    /// engine.checkpoint()?;
697    /// # Ok::<(), mentedb_core::error::MenteError>(())
698    /// ```
699    pub fn checkpoint(&self) -> MenteResult<()> {
700        let mut wal = self.wal.lock();
701        let mut pm = self.page_manager.lock();
702
703        wal.lock_exclusive()?;
704        let result = self.checkpoint_locked(&mut wal, &mut pm, None);
705        let unlocked = wal.unlock();
706        let lsn = result?;
707        unlocked?;
708
709        info!(lsn, "checkpoint complete");
710        Ok(())
711    }
712
713    /// Checkpoint under the WAL flock. `fresh` is a memory table known to be
714    /// current (a full scan just built it); otherwise the anchored table is
715    /// rolled forward over the WAL. The snapshot is rewritten only when the
716    /// map changed; an unchanged map keeps its token, so the new checkpoint
717    /// entry still anchors the existing snapshot.
718    fn checkpoint_locked(
719        &self,
720        wal: &mut Wal,
721        pm: &mut PageManager,
722        fresh: Option<MemoryTable>,
723    ) -> MenteResult<u64> {
724        wal.reload_lsn()?;
725
726        self.buffer_pool.flush_all(pm)?;
727        pm.sync()?;
728
729        let table = match fresh {
730            Some(t) => Some(t),
731            None => {
732                let current = self.map_anchor.lock().take();
733                match current {
734                    Some(mut anchor) => {
735                        let entries = wal.iterate()?;
736                        match anchor.advance(&entries) {
737                            Some(true) => Some(anchor.table),
738                            Some(false) => {
739                                // Unchanged: re-anchor on the same token, the
740                                // snapshot on disk already holds this map.
741                                let lsn = wal.append(WalEntryType::Checkpoint, 0, &anchor.token)?;
742                                wal.sync()?;
743                                *self.map_anchor.lock() = Some(anchor);
744                                wal.truncate(lsn)?;
745                                return Ok(lsn);
746                            }
747                            None => {
748                                tracing::warn!(
749                                    "memory map anchor left the WAL, next open will scan pages"
750                                );
751                                None
752                            }
753                        }
754                    }
755                    None => None,
756                }
757            }
758        };
759
760        match table {
761            Some(table) => self.anchor_checkpoint(wal, pm, table),
762            None => {
763                // No map to persist. A fresh token keeps any old snapshot from
764                // ever matching this WAL.
765                let lsn = wal.append(WalEntryType::Checkpoint, 0, &memory_map::new_token())?;
766                wal.sync()?;
767                wal.truncate(lsn)?;
768                Ok(lsn)
769            }
770        }
771    }
772
773    /// Every memory in storage with its page and write stamp, in page order:
774    /// the same memories, in the same order, as [`Self::scan_all_memories`].
775    ///
776    /// Served from the persisted memory map rolled forward over the WAL, which
777    /// reads no pages. When there is no usable map this falls back to a full
778    /// page scan and then checkpoints, persisting the map so the next call
779    /// (and the next open) is fast. Holds the WAL lock throughout, so writers
780    /// wait; it is meant for open, before the database serves traffic.
781    pub fn memory_map(&self) -> MenteResult<Vec<MappedMemory>> {
782        let mut wal = self.wal.lock();
783        wal.lock_exclusive()?;
784        let result = self.memory_map_locked(&mut wal);
785        let unlocked = wal.unlock();
786        let map = result?;
787        unlocked?;
788        Ok(map)
789    }
790
791    fn memory_map_locked(&self, wal: &mut Wal) -> MenteResult<Vec<MappedMemory>> {
792        let entries = wal.iterate()?;
793        {
794            let mut slot = self.map_anchor.lock();
795            if let Some(anchor) = slot.as_ref() {
796                let mut current = anchor.clone();
797                if current.advance(&entries).is_some() {
798                    return Ok(current.table.memories());
799                }
800                *slot = None;
801            }
802        }
803
804        let mut pm = self.page_manager.lock();
805        let table = self.scan_table(&mut pm)?;
806        let memories = table.memories();
807        info!(
808            memories = memories.len(),
809            "memory map rebuilt by full page scan"
810        );
811        self.checkpoint_locked(wal, &mut pm, Some(table))?;
812        Ok(memories)
813    }
814
815    /// Build the memory table by reading every page.
816    fn scan_table(&self, pm: &mut PageManager) -> MenteResult<MemoryTable> {
817        pm.reload_header()?;
818        let count = pm.page_count();
819        let mut table = MemoryTable::default();
820        for i in 1..count {
821            let page_id = PageId(i);
822            let Ok(page) = self.buffer_pool.fetch_page(page_id, pm) else {
823                continue;
824            };
825            self.buffer_pool.unpin_page(page_id, false).ok();
826            match PageType::from(page.header.page_type) {
827                PageType::Data => {
828                    if let Ok(node) = memory_map::decode_memory(&page.data) {
829                        table.set_scanned(i, node.id);
830                    }
831                }
832                PageType::Free => table.set_free(i),
833                _ => {}
834            }
835        }
836        Ok(table)
837    }
838
839    /// The memory and write stamp the persisted map records for `page`, as of
840    /// the last checkpoint. None when the map is not anchored or the page holds
841    /// no memory.
842    pub fn memory_stamp(&self, page: PageId) -> Option<(mentedb_core::types::MemoryId, u64)> {
843        self.map_anchor.lock().as_ref()?.current(page.0)
844    }
845
846    /// Record a logged write in the anchored map's pending set. Called with
847    /// the WAL lock held, right after the append, so it is ordered with the
848    /// checkpoint that later rolls the same entry into the table.
849    fn note_map_write(&self, page: PageId, id: Option<mentedb_core::types::MemoryId>, lsn: u64) {
850        if let Some(anchor) = self.map_anchor.lock().as_mut() {
851            anchor.note_write(page.0, id, lsn);
852        }
853    }
854
855    /// Memory loads served since open, whether the page came from the buffer
856    /// pool or from disk. Counts what a caller asked for, so a test can bound
857    /// the work of an operation independent of cache state.
858    pub fn memory_loads(&self) -> u64 {
859        self.memory_loads.load(std::sync::atomic::Ordering::Relaxed)
860    }
861
862    /// Pages read from disk since open (buffer pool misses and direct reads).
863    pub fn page_reads(&self) -> u64 {
864        self.page_manager.lock().reads()
865    }
866
867    /// Scan all pages and return (MemoryId, PageId) pairs for every valid memory node.
868    ///
869    /// Refreshes the page count from disk before scanning so pages written by
870    /// other processes are included. Used to rebuild the page map on startup.
871    ///
872    /// # Example
873    ///
874    /// ```no_run
875    /// # use mentedb_storage::StorageEngine;
876    /// # let engine = StorageEngine::open("/tmp/mentedb".as_ref())?;
877    /// let memories = engine.scan_all_memories();
878    /// for (memory_id, page_id) in &memories {
879    ///     println!("{memory_id} -> page {}", page_id.0);
880    /// }
881    /// # Ok::<(), mentedb_core::error::MenteError>(())
882    /// ```
883    pub fn scan_all_memories(&self) -> Vec<(mentedb_core::types::MemoryId, PageId)> {
884        let mut pm = self.page_manager.lock();
885        // Refresh from disk to see pages written by other processes
886        let _ = pm.reload_header();
887        let count = pm.page_count();
888        drop(pm);
889
890        let mut results = Vec::new();
891        for i in 1..count {
892            let page_id = PageId(i);
893            if let Ok(node) = self.load_memory(page_id) {
894                results.push((node.id, page_id));
895            }
896        }
897        results
898    }
899}
900
901#[cfg(test)]
902mod tests {
903    use super::*;
904    use crate::memory_map::MappedMemory;
905    use mentedb_core::memory::MemoryType;
906    use mentedb_core::types::AgentId;
907
908    fn setup() -> (tempfile::TempDir, StorageEngine) {
909        let dir = tempfile::tempdir().unwrap();
910        let engine = StorageEngine::open(dir.path()).unwrap();
911        (dir, engine)
912    }
913
914    #[test]
915    fn test_allocate_write_read() {
916        let (_dir, engine) = setup();
917
918        let pid = engine.allocate_page().unwrap();
919        engine.write_page(pid, b"hello storage engine").unwrap();
920
921        let page = engine.read_page(pid).unwrap();
922        assert_eq!(&page.data[..20], b"hello storage engine");
923        engine.buffer_pool.unpin_page(pid, false).ok();
924    }
925
926    #[test]
927    fn test_store_and_load_memory() {
928        let (_dir, engine) = setup();
929
930        let node = MemoryNode::new(
931            AgentId::new(),
932            MemoryType::Episodic,
933            "The user prefers Rust over Go".to_string(),
934            vec![0.1, 0.2, 0.3, 0.4],
935        );
936
937        let page_id = engine.store_memory(&node).unwrap();
938        let loaded = engine.load_memory(page_id).unwrap();
939
940        assert_eq!(node.id, loaded.id);
941        assert_eq!(node.content, loaded.content);
942        assert_eq!(node.embedding, loaded.embedding);
943        assert_eq!(node.memory_type, loaded.memory_type);
944    }
945
946    #[test]
947    fn test_checkpoint() {
948        let (_dir, engine) = setup();
949
950        let node = MemoryNode::new(
951            AgentId::new(),
952            MemoryType::Semantic,
953            "checkpoint test".to_string(),
954            vec![1.0, 2.0],
955        );
956
957        let pid = engine.store_memory(&node).unwrap();
958        engine.checkpoint().unwrap();
959
960        let loaded = engine.load_memory(pid).unwrap();
961        assert_eq!(loaded.content, "checkpoint test");
962    }
963
964    #[test]
965    fn test_close_and_reopen() {
966        let dir = tempfile::tempdir().unwrap();
967        let pid;
968        {
969            let engine = StorageEngine::open(dir.path()).unwrap();
970            let node = MemoryNode::new(
971                AgentId::new(),
972                MemoryType::Procedural,
973                "persist across close".to_string(),
974                vec![0.5],
975            );
976            pid = engine.store_memory(&node).unwrap();
977            engine.close().unwrap();
978        }
979        {
980            let engine = StorageEngine::open(dir.path()).unwrap();
981            let loaded = engine.load_memory(pid).unwrap();
982            assert_eq!(loaded.content, "persist across close");
983        }
984    }
985
986    #[test]
987    fn test_crash_recovery() {
988        let dir = tempfile::tempdir().unwrap();
989        let mut ids = Vec::new();
990        let mut contents = Vec::new();
991        {
992            let engine = StorageEngine::open(dir.path()).unwrap();
993            for i in 0..3 {
994                let content = format!("crash-recovery-{i}");
995                let node = MemoryNode::new(
996                    AgentId::new(),
997                    MemoryType::Episodic,
998                    content.clone(),
999                    vec![i as f32],
1000                );
1001                let pid = engine.store_memory(&node).unwrap();
1002                ids.push(pid);
1003                contents.push(content);
1004            }
1005            // Simulate crash: sync the WAL but do NOT call close/checkpoint.
1006            engine.wal.lock().sync().unwrap();
1007        }
1008        {
1009            let engine = StorageEngine::open(dir.path()).unwrap();
1010            for (pid, expected) in ids.iter().zip(contents.iter()) {
1011                let loaded = engine.load_memory(*pid).unwrap();
1012                assert_eq!(&loaded.content, expected);
1013            }
1014        }
1015    }
1016
1017    #[test]
1018    fn test_recovery_idempotent() {
1019        let dir = tempfile::tempdir().unwrap();
1020        let pid;
1021        let content = "idempotent-check".to_string();
1022        {
1023            let engine = StorageEngine::open(dir.path()).unwrap();
1024            let node = MemoryNode::new(
1025                AgentId::new(),
1026                MemoryType::Semantic,
1027                content.clone(),
1028                vec![1.0, 2.0],
1029            );
1030            pid = engine.store_memory(&node).unwrap();
1031            engine.checkpoint().unwrap();
1032            engine.close().unwrap();
1033        }
1034        {
1035            let engine = StorageEngine::open(dir.path()).unwrap();
1036            let loaded = engine.load_memory(pid).unwrap();
1037            assert_eq!(loaded.content, content);
1038        }
1039    }
1040
1041    #[test]
1042    fn test_partial_write_recovery() {
1043        let dir = tempfile::tempdir().unwrap();
1044        let mut ids = Vec::new();
1045        let mut contents = Vec::new();
1046        {
1047            let engine = StorageEngine::open(dir.path()).unwrap();
1048            for i in 0..3 {
1049                let content = format!("checkpointed-{i}");
1050                let node = MemoryNode::new(
1051                    AgentId::new(),
1052                    MemoryType::Semantic,
1053                    content.clone(),
1054                    vec![i as f32],
1055                );
1056                let pid = engine.store_memory(&node).unwrap();
1057                ids.push(pid);
1058                contents.push(content);
1059            }
1060            engine.checkpoint().unwrap();
1061
1062            for i in 3..5 {
1063                let content = format!("unckeckpointed-{i}");
1064                let node = MemoryNode::new(
1065                    AgentId::new(),
1066                    MemoryType::Episodic,
1067                    content.clone(),
1068                    vec![i as f32],
1069                );
1070                let pid = engine.store_memory(&node).unwrap();
1071                ids.push(pid);
1072                contents.push(content);
1073            }
1074            // Simulate crash — sync WAL but don't close.
1075            engine.wal.lock().sync().unwrap();
1076        }
1077        {
1078            let engine = StorageEngine::open(dir.path()).unwrap();
1079            for (pid, expected) in ids.iter().zip(contents.iter()) {
1080                let loaded = engine.load_memory(*pid).unwrap();
1081                assert_eq!(&loaded.content, expected);
1082            }
1083        }
1084    }
1085
1086    #[test]
1087    fn test_delete_memory_durable() {
1088        let dir = tempfile::tempdir().unwrap();
1089        let pid;
1090        {
1091            let engine = StorageEngine::open(dir.path()).unwrap();
1092            let node = MemoryNode::new(
1093                AgentId::new(),
1094                MemoryType::Semantic,
1095                "to be deleted".to_string(),
1096                vec![1.0],
1097            );
1098            pid = engine.store_memory(&node).unwrap();
1099            engine.delete_memory(pid).unwrap();
1100            assert!(engine.load_memory(pid).is_err());
1101            assert!(engine.scan_all_memories().is_empty());
1102            engine.close().unwrap();
1103        }
1104        {
1105            let engine = StorageEngine::open(dir.path()).unwrap();
1106            assert!(
1107                engine.load_memory(pid).is_err(),
1108                "deleted memory must not resurrect on reopen"
1109            );
1110            assert!(engine.scan_all_memories().is_empty());
1111        }
1112    }
1113
1114    #[test]
1115    fn test_delete_survives_crash() {
1116        let dir = tempfile::tempdir().unwrap();
1117        let pid;
1118        {
1119            let engine = StorageEngine::open(dir.path()).unwrap();
1120            let node = MemoryNode::new(
1121                AgentId::new(),
1122                MemoryType::Semantic,
1123                "crash delete".to_string(),
1124                vec![1.0],
1125            );
1126            pid = engine.store_memory(&node).unwrap();
1127            engine.delete_memory(pid).unwrap();
1128            // Simulate crash: no close, no checkpoint.
1129        }
1130        {
1131            let engine = StorageEngine::open(dir.path()).unwrap();
1132            assert!(
1133                engine.load_memory(pid).is_err(),
1134                "deletion must survive a crash via WAL replay"
1135            );
1136            assert!(engine.scan_all_memories().is_empty());
1137        }
1138    }
1139
1140    #[test]
1141    fn test_deleted_page_reused() {
1142        let (_dir, engine) = setup();
1143
1144        let a = MemoryNode::new(AgentId::new(), MemoryType::Semantic, "a".into(), vec![1.0]);
1145        let pid_a = engine.store_memory(&a).unwrap();
1146        engine.delete_memory(pid_a).unwrap();
1147
1148        let b = MemoryNode::new(AgentId::new(), MemoryType::Semantic, "b".into(), vec![2.0]);
1149        let pid_b = engine.store_memory(&b).unwrap();
1150        assert_eq!(pid_a, pid_b, "freed page should be reused");
1151
1152        let loaded = engine.load_memory(pid_b).unwrap();
1153        assert_eq!(loaded.content, "b");
1154    }
1155
1156    #[test]
1157    fn test_delete_reuse_crash_recovery() {
1158        let dir = tempfile::tempdir().unwrap();
1159        let pid;
1160        let b_id;
1161        {
1162            let engine = StorageEngine::open(dir.path()).unwrap();
1163            let a = MemoryNode::new(AgentId::new(), MemoryType::Semantic, "a".into(), vec![1.0]);
1164            pid = engine.store_memory(&a).unwrap();
1165            engine.delete_memory(pid).unwrap();
1166            let b = MemoryNode::new(AgentId::new(), MemoryType::Semantic, "b".into(), vec![2.0]);
1167            let pid_b = engine.store_memory(&b).unwrap();
1168            assert_eq!(pid, pid_b);
1169            b_id = b.id;
1170            // Simulate crash: the WAL now holds PageFree(p) then PageWrite(p).
1171        }
1172        {
1173            let engine = StorageEngine::open(dir.path()).unwrap();
1174            let loaded = engine.load_memory(pid).unwrap();
1175            assert_eq!(loaded.content, "b", "later write must win over the free");
1176            assert_eq!(loaded.id, b_id);
1177            // The page must NOT be on the free list: a fresh allocation must
1178            // not clobber b.
1179            let c = MemoryNode::new(AgentId::new(), MemoryType::Semantic, "c".into(), vec![3.0]);
1180            let pid_c = engine.store_memory(&c).unwrap();
1181            assert_ne!(pid_c, pid, "recovered free list must exclude reused page");
1182            assert_eq!(engine.load_memory(pid).unwrap().content, "b");
1183        }
1184    }
1185
1186    #[test]
1187    fn test_update_memory_in_place() {
1188        let dir = tempfile::tempdir().unwrap();
1189        let pid;
1190        let id;
1191        {
1192            let engine = StorageEngine::open(dir.path()).unwrap();
1193            let mut node = MemoryNode::new(
1194                AgentId::new(),
1195                MemoryType::Semantic,
1196                "original".to_string(),
1197                vec![1.0],
1198            );
1199            pid = engine.store_memory(&node).unwrap();
1200            id = node.id;
1201
1202            node.content = "updated".to_string();
1203            engine.update_memory(pid, &node).unwrap();
1204
1205            let loaded = engine.load_memory(pid).unwrap();
1206            assert_eq!(loaded.content, "updated");
1207            // No orphan copy: exactly one entry in a full scan.
1208            let scanned = engine.scan_all_memories();
1209            assert_eq!(scanned.len(), 1);
1210            engine.close().unwrap();
1211        }
1212        {
1213            let engine = StorageEngine::open(dir.path()).unwrap();
1214            let loaded = engine.load_memory(pid).unwrap();
1215            assert_eq!(loaded.content, "updated");
1216            assert_eq!(loaded.id, id);
1217            assert_eq!(engine.scan_all_memories().len(), 1);
1218        }
1219    }
1220
1221    #[test]
1222    fn test_concurrent_open_is_rejected() {
1223        let dir = tempfile::tempdir().unwrap();
1224
1225        // The per operation flock serializes individual write transactions,
1226        // but layers above the storage engine cache state across operations,
1227        // so a second concurrent engine rolls the database back to its own
1228        // open time snapshot. The process lock must refuse it.
1229        let engine1 = StorageEngine::open(dir.path()).unwrap();
1230        let second = StorageEngine::open(dir.path());
1231        assert!(second.is_err(), "second concurrent open must fail");
1232        let msg = second.err().unwrap().to_string();
1233        assert!(msg.contains("locked"), "error names the lock: {msg}");
1234
1235        // Close releases the lock; the directory can be opened again.
1236        engine1.close().unwrap();
1237        let engine2 = StorageEngine::open(dir.path()).unwrap();
1238        engine2.close().unwrap();
1239    }
1240
1241    #[test]
1242    fn test_concurrent_writes_from_threads() {
1243        use std::sync::Arc;
1244        let dir = tempfile::tempdir().unwrap();
1245        let engine = Arc::new(StorageEngine::open(dir.path()).unwrap());
1246
1247        let handles: Vec<_> = (0..10)
1248            .map(|i| {
1249                let eng = Arc::clone(&engine);
1250                std::thread::spawn(move || {
1251                    let node = MemoryNode::new(
1252                        AgentId::new(),
1253                        MemoryType::Episodic,
1254                        format!("thread-{i}"),
1255                        vec![i as f32],
1256                    );
1257                    eng.store_memory(&node).unwrap()
1258                })
1259            })
1260            .collect();
1261
1262        let pids: Vec<PageId> = handles.into_iter().map(|h| h.join().unwrap()).collect();
1263
1264        // All 10 writes succeeded and are readable
1265        for (i, pid) in pids.iter().enumerate() {
1266            let loaded = engine.load_memory(*pid).unwrap();
1267            assert_eq!(loaded.content, format!("thread-{i}"));
1268        }
1269    }
1270
1271    // ---- persisted memory map ----
1272
1273    fn node(content: &str) -> MemoryNode {
1274        MemoryNode::new(
1275            AgentId::new(),
1276            MemoryType::Semantic,
1277            content.to_string(),
1278            vec![1.0, 2.0],
1279        )
1280    }
1281
1282    /// (memory id, page) pairs, the part of the map a full scan can confirm.
1283    fn ids(map: &[MappedMemory]) -> Vec<(mentedb_core::types::MemoryId, PageId)> {
1284        map.iter().map(|m| (m.id, m.page)).collect()
1285    }
1286
1287    /// Open a second, independent engine over the same files after `engine`
1288    /// crashed, and return what a full page scan sees there.
1289    fn scan_truth(dir: &Path) -> Vec<(mentedb_core::types::MemoryId, PageId)> {
1290        let engine = StorageEngine::open(dir).unwrap();
1291        let scanned = engine.scan_all_memories();
1292        engine.release_process_lock();
1293        scanned
1294    }
1295
1296    #[test]
1297    fn open_after_checkpoint_reads_no_pages() {
1298        let dir = tempfile::tempdir().unwrap();
1299        let n = 300;
1300        let mut stored = Vec::new();
1301        {
1302            let engine = StorageEngine::open(dir.path()).unwrap();
1303            for i in 0..n {
1304                let m = node(&format!("m{i}"));
1305                let pid = engine.store_memory(&m).unwrap();
1306                stored.push((m.id, pid));
1307            }
1308            // First map request scans once and persists the map.
1309            assert_eq!(ids(&engine.memory_map().unwrap()), stored);
1310            engine.checkpoint().unwrap();
1311            engine.close().unwrap();
1312        }
1313        let engine = StorageEngine::open(dir.path()).unwrap();
1314        let map = engine.memory_map().unwrap();
1315        assert_eq!(ids(&map), stored);
1316        assert_eq!(
1317            engine.page_reads(),
1318            0,
1319            "open must serve the map from the snapshot, not by reading {n} pages"
1320        );
1321    }
1322
1323    #[test]
1324    fn open_replays_only_wal_pages_after_crash() {
1325        let dir = tempfile::tempdir().unwrap();
1326        let mut stored = Vec::new();
1327        {
1328            let engine = StorageEngine::open(dir.path()).unwrap();
1329            for i in 0..200 {
1330                let m = node(&format!("base{i}"));
1331                stored.push((m.id, engine.store_memory(&m).unwrap()));
1332            }
1333            engine.memory_map().unwrap();
1334            engine.checkpoint().unwrap();
1335            for i in 0..5 {
1336                let m = node(&format!("after{i}"));
1337                stored.push((m.id, engine.store_memory(&m).unwrap()));
1338            }
1339            // Crash: nothing flushed or checkpointed beyond the WAL fsync.
1340            engine.release_process_lock();
1341        }
1342        let engine = StorageEngine::open(dir.path()).unwrap();
1343        let map = engine.memory_map().unwrap();
1344        assert_eq!(ids(&map), stored, "uncheckpointed stores survive the crash");
1345        assert_eq!(
1346            engine.page_reads(),
1347            5,
1348            "replay reads the 5 WAL pages only, not the 200 checkpointed ones"
1349        );
1350        engine.release_process_lock();
1351        assert_eq!(scan_truth(dir.path()), stored);
1352    }
1353
1354    #[test]
1355    fn deletes_and_updates_after_checkpoint_are_replayed() {
1356        let dir = tempfile::tempdir().unwrap();
1357        let (keep, gone, updated);
1358        let updated_pid;
1359        {
1360            let engine = StorageEngine::open(dir.path()).unwrap();
1361            let k = node("keep");
1362            let g = node("gone");
1363            let mut u = node("old");
1364            let kp = engine.store_memory(&k).unwrap();
1365            let gp = engine.store_memory(&g).unwrap();
1366            updated_pid = engine.store_memory(&u).unwrap();
1367            engine.memory_map().unwrap();
1368            engine.checkpoint().unwrap();
1369            engine.delete_memory(gp).unwrap();
1370            u.content = "new".into();
1371            engine.update_memory(updated_pid, &u).unwrap();
1372            keep = (k.id, kp);
1373            gone = g.id;
1374            updated = u.id;
1375            engine.release_process_lock();
1376        }
1377        let engine = StorageEngine::open(dir.path()).unwrap();
1378        let map = engine.memory_map().unwrap();
1379        assert!(
1380            map.iter().all(|m| m.id != gone),
1381            "deleted memory stays deleted"
1382        );
1383        assert!(ids(&map).contains(&keep));
1384        assert!(ids(&map).contains(&(updated, updated_pid)));
1385        assert_eq!(engine.load_memory(updated_pid).unwrap().content, "new");
1386        assert_eq!(map.len(), 2);
1387        engine.release_process_lock();
1388        assert_eq!(scan_truth(dir.path()), ids(&map));
1389    }
1390
1391    #[test]
1392    fn free_list_reuse_after_checkpoint_maps_the_new_memory() {
1393        let dir = tempfile::tempdir().unwrap();
1394        let (a_id, b_id, pid);
1395        {
1396            let engine = StorageEngine::open(dir.path()).unwrap();
1397            let a = node("a");
1398            pid = engine.store_memory(&a).unwrap();
1399            engine.store_memory(&node("other")).unwrap();
1400            engine.memory_map().unwrap();
1401            engine.checkpoint().unwrap();
1402            engine.delete_memory(pid).unwrap();
1403            let b = node("b");
1404            assert_eq!(engine.store_memory(&b).unwrap(), pid, "freed page reused");
1405            a_id = a.id;
1406            b_id = b.id;
1407            engine.release_process_lock();
1408        }
1409        let engine = StorageEngine::open(dir.path()).unwrap();
1410        let map = engine.memory_map().unwrap();
1411        assert!(
1412            map.iter().all(|m| m.id != a_id),
1413            "no stale id for the reused page"
1414        );
1415        assert!(ids(&map).contains(&(b_id, pid)));
1416        // The reused page is not on the rebuilt free list.
1417        let c = node("c");
1418        assert_ne!(engine.store_memory(&c).unwrap(), pid);
1419        assert_eq!(engine.load_memory(pid).unwrap().id, b_id);
1420        engine.release_process_lock();
1421        let truth = scan_truth(dir.path());
1422        assert!(truth.contains(&(b_id, pid)));
1423        assert!(truth.iter().all(|(id, _)| *id != a_id));
1424    }
1425
1426    #[test]
1427    fn free_list_rebuilt_from_map_after_crash() {
1428        let dir = tempfile::tempdir().unwrap();
1429        let mut freed = Vec::new();
1430        {
1431            let engine = StorageEngine::open(dir.path()).unwrap();
1432            let mut pids = Vec::new();
1433            for i in 0..50 {
1434                pids.push(engine.store_memory(&node(&format!("m{i}"))).unwrap());
1435            }
1436            engine.delete_memory(pids[3]).unwrap();
1437            freed.push(pids[3]);
1438            engine.memory_map().unwrap();
1439            engine.checkpoint().unwrap();
1440            engine.delete_memory(pids[7]).unwrap();
1441            freed.push(pids[7]);
1442            engine.release_process_lock();
1443        }
1444        let engine = StorageEngine::open(dir.path()).unwrap();
1445        // One replayed free plus verification reads of the two free pages,
1446        // not a scan of all 50.
1447        assert!(engine.page_reads() <= 3, "reads {}", engine.page_reads());
1448        let a = engine.store_memory(&node("x")).unwrap();
1449        let b = engine.store_memory(&node("y")).unwrap();
1450        let mut got = vec![a, b];
1451        got.sort_by_key(|p| p.0);
1452        assert_eq!(got, freed, "both freed pages are reusable after recovery");
1453        let c = engine.store_memory(&node("z")).unwrap();
1454        assert_eq!(c, PageId(51), "then allocation grows the file");
1455    }
1456
1457    #[test]
1458    fn missing_corrupt_or_stale_snapshot_falls_back_to_scan() {
1459        let dir = tempfile::tempdir().unwrap();
1460        let mut stored = Vec::new();
1461        {
1462            let engine = StorageEngine::open(dir.path()).unwrap();
1463            for i in 0..40 {
1464                let m = node(&format!("m{i}"));
1465                stored.push((m.id, engine.store_memory(&m).unwrap()));
1466            }
1467            engine.memory_map().unwrap();
1468            engine.checkpoint().unwrap();
1469            engine.close().unwrap();
1470        }
1471        let snap = dir.path().join(memory_map::MEMORY_MAP_FILE);
1472        let good = std::fs::read(&snap).unwrap();
1473
1474        let check = |label: &str| {
1475            let engine = StorageEngine::open(dir.path()).unwrap();
1476            let map = engine.memory_map().unwrap();
1477            assert_eq!(ids(&map), stored, "{label}: same result as a scan");
1478            assert!(
1479                engine.page_reads() >= 40,
1480                "{label}: must have scanned, read {}",
1481                engine.page_reads()
1482            );
1483            engine.close().unwrap();
1484            // The scan re-persisted a valid map.
1485            let engine = StorageEngine::open(dir.path()).unwrap();
1486            assert_eq!(ids(&engine.memory_map().unwrap()), stored);
1487            assert_eq!(engine.page_reads(), 0, "{label}: next open is fast again");
1488            engine.close().unwrap();
1489        };
1490
1491        std::fs::remove_file(&snap).unwrap();
1492        check("missing");
1493
1494        let mut bad = std::fs::read(&snap).unwrap();
1495        let mid = bad.len() / 2;
1496        bad[mid] ^= 0x55;
1497        std::fs::write(&snap, &bad).unwrap();
1498        check("corrupt");
1499
1500        // Stale: a snapshot from an earlier checkpoint whose token has left
1501        // the WAL, even though the file itself is intact.
1502        std::fs::write(&snap, &good).unwrap();
1503        check("stale");
1504    }
1505
1506    #[test]
1507    fn stale_snapshot_with_unchanged_token_reuse_is_never_matched() {
1508        // A checkpoint whose snapshot write is skipped (no map) must not let an
1509        // older snapshot match afterwards: deletes after it would resurrect.
1510        let dir = tempfile::tempdir().unwrap();
1511        let (a, b);
1512        {
1513            let engine = StorageEngine::open(dir.path()).unwrap();
1514            let na = node("a");
1515            let nb = node("b");
1516            a = engine.store_memory(&na).unwrap();
1517            b = (nb.id, engine.store_memory(&nb).unwrap());
1518            engine.memory_map().unwrap();
1519            engine.checkpoint().unwrap();
1520            // Drop the anchor as if the map were never loaded, then change the
1521            // map and checkpoint without persisting it.
1522            *engine.map_anchor.lock() = None;
1523            engine.delete_memory(a).unwrap();
1524            engine.checkpoint().unwrap();
1525            engine.close().unwrap();
1526        }
1527        let engine = StorageEngine::open(dir.path()).unwrap();
1528        assert_eq!(ids(&engine.memory_map().unwrap()), vec![b]);
1529    }
1530
1531    #[test]
1532    fn handoff_between_handles_sees_writes_and_checkpoints() {
1533        let dir = tempfile::tempdir().unwrap();
1534        let mut stored = Vec::new();
1535        // Handle 1 builds the map and exits cleanly.
1536        {
1537            let e1 = StorageEngine::open(dir.path()).unwrap();
1538            for i in 0..20 {
1539                let m = node(&format!("h1-{i}"));
1540                stored.push((m.id, e1.store_memory(&m).unwrap()));
1541            }
1542            e1.memory_map().unwrap();
1543            e1.checkpoint().unwrap();
1544            e1.close().unwrap();
1545        }
1546        // Handle 2 (another process taking over) writes, checkpoints, writes
1547        // more, then dies.
1548        {
1549            let e2 = StorageEngine::open(dir.path()).unwrap();
1550            e2.memory_map().unwrap();
1551            for i in 0..10 {
1552                let m = node(&format!("h2-{i}"));
1553                stored.push((m.id, e2.store_memory(&m).unwrap()));
1554            }
1555            e2.checkpoint().unwrap();
1556            e2.delete_memory(stored[0].1).unwrap();
1557            stored.remove(0);
1558            let m = node("h2-last");
1559            stored.push((m.id, e2.store_memory(&m).unwrap()));
1560            e2.release_process_lock();
1561        }
1562        // Handle 3 reopens and sees all of it.
1563        let e3 = StorageEngine::open(dir.path()).unwrap();
1564        let mut got = ids(&e3.memory_map().unwrap());
1565        got.sort_by_key(|(_, p)| p.0);
1566        stored.sort_by_key(|(_, p)| p.0);
1567        assert_eq!(got, stored);
1568        assert!(e3.page_reads() < 10, "reads {}", e3.page_reads());
1569        e3.release_process_lock();
1570        assert_eq!(scan_truth(dir.path()), stored);
1571    }
1572
1573    #[test]
1574    fn live_stamps_survive_checkpoint_and_crash_replay() {
1575        let dir = tempfile::tempdir().unwrap();
1576        let (b_page, b_live);
1577        {
1578            let engine = StorageEngine::open(dir.path()).unwrap();
1579            engine.store_memory(&node("a")).unwrap();
1580            engine.memory_map().unwrap();
1581            engine.checkpoint().unwrap();
1582            // A write after the checkpoint has its stamp before any checkpoint.
1583            let c = engine.store_memory(&node("c")).unwrap();
1584            let c_live = engine.memory_stamp(c).unwrap();
1585            engine.checkpoint().unwrap();
1586            assert_eq!(
1587                engine.memory_stamp(c).unwrap(),
1588                c_live,
1589                "checkpoint keeps it"
1590            );
1591            b_page = engine.store_memory(&node("b")).unwrap();
1592            b_live = engine.memory_stamp(b_page).unwrap();
1593            engine.release_process_lock();
1594        }
1595        let engine = StorageEngine::open(dir.path()).unwrap();
1596        let map = engine.memory_map().unwrap();
1597        let b = map.iter().find(|m| m.page == b_page).unwrap();
1598        assert_eq!(
1599            (b.id, b.stamp),
1600            b_live,
1601            "WAL replay reproduces the live stamp"
1602        );
1603    }
1604
1605    #[test]
1606    fn unchanged_checkpoints_keep_the_snapshot_and_stamps() {
1607        let dir = tempfile::tempdir().unwrap();
1608        let engine = StorageEngine::open(dir.path()).unwrap();
1609        let pid = engine.store_memory(&node("a")).unwrap();
1610        engine.memory_map().unwrap();
1611        engine.checkpoint().unwrap();
1612        let snap = dir.path().join(memory_map::MEMORY_MAP_FILE);
1613        let before = std::fs::metadata(&snap).unwrap().modified().unwrap();
1614        let stamp = engine.memory_stamp(pid).unwrap();
1615        std::thread::sleep(std::time::Duration::from_millis(20));
1616        engine.checkpoint().unwrap();
1617        engine.checkpoint().unwrap();
1618        assert_eq!(
1619            std::fs::metadata(&snap).unwrap().modified().unwrap(),
1620            before
1621        );
1622        assert_eq!(engine.memory_stamp(pid).unwrap(), stamp);
1623        // A write to the page changes its stamp at the next checkpoint.
1624        let mut n = engine.load_memory(pid).unwrap();
1625        n.content = "b".into();
1626        engine.update_memory(pid, &n).unwrap();
1627        engine.checkpoint().unwrap();
1628        assert_ne!(engine.memory_stamp(pid).unwrap().1, stamp.1);
1629        engine.close().unwrap();
1630        let engine = StorageEngine::open(dir.path()).unwrap();
1631        let map = engine.memory_map().unwrap();
1632        assert_eq!(map[0].stamp, engine.memory_stamp(pid).unwrap().1);
1633    }
1634}