Skip to main content

cu29_unifiedlog/
memmap.rs

1//! This is the memory map file implementation for the unified logger for Copper.
2//! It is std only.
3
4use crate::{
5    AllocatedSection, MAIN_MAGIC, MainHeader, SECTION_MAGIC, SectionHandle, SectionHeader,
6    SectionStorage, UNIFIED_LOG_FORMAT_VERSION, UnifiedLogRead, UnifiedLogStatus, UnifiedLogWrite,
7};
8
9use crate::SECTION_HEADER_COMPACT_SIZE;
10
11use AllocatedSection::Section;
12use bincode::config::standard;
13use bincode::enc::EncoderImpl;
14use bincode::enc::write::SliceWriter;
15use bincode::error::EncodeError;
16use bincode::{Encode, decode_from_slice, encode_into_slice};
17use core::slice::from_raw_parts_mut;
18use cu29_traits::{
19    CuError, CuResult, ObservedWriter, UnifiedLogType, abort_observed_encode,
20    begin_observed_encode, finish_observed_encode,
21};
22use memmap2::{Mmap, MmapMut};
23use std::fs::{File, OpenOptions};
24use std::io::Read;
25use std::mem::ManuallyDrop;
26use std::path::{Path, PathBuf};
27use std::{io, mem};
28
29pub struct MmapSectionStorage {
30    buffer: &'static mut [u8],
31    offset: usize,
32    block_size: usize,
33}
34
35impl MmapSectionStorage {
36    pub fn new(buffer: &'static mut [u8], block_size: usize) -> Self {
37        Self {
38            buffer,
39            offset: 0,
40            block_size,
41        }
42    }
43
44    pub fn buffer_ptr(&self) -> *const u8 {
45        &self.buffer[0] as *const u8
46    }
47}
48
49impl SectionStorage for MmapSectionStorage {
50    fn initialize<E: Encode>(&mut self, header: &E) -> Result<usize, EncodeError> {
51        self.post_update_header(header)?;
52        self.offset = self.block_size;
53        Ok(self.offset)
54    }
55
56    fn post_update_header<E: Encode>(&mut self, header: &E) -> Result<usize, EncodeError> {
57        encode_into_slice(header, &mut self.buffer[0..], standard())
58    }
59
60    fn append<E: Encode>(&mut self, entry: &E) -> Result<usize, EncodeError> {
61        begin_observed_encode();
62        let result = (|| {
63            let mut encoder = EncoderImpl::new(
64                ObservedWriter::new(SliceWriter::new(&mut self.buffer[self.offset..])),
65                standard(),
66            );
67            entry.encode(&mut encoder)?;
68            Ok(encoder.into_writer().into_inner().bytes_written())
69        })();
70        let size = match result {
71            Ok(size) => {
72                debug_assert_eq!(size, finish_observed_encode());
73                size
74            }
75            Err(err) => {
76                abort_observed_encode();
77                return Err(err);
78            }
79        };
80        self.offset += size;
81        Ok(size)
82    }
83
84    fn flush(&mut self) -> CuResult<usize> {
85        // Flushing is handled at the slab level for mmap-backed storage.
86        Ok(self.offset)
87    }
88}
89
90///
91/// Holds the read or write side of the datalogger.
92pub enum MmapUnifiedLogger {
93    Read(MmapUnifiedLoggerRead),
94    Write(MmapUnifiedLoggerWrite),
95}
96
97/// Use this builder to create a new DataLogger.
98pub struct MmapUnifiedLoggerBuilder {
99    file_base_name: Option<PathBuf>,
100    preallocated_size: Option<usize>,
101    write: bool,
102    create: bool,
103    append: bool,
104}
105
106impl Default for MmapUnifiedLoggerBuilder {
107    fn default() -> Self {
108        Self::new()
109    }
110}
111
112impl MmapUnifiedLoggerBuilder {
113    pub fn new() -> Self {
114        Self {
115            file_base_name: None,
116            preallocated_size: None,
117            write: false,
118            create: false, // This is the safest default
119            append: false,
120        }
121    }
122
123    /// If "something/toto.copper" is given, it will find or create "something/toto_0.copper",  "something/toto_1.copper" etc.
124    pub fn file_base_name(mut self, file_path: &Path) -> Self {
125        self.file_base_name = Some(file_path.to_path_buf());
126        self
127    }
128
129    pub fn preallocated_size(mut self, preallocated_size: usize) -> Self {
130        self.preallocated_size = Some(preallocated_size);
131        self
132    }
133
134    pub fn write(mut self, write: bool) -> Self {
135        self.write = write;
136        self
137    }
138
139    pub fn create(mut self, create: bool) -> Self {
140        self.create = create;
141        self
142    }
143
144    /// When `write` and `create` are both set, resume writing at the end of an
145    /// existing log instead of truncating it.
146    ///
147    /// Requires a cleanly closed log (one ending with a permanent end-of-log
148    /// marker). Returns an error if the log does not exist or is incomplete.
149    pub fn append(mut self, append: bool) -> Self {
150        self.append = append;
151        self
152    }
153
154    pub fn build(self) -> io::Result<MmapUnifiedLogger> {
155        let page_size = page_size::get();
156
157        if self.write && self.create {
158            let file_path = self.file_base_name.ok_or_else(|| {
159                io::Error::new(
160                    io::ErrorKind::InvalidInput,
161                    "File path is required for write mode",
162                )
163            })?;
164            let preallocated_size = self.preallocated_size.ok_or_else(|| {
165                io::Error::new(
166                    io::ErrorKind::InvalidInput,
167                    "Preallocated size is required for write mode",
168                )
169            })?;
170            let ulw = if self.append {
171                MmapUnifiedLoggerWrite::append(&file_path, preallocated_size)?
172            } else {
173                MmapUnifiedLoggerWrite::new(&file_path, preallocated_size, page_size)?
174            };
175            Ok(MmapUnifiedLogger::Write(ulw))
176        } else {
177            let file_path = self.file_base_name.ok_or_else(|| {
178                io::Error::new(io::ErrorKind::InvalidInput, "File path is required")
179            })?;
180            let ulr = MmapUnifiedLoggerRead::new(&file_path)?;
181            Ok(MmapUnifiedLogger::Read(ulr))
182        }
183    }
184}
185
186struct SlabEntry {
187    file: File,
188    mmap_buffer: ManuallyDrop<MmapMut>,
189    current_global_position: usize,
190    sections_offsets_in_flight: Vec<usize>,
191    flushed_until_offset: usize,
192    page_size: usize,
193    temporary_end_marker: Option<usize>,
194    #[cfg(test)]
195    closed_sections: Vec<(usize, usize)>,
196    #[cfg(test)]
197    flushed_ranges: Vec<(usize, usize)>,
198    #[cfg(all(test, feature = "mmap-fsync"))]
199    sync_call_count: usize,
200}
201
202impl Drop for SlabEntry {
203    fn drop(&mut self) {
204        self.flush_until(self.current_global_position);
205        // SAFETY: We own the mapping and must drop it before trimming the file.
206        unsafe { ManuallyDrop::drop(&mut self.mmap_buffer) };
207        if let Err(error) = self.file.set_len(self.current_global_position as u64) {
208            eprintln!("Failed to trim datalogger file: {}", error);
209        }
210        self.sync_file();
211
212        if !self.sections_offsets_in_flight.is_empty() {
213            eprintln!("Error: Slab not full flushed.");
214        }
215    }
216}
217
218impl SlabEntry {
219    fn new(file: File, page_size: usize) -> io::Result<Self> {
220        let mmap_buffer = ManuallyDrop::new(
221            // SAFETY: The file descriptor is valid and mapping is confined to this struct.
222            unsafe { MmapMut::map_mut(&file) }
223                .map_err(|e| io::Error::new(e.kind(), format!("Failed to map file: {e}")))?,
224        );
225        Ok(Self {
226            file,
227            mmap_buffer,
228            current_global_position: 0,
229            sections_offsets_in_flight: Vec::with_capacity(16),
230            flushed_until_offset: 0,
231            page_size,
232            temporary_end_marker: None,
233            #[cfg(test)]
234            closed_sections: Vec::new(),
235            #[cfg(test)]
236            flushed_ranges: Vec::new(),
237            #[cfg(all(test, feature = "mmap-fsync"))]
238            sync_call_count: 0,
239        })
240    }
241
242    fn flush_range(&mut self, start: usize, len: usize) {
243        if len == 0 {
244            return;
245        }
246        self.mmap_buffer
247            .flush_async_range(start, len)
248            .expect("Failed to flush memory map");
249        self.sync_file();
250        #[cfg(test)]
251        self.record_flushed_range(start, len);
252    }
253
254    fn sync_file(&mut self) {
255        #[cfg(feature = "mmap-fsync")]
256        {
257            self.file.sync_all().expect("Failed to fsync log file");
258            #[cfg(test)]
259            {
260                self.sync_call_count += 1;
261            }
262        }
263    }
264    /// Unsure the underlying mmap is flush to disk until the given position.
265    fn flush_until(&mut self, until_position: usize) {
266        // This is tolerated under linux, but crashes on macos
267        if (self.flushed_until_offset == until_position) || (until_position == 0) {
268            return;
269        }
270        self.flush_range(
271            self.flushed_until_offset,
272            until_position - self.flushed_until_offset,
273        );
274        self.flushed_until_offset = until_position;
275    }
276
277    fn clear_temporary_end_marker(&mut self) {
278        if let Some(marker_start) = self.temporary_end_marker.take() {
279            self.current_global_position = marker_start;
280            if self.flushed_until_offset > marker_start {
281                self.flushed_until_offset = marker_start;
282            }
283        }
284    }
285
286    fn write_end_marker(&mut self, temporary: bool) -> CuResult<()> {
287        let block_size = SECTION_HEADER_COMPACT_SIZE as usize;
288        let marker_start = self.align_to_next_page(self.current_global_position);
289        let total_marker_size = block_size; // header only
290        let marker_end = marker_start + total_marker_size;
291        if marker_end > self.mmap_buffer.len() {
292            return Err("Not enough space to write end-of-log marker".into());
293        }
294
295        let header = SectionHeader {
296            magic: SECTION_MAGIC,
297            block_size: SECTION_HEADER_COMPACT_SIZE,
298            entry_type: UnifiedLogType::LastEntry,
299            offset_to_next_section: total_marker_size as u32,
300            used: 0,
301            is_open: temporary,
302        };
303
304        encode_into_slice(
305            &header,
306            &mut self.mmap_buffer
307                [marker_start..marker_start + SECTION_HEADER_COMPACT_SIZE as usize],
308            standard(),
309        )
310        .map_err(|e| CuError::new_with_cause("Failed to encode end-of-log header", e))?;
311
312        self.temporary_end_marker = Some(marker_start);
313        self.current_global_position = marker_end;
314        Ok(())
315    }
316
317    fn is_it_my_section(&self, section: &SectionHandle<MmapSectionStorage>) -> bool {
318        let storage = section.get_storage();
319        let ptr = storage.buffer_ptr();
320        (ptr >= self.mmap_buffer.as_ptr())
321            && (ptr as usize)
322                < (self.mmap_buffer.as_ref().as_ptr() as usize + self.mmap_buffer.as_ref().len())
323    }
324
325    /// Flush the section to disk.
326    /// the flushing is permanent and the section is considered closed.
327    fn flush_section(&mut self, section: &mut SectionHandle<MmapSectionStorage>) {
328        section
329            .post_update_header()
330            .expect("Failed to update section header");
331
332        let storage = section.get_storage();
333        let ptr = storage.buffer_ptr();
334
335        if ptr < self.mmap_buffer.as_ptr()
336            || ptr as usize > self.mmap_buffer.as_ptr() as usize + self.mmap_buffer.len()
337        {
338            panic!("Invalid section buffer, not in the slab");
339        }
340
341        let base = self.mmap_buffer.as_ptr() as usize;
342        let section_start = ptr as usize - base;
343        let section_len = section.header.offset_to_next_section as usize;
344        #[cfg(test)]
345        self.record_closed_section(section_start, section_len);
346        self.sections_offsets_in_flight
347            .retain(|&x| x != section_start);
348
349        if self.sections_offsets_in_flight.is_empty() {
350            self.flush_until(self.current_global_position);
351            return;
352        }
353        let next_open_offset = self.sections_offsets_in_flight[0];
354        if self.flushed_until_offset < next_open_offset {
355            self.flush_until(next_open_offset);
356        }
357        if section_start + section_len > self.flushed_until_offset {
358            // A long-lived early section can otherwise pin later closed sections
359            // behind the prefix cursor until shutdown.
360            self.flush_range(section_start, section_len);
361        }
362    }
363
364    #[cfg(test)]
365    fn record_closed_section(&mut self, start: usize, len: usize) {
366        self.closed_sections.push((start, len));
367    }
368
369    #[cfg(test)]
370    fn record_flushed_range(&mut self, start: usize, len: usize) {
371        let mut merged_start = start;
372        let mut merged_end = start + len;
373        let mut merged_ranges = Vec::with_capacity(self.flushed_ranges.len() + 1);
374        let mut inserted = false;
375
376        for (range_start, range_len) in self.flushed_ranges.drain(..) {
377            let range_end = range_start + range_len;
378            if range_end < merged_start {
379                merged_ranges.push((range_start, range_len));
380                continue;
381            }
382            if merged_end < range_start {
383                if !inserted {
384                    merged_ranges.push((merged_start, merged_end - merged_start));
385                    inserted = true;
386                }
387                merged_ranges.push((range_start, range_len));
388                continue;
389            }
390
391            merged_start = merged_start.min(range_start);
392            merged_end = merged_end.max(range_end);
393        }
394
395        if !inserted {
396            merged_ranges.push((merged_start, merged_end - merged_start));
397        }
398
399        self.flushed_ranges = merged_ranges;
400    }
401
402    #[cfg(test)]
403    fn pending_closed_bytes(&self) -> usize {
404        let mut pending = 0;
405
406        for (section_start, section_len) in &self.closed_sections {
407            let section_end = section_start + section_len;
408            let mut cursor = *section_start;
409
410            for (range_start, range_len) in &self.flushed_ranges {
411                let range_end = range_start + range_len;
412                if range_end <= cursor {
413                    continue;
414                }
415                if *range_start >= section_end {
416                    break;
417                }
418                if *range_start > cursor {
419                    pending += *range_start - cursor;
420                }
421                cursor = cursor.max(range_end);
422                if cursor >= section_end {
423                    break;
424                }
425            }
426
427            if cursor < section_end {
428                pending += section_end - cursor;
429            }
430        }
431
432        pending
433    }
434
435    #[inline]
436    fn align_to_next_page(&self, ptr: usize) -> usize {
437        (ptr + self.page_size - 1) & !(self.page_size - 1)
438    }
439
440    /// The returned slice is section_size or greater.
441    fn add_section(
442        &mut self,
443        entry_type: UnifiedLogType,
444        requested_section_size: usize,
445    ) -> AllocatedSection<MmapSectionStorage> {
446        // align current_position to the next page
447        self.current_global_position = self.align_to_next_page(self.current_global_position);
448        let section_size = self.align_to_next_page(requested_section_size) as u32;
449
450        // We need to have enough space to store the section in that slab
451        if self.current_global_position + section_size as usize > self.mmap_buffer.len() {
452            return AllocatedSection::NoMoreSpace;
453        }
454
455        #[cfg(feature = "compact")]
456        let block_size = SECTION_HEADER_COMPACT_SIZE;
457
458        #[cfg(not(feature = "compact"))]
459        let block_size = self.page_size as u16;
460
461        let section_header = SectionHeader {
462            magic: SECTION_MAGIC,
463            block_size,
464            entry_type,
465            offset_to_next_section: section_size,
466            used: 0u32,
467            is_open: true,
468        };
469
470        // save the position to keep track for in flight sections
471        self.sections_offsets_in_flight
472            .push(self.current_global_position);
473        let end_of_section = self.current_global_position + requested_section_size;
474        let user_buffer = &mut self.mmap_buffer[self.current_global_position..end_of_section];
475
476        // SAFETY: We have exclusive access to user_buffer for the handle's lifetime.
477        let handle_buffer =
478            unsafe { from_raw_parts_mut(user_buffer.as_mut_ptr(), user_buffer.len()) };
479        let storage = MmapSectionStorage::new(handle_buffer, block_size as usize);
480
481        self.current_global_position = end_of_section;
482
483        Section(SectionHandle::create(section_header, storage).expect("Failed to create section"))
484    }
485
486    #[cfg(test)]
487    fn used(&self) -> usize {
488        self.current_global_position
489    }
490}
491
492/// A write side of the datalogger.
493pub struct MmapUnifiedLoggerWrite {
494    /// the front slab is the current active slab for any new section.
495    front_slab: SlabEntry,
496    /// the back slab is the previous slab that is being flushed.
497    back_slabs: Vec<SlabEntry>,
498    /// base file path to create the backing files from.
499    base_file_path: PathBuf,
500    /// allocation size for the backing files.
501    slab_size: usize,
502    /// current suffix for the backing files.
503    front_slab_suffix: usize,
504}
505
506fn build_slab_path(base_file_path: &Path, slab_index: usize) -> io::Result<PathBuf> {
507    let mut file_path = base_file_path.to_path_buf();
508    let stem = file_path.file_stem().ok_or_else(|| {
509        io::Error::new(
510            io::ErrorKind::InvalidInput,
511            "Base file path has no file name",
512        )
513    })?;
514    let stem = stem.to_str().ok_or_else(|| {
515        io::Error::new(
516            io::ErrorKind::InvalidInput,
517            "Base file name is not valid UTF-8",
518        )
519    })?;
520    let extension = file_path.extension().ok_or_else(|| {
521        io::Error::new(
522            io::ErrorKind::InvalidInput,
523            "Base file path has no extension",
524        )
525    })?;
526    let extension = extension.to_str().ok_or_else(|| {
527        io::Error::new(
528            io::ErrorKind::InvalidInput,
529            "Base file extension is not valid UTF-8",
530        )
531    })?;
532    if stem.is_empty() {
533        return Err(io::Error::new(
534            io::ErrorKind::InvalidInput,
535            "Base file name is empty",
536        ));
537    }
538    let file_name = format!("{stem}_{slab_index}.{extension}");
539    file_path.set_file_name(file_name);
540    Ok(file_path)
541}
542
543fn make_slab_file(base_file_path: &Path, slab_size: usize, slab_suffix: usize) -> io::Result<File> {
544    let file_path = build_slab_path(base_file_path, slab_suffix)?;
545    let file = OpenOptions::new()
546        .read(true)
547        .write(true)
548        .create(true)
549        .truncate(true)
550        .open(&file_path)
551        .map_err(|e| {
552            io::Error::new(
553                e.kind(),
554                format!("Failed to open file {}: {e}", file_path.display()),
555            )
556        })?;
557    file.set_len(slab_size as u64).map_err(|e| {
558        io::Error::new(
559            e.kind(),
560            format!("Failed to set file length for {}: {e}", file_path.display()),
561        )
562    })?;
563    Ok(file)
564}
565
566fn remove_existing_alias(base_file_path: &Path) -> io::Result<()> {
567    match std::fs::symlink_metadata(base_file_path) {
568        Ok(meta) => {
569            if meta.is_dir() {
570                return Err(io::Error::new(
571                    io::ErrorKind::AlreadyExists,
572                    format!(
573                        "Cannot create base log alias at {} because a directory already exists there",
574                        base_file_path.display()
575                    ),
576                ));
577            }
578            std::fs::remove_file(base_file_path).map_err(|e| {
579                io::Error::new(
580                    e.kind(),
581                    format!(
582                        "Failed to remove existing base log alias {}: {e}",
583                        base_file_path.display()
584                    ),
585                )
586            })
587        }
588        Err(e) if e.kind() == io::ErrorKind::NotFound => Ok(()),
589        Err(e) => Err(io::Error::new(
590            e.kind(),
591            format!(
592                "Failed to inspect existing base log alias {}: {e}",
593                base_file_path.display()
594            ),
595        )),
596    }
597}
598
599fn create_base_alias_link(base_file_path: &Path) -> io::Result<()> {
600    let first_slab_path = build_slab_path(base_file_path, 0)?;
601    remove_existing_alias(base_file_path)?;
602
603    #[cfg(unix)]
604    {
605        use std::os::unix::fs::symlink;
606        let relative_target = Path::new(first_slab_path.file_name().ok_or_else(|| {
607            io::Error::new(
608                io::ErrorKind::InvalidInput,
609                "First slab file has no name component",
610            )
611        })?);
612        symlink(relative_target, base_file_path).map_err(|e| {
613            io::Error::new(
614                e.kind(),
615                format!(
616                    "Failed to create base log alias {} -> {}: {e}",
617                    base_file_path.display(),
618                    first_slab_path.display()
619                ),
620            )
621        })
622    }
623
624    #[cfg(windows)]
625    {
626        use std::os::windows::fs::symlink_file;
627        let relative_target = Path::new(first_slab_path.file_name().ok_or_else(|| {
628            io::Error::new(
629                io::ErrorKind::InvalidInput,
630                "First slab file has no name component",
631            )
632        })?);
633        match symlink_file(relative_target, base_file_path) {
634            Ok(()) => Ok(()),
635            Err(symlink_err) => std::fs::hard_link(&first_slab_path, base_file_path).map_err(
636                |hard_link_err| {
637                    io::Error::other(format!(
638                        "Failed to create base log alias {}. Symlink error: {symlink_err}. Hard-link fallback error: {hard_link_err}",
639                        base_file_path.display()
640                    ))
641                },
642            ),
643        }?;
644        Ok(())
645    }
646
647    #[cfg(not(any(unix, windows)))]
648    {
649        std::fs::hard_link(&first_slab_path, base_file_path).map_err(|e| {
650            io::Error::new(
651                e.kind(),
652                format!(
653                    "Failed to create base log alias {} -> {}: {e}",
654                    base_file_path.display(),
655                    first_slab_path.display()
656                ),
657            )
658        })
659    }
660}
661
662impl UnifiedLogWrite<MmapSectionStorage> for MmapUnifiedLoggerWrite {
663    /// The returned slice is section_size or greater.
664    fn add_section(
665        &mut self,
666        entry_type: UnifiedLogType,
667        requested_section_size: usize,
668    ) -> CuResult<SectionHandle<MmapSectionStorage>> {
669        self.garbage_collect_backslabs(); // Take the opportunity to keep up and close stale back slabs.
670        self.front_slab.clear_temporary_end_marker();
671        let maybe_section = self
672            .front_slab
673            .add_section(entry_type, requested_section_size);
674
675        match maybe_section {
676            AllocatedSection::NoMoreSpace => {
677                // move the front slab to the back slab.
678                let new_slab = self.create_slab()?;
679                // keep the slab until all its sections has been flushed.
680                self.back_slabs
681                    .push(mem::replace(&mut self.front_slab, new_slab));
682                match self
683                    .front_slab
684                    .add_section(entry_type, requested_section_size)
685                {
686                    AllocatedSection::NoMoreSpace => Err(CuError::from("out of space")),
687                    Section(section) => {
688                        self.place_end_marker(true)?;
689                        Ok(section)
690                    }
691                }
692            }
693            Section(section) => {
694                self.place_end_marker(true)?;
695                Ok(section)
696            }
697        }
698    }
699
700    fn flush_section(&mut self, section: &mut SectionHandle<MmapSectionStorage>) {
701        section.mark_closed();
702        for slab in self.back_slabs.iter_mut() {
703            if slab.is_it_my_section(section) {
704                slab.flush_section(section);
705                return;
706            }
707        }
708        self.front_slab.flush_section(section);
709    }
710
711    fn status(&self) -> UnifiedLogStatus {
712        UnifiedLogStatus {
713            total_used_space: self.front_slab.current_global_position,
714            total_allocated_space: self.slab_size * self.front_slab_suffix,
715        }
716    }
717}
718
719impl MmapUnifiedLoggerWrite {
720    fn next_slab(&mut self) -> io::Result<File> {
721        let next_suffix = self.front_slab_suffix + 1;
722        let file = make_slab_file(&self.base_file_path, self.slab_size, next_suffix)?;
723        self.front_slab_suffix = next_suffix;
724        Ok(file)
725    }
726
727    fn new(base_file_path: &Path, slab_size: usize, page_size: usize) -> io::Result<Self> {
728        let file = make_slab_file(base_file_path, slab_size, 0)?;
729        create_base_alias_link(base_file_path)?;
730        let mut front_slab = SlabEntry::new(file, page_size)?;
731
732        // This is the first slab so add the main header.
733        let main_header = MainHeader {
734            magic: MAIN_MAGIC,
735            format_version: UNIFIED_LOG_FORMAT_VERSION,
736            first_section_offset: page_size as u16,
737            page_size: page_size as u16,
738        };
739        let nb_bytes = encode_into_slice(&main_header, &mut front_slab.mmap_buffer[..], standard())
740            .map_err(|e| io::Error::other(format!("Failed to encode main header: {e}")))?;
741        assert!(nb_bytes < page_size);
742        front_slab.current_global_position = page_size; // align to the next page
743
744        Ok(Self {
745            front_slab,
746            back_slabs: Vec::new(),
747            base_file_path: base_file_path.to_path_buf(),
748            slab_size,
749            front_slab_suffix: 0,
750        })
751    }
752
753    /// Resume writing at the end of a cleanly closed log instead of zapping it.
754    ///
755    /// Scans the existing log for the permanent end-of-log marker, reopens the
756    /// last slab for writing (re-extending it to `slab_size`, since it is trimmed
757    /// on clean shutdown), and positions the cursor right after that marker so new
758    /// sections overwrite it. The main header and earlier slabs are left untouched.
759    fn append(base_file_path: &Path, slab_size: usize) -> io::Result<Self> {
760        // Locate the end-of-log marker using the read side.
761        let mut reader = MmapUnifiedLoggerRead::new(base_file_path)?;
762        let end = reader.end_of_log().map_err(io::Error::other)?;
763        let last_slab_index = end.slab_index;
764        let resume_offset = end.offset;
765        // Preserve the original page alignment recorded in the main header.
766        let original_page_size = reader.raw_main_header().page_size as usize;
767        drop(reader);
768
769        let slab_path = build_slab_path(base_file_path, last_slab_index)?;
770        let file = OpenOptions::new()
771            .read(true)
772            .write(true)
773            .open(&slab_path)
774            .map_err(|e| {
775                io::Error::new(
776                    e.kind(),
777                    format!(
778                        "Failed to open slab {} for append: {e}",
779                        slab_path.display()
780                    ),
781                )
782            })?;
783
784        // The last slab is trimmed to its used size on clean shutdown; give it
785        // back room for new sections.
786        let current_len = file
787            .metadata()
788            .map_err(|e| {
789                io::Error::new(
790                    e.kind(),
791                    format!("Failed to read metadata for {}", slab_path.display()),
792                )
793            })?
794            .len();
795        if (current_len as usize) < slab_size {
796            file.set_len(slab_size as u64).map_err(|e| {
797                io::Error::new(
798                    e.kind(),
799                    format!(
800                        "Failed to extend slab {} for append: {e}",
801                        slab_path.display()
802                    ),
803                )
804            })?;
805        }
806
807        let mut front_slab = SlabEntry::new(file, original_page_size)?;
808        front_slab.current_global_position = resume_offset;
809        front_slab.flushed_until_offset = resume_offset;
810
811        Ok(Self {
812            front_slab,
813            back_slabs: Vec::new(),
814            base_file_path: base_file_path.to_path_buf(),
815            slab_size,
816            front_slab_suffix: last_slab_index,
817        })
818    }
819
820    fn garbage_collect_backslabs(&mut self) {
821        self.back_slabs
822            .retain_mut(|slab| !slab.sections_offsets_in_flight.is_empty());
823    }
824
825    fn place_end_marker(&mut self, temporary: bool) -> CuResult<()> {
826        match self.front_slab.write_end_marker(temporary) {
827            Ok(_) => Ok(()),
828            Err(_) => {
829                // Not enough space in the current slab, roll to a new one.
830                let new_slab = self.create_slab()?;
831                self.back_slabs
832                    .push(mem::replace(&mut self.front_slab, new_slab));
833                self.front_slab.write_end_marker(temporary)
834            }
835        }
836    }
837
838    pub fn stats(&self) -> (usize, Vec<usize>, usize) {
839        (
840            self.front_slab.current_global_position,
841            self.front_slab.sections_offsets_in_flight.clone(),
842            self.back_slabs.len(),
843        )
844    }
845
846    fn create_slab(&mut self) -> CuResult<SlabEntry> {
847        let file = self
848            .next_slab()
849            .map_err(|e| CuError::new_with_cause("Failed to create slab file", e))?;
850        SlabEntry::new(file, self.front_slab.page_size)
851            .map_err(|e| CuError::new_with_cause("Failed to create slab memory map", e))
852    }
853}
854
855impl Drop for MmapUnifiedLoggerWrite {
856    fn drop(&mut self) {
857        #[cfg(debug_assertions)]
858        eprintln!("Flushing the unified Logger ... "); // Note this cannot be a structured log writing in this log.
859
860        self.front_slab.clear_temporary_end_marker();
861        if let Err(e) = self.place_end_marker(false) {
862            panic!("Failed to flush the unified logger: {}", e);
863        }
864        self.front_slab
865            .flush_until(self.front_slab.current_global_position);
866        self.garbage_collect_backslabs();
867        #[cfg(debug_assertions)]
868        eprintln!("Unified Logger flushed."); // Note this cannot be a structured log writing in this log.
869    }
870}
871
872fn open_slab_index(
873    base_file_path: &Path,
874    slab_index: usize,
875) -> io::Result<(File, Mmap, u16, Option<MainHeader>)> {
876    let mut options = OpenOptions::new();
877    let options = options.read(true);
878
879    let file_path = build_slab_path(base_file_path, slab_index)?;
880    let file = options.open(&file_path).map_err(|e| {
881        io::Error::new(
882            e.kind(),
883            format!("Failed to open slab file {}: {e}", file_path.display()),
884        )
885    })?;
886    // SAFETY: The file is kept open for the lifetime of the mapping.
887    let mmap = unsafe { Mmap::map(&file) }
888        .map_err(|e| io::Error::new(e.kind(), format!("Failed to map slab file: {e}")))?;
889    let mut prolog = 0u16;
890    let mut maybe_main_header: Option<MainHeader> = None;
891    if slab_index == 0 {
892        let main_header: MainHeader;
893        let _read: usize;
894        (main_header, _read) = decode_from_slice(&mmap[..], standard()).map_err(|e| {
895            io::Error::new(
896                io::ErrorKind::InvalidData,
897                format!("Failed to decode main header: {e}"),
898            )
899        })?;
900        if main_header.magic != MAIN_MAGIC {
901            return Err(io::Error::new(
902                io::ErrorKind::InvalidData,
903                "Invalid magic number in main header",
904            ));
905        }
906        if main_header.format_version != UNIFIED_LOG_FORMAT_VERSION {
907            return Err(io::Error::new(
908                io::ErrorKind::InvalidData,
909                format!(
910                    "Unsupported unified log format version {} in main header; this reader supports version {}",
911                    main_header.format_version, UNIFIED_LOG_FORMAT_VERSION
912                ),
913            ));
914        }
915        prolog = main_header.first_section_offset;
916        maybe_main_header = Some(main_header);
917    }
918    Ok((file, mmap, prolog, maybe_main_header))
919}
920
921/// A read side of the memory map based unified logger.
922pub struct MmapUnifiedLoggerRead {
923    base_file_path: PathBuf,
924    main_header: MainHeader,
925    current_mmap_buffer: Mmap,
926    current_file: File,
927    current_slab_index: usize,
928    current_reading_position: usize,
929}
930
931/// Absolute position inside a unified log (slab index + byte offset).
932#[derive(Clone, Copy, Debug, PartialEq, Eq)]
933pub struct LogPosition {
934    pub slab_index: usize,
935    pub offset: usize,
936}
937
938impl UnifiedLogRead for MmapUnifiedLoggerRead {
939    fn read_next_section_type(&mut self, datalogtype: UnifiedLogType) -> CuResult<Option<Vec<u8>>> {
940        // TODO: eventually implement a 0 copy of this too.
941        loop {
942            if self.current_reading_position >= self.current_mmap_buffer.len() {
943                self.next_slab().map_err(|e| {
944                    CuError::new_with_cause("Failed to read next slab, is the log complete?", e)
945                })?;
946            }
947
948            let header_result = self.read_section_header();
949            let header = header_result.map_err(|error| {
950                CuError::new_with_cause(
951                    &format!(
952                        "Could not read a sections header: {}/{}:{}",
953                        self.base_file_path.as_os_str().to_string_lossy(),
954                        self.current_slab_index,
955                        self.current_reading_position,
956                    ),
957                    error,
958                )
959            })?;
960
961            // Reached the end of file
962            if header.entry_type == UnifiedLogType::LastEntry {
963                return Ok(None);
964            }
965
966            // Found a section of the requested type
967            if header.entry_type == datalogtype {
968                let result = Some(self.read_section_content(&header)?);
969                self.current_reading_position += header.offset_to_next_section as usize;
970                return Ok(result);
971            }
972
973            // Keep reading until we find the requested type
974            self.current_reading_position += header.offset_to_next_section as usize;
975        }
976    }
977
978    /// Reads the section from the section header pos.
979    fn raw_read_section(&mut self) -> CuResult<(SectionHeader, Vec<u8>)> {
980        if self.current_reading_position >= self.current_mmap_buffer.len() {
981            self.next_slab().map_err(|e| {
982                CuError::new_with_cause("Failed to read next slab, is the log complete?", e)
983            })?;
984        }
985
986        let read_result = self.read_section_header();
987
988        match read_result {
989            Err(error) => Err(CuError::new_with_cause(
990                &format!(
991                    "Could not read a sections header: {}/{}:{}",
992                    self.base_file_path.as_os_str().to_string_lossy(),
993                    self.current_slab_index,
994                    self.current_reading_position,
995                ),
996                error,
997            )),
998            Ok(header) => {
999                let data = self.read_section_content(&header)?;
1000                self.current_reading_position += header.offset_to_next_section as usize;
1001                Ok((header, data))
1002            }
1003        }
1004    }
1005}
1006
1007impl MmapUnifiedLoggerRead {
1008    /// Advance past the next section without copying its payload.
1009    pub fn raw_skip_section(&mut self) -> CuResult<SectionHeader> {
1010        if self.current_reading_position >= self.current_mmap_buffer.len() {
1011            self.next_slab().map_err(|e| {
1012                CuError::new_with_cause("Failed to read next slab, is the log complete?", e)
1013            })?;
1014        }
1015
1016        let header = self.read_section_header().map_err(|error| {
1017            CuError::new_with_cause(
1018                &format!(
1019                    "Could not read a sections header: {}/{}:{}",
1020                    self.base_file_path.as_os_str().to_string_lossy(),
1021                    self.current_slab_index,
1022                    self.current_reading_position,
1023                ),
1024                error,
1025            )
1026        })?;
1027        self.current_reading_position += header.offset_to_next_section as usize;
1028        Ok(header)
1029    }
1030
1031    pub fn new(base_file_path: &Path) -> io::Result<Self> {
1032        let (file, mmap, prolog, header) = open_slab_index(base_file_path, 0)?;
1033        let main_header = header.ok_or_else(|| {
1034            io::Error::new(io::ErrorKind::InvalidData, "Missing main header in slab 0")
1035        })?;
1036
1037        Ok(Self {
1038            base_file_path: base_file_path.to_path_buf(),
1039            main_header,
1040            current_file: file,
1041            current_mmap_buffer: mmap,
1042            current_slab_index: 0,
1043            current_reading_position: prolog as usize,
1044        })
1045    }
1046
1047    /// Current cursor position (start of next section header).
1048    pub fn position(&self) -> LogPosition {
1049        LogPosition {
1050            slab_index: self.current_slab_index,
1051            offset: self.current_reading_position,
1052        }
1053    }
1054
1055    /// Seek to an absolute position (start of a section header).
1056    pub fn seek(&mut self, pos: LogPosition) -> CuResult<()> {
1057        if pos.slab_index != self.current_slab_index {
1058            let (file, mmap, _prolog, _header) =
1059                open_slab_index(&self.base_file_path, pos.slab_index).map_err(|e| {
1060                    CuError::new_with_cause(
1061                        &format!("Failed to open slab {} for seek", pos.slab_index),
1062                        e,
1063                    )
1064                })?;
1065            self.current_file = file;
1066            self.current_mmap_buffer = mmap;
1067            self.current_slab_index = pos.slab_index;
1068        }
1069        self.current_reading_position = pos.offset;
1070        Ok(())
1071    }
1072
1073    fn next_slab(&mut self) -> io::Result<()> {
1074        self.current_slab_index += 1;
1075        let (file, mmap, prolog, _) =
1076            open_slab_index(&self.base_file_path, self.current_slab_index)?;
1077        self.current_file = file;
1078        self.current_mmap_buffer = mmap;
1079        self.current_reading_position = prolog as usize;
1080        Ok(())
1081    }
1082
1083    pub fn raw_main_header(&self) -> &MainHeader {
1084        &self.main_header
1085    }
1086
1087    /// Scan forward to the permanent end-of-log marker and return its position
1088    /// (the offset at which a writer may resume appending).
1089    pub fn end_of_log(&mut self) -> CuResult<LogPosition> {
1090        loop {
1091            if self.current_reading_position >= self.current_mmap_buffer.len() {
1092                self.next_slab().map_err(|e| {
1093                    CuError::new_with_cause(
1094                        "Failed to advance to the next slab while locating the end of the log",
1095                        e,
1096                    )
1097                })?;
1098            }
1099
1100            let header = self.read_section_header()?;
1101            if header.entry_type == UnifiedLogType::LastEntry {
1102                return Ok(self.position());
1103            }
1104            self.current_reading_position += header.offset_to_next_section as usize;
1105        }
1106    }
1107
1108    pub fn scan_section_bytes(&mut self, datalogtype: UnifiedLogType) -> CuResult<u64> {
1109        let mut total = 0u64;
1110
1111        loop {
1112            if self.current_reading_position >= self.current_mmap_buffer.len() {
1113                self.next_slab().map_err(|e| {
1114                    CuError::new_with_cause("Failed to read next slab, is the log complete?", e)
1115                })?;
1116            }
1117
1118            let header = self.read_section_header()?;
1119
1120            if header.entry_type == UnifiedLogType::LastEntry {
1121                return Ok(total);
1122            }
1123
1124            if header.entry_type == datalogtype {
1125                total = total.saturating_add(header.used as u64);
1126            }
1127
1128            self.current_reading_position += header.offset_to_next_section as usize;
1129        }
1130    }
1131
1132    /// Reads the section content from the section header pos.
1133    fn read_section_content(&mut self, header: &SectionHeader) -> CuResult<Vec<u8>> {
1134        // TODO: we could optimize by asking the buffer to fill
1135        let mut section_data = vec![0; header.used as usize];
1136        let start_of_data = self.current_reading_position + header.block_size as usize;
1137        section_data.copy_from_slice(
1138            &self.current_mmap_buffer[start_of_data..start_of_data + header.used as usize],
1139        );
1140
1141        Ok(section_data)
1142    }
1143
1144    fn read_section_header(&mut self) -> CuResult<SectionHeader> {
1145        let section_header: SectionHeader;
1146        (section_header, _) = decode_from_slice(
1147            &self.current_mmap_buffer[self.current_reading_position..],
1148            standard(),
1149        )
1150        .map_err(|e| {
1151            CuError::new_with_cause(
1152                &format!(
1153                    "Could not read a sections header: {}/{}:{}",
1154                    self.base_file_path.as_os_str().to_string_lossy(),
1155                    self.current_slab_index,
1156                    self.current_reading_position,
1157                ),
1158                e,
1159            )
1160        })?;
1161        if section_header.magic != SECTION_MAGIC {
1162            return Err("Invalid magic number in section header".into());
1163        }
1164
1165        Ok(section_header)
1166    }
1167}
1168
1169/// This a convenience wrapper around the UnifiedLoggerRead to implement the Read trait.
1170pub struct UnifiedLoggerIOReader {
1171    logger: MmapUnifiedLoggerRead,
1172    log_type: UnifiedLogType,
1173    buffer: Vec<u8>,
1174    buffer_pos: usize,
1175}
1176
1177impl UnifiedLoggerIOReader {
1178    pub fn new(logger: MmapUnifiedLoggerRead, log_type: UnifiedLogType) -> Self {
1179        Self {
1180            logger,
1181            log_type,
1182            buffer: Vec::new(),
1183            buffer_pos: 0,
1184        }
1185    }
1186
1187    /// returns true if there is more data to read.
1188    fn fill_buffer(&mut self) -> io::Result<bool> {
1189        match self.logger.read_next_section_type(self.log_type) {
1190            Ok(Some(section)) => {
1191                self.buffer = section;
1192                self.buffer_pos = 0;
1193                Ok(true)
1194            }
1195            Ok(None) => Ok(false), // No more sections of this type
1196            Err(e) => Err(io::Error::other(e.to_string())),
1197        }
1198    }
1199}
1200
1201impl Read for UnifiedLoggerIOReader {
1202    fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
1203        if self.buffer_pos >= self.buffer.len() && !self.fill_buffer()? {
1204            // This means we hit the last section.
1205            return Ok(0);
1206        }
1207
1208        // If we still have no data after trying to fill the buffer, we're at EOF
1209        if self.buffer_pos >= self.buffer.len() {
1210            return Ok(0);
1211        }
1212
1213        // Copy as much as we can from the buffer to `buf`
1214        let len = std::cmp::min(buf.len(), self.buffer.len() - self.buffer_pos);
1215        buf[..len].copy_from_slice(&self.buffer[self.buffer_pos..self.buffer_pos + len]);
1216        self.buffer_pos += len;
1217        Ok(len)
1218    }
1219}
1220
1221#[cfg(feature = "std")]
1222#[cfg(test)]
1223mod tests {
1224    use super::*;
1225    use crate::stream_write;
1226    use bincode::de::read::SliceReader;
1227    use bincode::{Decode, Encode, decode_from_reader, decode_from_slice};
1228    use cu29_traits::WriteStream;
1229    use std::io::{Seek, SeekFrom, Write};
1230    use std::path::PathBuf;
1231    use std::sync::{Arc, Mutex};
1232    use tempfile::TempDir;
1233
1234    const LARGE_SLAB: usize = 100 * 1024; // 100KB
1235    const SMALL_SLAB: usize = 16 * 2 * 1024; // 16KB is the page size on MacOS for example
1236
1237    fn make_a_logger(
1238        tmp_dir: &TempDir,
1239        slab_size: usize,
1240    ) -> (Arc<Mutex<MmapUnifiedLoggerWrite>>, PathBuf) {
1241        let file_path = tmp_dir.path().join("test.bin");
1242        let MmapUnifiedLogger::Write(data_logger) = MmapUnifiedLoggerBuilder::new()
1243            .write(true)
1244            .create(true)
1245            .file_base_name(&file_path)
1246            .preallocated_size(slab_size)
1247            .build()
1248            .expect("Failed to create logger")
1249        else {
1250            panic!("Failed to create logger")
1251        };
1252
1253        (Arc::new(Mutex::new(data_logger)), file_path)
1254    }
1255
1256    #[test]
1257    fn test_truncation_and_sections_creations() {
1258        let tmp_dir = TempDir::new().expect("could not create a tmp dir");
1259        let file_path = tmp_dir.path().join("test.bin");
1260        let _used = {
1261            let MmapUnifiedLogger::Write(mut logger) = MmapUnifiedLoggerBuilder::new()
1262                .write(true)
1263                .create(true)
1264                .file_base_name(&file_path)
1265                .preallocated_size(100000)
1266                .build()
1267                .expect("Failed to create logger")
1268            else {
1269                panic!("Failed to create logger")
1270            };
1271            logger
1272                .add_section(UnifiedLogType::StructuredLogLine, 1024)
1273                .unwrap();
1274            logger
1275                .add_section(UnifiedLogType::CopperList, 2048)
1276                .unwrap();
1277            let used = logger.front_slab.used();
1278            assert!(used < 4 * page_size::get()); // ie. 3 headers, 1 page max per
1279            // logger drops
1280
1281            used
1282        };
1283
1284        let _file = OpenOptions::new()
1285            .read(true)
1286            .open(tmp_dir.path().join("test_0.bin"))
1287            .expect("Could not reopen the file");
1288        // Check if we have correctly truncated the file
1289        // TODO: recompute this math
1290        //assert_eq!(
1291        //    file.metadata().unwrap().len(),
1292        //    (used + size_of::<SectionHeader>()) as u64
1293        //);
1294    }
1295
1296    #[test]
1297    fn test_unsupported_main_header_format_version_is_rejected() {
1298        let tmp_dir = TempDir::new().expect("could not create a tmp dir");
1299        let file_path = tmp_dir.path().join("test.bin");
1300        {
1301            let MmapUnifiedLogger::Write(_logger) = MmapUnifiedLoggerBuilder::new()
1302                .write(true)
1303                .create(true)
1304                .file_base_name(&file_path)
1305                .preallocated_size(100000)
1306                .build()
1307                .expect("Failed to create logger")
1308            else {
1309                panic!("Failed to create logger")
1310            };
1311        }
1312
1313        let mut file = OpenOptions::new()
1314            .read(true)
1315            .write(true)
1316            .open(tmp_dir.path().join("test_0.bin"))
1317            .expect("Could not reopen the slab");
1318        let unsupported_version = UNIFIED_LOG_FORMAT_VERSION + 1;
1319        file.seek(SeekFrom::Start(MAIN_MAGIC.len() as u64))
1320            .expect("Could not seek to format version");
1321        file.write_all(&[unsupported_version])
1322            .expect("Could not write unsupported format version");
1323        drop(file);
1324
1325        let err = match MmapUnifiedLoggerBuilder::new()
1326            .file_base_name(&file_path)
1327            .build()
1328        {
1329            Ok(_) => panic!("Reader accepted unsupported unified log format version"),
1330            Err(err) => err,
1331        };
1332
1333        assert_eq!(err.kind(), io::ErrorKind::InvalidData);
1334        assert_eq!(
1335            err.to_string(),
1336            format!(
1337                "Unsupported unified log format version {unsupported_version} in main header; this reader supports version {UNIFIED_LOG_FORMAT_VERSION}"
1338            )
1339        );
1340    }
1341
1342    #[test]
1343    fn test_base_alias_exists_and_matches_first_slab() {
1344        let tmp_dir = TempDir::new().expect("could not create a tmp dir");
1345        let file_path = tmp_dir.path().join("test.bin");
1346        let _logger = MmapUnifiedLoggerBuilder::new()
1347            .write(true)
1348            .create(true)
1349            .file_base_name(&file_path)
1350            .preallocated_size(LARGE_SLAB)
1351            .build()
1352            .expect("Failed to create logger");
1353
1354        let first_slab = build_slab_path(&file_path, 0).expect("Failed to build first slab path");
1355        assert!(file_path.exists(), "base alias does not exist");
1356        assert!(first_slab.exists(), "first slab does not exist");
1357
1358        let alias_bytes = std::fs::read(&file_path).expect("Failed to read base alias");
1359        let slab_bytes = std::fs::read(&first_slab).expect("Failed to read first slab");
1360        assert_eq!(alias_bytes, slab_bytes);
1361    }
1362
1363    #[test]
1364    fn test_one_section_self_cleaning() {
1365        let tmp_dir = TempDir::new().expect("could not create a tmp dir");
1366        let (logger, _) = make_a_logger(&tmp_dir, LARGE_SLAB);
1367        {
1368            let _stream = stream_write::<(), MmapSectionStorage>(
1369                logger.clone(),
1370                UnifiedLogType::StructuredLogLine,
1371                1024,
1372            );
1373            assert_eq!(
1374                logger
1375                    .lock()
1376                    .unwrap()
1377                    .front_slab
1378                    .sections_offsets_in_flight
1379                    .len(),
1380                1
1381            );
1382        }
1383        assert_eq!(
1384            logger
1385                .lock()
1386                .unwrap()
1387                .front_slab
1388                .sections_offsets_in_flight
1389                .len(),
1390            0
1391        );
1392        let logger = logger.lock().unwrap();
1393        assert_eq!(
1394            logger.front_slab.flushed_until_offset,
1395            logger.front_slab.current_global_position
1396        );
1397    }
1398
1399    #[test]
1400    fn test_temporary_end_marker_is_created() {
1401        let tmp_dir = TempDir::new().expect("could not create a tmp dir");
1402        let (logger, _) = make_a_logger(&tmp_dir, LARGE_SLAB);
1403        {
1404            let mut stream = stream_write::<u32, MmapSectionStorage>(
1405                logger.clone(),
1406                UnifiedLogType::StructuredLogLine,
1407                1024,
1408            )
1409            .unwrap();
1410            stream.log(&42u32).unwrap();
1411        }
1412
1413        let logger_guard = logger.lock().unwrap();
1414        let slab = &logger_guard.front_slab;
1415        let marker_start = slab
1416            .temporary_end_marker
1417            .expect("temporary end-of-log marker missing");
1418        let (eof_header, _) =
1419            decode_from_slice::<SectionHeader, _>(&slab.mmap_buffer[marker_start..], standard())
1420                .expect("Could not decode end-of-log marker header");
1421        assert_eq!(eof_header.entry_type, UnifiedLogType::LastEntry);
1422        assert!(eof_header.is_open);
1423        assert_eq!(eof_header.used, 0);
1424    }
1425
1426    #[test]
1427    fn test_final_end_marker_is_not_temporary() {
1428        let tmp_dir = TempDir::new().expect("could not create a tmp dir");
1429        let (logger, f) = make_a_logger(&tmp_dir, LARGE_SLAB);
1430        {
1431            let mut stream = stream_write::<u32, MmapSectionStorage>(
1432                logger.clone(),
1433                UnifiedLogType::CopperList,
1434                1024,
1435            )
1436            .unwrap();
1437            stream.log(&1u32).unwrap();
1438        }
1439        drop(logger);
1440
1441        let MmapUnifiedLogger::Read(mut reader) = MmapUnifiedLoggerBuilder::new()
1442            .file_base_name(&f)
1443            .build()
1444            .expect("Failed to build reader")
1445        else {
1446            panic!("Failed to create reader");
1447        };
1448
1449        loop {
1450            let (header, _data) = reader
1451                .raw_read_section()
1452                .expect("Failed to read section while searching for EOF");
1453            if header.entry_type == UnifiedLogType::LastEntry {
1454                assert!(!header.is_open);
1455                break;
1456            }
1457        }
1458    }
1459
1460    #[test]
1461    fn test_two_sections_self_cleaning_in_order() {
1462        let tmp_dir = TempDir::new().expect("could not create a tmp dir");
1463        let (logger, _) = make_a_logger(&tmp_dir, LARGE_SLAB);
1464        let s1 = stream_write::<(), MmapSectionStorage>(
1465            logger.clone(),
1466            UnifiedLogType::StructuredLogLine,
1467            1024,
1468        );
1469        assert_eq!(
1470            logger
1471                .lock()
1472                .unwrap()
1473                .front_slab
1474                .sections_offsets_in_flight
1475                .len(),
1476            1
1477        );
1478        let s2 = stream_write::<(), MmapSectionStorage>(
1479            logger.clone(),
1480            UnifiedLogType::StructuredLogLine,
1481            1024,
1482        );
1483        assert_eq!(
1484            logger
1485                .lock()
1486                .unwrap()
1487                .front_slab
1488                .sections_offsets_in_flight
1489                .len(),
1490            2
1491        );
1492        drop(s2);
1493        assert_eq!(
1494            logger
1495                .lock()
1496                .unwrap()
1497                .front_slab
1498                .sections_offsets_in_flight
1499                .len(),
1500            1
1501        );
1502        drop(s1);
1503        let lg = logger.lock().unwrap();
1504        assert_eq!(lg.front_slab.sections_offsets_in_flight.len(), 0);
1505        assert_eq!(
1506            lg.front_slab.flushed_until_offset,
1507            lg.front_slab.current_global_position
1508        );
1509    }
1510
1511    #[test]
1512    fn test_two_sections_self_cleaning_out_of_order() {
1513        let tmp_dir = TempDir::new().expect("could not create a tmp dir");
1514        let (logger, _) = make_a_logger(&tmp_dir, LARGE_SLAB);
1515        let s1 = stream_write::<(), MmapSectionStorage>(
1516            logger.clone(),
1517            UnifiedLogType::StructuredLogLine,
1518            1024,
1519        );
1520        assert_eq!(
1521            logger
1522                .lock()
1523                .unwrap()
1524                .front_slab
1525                .sections_offsets_in_flight
1526                .len(),
1527            1
1528        );
1529        let s2 = stream_write::<(), MmapSectionStorage>(
1530            logger.clone(),
1531            UnifiedLogType::StructuredLogLine,
1532            1024,
1533        );
1534        assert_eq!(
1535            logger
1536                .lock()
1537                .unwrap()
1538                .front_slab
1539                .sections_offsets_in_flight
1540                .len(),
1541            2
1542        );
1543        drop(s1);
1544        assert_eq!(
1545            logger
1546                .lock()
1547                .unwrap()
1548                .front_slab
1549                .sections_offsets_in_flight
1550                .len(),
1551            1
1552        );
1553        drop(s2);
1554        let lg = logger.lock().unwrap();
1555        assert_eq!(lg.front_slab.sections_offsets_in_flight.len(), 0);
1556        assert_eq!(
1557            lg.front_slab.flushed_until_offset,
1558            lg.front_slab.current_global_position
1559        );
1560    }
1561
1562    #[test]
1563    fn test_closed_section_flushes_behind_open_earlier_section() {
1564        let tmp_dir = TempDir::new().expect("could not create a tmp dir");
1565        let (logger, _) = make_a_logger(&tmp_dir, LARGE_SLAB);
1566        let s1 = stream_write::<(), MmapSectionStorage>(
1567            logger.clone(),
1568            UnifiedLogType::StructuredLogLine,
1569            1024,
1570        )
1571        .unwrap();
1572        {
1573            let mut s2 = stream_write::<u32, MmapSectionStorage>(
1574                logger.clone(),
1575                UnifiedLogType::CopperList,
1576                1024,
1577            )
1578            .unwrap();
1579            s2.log(&42u32).unwrap();
1580        }
1581
1582        let logger_guard = logger.lock().unwrap();
1583        assert_eq!(logger_guard.front_slab.sections_offsets_in_flight.len(), 1);
1584        assert!(
1585            logger_guard.front_slab.flushed_until_offset
1586                < logger_guard.front_slab.current_global_position
1587        );
1588        assert_eq!(logger_guard.front_slab.pending_closed_bytes(), 0);
1589        drop(logger_guard);
1590        drop(s1);
1591    }
1592
1593    #[test]
1594    fn test_append_preserves_existing_and_adds_new_sections() {
1595        let tmp_dir = TempDir::new().expect("could not create a tmp dir");
1596        let file_path = tmp_dir.path().join("test.bin");
1597
1598        // First run: one section with three entries, then a clean close.
1599        {
1600            let MmapUnifiedLogger::Write(logger) = MmapUnifiedLoggerBuilder::new()
1601                .write(true)
1602                .create(true)
1603                .file_base_name(&file_path)
1604                .preallocated_size(LARGE_SLAB)
1605                .build()
1606                .expect("Failed to create logger")
1607            else {
1608                panic!("Failed to create logger")
1609            };
1610            let logger = Arc::new(Mutex::new(logger));
1611            {
1612                let mut stream = stream_write::<u32, MmapSectionStorage>(
1613                    logger.clone(),
1614                    UnifiedLogType::StructuredLogLine,
1615                    1024,
1616                )
1617                .unwrap();
1618                stream.log(&1u32).unwrap();
1619                stream.log(&2u32).unwrap();
1620                stream.log(&3u32).unwrap();
1621            }
1622        } // logger drops -> clean close
1623
1624        // Second run: append one more section with two entries.
1625        {
1626            let MmapUnifiedLogger::Write(logger) = MmapUnifiedLoggerBuilder::new()
1627                .write(true)
1628                .create(true)
1629                .append(true)
1630                .file_base_name(&file_path)
1631                .preallocated_size(LARGE_SLAB)
1632                .build()
1633                .expect("Failed to append to logger")
1634            else {
1635                panic!("Failed to append to logger")
1636            };
1637            let logger = Arc::new(Mutex::new(logger));
1638            {
1639                let mut stream = stream_write::<u32, MmapSectionStorage>(
1640                    logger.clone(),
1641                    UnifiedLogType::StructuredLogLine,
1642                    1024,
1643                )
1644                .unwrap();
1645                stream.log(&4u32).unwrap();
1646                stream.log(&5u32).unwrap();
1647            }
1648        }
1649
1650        // Read back both sections: the original three entries then the two appended.
1651        let MmapUnifiedLogger::Read(mut dl) = MmapUnifiedLoggerBuilder::new()
1652            .file_base_name(&file_path)
1653            .build()
1654            .expect("Failed to build logger")
1655        else {
1656            panic!("Failed to build logger")
1657        };
1658
1659        let first = dl
1660            .read_next_section_type(UnifiedLogType::StructuredLogLine)
1661            .expect("Failed to read first section")
1662            .expect("Missing first section");
1663        let mut reader = SliceReader::new(&first[..]);
1664        assert_eq!(
1665            decode_from_reader::<u32, _, _>(&mut reader, standard()).unwrap(),
1666            1
1667        );
1668        assert_eq!(
1669            decode_from_reader::<u32, _, _>(&mut reader, standard()).unwrap(),
1670            2
1671        );
1672        assert_eq!(
1673            decode_from_reader::<u32, _, _>(&mut reader, standard()).unwrap(),
1674            3
1675        );
1676
1677        let second = dl
1678            .read_next_section_type(UnifiedLogType::StructuredLogLine)
1679            .expect("Failed to read second section")
1680            .expect("Missing second section");
1681        let mut reader = SliceReader::new(&second[..]);
1682        assert_eq!(
1683            decode_from_reader::<u32, _, _>(&mut reader, standard()).unwrap(),
1684            4
1685        );
1686        assert_eq!(
1687            decode_from_reader::<u32, _, _>(&mut reader, standard()).unwrap(),
1688            5
1689        );
1690
1691        assert!(
1692            dl.read_next_section_type(UnifiedLogType::StructuredLogLine)
1693                .expect("Failed to read past sections")
1694                .is_none()
1695        );
1696    }
1697
1698    #[test]
1699    fn test_write_then_read_one_section() {
1700        let tmp_dir = TempDir::new().expect("could not create a tmp dir");
1701        let (logger, f) = make_a_logger(&tmp_dir, LARGE_SLAB);
1702        {
1703            let mut stream =
1704                stream_write(logger.clone(), UnifiedLogType::StructuredLogLine, 1024).unwrap();
1705            stream.log(&1u32).unwrap();
1706            stream.log(&2u32).unwrap();
1707            stream.log(&3u32).unwrap();
1708        }
1709        drop(logger);
1710        let MmapUnifiedLogger::Read(mut dl) = MmapUnifiedLoggerBuilder::new()
1711            .file_base_name(&f)
1712            .build()
1713            .expect("Failed to build logger")
1714        else {
1715            panic!("Failed to build logger");
1716        };
1717        let section = dl
1718            .read_next_section_type(UnifiedLogType::StructuredLogLine)
1719            .expect("Failed to read section");
1720        assert!(section.is_some());
1721        let section = section.unwrap();
1722        let mut reader = SliceReader::new(&section[..]);
1723        let v1: u32 = decode_from_reader(&mut reader, standard()).unwrap();
1724        let v2: u32 = decode_from_reader(&mut reader, standard()).unwrap();
1725        let v3: u32 = decode_from_reader(&mut reader, standard()).unwrap();
1726        assert_eq!(v1, 1);
1727        assert_eq!(v2, 2);
1728        assert_eq!(v3, 3);
1729    }
1730
1731    #[cfg(feature = "mmap-fsync")]
1732    #[test]
1733    fn test_fsync_feature_syncs_on_section_flush() {
1734        let tmp_dir = TempDir::new().expect("could not create a tmp dir");
1735        let (logger, _) = make_a_logger(&tmp_dir, LARGE_SLAB);
1736        {
1737            let mut stream =
1738                stream_write(logger.clone(), UnifiedLogType::StructuredLogLine, 1024).unwrap();
1739            stream.log(&1u32).unwrap();
1740        }
1741
1742        let logger = logger.lock().unwrap();
1743        assert!(
1744            logger.front_slab.sync_call_count > 0,
1745            "expected mmap-fsync to issue at least one sync_all call"
1746        );
1747    }
1748
1749    /// Mimic a basic CopperList implementation.
1750
1751    #[derive(Debug, Encode, Decode)]
1752    enum CopperListStateMock {
1753        Free,
1754        ProcessingTasks,
1755        BeingSerialized,
1756    }
1757
1758    #[derive(Encode, Decode)]
1759    struct CopperList<P: bincode::enc::Encode> {
1760        state: CopperListStateMock,
1761        payload: P, // This is generated from the runtime.
1762    }
1763
1764    #[test]
1765    fn test_copperlist_list_like_logging() {
1766        let tmp_dir = TempDir::new().expect("could not create a tmp dir");
1767        let (logger, f) = make_a_logger(&tmp_dir, LARGE_SLAB);
1768        {
1769            let mut stream =
1770                stream_write(logger.clone(), UnifiedLogType::CopperList, 1024).unwrap();
1771            let cl0 = CopperList {
1772                state: CopperListStateMock::Free,
1773                payload: (1u32, 2u32, 3u32),
1774            };
1775            let cl1 = CopperList {
1776                state: CopperListStateMock::ProcessingTasks,
1777                payload: (4u32, 5u32, 6u32),
1778            };
1779            stream.log(&cl0).unwrap();
1780            stream.log(&cl1).unwrap();
1781        }
1782        drop(logger);
1783
1784        let MmapUnifiedLogger::Read(mut dl) = MmapUnifiedLoggerBuilder::new()
1785            .file_base_name(&f)
1786            .build()
1787            .expect("Failed to build logger")
1788        else {
1789            panic!("Failed to build logger");
1790        };
1791        let section = dl
1792            .read_next_section_type(UnifiedLogType::CopperList)
1793            .expect("Failed to read section");
1794        assert!(section.is_some());
1795        let section = section.unwrap();
1796
1797        let mut reader = SliceReader::new(&section[..]);
1798        let cl0: CopperList<(u32, u32, u32)> = decode_from_reader(&mut reader, standard()).unwrap();
1799        let cl1: CopperList<(u32, u32, u32)> = decode_from_reader(&mut reader, standard()).unwrap();
1800        assert_eq!(cl0.payload.1, 2);
1801        assert_eq!(cl1.payload.2, 6);
1802    }
1803
1804    #[test]
1805    fn test_multi_slab_end2end() {
1806        let tmp_dir = TempDir::new().expect("could not create a tmp dir");
1807        let (logger, f) = make_a_logger(&tmp_dir, SMALL_SLAB);
1808        {
1809            let mut stream =
1810                stream_write(logger.clone(), UnifiedLogType::CopperList, 1024).unwrap();
1811            let cl0 = CopperList {
1812                state: CopperListStateMock::Free,
1813                payload: (1u32, 2u32, 3u32),
1814            };
1815            // large enough so we are sure to create a few slabs
1816            for _ in 0..10000 {
1817                stream.log(&cl0).unwrap();
1818            }
1819        }
1820        drop(logger);
1821
1822        let MmapUnifiedLogger::Read(mut dl) = MmapUnifiedLoggerBuilder::new()
1823            .file_base_name(&f)
1824            .build()
1825            .expect("Failed to build logger")
1826        else {
1827            panic!("Failed to build logger");
1828        };
1829        let mut total_readback = 0;
1830        loop {
1831            let section = dl.read_next_section_type(UnifiedLogType::CopperList);
1832            if section.is_err() {
1833                break;
1834            }
1835            let section = section.unwrap();
1836            if section.is_none() {
1837                break;
1838            }
1839            let section = section.unwrap();
1840
1841            let mut reader = SliceReader::new(&section[..]);
1842            loop {
1843                let maybe_cl: Result<CopperList<(u32, u32, u32)>, _> =
1844                    decode_from_reader(&mut reader, standard());
1845                if maybe_cl.is_ok() {
1846                    total_readback += 1;
1847                } else {
1848                    break;
1849                }
1850            }
1851        }
1852        assert_eq!(total_readback, 10000);
1853    }
1854}