coven_foundation/files/
flush_batch.rs1use super::FileError;
18#[cfg(unix)]
19use super::StoreReadLock;
20#[cfg(unix)]
21use std::path::PathBuf;
22#[cfg(unix)]
23use std::sync::Arc;
24
25#[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 bugs: crate::task::BugSinkRef,
34 #[cfg(unix)]
37 folders: Vec<BatchFolder>,
38 #[cfg(any(test, feature = "test-utils"))]
39 hook: Option<FlushHook>,
40}
41
42#[derive(Debug)]
44#[must_use = "a flushed batch is for the commit that records its files"]
45pub struct Flushed(());
46
47#[cfg(any(test, feature = "test-utils"))]
50pub type FlushHook = std::sync::Arc<dyn Fn() -> std::io::Result<()> + Send + Sync>;
51
52#[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 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 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 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 #[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 #[cfg(unix)]
115 pub(super) fn add(&mut self, folder: BatchFolder) {
116 self.files += 1;
117 self.add_folder(folder);
118 }
119
120 #[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 #[cfg(windows)]
131 pub(super) fn add(&mut self) {
132 self.files += 1;
133 }
134
135 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 #[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 #[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 #[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;