umadb_core/
pager.rs

1use crate::common::PageID;
2use memmap2::{Mmap, MmapOptions};
3use std::collections::HashMap;
4use std::fs::{File, OpenOptions};
5use std::io;
6use std::io::{BufWriter, Seek, SeekFrom, Write};
7use std::os::fd::{AsRawFd, RawFd};
8use std::path::Path;
9use std::sync::{Arc, Mutex, RwLock};
10use umadb_dcb::{DCBError, DCBResult};
11// use memmap2::{Advice, Mmap, MmapOptions};
12
13// Pager for file I/O
14pub struct Pager {
15    pub reader: Arc<File>,
16    // pub writer: Arc<File>,
17    pub writer: Mutex<BufWriter<File>>,
18    pub writer_raw_fd: RawFd,
19    pub page_size: usize,
20    pub is_file_new: bool,
21    // Number of logical database pages contained in a single mmap window.
22    mmap_pages_per_map: usize,
23    // Cache of memory maps, keyed by map identifier (floor(page_id / mmap_pages_per_map)).
24    mmaps: RwLock<HashMap<u64, Arc<Mmap>>>,
25}
26
27// Implementation for Pager
28impl Pager {
29    pub fn new(path: &Path, page_size: usize) -> io::Result<Self> {
30        let is_file_new = !path.exists();
31
32        let reader_file = if is_file_new {
33            OpenOptions::new()
34                .read(true)
35                .write(true)
36                .create(true)
37                .truncate(false)
38                .open(path)?
39        } else {
40            OpenOptions::new().read(true).write(true).open(path)?
41        };
42
43        let writer_file = if is_file_new {
44            OpenOptions::new()
45                .read(true)
46                .write(true)
47                .create(true)
48                .truncate(false)
49                .open(path)?
50        } else {
51            OpenOptions::new().read(true).write(true).open(path)?
52        };
53        let writer_raw_fd = writer_file.as_raw_fd();
54
55        // Compute pages per mmap so that:
56        // - Each mmap offset is aligned to OS page size (and implicitly DB page size), and
57        // - Each mapping window is at least 10 MiB.
58        let os_ps = Self::os_page_size();
59        let g = Self::gcd(os_ps, page_size);
60        // Alignment factor: number of DB pages per window must be a multiple of this to keep
61        // mmap offsets aligned to OS page boundaries.
62        let align_pages = usize::max(1, os_ps / g);
63        // Minimum window size in bytes: 10 MiB
64        let min_window_bytes: usize = 10 * 1024 * 1024;
65        // Minimum number of DB pages to reach at least the min window size
66        let min_pages = min_window_bytes.div_ceil(page_size);
67        // Choose smallest multiple of align_pages that is >= min_pages
68        let k = if min_pages == 0 {
69            1
70        } else {
71            min_pages.div_ceil(align_pages)
72        };
73        let mmap_pages_per_map = align_pages * usize::max(1, k);
74
75        Ok(Self {
76            reader: Arc::new(reader_file),
77            writer: Mutex::new(BufWriter::with_capacity(100 * page_size, writer_file)),
78            writer_raw_fd,
79            page_size,
80            is_file_new,
81            mmap_pages_per_map,
82            mmaps: RwLock::new(HashMap::new()),
83        })
84    }
85
86    fn gcd(mut a: usize, mut b: usize) -> usize {
87        while b != 0 {
88            let t = b;
89            b = a % t;
90            a = t;
91        }
92        a
93    }
94
95    #[cfg(unix)]
96    fn os_page_size() -> usize {
97        // Safe: sysconf is thread-safe and returns a constant for page size
98        unsafe {
99            let sz = libc::sysconf(libc::_SC_PAGESIZE);
100            if sz <= 0 {
101                4096usize // sensible default
102            } else {
103                sz as usize
104            }
105        }
106    }
107
108    pub fn read_page(&self, page_id: PageID) -> io::Result<Vec<u8>> {
109        use std::os::unix::fs::FileExt;
110        let file = self.reader.clone();
111        let offset = page_id.0 * (self.page_size as u64);
112        let mut page = vec![0u8; self.page_size];
113        let bytes_read = file.read_at(&mut page, offset)?;
114        if bytes_read < self.page_size {
115            return Err(io::Error::new(
116                io::ErrorKind::UnexpectedEof,
117                format!("Page {page_id:?} not found"),
118            ));
119        }
120        Ok(page)
121    }
122
123    // pub fn read_page_mmap(&self, page_id: PageID) -> io::Result<Vec<u8>> {
124    //     // Precompute addressing values
125    //     let page_size_u64 = self.page_size as u64;
126    //     let offset = page_id.0 * page_size_u64;
127    //     let pages_per_map = self.mmap_pages_per_map as u64;
128    //     let map_id = page_id.0 / pages_per_map;
129    //     let map_offset = map_id * pages_per_map * page_size_u64;
130    //     let within = (offset - map_offset) as usize;
131    //
132    //     // Fast path: if mapping exists, do not obtain file lock; take Arc clone and release map lock
133    //     if let Some(mmap_arc) = {
134    //         let maps = self.mmaps.lock().unwrap();
135    //         maps.get(&map_id).cloned()
136    //     } {
137    //         let start = within;
138    //         let stop = start + self.page_size;
139    //         if stop > mmap_arc.len() {
140    //             return Err(io::Error::new(
141    //                 io::ErrorKind::UnexpectedEof,
142    //                 format!("Page {page_id:?} not found"),
143    //             ));
144    //         }
145    //         return Ok(mmap_arc[start..stop].to_vec());
146    //     }
147    //
148    //     // Slow path: need to create the mapping with double-checked locking
149    //     let file = self.reader.lock().unwrap();
150    //     let file_len = file.metadata()?.len();
151    //
152    //     // Re-check if mapping appeared while we acquired the file lock
153    //     if let Some(mmap_arc) = {
154    //         let maps = self.mmaps.lock().unwrap();
155    //         maps.get(&map_id).cloned()
156    //     } {
157    //         let start = within;
158    //         let stop = start + self.page_size;
159    //         if stop > mmap_arc.len() {
160    //             return Err(io::Error::new(
161    //                 io::ErrorKind::UnexpectedEof,
162    //                 format!("Page {page_id:?} not found"),
163    //             ));
164    //         }
165    //         return Ok(mmap_arc[start..stop].to_vec());
166    //     }
167    //
168    //     // Calculate standard mapping window length (does not depend on current file length)
169    //     let max_len = pages_per_map * page_size_u64;
170    //
171    //     // Before mapping, ensure the requested page is within the current file length.
172    //     let page_end = offset + page_size_u64;
173    //     if page_end > file_len {
174    //         return Err(io::Error::new(
175    //             io::ErrorKind::UnexpectedEof,
176    //             format!("Page {page_id:?} not found"),
177    //         ));
178    //     }
179    //
180    //     // Ensure the underlying file is large enough to permit a full standard-length mapping.
181    //     let required_len = map_offset + max_len;
182    //     if file_len < required_len {
183    //         file.set_len(required_len)?;
184    //     }
185    //
186    //     // Create the mmap and insert it, but guard with a double-check
187    //     let mmap_new = unsafe {
188    //         MmapOptions::new()
189    //             .offset(map_offset)
190    //             .len(max_len as usize)
191    //             .map(&*file)?
192    //     };
193    //     let mmap_arc = {
194    //         let mut maps = self.mmaps.lock().unwrap();
195    //         // Another thread could have inserted meanwhile
196    //         if let Some(existing) = maps.get(&map_id) {
197    //             existing.clone()
198    //         } else {
199    //             let arc = Arc::new(mmap_new);
200    //             maps.insert(map_id, arc.clone());
201    //             // println!(
202    //             //     "Created new mmap: map_id={} offset={} len={}",
203    //             //     map_id, map_offset, max_len
204    //             // );
205    //             arc
206    //         }
207    //     };
208    //
209    //     // Now copy the requested page
210    //     let start = within;
211    //     let stop = start + self.page_size;
212    //     if stop > mmap_arc.len() {
213    //         return Err(io::Error::new(
214    //             io::ErrorKind::UnexpectedEof,
215    //             format!("Page {page_id:?} not found"),
216    //         ));
217    //     }
218    //     Ok(mmap_arc[start..stop].to_vec())
219    // }
220
221    pub fn read_page_mmap_slice(&self, page_id: PageID) -> io::Result<MappedPage> {
222        // Precompute addressing values
223        let page_size_u64 = self.page_size as u64;
224        let offset = page_id.0 * page_size_u64;
225        let pages_per_map = self.mmap_pages_per_map as u64;
226        let map_id = page_id.0 / pages_per_map;
227        let map_offset = map_id * pages_per_map * page_size_u64;
228        let within = (offset - map_offset) as usize;
229
230        // Fast path: if mapping exists, do not obtain file lock; take Arc clone and release map lock
231        if let Some(mmap_arc) = {
232            let maps = self.mmaps.read().unwrap();
233            maps.get(&map_id).cloned()
234        } {
235            let start = within;
236            let stop = start + self.page_size;
237            if stop > mmap_arc.len() {
238                return Err(io::Error::new(
239                    io::ErrorKind::UnexpectedEof,
240                    format!("Page {page_id:?} not found"),
241                ));
242            }
243            return Ok(MappedPage {
244                mmap: mmap_arc,
245                start,
246                len: self.page_size,
247            });
248        }
249
250        // Slow path: need to create the mapping with double-checked locking
251        let file = self.reader.clone();
252        let file_len = file.metadata()?.len();
253
254        // Re-check if mapping appeared while we acquired the file lock
255        if let Some(mmap_arc) = {
256            let maps = self.mmaps.read().unwrap();
257            maps.get(&map_id).cloned()
258        } {
259            let start = within;
260            let stop = start + self.page_size;
261            if stop > mmap_arc.len() {
262                return Err(io::Error::new(
263                    io::ErrorKind::UnexpectedEof,
264                    format!("Page {page_id:?} not found"),
265                ));
266            }
267            return Ok(MappedPage {
268                mmap: mmap_arc,
269                start,
270                len: self.page_size,
271            });
272        }
273
274        // Calculate standard mapping window length (does not depend on current file length)
275        let max_len = pages_per_map * page_size_u64;
276
277        // Before mapping, ensure the requested page is within the current file length.
278        let page_end = offset + page_size_u64;
279        if page_end > file_len {
280            return Err(io::Error::new(
281                io::ErrorKind::UnexpectedEof,
282                format!("Page {page_id:?} not found"),
283            ));
284        }
285
286        // Ensure the underlying file is large enough to permit a full standard-length mapping.
287        let required_len = map_offset + max_len;
288        if file_len < required_len {
289            file.set_len(required_len)?;
290        }
291
292        // Create the mmap and insert it, but guard with a double-check
293        let mmap_new = unsafe {
294            MmapOptions::new()
295                .offset(map_offset)
296                .len(max_len as usize)
297                .map(&*file)?
298        };
299        // mmap_new.advise(Advice::Random)?;
300        // mmap_new.advise(Advice::WillNeed)?;
301        let mmap_arc = {
302            let mut maps = self.mmaps.write().unwrap();
303            // Another thread could have inserted meanwhile
304            if let Some(existing) = maps.get(&map_id) {
305                existing.clone()
306            } else {
307                let arc = Arc::new(mmap_new);
308                maps.insert(map_id, arc.clone());
309                // println!(
310                //     "Created new mmap: map_id={} offset={} len={}",
311                //     map_id, map_offset, max_len
312                // );
313                arc
314            }
315        };
316
317        // Now form the mapped page view
318        let start = within;
319        let stop = start + self.page_size;
320        if stop > mmap_arc.len() {
321            return Err(io::Error::new(
322                io::ErrorKind::UnexpectedEof,
323                format!("Page {page_id:?} not found"),
324            ));
325        }
326        Ok(MappedPage {
327            mmap: mmap_arc,
328            start,
329            len: self.page_size,
330        })
331    }
332
333    pub fn write_page(&self, page_id: PageID, page: &[u8]) -> DCBResult<()> {
334        let mut file = self.writer.lock().unwrap();
335        if page.len() != self.page_size {
336            return Err(DCBError::InternalError(format!(
337                "Page size mismatch: page_id={:?} size={} > PAGE_SIZE={}",
338                page_id,
339                page.len(),
340                self.page_size
341            )));
342        }
343
344        // Seek to the correct position
345        file.seek(SeekFrom::Start(page_id.0 * (self.page_size as u64)))?;
346
347        // Write the page data
348        file.write_all(page)?;
349
350        Ok(())
351    }
352
353    pub fn flush(&self) -> io::Result<()> {
354        let mut file = self.writer.lock().unwrap();
355        file.flush()?;
356        // fsync equivalent in Rust
357        #[cfg(unix)]
358        unsafe {
359            let result = libc::fsync(self.writer_raw_fd);
360            if result != 0 {
361                return Err(io::Error::last_os_error());
362            }
363        }
364        Ok(())
365    }
366
367    #[cfg(test)]
368    pub fn debug_mmap_count(&self) -> usize {
369        self.mmaps.read().unwrap().len()
370    }
371
372    #[cfg(test)]
373    pub fn debug_pages_per_mmap(&self) -> usize {
374        self.mmap_pages_per_map
375    }
376}
377
378// A zero-copy view over a page backed by a memory map. Holds an Arc to keep the mapping alive.
379#[derive(Debug)]
380pub struct MappedPage {
381    mmap: Arc<Mmap>,
382    start: usize,
383    len: usize,
384}
385
386impl MappedPage {
387    pub fn as_slice(&self) -> &[u8] {
388        &self.mmap[self.start..self.start + self.len]
389    }
390}
391
392#[cfg(test)]
393mod tests {
394    use super::Pager;
395    use crate::common::PageID;
396    use std::path::PathBuf;
397    use tempfile::tempdir;
398    use umadb_dcb::DCBError;
399
400    fn temp_file_path(name: &str) -> PathBuf {
401        let dir = tempdir().expect("tempdir");
402        dir.keep().join(name)
403    }
404
405    #[test]
406    fn test_write_page_size_mismatch_error() {
407        let page_size = 1024usize;
408        let path = temp_file_path("pager_mmap_test.db");
409        let pager = Pager::new(&path, page_size).expect("pager new");
410
411        let data1 = vec![1u8; 100];
412
413        let err = pager.write_page(PageID(0), &data1);
414        assert!(matches!(err, Err(DCBError::InternalError(_))));
415    }
416
417    #[test]
418    fn mmap_read_matches_normal_read() {
419        let page_size = 1024usize;
420        let path = temp_file_path("pager_mmap_test.db");
421        let pager = Pager::new(&path, page_size).expect("pager new");
422
423        let data1 = vec![1u8; page_size];
424        let data2 = (0..page_size).map(|i| (i % 256) as u8).collect::<Vec<_>>();
425
426        pager.write_page(PageID(0), &data1).expect("write page 0");
427        pager.write_page(PageID(1), &data2).expect("write page 1");
428        pager.flush().expect("flush");
429
430        let r0 = pager.read_page(PageID(0)).expect("read0");
431        let r0m = pager.read_page_mmap_slice(PageID(0)).expect("read0m");
432        assert_eq!(
433            r0,
434            r0m.as_slice(),
435            "mmap read should match std read for page 0"
436        );
437
438        let r1 = pager.read_page(PageID(1)).expect("read1");
439        let r1m = pager.read_page_mmap_slice(PageID(1)).expect("read1m");
440        assert_eq!(
441            r1,
442            r1m.as_slice(),
443            "mmap read should match std read for page 1"
444        );
445        assert_eq!(&r1[..], &data2[..]);
446    }
447
448    #[test]
449    fn mmap_read_out_of_bounds() {
450        let page_size = 512usize;
451        let path = temp_file_path("pager_mmap_oob.db");
452        let pager = Pager::new(&path, page_size).expect("pager new");
453
454        let buf = vec![0u8; page_size];
455        pager.write_page(PageID(0), &buf).expect("write p0");
456        pager.flush().expect("flush");
457
458        let err = pager.read_page_mmap_slice(PageID(1)).unwrap_err();
459        assert_eq!(err.kind(), std::io::ErrorKind::UnexpectedEof);
460    }
461
462    #[test]
463    fn mmap_reuse_within_same_map() {
464        // Try a few page sizes to ensure we get at least 2 pages per mmap window
465        let candidates = [512usize, 1024, 2048, 4096, 8192];
466        let mut pager_opt = None;
467        for ps in candidates {
468            let path = temp_file_path("pager_mmap_reuse.db");
469            let pager = Pager::new(&path, ps).expect("pager new");
470            if pager.debug_pages_per_mmap() >= 2 {
471                pager_opt = Some((pager, ps));
472                break;
473            }
474        }
475        let (pager, page_size) = pager_opt.expect("could not find suitable page size for test");
476
477        // Write two pages
478        pager
479            .write_page(PageID(0), &vec![1u8; page_size])
480            .expect("write p0");
481        pager
482            .write_page(PageID(1), &vec![2u8; page_size])
483            .expect("write p1");
484        pager.flush().expect("flush");
485
486        // Read both pages via mmap
487        let _ = pager.read_page_mmap_slice(PageID(0)).expect("read0m");
488        assert_eq!(pager.debug_mmap_count(), 1, "first mmap created");
489        let _ = pager.read_page_mmap_slice(PageID(1)).expect("read1m");
490        assert_eq!(
491            pager.debug_mmap_count(),
492            1,
493            "should reuse same mmap for pages in same window"
494        );
495    }
496
497    #[test]
498    fn mmap_creates_new_on_boundary() {
499        // Ensure pages_per_mmap is available and >= 2 by selecting suitable page size
500        let candidates = [512usize, 1024, 2048, 4096, 8192];
501        let mut pager_opt = None;
502        for ps in candidates {
503            let path = temp_file_path("pager_mmap_boundary.db");
504            let pager = Pager::new(&path, ps).expect("pager new");
505            if pager.debug_pages_per_mmap() >= 1 {
506                pager_opt = Some((pager, ps));
507                break;
508            }
509        }
510        let (pager, page_size) = pager_opt.expect("failed to create pager");
511        let ppm = pager.debug_pages_per_mmap();
512
513        // Write enough pages to cover two windows
514        for p in 0..(ppm as u64 + 1) {
515            let fill = if (p % 2) == 0 { 0xAA } else { 0x55 };
516            pager
517                .write_page(PageID(p), &vec![fill; page_size])
518                .expect("write page");
519        }
520        pager.flush().expect("flush");
521
522        // First read creates first mmap
523        let _ = pager
524            .read_page_mmap_slice(PageID(0))
525            .expect("read first window");
526        assert_eq!(pager.debug_mmap_count(), 1);
527        // Reading the first page in the next window should create a second mmap
528        let _ = pager
529            .read_page_mmap_slice(PageID(ppm as u64))
530            .expect("read second window");
531        assert_eq!(pager.debug_mmap_count(), 2);
532    }
533}