Skip to main content

coven_foundation/files/
flush_batch.rs

1//! The files one commit records, made durable together before it (Appendix
2//! C.3). Each file is synced as it finishes; its folder and the drive's own
3//! cache are flushed once for the whole batch.
4//!
5//! Per file, a batched write hands the bytes to the drive with a plain
6//! `fsync` on Unix and `FlushFileBuffers` on Windows. On Apple systems that
7//! `fsync` can leave them in the drive's cache, in any order. At the batch's
8//! flush, each folder a file was made in is synced with `fsync`, then Apple
9//! systems flush the drive's cache once per device with `F_FULLFSYNC`. The
10//! cache is the device's, so that one flush covers every file the batch
11//! already handed to it. Elsewhere on Unix `fsync` already reaches the
12//! drive's permanent storage, and the batch's flush is the folders' `fsync`.
13//! Windows writes coven's files through and has no folder to sync: its
14//! `FlushFileBuffers` per file already flushed the drive's cache, and the
15//! batch adds nothing.
16
17use super::FileError;
18#[cfg(unix)]
19use super::StoreReadLock;
20#[cfg(unix)]
21use std::path::PathBuf;
22#[cfg(unix)]
23use std::sync::Arc;
24
25/// Files written for one commit, each synced as it finished, whose folders
26/// and drive cache are flushed together by [`FlushBatch::flush`]. The commit
27/// that records them takes the [`Flushed`] it returns, so nothing commits
28/// ahead of its bytes reaching the drive.
29#[must_use = "a batch's files may still be in the drive's cache until it is flushed"]
30pub struct FlushBatch {
31    files: usize,
32    /// Where a panic in a flush its caller stopped waiting for goes (E15).
33    bugs: crate::task::BugSinkRef,
34    /// Each folder a file was made in, once, holding the store read lock of
35    /// the first file made there until the flush ends.
36    #[cfg(unix)]
37    folders: Vec<BatchFolder>,
38    #[cfg(any(test, feature = "test-utils"))]
39    hook: Option<FlushHook>,
40}
41
42/// A batch's files reached the drive: what a commit recording them takes.
43#[derive(Debug)]
44#[must_use = "a flushed batch is for the commit that records its files"]
45pub struct Flushed(());
46
47/// Run where a batch's flush has returned, in tests; an error from it fails
48/// the flush as the drive's would.
49#[cfg(any(test, feature = "test-utils"))]
50pub type FlushHook = std::sync::Arc<dyn Fn() -> std::io::Result<()> + Send + Sync>;
51
52/// An open folder a batch made a file in, known by its device and inode.
53#[cfg(unix)]
54pub(super) struct BatchFolder {
55    path: PathBuf,
56    handle: Arc<rustix::fd::OwnedFd>,
57    stat: rustix::fs::Stat,
58    _lock: Option<Arc<StoreReadLock>>,
59}
60
61#[cfg(unix)]
62impl BatchFolder {
63    fn is(&self, other: &Self) -> bool {
64        (self.stat.st_dev, self.stat.st_ino) == (other.stat.st_dev, other.stat.st_ino)
65    }
66}
67
68#[cfg(unix)]
69impl BatchFolder {
70    /// Open the folder at `path`, keeping `lock` until the batch's flush ends.
71    pub(super) fn open(path: PathBuf, lock: Option<Arc<StoreReadLock>>) -> std::io::Result<Self> {
72        use rustix::fs::{Mode, OFlags};
73        let handle = rustix::fs::open(
74            &path,
75            OFlags::DIRECTORY | OFlags::RDONLY | OFlags::CLOEXEC,
76            Mode::empty(),
77        )?;
78        Self::held(path, Arc::new(handle), lock)
79    }
80
81    /// A folder already open at `handle`.
82    pub(super) fn held(
83        path: PathBuf,
84        handle: Arc<rustix::fd::OwnedFd>,
85        lock: Option<Arc<StoreReadLock>>,
86    ) -> std::io::Result<Self> {
87        let stat = rustix::fs::fstat(&*handle)?;
88        Ok(Self { path, handle, stat, _lock: lock })
89    }
90}
91
92impl FlushBatch {
93    /// An empty batch; flushing it does nothing. A panic in a flush whose
94    /// caller stopped waiting goes to `bugs` (E15).
95    pub fn new(bugs: crate::task::BugSinkRef) -> Self {
96        Self {
97            files: 0,
98            bugs,
99            #[cfg(unix)]
100            folders: Vec::new(),
101            #[cfg(any(test, feature = "test-utils"))]
102            hook: None,
103        }
104    }
105
106    /// Run `hook` where this batch's flush returns, if it holds a file.
107    #[cfg(any(test, feature = "test-utils"))]
108    pub fn with_hook(mut self, hook: Option<FlushHook>) -> Self {
109        self.hook = hook;
110        self
111    }
112
113    /// Count one synced file, whose folder, on Unix, is `folder`.
114    #[cfg(unix)]
115    pub(super) fn add(&mut self, folder: BatchFolder) {
116        self.files += 1;
117        self.add_folder(folder);
118    }
119
120    /// Sync `folder` with the batch: a folder whose entries the batch
121    /// changed, such as the one a staging's folder was made in.
122    #[cfg(unix)]
123    pub(super) fn add_folder(&mut self, folder: BatchFolder) {
124        if !self.folders.iter().any(|known| known.is(&folder)) {
125            self.folders.push(folder);
126        }
127    }
128
129    /// Count one synced file.
130    #[cfg(windows)]
131    pub(super) fn add(&mut self) {
132        self.files += 1;
133    }
134
135    /// Sync each folder, then flush each device's cache once, so every file
136    /// of the batch is on the drive's permanent storage. A failure means the
137    /// files may not be, and the commit that would record them doesn't run.
138    pub async fn flush(self) -> Result<Flushed, FileError> {
139        if self.files == 0 {
140            return Ok(Flushed(()));
141        }
142        let bugs = self.bugs.clone();
143        crate::task::blocking_completing("flush the batch", bugs, move || self.flush_now()).await?
144    }
145
146    fn flush_now(self) -> Result<Flushed, FileError> {
147        #[cfg(unix)]
148        self.sync_folders()?;
149        // After every folder's `fsync`, so the flush covers them all.
150        #[cfg(target_vendor = "apple")]
151        self.flush_devices()?;
152        #[cfg(any(test, feature = "test-utils"))]
153        if let Some(hook) = &self.hook {
154            hook().map_err(|source| {
155                FileError::at("flush drive cache", std::path::Path::new("."), source)
156            })?;
157        }
158        tracing::debug!(files = self.files, "flushed a batch of files");
159        Ok(Flushed(()))
160    }
161
162    /// `fsync` each folder a file was made in.
163    #[cfg(unix)]
164    fn sync_folders(&self) -> Result<(), FileError> {
165        for folder in &self.folders {
166            rustix::fs::fsync(&*folder.handle)
167                .map_err(|errno| FileError::at("sync folder", &folder.path, errno.into()))?;
168        }
169        Ok(())
170    }
171
172    /// Flush the drive's cache once for each device a folder is on.
173    #[cfg(target_vendor = "apple")]
174    fn flush_devices(&self) -> Result<(), FileError> {
175        let mut devices = std::collections::BTreeSet::new();
176        for folder in &self.folders {
177            if devices.insert(folder.stat.st_dev) {
178                rustix::fs::fcntl_fullfsync(&*folder.handle).map_err(|errno| {
179                    FileError::at("flush drive cache", &folder.path, errno.into())
180                })?;
181            }
182        }
183        Ok(())
184    }
185}
186
187#[cfg(test)]
188#[path = "flush_batch_tests.rs"]
189mod tests;