Skip to main content

fast_pull/pusher/
mmap.rs

1//! Memory-mapped file pusher (feature `file`).
2
3use crate::{ProgressEntry, ProgressListener, Pusher};
4use bytes::Bytes;
5use memmap2::MmapMut;
6
7#[cfg(unix)]
8use std::os::unix::io::AsRawFd;
9#[cfg(windows)]
10use std::os::windows::io::AsRawHandle;
11
12/// File pusher using memory-mapped I/O for zero-copy writes.
13///
14/// Delegates writes to the OS via `MmapMut`. The file size is fixed at
15/// construction time via `file.set_len(size)`. On [`flush`](Pusher::flush),
16/// if `sync_all` is true an `fsync` is performed; otherwise an async flush
17/// is issued.
18pub struct MmapFilePusher {
19    mmap: MmapMut,
20    sync_all: bool,
21    listener: Option<ProgressListener>,
22}
23
24impl std::fmt::Debug for MmapFilePusher {
25    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
26        f.debug_struct("MmapFilePusher")
27            .field("sync_all", &self.sync_all)
28            .finish_non_exhaustive()
29    }
30}
31impl MmapFilePusher {
32    /// # Errors
33    /// 1. Returns an error if `fs::set_len` fails.
34    /// 2. Returns an error if `MmapMut::map_mut` fails.
35    pub async fn new(file: &tokio::fs::File, size: u64, sync_all: bool) -> std::io::Result<Self> {
36        file.set_len(size).await?;
37        // `MmapMut::map_mut` performs a synchronous `mmap()` syscall, but it only
38        // needs the raw fd/handle — it never takes ownership of the `File`. Moving
39        // just the raw descriptor (always `Send`) into the blocking pool satisfies
40        // `spawn_blocking`'s `'static` bound with zero extra `dup`/`CloneFile`.
41        // (`RawHandle` is a `*mut c_void` and `!Send` on Windows, so it is carried
42        // as `usize` and reconstructed inside the blocking closure.)
43        #[cfg(unix)]
44        let raw = file.as_raw_fd();
45        #[cfg(windows)]
46        let raw = file.as_raw_handle() as usize;
47        let mmap = tokio::task::spawn_blocking(move || unsafe {
48            #[cfg(unix)]
49            let desc = raw;
50            #[cfg(windows)]
51            let desc = raw as std::os::windows::io::RawHandle;
52            MmapMut::map_mut(desc)
53        })
54        .await
55        .map_err(std::io::Error::other)??;
56        Ok(Self {
57            mmap,
58            sync_all,
59            listener: None,
60        })
61    }
62}
63impl Pusher for MmapFilePusher {
64    type Error = std::io::Error;
65
66    fn set_listener(&mut self, cb: ProgressListener) {
67        self.listener = Some(cb);
68    }
69
70    fn push(&mut self, range: &ProgressEntry, bytes: Bytes) -> Result<(), (Self::Error, Bytes)> {
71        #[allow(clippy::cast_possible_truncation)]
72        self.mmap[range.start as usize..range.end as usize].copy_from_slice(&bytes);
73        if let Some(l) = &mut self.listener {
74            l(range.clone());
75        }
76        Ok(())
77    }
78    /// Data is already in the shared mapping, visible to any other reader.
79    ///
80    /// Only `sync_all` forces the dirty pages out; otherwise this is a no-op so
81    /// the writeback is left to the OS instead of blocking the download on
82    /// `msync`/`FlushViewOfFile`. The mapping keeps the pages reachable until the
83    /// pusher is dropped, so the data is never lost.
84    fn flush(&mut self) -> Result<(), Self::Error> {
85        if self.sync_all {
86            self.mmap.flush()
87        } else {
88            Ok(())
89        }
90    }
91}
92
93#[cfg(test)]
94mod tests {
95    #![allow(clippy::unwrap_used)]
96    use super::*;
97    use std::sync::{Arc, Mutex};
98    use std::{fs::File, io::Read, vec::Vec};
99    use tempfile::NamedTempFile;
100
101    #[tokio::test]
102    async fn test_rand_file_pusher() {
103        // Create a temp file for testing
104        let temp_file = NamedTempFile::new().unwrap();
105        let file_path = temp_file.path();
106
107        // Initialize MmapFilePusher with a file size of 10 bytes
108        let mut pusher = MmapFilePusher::new(&temp_file.reopen().unwrap().into(), 10, false)
109            .await
110            .unwrap();
111
112        // Write data
113        let data = b"234";
114        let range = 2..5;
115        pusher.push(&range, data[..].into()).unwrap();
116        pusher.flush().unwrap();
117
118        // Verify file content
119        let mut file_content = Vec::new();
120        File::open(file_path)
121            .unwrap()
122            .read_to_end(&mut file_content)
123            .unwrap();
124        assert_eq!(file_content, b"\0\x00234\0\0\0\0\0");
125    }
126
127    #[tokio::test]
128    async fn test_debug_and_sync_all_flush() {
129        // Lines 25-29: `Debug` impl. Line 80: the `sync_all == true` branch of `flush`.
130        let temp_file = NamedTempFile::new().unwrap();
131        let file_path = temp_file.path().to_path_buf();
132        let mut pusher = MmapFilePusher::new(&temp_file.reopen().unwrap().into(), 10, true)
133            .await
134            .unwrap();
135        let _ = format!("{pusher:?}");
136        pusher.push(&(2..5), b"234"[..].into()).unwrap();
137        pusher.flush().unwrap();
138        let mut file_content = Vec::new();
139        File::open(&file_path)
140            .unwrap()
141            .read_to_end(&mut file_content)
142            .unwrap();
143        assert_eq!(file_content, b"\0\x00234\0\0\0\0\0");
144    }
145
146    #[tokio::test]
147    async fn test_listener_invoked_on_push() {
148        // Lines 66-68 (`set_listener`) and 73-75 (listener call inside `push`).
149        let temp_file = NamedTempFile::new().unwrap();
150        let mut pusher = MmapFilePusher::new(&temp_file.reopen().unwrap().into(), 10, false)
151            .await
152            .unwrap();
153        let seen = Arc::new(Mutex::new(None::<ProgressEntry>));
154        let seen2 = seen.clone();
155        pusher.set_listener(Box::new(move |r| {
156            *seen2.lock().unwrap() = Some(r);
157        }));
158        pusher.push(&(2..5), b"234"[..].into()).unwrap();
159        assert_eq!(*seen.lock().unwrap(), Some(2..5));
160    }
161
162    #[tokio::test]
163    async fn test_mmap_pusher_zero_length_file() {
164        // Hypothesis: an empty download (size == 0) must still construct a usable
165        // pusher. `MmapMut::map_mut` on a zero-length file returns EINVAL on Unix,
166        // so this probes whether `new(0)` is a latent break for 0-byte resources.
167        let temp_file = NamedTempFile::new().unwrap();
168        let result = MmapFilePusher::new(&temp_file.reopen().unwrap().into(), 0, false).await;
169        assert!(
170            result.is_ok(),
171            "zero-length download should construct an mmap pusher, got: {result:?}"
172        );
173    }
174}