Skip to main content

moirai_async/fs/
file.rs

1//! Async file handle over the file-system blocking pool.
2//!
3//! Every syscall runs on the file-system blocking pool, never inside `poll`.
4//! A handle has at most one stream operation in flight, and every operation
5//! first settles the one before it, so stream operations take effect in call
6//! order even when a caller drops a future mid-flight.
7//!
8//! Cancellation keeps stream semantics:
9//! - A read whose future is dropped still consumes its bytes. They are kept
10//!   and delivered, in order, to the next read.
11//! - A write, seek, or sync submitted before its future is dropped still takes
12//!   effect. Writes run even if the handle itself is dropped.
13//! - An error from an operation whose caller has gone is returned by the next
14//!   operation on the handle, which then does not run.
15//!
16//! `AsyncWrite::poll_write` queues the write and reports it complete. Its
17//! failure, if any, is returned by the next operation, as for a dropped write.
18//! [`File::write`] and [`File::write_all`] wait for the write itself. A handle
19//! dropped after queued writes, without a flush, still writes the bytes, but
20//! the error of a failed write is lost with the handle. Flush to observe it.
21//!
22//! [`File::metadata`] and positioned reads (`read_at`) take `&self` and run as
23//! separate jobs. Each first waits for every stream operation already queued
24//! on the handle, including a queued write, so it observes those writes.
25
26use std::future::{Future, poll_fn};
27use std::io::{self, SeekFrom};
28use std::mem;
29use std::path::{Path, PathBuf};
30use std::pin::Pin;
31use std::sync::Arc;
32use std::task::{Context, Poll, ready};
33
34use moirai_pal::fs::{File as Handle, FileOpenOptions};
35
36use crate::blocking::{Abandoned, Admission, Completion};
37use crate::fs::pool;
38use crate::fs::stats::FileStats;
39
40mod fence;
41pub(in crate::fs) mod request;
42mod traits;
43
44use fence::Fence;
45use request::{Outcome, Request};
46
47/// A pending admission, stored so repeated polls continue the same wait.
48type Admitting = Pin<Box<dyn Future<Output = io::Result<Admission>> + Send + Sync>>;
49
50enum State {
51    Idle,
52    /// Waiting for a pool slot for the next operation.
53    Admitting(Admitting),
54    /// A stream operation is queued or running on the pool.
55    Busy(Completion<Outcome>),
56}
57
58/// What settling a finished operation left behind.
59enum Settled {
60    /// Nothing was in flight, or its outcome was absorbed.
61    Absorbed,
62    /// A read reached the end of the file.
63    EndOfFile,
64}
65
66/// Async file handle supporting stateful streams and positioned reads.
67pub struct File {
68    handle: Arc<Handle>,
69    state: State,
70    /// Orders `&self` observers after every stream operation queued before
71    /// them.
72    fence: Arc<Fence>,
73    /// Bytes read from the file that no caller has taken yet. The OS cursor is
74    /// past them, so cursor-relative operations rewind over them first.
75    unread: Vec<u8>,
76    /// The error of an operation that finished after its caller left.
77    deferred: Option<io::Error>,
78    path: PathBuf,
79    stats: FileStats,
80}
81
82impl File {
83    /// Open a file with default options (read-only)
84    pub async fn open<P: AsRef<Path>>(path: P) -> io::Result<Self> {
85        Self::open_with_options(path, FileOpenOptions::read_only()).await
86    }
87
88    /// Create a new file for writing (truncates if exists)
89    pub async fn create<P: AsRef<Path>>(path: P) -> io::Result<Self> {
90        Self::open_with_options(path, FileOpenOptions::write_only()).await
91    }
92
93    /// Open a file with custom options
94    pub async fn open_with_options<P: AsRef<Path>>(
95        path: P,
96        options: FileOpenOptions,
97    ) -> io::Result<Self> {
98        let path = path.as_ref().to_path_buf();
99        let target = path.clone();
100        let handle = pool()
101            .run(Abandoned::Skip, move || Handle::open_with(target, options))
102            .await??;
103        Ok(Self {
104            handle: Arc::new(handle),
105            state: State::Idle,
106            fence: Arc::default(),
107            unread: Vec::new(),
108            deferred: None,
109            path,
110            stats: FileStats::default(),
111        })
112    }
113
114    /// Read entire file contents into a string
115    pub async fn read_to_string(&mut self) -> io::Result<String> {
116        let contents = String::from_utf8(self.read_all().await?)
117            .map_err(|error| io::Error::new(io::ErrorKind::InvalidData, error))?;
118        Ok(contents)
119    }
120
121    /// Read entire file contents into a byte vector
122    pub async fn read_to_end(&mut self) -> io::Result<Vec<u8>> {
123        self.read_all().await
124    }
125
126    async fn read_all(&mut self) -> io::Result<Vec<u8>> {
127        let Outcome::Read(contents) = self
128            .perform(Request::ReadToEnd { prefix: Vec::new() })
129            .await?
130        else {
131            unreachable!("invariant: a read-to-end request yields a read outcome");
132        };
133        let contents = contents?;
134        self.stats.bytes_read += contents.len() as u64;
135        self.stats.read_operations += 1;
136        Ok(contents)
137    }
138
139    /// Read data into a buffer
140    pub async fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
141        let bytes_read = poll_fn(|cx| self.poll_read_into(cx, buf)).await?;
142        self.stats.bytes_read += bytes_read as u64;
143        self.stats.read_operations += 1;
144        Ok(bytes_read)
145    }
146
147    /// Write data from a buffer
148    pub async fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
149        let Outcome::Wrote(written) = self
150            .perform(Request::Write {
151                data: buf.to_vec(),
152                rewind: 0,
153            })
154            .await?
155        else {
156            unreachable!("invariant: a write request yields a write outcome");
157        };
158        let bytes_written = written?;
159        self.stats.bytes_written += bytes_written as u64;
160        self.stats.write_operations += 1;
161        Ok(bytes_written)
162    }
163
164    /// Write all data from a buffer.
165    ///
166    /// The whole buffer is one pool job: once submitted, it is written in full
167    /// even if this future is dropped.
168    pub async fn write_all(&mut self, buf: &[u8]) -> io::Result<()> {
169        if buf.is_empty() {
170            return Ok(());
171        }
172        let Outcome::Done(done) = self
173            .perform(Request::WriteAll {
174                data: buf.to_vec(),
175                rewind: 0,
176            })
177            .await?
178        else {
179            unreachable!("invariant: a write-all request yields a done outcome");
180        };
181        done?;
182        self.stats.bytes_written += buf.len() as u64;
183        self.stats.write_operations += 1;
184        Ok(())
185    }
186
187    /// Write a string to the file
188    pub async fn write_str(&mut self, s: &str) -> io::Result<()> {
189        self.write_all(s.as_bytes()).await
190    }
191
192    /// Flush any buffered data to disk
193    pub async fn flush(&mut self) -> io::Result<()> {
194        poll_fn(|cx| self.poll_settled(cx)).await
195    }
196
197    /// Synchronize all data and metadata to disk
198    pub async fn sync_all(&mut self) -> io::Result<()> {
199        self.perform_unit(Request::SyncAll).await
200    }
201
202    /// Synchronize data (but not metadata) to disk
203    pub async fn sync_data(&mut self) -> io::Result<()> {
204        self.perform_unit(Request::SyncData).await
205    }
206
207    async fn perform_unit(&mut self, request: Request) -> io::Result<()> {
208        let Outcome::Done(done) = self.perform(request).await? else {
209            unreachable!("invariant: a sync request yields a done outcome");
210        };
211        done
212    }
213
214    /// Seek to a specific position in the file
215    pub async fn seek(&mut self, pos: SeekFrom) -> io::Result<u64> {
216        let Outcome::Sought(position) = self.perform(Request::Seek(pos)).await? else {
217            unreachable!("invariant: a seek request yields a seek outcome");
218        };
219        let new_pos = position?;
220        self.stats.seek_operations += 1;
221        Ok(new_pos)
222    }
223
224    /// Get current position in the file
225    pub async fn stream_position(&mut self) -> io::Result<u64> {
226        let Outcome::Sought(position) = self.perform(Request::Seek(SeekFrom::Current(0))).await?
227        else {
228            unreachable!("invariant: a seek request yields a seek outcome");
229        };
230        position
231    }
232
233    /// Get file metadata, after every stream operation already queued on this
234    /// handle has finished.
235    pub async fn metadata(&self) -> io::Result<std::fs::Metadata> {
236        self.fence.settled().await;
237        let handle = Arc::clone(&self.handle);
238        pool()
239            .run(Abandoned::Skip, move || handle.metadata())
240            .await?
241    }
242
243    /// Get file path
244    pub fn path(&self) -> &Path {
245        &self.path
246    }
247
248    /// Get statistics for stateful stream operations.
249    ///
250    /// Positioned reads do not advance the stream and are not included.
251    pub fn stats(&self) -> &FileStats {
252        &self.stats
253    }
254
255    /// Settle the previous operation, then run `request` and return its
256    /// outcome. `Err` means the request did not run: a deferred error, an
257    /// admission failure, or a panicked job.
258    async fn perform(&mut self, request: Request) -> io::Result<Outcome> {
259        let mut request = Some(request);
260        poll_fn(|cx| {
261            if request.is_some() {
262                ready!(self.poll_settle(cx));
263                if let Some(error) = self.deferred.take() {
264                    request = None;
265                    return Poll::Ready(Err(error));
266                }
267                let admission = ready!(self.poll_admission(cx))?;
268                let pending = request
269                    .take()
270                    .expect("invariant: the request is submitted at most once, checked above");
271                self.submit(admission, pending)?;
272            }
273            let State::Busy(completion) = &mut self.state else {
274                unreachable!(
275                    "invariant: this call's request is in flight while it borrows the file"
276                );
277            };
278            let outcome = ready!(Pin::new(completion).poll(cx));
279            self.state = State::Idle;
280            Poll::Ready(outcome)
281        })
282        .await
283    }
284
285    /// Queue `request`, first adjusting it for bytes read but not delivered.
286    fn submit(&mut self, admission: Admission, request: Request) -> io::Result<()> {
287        let request = match request {
288            Request::ReadToEnd { .. } => Request::ReadToEnd {
289                prefix: mem::take(&mut self.unread),
290            },
291            Request::Write { data, .. } => Request::Write {
292                data,
293                rewind: self.take_unread_len(),
294            },
295            Request::WriteAll { data, .. } => Request::WriteAll {
296                data,
297                rewind: self.take_unread_len(),
298            },
299            Request::Seek(SeekFrom::Current(offset)) => {
300                let rewind = i64::try_from(self.unread.len()).map_err(|_| {
301                    io::Error::new(
302                        io::ErrorKind::InvalidInput,
303                        "unread bytes exceed a seek offset",
304                    )
305                })?;
306                let offset = offset.checked_sub(rewind).ok_or_else(|| {
307                    io::Error::new(io::ErrorKind::InvalidInput, "seek offset overflows i64")
308                })?;
309                self.unread.clear();
310                Request::Seek(SeekFrom::Current(offset))
311            }
312            Request::Seek(absolute) => {
313                self.unread.clear();
314                Request::Seek(absolute)
315            }
316            other => other,
317        };
318        let handle = Arc::clone(&self.handle);
319        let abandoned = request.abandoned();
320        // Moved into the job, so it completes when the job runs, is skipped,
321        // or unwinds: each of those drops the job.
322        let ticket = self.fence.issue();
323        let completion = admission.submit(abandoned, move || {
324            let outcome = request.run(&handle);
325            #[cfg(test)]
326            request::test_hooks::after_run();
327            drop(ticket);
328            outcome
329        })?;
330        self.state = State::Busy(completion);
331        Ok(())
332    }
333
334    fn take_unread_len(&mut self) -> u64 {
335        let rewind = self.unread.len() as u64;
336        self.unread.clear();
337        rewind
338    }
339
340    /// Wait for any operation in flight and absorb its outcome: read bytes
341    /// join the unread buffer, and an error is deferred to the next caller.
342    fn poll_settle(&mut self, cx: &mut Context<'_>) -> Poll<Settled> {
343        let State::Busy(completion) = &mut self.state else {
344            return Poll::Ready(Settled::Absorbed);
345        };
346        let outcome = ready!(Pin::new(completion).poll(cx));
347        self.state = State::Idle;
348        Poll::Ready(match outcome {
349            Ok(Outcome::Read(Ok(data))) if data.is_empty() => Settled::EndOfFile,
350            Ok(Outcome::Read(Ok(data))) => {
351                self.unread.extend_from_slice(&data);
352                Settled::Absorbed
353            }
354            Ok(outcome) => {
355                if let Some(error) = outcome.into_error() {
356                    self.defer(error);
357                }
358                Settled::Absorbed
359            }
360            Err(error) => {
361                self.defer(error);
362                Settled::Absorbed
363            }
364        })
365    }
366
367    /// Settle the previous operation and report its deferred error, if any.
368    fn poll_settled(&mut self, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
369        ready!(self.poll_settle(cx));
370        Poll::Ready(self.deferred.take().map_or(Ok(()), Err))
371    }
372
373    fn defer(&mut self, error: io::Error) {
374        if self.deferred.is_none() {
375            self.deferred = Some(error);
376        }
377    }
378
379    fn poll_admission(&mut self, cx: &mut Context<'_>) -> Poll<io::Result<Admission>> {
380        if matches!(self.state, State::Idle) {
381            self.state = State::Admitting(Box::pin(pool().admit()));
382        }
383        let State::Admitting(admitting) = &mut self.state else {
384            unreachable!("invariant: callers settle a busy state before admission");
385        };
386        let admitted = ready!(admitting.as_mut().poll(cx));
387        self.state = State::Idle;
388        Poll::Ready(admitted)
389    }
390
391    fn poll_read_into(&mut self, cx: &mut Context<'_>, buf: &mut [u8]) -> Poll<io::Result<usize>> {
392        loop {
393            if !self.unread.is_empty() {
394                let delivered = self.unread.len().min(buf.len());
395                buf[..delivered].copy_from_slice(&self.unread[..delivered]);
396                self.unread.drain(..delivered);
397                return Poll::Ready(Ok(delivered));
398            }
399            if buf.is_empty() {
400                return Poll::Ready(Ok(0));
401            }
402            if matches!(self.state, State::Busy(_)) {
403                if let Settled::EndOfFile = ready!(self.poll_settle(cx)) {
404                    return Poll::Ready(Ok(0));
405                }
406                continue;
407            }
408            if let Some(error) = self.deferred.take() {
409                return Poll::Ready(Err(error));
410            }
411            let admission = ready!(self.poll_admission(cx))?;
412            self.submit(admission, Request::Read { len: buf.len() })?;
413        }
414    }
415
416    fn poll_write_behind(&mut self, cx: &mut Context<'_>, buf: &[u8]) -> Poll<io::Result<usize>> {
417        if buf.is_empty() {
418            return Poll::Ready(Ok(0));
419        }
420        ready!(self.poll_settled(cx))?;
421        let admission = ready!(self.poll_admission(cx))?;
422        self.submit(
423            admission,
424            Request::WriteAll {
425                data: buf.to_vec(),
426                rewind: 0,
427            },
428        )?;
429        Poll::Ready(Ok(buf.len()))
430    }
431}