Skip to main content

cu29_unifiedlog/
memmap.rs

1//! Mapped-file adapter for the byte-addressed section chain.
2use crate::byte_log::{ByteLogger, ByteRegion, ByteSection, ByteStorage, read_header, read_main};
3use crate::{
4    ApplicationMetadata, CapacityPolicy, MainHeader, SECTION_HEADER_COMPACT_SIZE, SectionHeader,
5    UnifiedLogRead,
6};
7use alloc::sync::Arc;
8use bincode::{config::standard, decode_from_slice};
9use cu29_traits::{CuError, CuResult, UnifiedLogType};
10use memmap2::{Mmap, MmapMut};
11use std::fs::{File, OpenOptions};
12use std::io::{self, Read};
13use std::path::{Path, PathBuf};
14
15const MAX_SPANS: usize = 64;
16enum Mapping {
17    Read(Mmap),
18    Write(core::cell::UnsafeCell<MmapMut>),
19}
20struct MappedFile {
21    mapping: Mapping,
22    pointer: *mut u8,
23    length: usize,
24    #[cfg(feature = "mmap-fsync")]
25    file: File,
26}
27// SAFETY: Sections receive disjoint ranges and exclusive generation leases. Header
28// access uses the same lease. The mapping never exposes borrowed payload slices.
29unsafe impl Send for MappedFile {}
30unsafe impl Sync for MappedFile {}
31impl MappedFile {
32    fn new(file: File, writable: bool) -> io::Result<Self> {
33        // SAFETY: Copper owns these backing files; external mutation is unsupported.
34        let (mapping, pointer, length) = unsafe {
35            if writable {
36                let mut mapping = MmapMut::map_mut(&file)?;
37                let pointer = mapping.as_mut_ptr();
38                let length = mapping.len();
39                (
40                    Mapping::Write(core::cell::UnsafeCell::new(mapping)),
41                    pointer,
42                    length,
43                )
44            } else {
45                let mapping = Mmap::map(&file)?;
46                let pointer = mapping.as_ptr().cast_mut();
47                let length = mapping.len();
48                (Mapping::Read(mapping), pointer, length)
49            }
50        };
51        Ok(Self {
52            mapping,
53            pointer,
54            length,
55            #[cfg(feature = "mmap-fsync")]
56            file,
57        })
58    }
59}
60
61/// A range keeps mappings alive even after the logger is dropped.
62pub struct MmapRegion {
63    maps: [Option<Arc<MappedFile>>; MAX_SPANS],
64    first: usize,
65    slab_size: usize,
66    length: u64,
67}
68impl MmapRegion {
69    fn visit(
70        &self,
71        offset: u64,
72        len: usize,
73        mut visit: impl FnMut(&MappedFile, usize, usize, usize) -> CuResult<()>,
74    ) -> CuResult<()> {
75        if offset
76            .checked_add(len as u64)
77            .is_none_or(|end| end > self.length)
78        {
79            return Err(CuError::from("Mapped range exceeds section"));
80        }
81        let mut cursor = self.first
82            + usize::try_from(offset).map_err(|_| CuError::from("Offset exceeds address space"))?;
83        let mut done = 0;
84        while done < len {
85            let index = cursor / self.slab_size;
86            let local = cursor % self.slab_size;
87            let map = self
88                .maps
89                .get(index)
90                .and_then(Option::as_ref)
91                .ok_or_else(|| CuError::from("Missing mapped slab"))?;
92            let count = (len - done).min(map.length.saturating_sub(local));
93            if count == 0 {
94                return Err(CuError::from("Truncated mapped slab"));
95            }
96            visit(map, local, done, count)?;
97            cursor += count;
98            done += count;
99        }
100        Ok(())
101    }
102}
103impl ByteRegion for MmapRegion {
104    fn len(&self) -> u64 {
105        self.length
106    }
107    fn read(&self, offset: u64, bytes: &mut [u8]) -> CuResult<()> {
108        self.visit(offset, bytes.len(), |map, local, done, count| {
109            // SAFETY: Reads are bounded to a leased section or read-only file mapping.
110            unsafe {
111                core::ptr::copy_nonoverlapping(
112                    map.pointer.add(local),
113                    bytes[done..].as_mut_ptr(),
114                    count,
115                );
116            }
117            Ok(())
118        })
119    }
120    fn write(&mut self, offset: u64, bytes: &[u8]) -> CuResult<()> {
121        self.visit(offset, bytes.len(), |map, local, done, count| {
122            let Mapping::Write(_) = &map.mapping else {
123                return Err(CuError::from("Read-only mapped storage"));
124            };
125            // SAFETY: The allocator gives disjoint sections exclusive generation leases.
126            // A header update seals its lease before writing; reclamation cannot overlap
127            // an encoder holding BUSY. No mutable Rust references to a mapping escape.
128            unsafe {
129                core::ptr::copy_nonoverlapping(
130                    bytes[done..].as_ptr(),
131                    map.pointer.add(local),
132                    count,
133                );
134            }
135            Ok(())
136        })
137    }
138    fn commit(&mut self) -> CuResult<()> {
139        Ok(())
140    }
141    fn flush(&mut self) -> CuResult<()> {
142        for map in self.maps.iter().flatten() {
143            match &map.mapping {
144                Mapping::Write(mapping) => {
145                    // SAFETY: Flushing observes the mapping without creating payload slices.
146                    unsafe { &*mapping.get() }
147                        .flush()
148                        .map_err(|e| CuError::new_with_cause("Cannot flush mapped section", e))?;
149                    #[cfg(feature = "mmap-fsync")]
150                    map.file
151                        .sync_all()
152                        .map_err(|e| CuError::new_with_cause("Cannot sync backing file", e))?;
153                }
154                Mapping::Read(mapping) => {
155                    let _ = mapping.len();
156                }
157            }
158        }
159        Ok(())
160    }
161}
162
163#[doc(hidden)]
164pub struct MmapStorage {
165    path: PathBuf,
166    slab_size: usize,
167    maps: Vec<Arc<MappedFile>>,
168    writable: bool,
169}
170fn slab_path(path: &Path, index: usize) -> io::Result<PathBuf> {
171    let stem = path
172        .file_stem()
173        .and_then(|s| s.to_str())
174        .ok_or_else(|| io::Error::other("Invalid log file name"))?;
175    let extension = path
176        .extension()
177        .and_then(|s| s.to_str())
178        .ok_or_else(|| io::Error::other("Log file requires an extension"))?;
179    Ok(path.with_file_name(format!("{stem}_{index}.{extension}")))
180}
181impl MmapStorage {
182    fn open(path: &Path, writable: bool) -> io::Result<Self> {
183        let first = File::open(slab_path(path, 0)?)?;
184        let slab_size = first.metadata()?.len() as usize;
185        if slab_size < 512 || !slab_size.is_multiple_of(512) {
186            return Err(io::Error::other("Invalid backing slab size"));
187        }
188        let mut maps = Vec::new();
189        for index in 0.. {
190            let file = match OpenOptions::new()
191                .read(true)
192                .write(writable)
193                .open(slab_path(path, index)?)
194            {
195                Ok(file) => file,
196                Err(e) if e.kind() == io::ErrorKind::NotFound => break,
197                Err(e) => return Err(e),
198            };
199            if file.metadata()?.len() != slab_size as u64 {
200                return Err(io::Error::other("Inconsistent backing slab sizes"));
201            }
202            maps.push(Arc::new(MappedFile::new(file, writable)?));
203        }
204        Ok(Self {
205            path: path.to_path_buf(),
206            slab_size,
207            maps,
208            writable,
209        })
210    }
211    fn create(path: &Path, slab_size: usize) -> io::Result<Self> {
212        if slab_size < 1024 || !slab_size.is_multiple_of(512) {
213            return Err(io::Error::other(
214                "Slab size must be a multiple of 512 bytes",
215            ));
216        }
217        let mut storage = Self {
218            path: path.to_path_buf(),
219            slab_size,
220            maps: Vec::new(),
221            writable: true,
222        };
223        storage.grow(slab_size as u64).map_err(io::Error::other)?;
224        // Remove old suffixes only for this explicitly replaced log.
225        for index in 1.. {
226            match std::fs::remove_file(slab_path(path, index)?) {
227                Ok(()) => {}
228                Err(e) if e.kind() == io::ErrorKind::NotFound => break,
229                Err(e) => return Err(e),
230            }
231        }
232        match std::fs::remove_file(path) {
233            Ok(()) => {}
234            Err(e) if e.kind() == io::ErrorKind::NotFound => {}
235            Err(e) => return Err(e),
236        }
237        let target = slab_path(path, 0)?;
238        #[cfg(unix)]
239        std::os::unix::fs::symlink(target.file_name().unwrap(), path)?;
240        #[cfg(not(unix))]
241        std::fs::hard_link(target, path)?;
242        Ok(storage)
243    }
244}
245impl ByteStorage for MmapStorage {
246    type Region = MmapRegion;
247    fn len(&self) -> u64 {
248        self.maps.len() as u64 * self.slab_size as u64
249    }
250    fn grow(&mut self, end: u64) -> CuResult<()> {
251        if end <= self.len() {
252            return Ok(());
253        }
254        if !self.writable {
255            return Err(CuError::from("Read-only storage cannot grow"));
256        }
257        let count = end.div_ceil(self.slab_size as u64);
258        while (self.maps.len() as u64) < count {
259            let path = slab_path(&self.path, self.maps.len())
260                .map_err(|e| CuError::new_with_cause("Invalid slab path", e))?;
261            let file = OpenOptions::new()
262                .read(true)
263                .write(true)
264                .create(true)
265                .truncate(true)
266                .open(path)
267                .map_err(|e| CuError::new_with_cause("Cannot create backing slab", e))?;
268            file.set_len(self.slab_size as u64)
269                .map_err(|e| CuError::new_with_cause("Cannot allocate backing slab", e))?;
270            self.maps
271                .push(Arc::new(MappedFile::new(file, true).map_err(|e| {
272                    CuError::new_with_cause("Cannot map backing slab", e)
273                })?));
274        }
275        Ok(())
276    }
277    fn region(&self, offset: u64, length: u64) -> CuResult<MmapRegion> {
278        if offset
279            .checked_add(length)
280            .is_none_or(|end| end > self.len())
281        {
282            return Err(CuError::from("Byte range exceeds backing storage"));
283        }
284        let begin = usize::try_from(offset / self.slab_size as u64)
285            .map_err(|_| CuError::from("Offset exceeds address space"))?;
286        let count =
287            ((offset % self.slab_size as u64 + length).div_ceil(self.slab_size as u64)) as usize;
288        if count > MAX_SPANS {
289            return Err(CuError::from("Section spans more than 64 backing files"));
290        }
291        let mut maps = core::array::from_fn(|_| None);
292        for (slot, mapping) in maps.iter_mut().zip(self.maps[begin..begin + count].iter()) {
293            *slot = Some(mapping.clone());
294        }
295        Ok(MmapRegion {
296            maps,
297            first: (offset % self.slab_size as u64) as usize,
298            slab_size: self.slab_size,
299            length,
300        })
301    }
302}
303pub type MmapSectionStorage = ByteSection<MmapRegion>;
304pub type MmapUnifiedLoggerWrite = ByteLogger<MmapStorage>;
305pub enum MmapUnifiedLogger {
306    Read(MmapUnifiedLoggerRead),
307    Write(MmapUnifiedLoggerWrite),
308}
309
310#[derive(Default)]
311pub struct MmapUnifiedLoggerBuilder {
312    file_base_name: Option<PathBuf>,
313    preallocated_size: Option<usize>,
314    write: bool,
315    create: bool,
316    append: bool,
317    capacity: Option<u64>,
318    policy: CapacityPolicy,
319}
320impl MmapUnifiedLoggerBuilder {
321    pub fn new() -> Self {
322        Self::default()
323    }
324    pub fn file_base_name(mut self, path: &Path) -> Self {
325        self.file_base_name = Some(path.to_path_buf());
326        self
327    }
328    pub fn preallocated_size(mut self, size: usize) -> Self {
329        self.preallocated_size = Some(size);
330        self
331    }
332    pub fn write(mut self, write: bool) -> Self {
333        self.write = write;
334        self
335    }
336    pub fn create(mut self, create: bool) -> Self {
337        self.create = create;
338        self
339    }
340    pub fn append(mut self, append: bool) -> Self {
341        self.append = append;
342        self
343    }
344    /// Bound rotating sections by a byte capacity, retaining static metadata.
345    pub fn rollover(mut self, size: usize) -> Self {
346        self.capacity = Some(size as u64);
347        self.policy = CapacityPolicy::OverwriteOldest;
348        self
349    }
350    pub fn capacity(mut self, size: u64, policy: CapacityPolicy) -> Self {
351        self.capacity = Some(size);
352        self.policy = policy;
353        self
354    }
355    pub fn build(self) -> io::Result<MmapUnifiedLogger> {
356        let path = self
357            .file_base_name
358            .ok_or_else(|| io::Error::other("Log file path is required"))?;
359        if !(self.write && self.create) {
360            return MmapUnifiedLoggerRead::new(&path).map(MmapUnifiedLogger::Read);
361        }
362        let slab_size = self
363            .preallocated_size
364            .ok_or_else(|| io::Error::other("Preallocated size is required"))?;
365        let slab_size = slab_size
366            .checked_add(511)
367            .ok_or_else(|| io::Error::other("Slab size overflow"))?
368            / 512
369            * 512;
370        let slab_size = slab_size.max(1024);
371        let logger = if self.append {
372            let storage = MmapStorage::open(&path, true)?;
373            if storage.slab_size != slab_size {
374                return Err(io::Error::other("Append slab allocation size mismatch"));
375            }
376            let capacity = self.capacity.unwrap_or(storage.len());
377            ByteLogger::append(storage, self.policy, capacity)
378        } else {
379            ByteLogger::new(
380                MmapStorage::create(&path, slab_size)?,
381                512,
382                self.policy,
383                self.capacity.unwrap_or(slab_size as u64),
384            )
385        }
386        .map_err(io::Error::other)?;
387        Ok(MmapUnifiedLogger::Write(logger))
388    }
389}
390
391/// A byte offset relative to the beginning of the log.
392#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord)]
393pub struct LogPosition(pub u64);
394pub struct MmapUnifiedLoggerRead {
395    storage: MmapStorage,
396    main_header: MainHeader,
397    cursor: u64,
398    remaining: u64,
399}
400impl MmapUnifiedLoggerRead {
401    pub fn new(path: &Path) -> io::Result<Self> {
402        let storage = MmapStorage::open(path, false)?;
403        let main_header = read_main(&storage).map_err(io::Error::other)?;
404        let cursor = if main_header.metadata_offset != 0 {
405            main_header.metadata_offset
406        } else {
407            main_header.head_section
408        };
409        let remaining = main_header.sections_end / 512 + 2;
410        Ok(Self {
411            storage,
412            main_header,
413            cursor,
414            remaining,
415        })
416    }
417    pub fn raw_main_header(&self) -> &MainHeader {
418        &self.main_header
419    }
420    pub fn position(&self) -> LogPosition {
421        LogPosition(self.cursor)
422    }
423    pub fn seek(&mut self, position: LogPosition) -> CuResult<()> {
424        if position.0 != 0 {
425            self.section_header(position.0)?;
426        }
427        self.cursor = position.0;
428        self.remaining = self.main_header.sections_end / 512 + 2;
429        Ok(())
430    }
431    pub fn application_metadata(&self) -> CuResult<Option<ApplicationMetadata>> {
432        if self.main_header.metadata_offset == 0 {
433            return Ok(None);
434        }
435        let h = self.section_header(self.main_header.metadata_offset)?;
436        if h.entry_type != UnifiedLogType::ApplicationMetadata || h.is_open {
437            return Err(CuError::from("Invalid static application metadata"));
438        }
439        let mut data = vec![0; h.used as usize];
440        self.storage
441            .region(self.main_header.metadata_offset + 512, h.used as u64)?
442            .read(0, &mut data)?;
443        let (metadata, used) = decode_from_slice::<ApplicationMetadata, _>(&data, standard())
444            .map_err(|e| CuError::new_with_cause("Invalid application metadata", e))?;
445        if used != data.len() || metadata.catalog_offset != h.next_section {
446            return Err(CuError::from("Invalid metadata body or catalog offset"));
447        }
448        Ok(Some(metadata))
449    }
450    fn section_header(&self, offset: u64) -> CuResult<SectionHeader> {
451        if !offset.is_multiple_of(512)
452            || offset < self.main_header.page_size as u64
453            || offset >= self.main_header.sections_end
454        {
455            return Err(CuError::from("Invalid section offset"));
456        }
457        let h = read_header(&self.storage, offset)?;
458        let limit = if offset < self.main_header.sections_begin {
459            self.main_header.sections_begin
460        } else {
461            self.main_header.sections_end
462        };
463        if offset
464            .checked_add(h.allocated)
465            .is_none_or(|end| end > limit)
466        {
467            return Err(CuError::from("Section exceeds logical byte bounds"));
468        }
469        if h.next_section != 0
470            && (!h.next_section.is_multiple_of(512)
471                || h.next_section < self.main_header.page_size as u64
472                || h.next_section >= self.main_header.sections_end)
473        {
474            return Err(CuError::from("Invalid section link"));
475        }
476        if offset >= self.main_header.sections_begin {
477            if h.next_section != 0 && h.next_section < self.main_header.sections_begin {
478                return Err(CuError::from("Data links into static metadata"));
479            }
480            if (offset == self.main_header.tail_section) != (h.next_section == 0) {
481                return Err(CuError::from("Invalid tail link"));
482            }
483        }
484        Ok(h)
485    }
486    fn advance(&mut self, h: &SectionHeader) -> CuResult<()> {
487        if self.remaining == 0 {
488            return Err(CuError::from("Cyclic section links"));
489        }
490        self.remaining -= 1;
491        self.cursor = if self.cursor < self.main_header.sections_begin && h.next_section == 0 {
492            self.main_header.head_section
493        } else {
494            h.next_section
495        };
496        Ok(())
497    }
498    pub fn raw_skip_section(&mut self) -> CuResult<SectionHeader> {
499        if self.cursor == 0 {
500            return Ok(SectionHeader {
501                entry_type: UnifiedLogType::LastEntry,
502                is_open: !self.main_header.clean_close,
503                allocated: 512,
504                ..SectionHeader::default()
505            });
506        }
507        let h = self.section_header(self.cursor)?;
508        self.advance(&h)?;
509        Ok(h)
510    }
511    pub fn end_of_log(&mut self) -> CuResult<LogPosition> {
512        if !self.main_header.clean_close {
513            return Err(CuError::from("Log was not cleanly closed"));
514        }
515        while self.cursor != 0 {
516            if self.raw_skip_section()?.is_open {
517                return Err(CuError::from("Log contains an open section"));
518            }
519        }
520        Ok(LogPosition(0))
521    }
522    pub fn scan_section_bytes(&mut self, kind: UnifiedLogType) -> CuResult<u64> {
523        let mut bytes = 0;
524        while self.cursor != 0 {
525            let h = self.raw_skip_section()?;
526            if h.entry_type == kind {
527                bytes += h.used as u64;
528            }
529        }
530        Ok(bytes)
531    }
532}
533impl UnifiedLogRead for MmapUnifiedLoggerRead {
534    fn raw_read_section(&mut self) -> CuResult<(SectionHeader, Vec<u8>)> {
535        if self.cursor == 0 {
536            return Ok((self.raw_skip_section()?, Vec::new()));
537        }
538        let h = self.section_header(self.cursor)?;
539        let mut bytes = vec![0; h.used as usize];
540        self.storage
541            .region(
542                self.cursor + SECTION_HEADER_COMPACT_SIZE as u64,
543                h.used as u64,
544            )?
545            .read(0, &mut bytes)?;
546        self.advance(&h)?;
547        Ok((h, bytes))
548    }
549    fn read_next_section_type(&mut self, kind: UnifiedLogType) -> CuResult<Option<Vec<u8>>> {
550        while self.cursor != 0 {
551            let h = self.section_header(self.cursor)?;
552            if h.entry_type == kind {
553                return self.raw_read_section().map(|(_, bytes)| Some(bytes));
554            }
555            self.advance(&h)?;
556        }
557        Ok(None)
558    }
559}
560pub struct UnifiedLoggerIOReader {
561    logger: MmapUnifiedLoggerRead,
562    log_type: UnifiedLogType,
563    buffer: Vec<u8>,
564    buffer_pos: usize,
565}
566impl UnifiedLoggerIOReader {
567    pub fn new(logger: MmapUnifiedLoggerRead, log_type: UnifiedLogType) -> Self {
568        Self {
569            logger,
570            log_type,
571            buffer: Vec::new(),
572            buffer_pos: 0,
573        }
574    }
575}
576impl Read for UnifiedLoggerIOReader {
577    fn read(&mut self, bytes: &mut [u8]) -> io::Result<usize> {
578        if bytes.is_empty() {
579            return Ok(0);
580        }
581        while self.buffer_pos == self.buffer.len() {
582            match self
583                .logger
584                .read_next_section_type(self.log_type)
585                .map_err(io::Error::other)?
586            {
587                Some(buffer) => {
588                    self.buffer = buffer;
589                    self.buffer_pos = 0;
590                }
591                None => return Ok(0),
592            }
593        }
594        let count = bytes.len().min(self.buffer.len() - self.buffer_pos);
595        bytes[..count].copy_from_slice(&self.buffer[self.buffer_pos..self.buffer_pos + count]);
596        self.buffer_pos += count;
597        Ok(count)
598    }
599}