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};
11pub struct Pager {
15 pub reader: Arc<File>,
16 pub writer: Mutex<BufWriter<File>>,
18 pub writer_raw_fd: RawFd,
19 pub page_size: usize,
20 pub is_file_new: bool,
21 mmap_pages_per_map: usize,
23 mmaps: RwLock<HashMap<u64, Arc<Mmap>>>,
25}
26
27impl 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 let os_ps = Self::os_page_size();
59 let g = Self::gcd(os_ps, page_size);
60 let align_pages = usize::max(1, os_ps / g);
63 let min_window_bytes: usize = 10 * 1024 * 1024;
65 let min_pages = min_window_bytes.div_ceil(page_size);
67 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 unsafe {
99 let sz = libc::sysconf(libc::_SC_PAGESIZE);
100 if sz <= 0 {
101 4096usize } 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_slice(&self, page_id: PageID) -> io::Result<MappedPage> {
222 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 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 let file = self.reader.clone();
252 let file_len = file.metadata()?.len();
253
254 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 let max_len = pages_per_map * page_size_u64;
276
277 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 let required_len = map_offset + max_len;
288 if file_len < required_len {
289 file.set_len(required_len)?;
290 }
291
292 let mmap_new = unsafe {
294 MmapOptions::new()
295 .offset(map_offset)
296 .len(max_len as usize)
297 .map(&*file)?
298 };
299 let mmap_arc = {
302 let mut maps = self.mmaps.write().unwrap();
303 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 arc
314 }
315 };
316
317 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 file.seek(SeekFrom::Start(page_id.0 * (self.page_size as u64)))?;
346
347 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 #[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#[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 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 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 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 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 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 let _ = pager
524 .read_page_mmap_slice(PageID(0))
525 .expect("read first window");
526 assert_eq!(pager.debug_mmap_count(), 1);
527 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}