1use std::fs::File;
3use std::io;
4use std::sync::Mutex;
5use std::sync::atomic::{AtomicU64, AtomicUsize, Ordering};
6use std::time::Instant;
7
8use crate::FileIdentity;
9
10static CAPTURES: AtomicUsize = AtomicUsize::new(0);
11static FSYNC_CALLS: AtomicU64 = AtomicU64::new(0);
12static FSYNC_NS: AtomicU64 = AtomicU64::new(0);
13static FSYNC_DIRECTORY_CALLS: AtomicU64 = AtomicU64::new(0);
14
15#[derive(Debug)]
17pub struct Observation(());
18impl Observation {
19 #[must_use]
21 pub fn start() -> Self {
22 CAPTURES.fetch_add(1, Ordering::Relaxed);
23 Self(())
24 }
25}
26impl Drop for Observation {
27 fn drop(&mut self) {
28 CAPTURES.fetch_sub(1, Ordering::Relaxed);
29 }
30}
31static AUDITS: AtomicUsize = AtomicUsize::new(0);
34static AUDIT: Mutex<AuditState> = Mutex::new(AuditState {
35 holders: 0,
36 log: Vec::new(),
37});
38
39struct AuditState {
40 holders: usize,
42 log: Vec<BarrierEntry>,
43}
44
45#[derive(Clone, Debug)]
48enum BarrierEntry {
49 Record(BarrierRecord),
50 Unattributed(String),
51}
52
53fn audit() -> std::sync::MutexGuard<'static, AuditState> {
54 AUDIT
55 .lock()
56 .unwrap_or_else(std::sync::PoisonError::into_inner)
57}
58
59#[derive(Clone, Copy, Debug, PartialEq, Eq)]
65pub struct BarrierRecord {
66 pub identity: FileIdentity,
68 pub generation: u32,
70 pub directory: bool,
72}
73
74#[derive(Clone, Debug, Default, PartialEq, Eq)]
76pub struct BarrierLog {
77 pub records: Vec<BarrierRecord>,
79 pub unattributed: Vec<String>,
83}
84
85#[derive(Debug)]
91pub struct BarrierAudit {
92 start: usize,
93}
94impl BarrierAudit {
95 #[must_use]
97 pub fn start() -> Self {
98 let mut state = audit();
99 state.holders += 1;
100 AUDITS.store(state.holders, Ordering::SeqCst);
101 Self {
102 start: state.log.len(),
103 }
104 }
105
106 #[must_use]
108 pub fn finish(self) -> BarrierLog {
109 let state = audit();
110 let mut log = BarrierLog::default();
111 for entry in &state.log[self.start.min(state.log.len())..] {
113 match entry {
114 BarrierEntry::Record(record) => log.records.push(*record),
115 BarrierEntry::Unattributed(reason) => log.unattributed.push(reason.clone()),
116 }
117 }
118 log
119 }
120}
121impl Drop for BarrierAudit {
122 fn drop(&mut self) {
123 let mut state = audit();
124 state.holders -= 1;
125 AUDITS.store(state.holders, Ordering::SeqCst);
126 if state.holders == 0 {
127 state.log.clear();
128 }
129 }
130}
131
132#[must_use]
134pub fn active() -> bool {
135 CAPTURES.load(Ordering::Relaxed) != 0 || AUDITS.load(Ordering::Relaxed) != 0
136}
137#[must_use]
139pub fn fsync_totals() -> (u64, u64) {
140 (
141 FSYNC_CALLS.load(Ordering::Relaxed),
142 FSYNC_NS.load(Ordering::Relaxed),
143 )
144}
145
146#[must_use]
149pub fn fsync_directory_calls() -> u64 {
150 FSYNC_DIRECTORY_CALLS.load(Ordering::Relaxed)
151}
152
153pub trait ObservedSync {
155 fn observed_sync_all(&self) -> io::Result<()>;
157 fn observed_sync_data(&self) -> io::Result<()>;
159}
160impl ObservedSync for File {
161 fn observed_sync_all(&self) -> io::Result<()> {
162 observe(self, || self.sync_all())
163 }
164 fn observed_sync_data(&self) -> io::Result<()> {
165 observe(self, || self.sync_data())
166 }
167}
168fn observe(file: &File, operation: impl FnOnce() -> io::Result<()>) -> io::Result<()> {
169 if !active() {
170 return operation();
171 }
172 let metadata = file.metadata();
173 let directory = metadata.as_ref().is_ok_and(std::fs::Metadata::is_dir);
174 if directory {
175 FSYNC_DIRECTORY_CALLS.fetch_add(1, Ordering::Relaxed);
176 }
177 if AUDITS.load(Ordering::SeqCst) != 0 {
178 let entry = match (
179 &metadata,
180 crate::file_identity(file),
181 crate::inode_generation(file),
182 ) {
183 (Ok(_), Ok(identity), Ok(generation)) => BarrierEntry::Record(BarrierRecord {
184 identity,
185 generation,
186 directory,
187 }),
188 (metadata, identity, generation) => BarrierEntry::Unattributed(format!(
189 "metadata: {:?}; identity: {:?}; generation: {:?}",
190 metadata.as_ref().err(),
191 identity.err(),
192 generation.err()
193 )),
194 };
195 let mut state = audit();
196 if state.holders != 0 {
197 state.log.push(entry);
198 }
199 }
200 let started = Instant::now();
201 let result = operation();
202 FSYNC_NS.fetch_add(
203 u64::try_from(started.elapsed().as_nanos()).unwrap_or(u64::MAX),
204 Ordering::Relaxed,
205 );
206 FSYNC_CALLS.fetch_add(1, Ordering::Relaxed);
207 result
208}
209
210#[cfg(test)]
211mod tests {
212 use super::*;
213
214 static TURNS: Mutex<()> = Mutex::new(());
218
219 fn take_turn() -> std::sync::MutexGuard<'static, ()> {
220 TURNS
221 .lock()
222 .unwrap_or_else(std::sync::PoisonError::into_inner)
223 }
224
225 fn barrier_on(file: &File) {
226 file.observed_sync_all().unwrap();
227 }
228
229 fn mine(log: &BarrierLog, identity: FileIdentity) -> usize {
235 if cfg!(target_os = "linux") {
236 assert!(log.unattributed.is_empty(), "{:?}", log.unattributed);
237 log.records
238 .iter()
239 .filter(|record| record.identity == identity)
240 .count()
241 } else {
242 assert!(log.records.is_empty(), "{:?}", log.records);
245 log.unattributed.len()
246 }
247 }
248
249 fn assert_mine(log: &BarrierLog, identity: FileIdentity, expected: usize, what: &str) {
253 let held = mine(log, identity);
254 if cfg!(target_os = "linux") {
255 assert_eq!(held, expected, "{what}");
256 } else {
257 assert!(held >= expected, "{what}: {held} < {expected}");
258 }
259 }
260
261 #[test]
265 fn overlapping_audits_keep_their_logs_until_the_last_ends() {
266 let _turn = take_turn();
267 let directory = tempfile::tempdir().unwrap();
268 let file = File::create(directory.path().join("file")).unwrap();
269 let identity = crate::file_identity(&file).unwrap();
270 let first = BarrierAudit::start();
271 barrier_on(&file);
272 let second = BarrierAudit::start();
273 barrier_on(&file);
274 assert_mine(&first.finish(), identity, 2, "first");
276 barrier_on(&file);
277 let second = second.finish();
278 assert_mine(&second, identity, 2, "second");
279 if cfg!(target_os = "linux") {
280 assert!(
281 second
282 .records
283 .iter()
284 .filter(|record| record.identity == identity)
285 .all(|record| !record.directory)
286 );
287 }
288 barrier_on(&file);
290 assert!(audit().log.is_empty());
291 assert_mine(&BarrierAudit::start().finish(), identity, 0, "later");
293 }
294
295 #[test]
304 fn audits_started_and_ended_concurrently_keep_their_own_slices() {
305 const THREADS: usize = 16;
306 const ROUNDS: usize = 60_000;
307 let _turn = take_turn();
308 let directory = tempfile::tempdir().unwrap();
309 std::thread::scope(|scope| {
310 for thread in 0..THREADS {
311 let directory = directory.path();
312 scope.spawn(move || {
313 let file = File::create(directory.join(format!("file-{thread}"))).unwrap();
314 let identity = crate::file_identity(&file).unwrap();
315 for round in 0..ROUNDS {
316 let audit = BarrierAudit::start();
317 let barriers = 1 + (round % 3);
318 for _ in 0..barriers {
319 barrier_on(&file);
320 }
321 if round % 4 == 3 {
322 drop(audit);
323 } else {
324 assert_mine(&audit.finish(), identity, barriers, "own slice");
325 }
326 }
327 });
328 }
329 });
330 assert!(audit().log.is_empty());
331 assert_eq!(audit().holders, 0);
332 }
333
334 #[cfg(target_os = "linux")]
337 #[test]
338 fn a_barrier_the_audit_cannot_name_is_reported() {
339 let _turn = take_turn();
340 let file = File::open("/proc/self/status").unwrap();
341 let audit = BarrierAudit::start();
342 let _ = file.observed_sync_all();
344 let log = audit.finish();
345 assert!(log.records.is_empty(), "{:?}", log.records);
346 assert_eq!(log.unattributed.len(), 1, "{:?}", log.unattributed);
347 assert!(log.unattributed[0].contains("generation"));
348 }
349}