use std::fs::File;
use std::io;
use std::sync::Mutex;
use std::sync::atomic::{AtomicU64, AtomicUsize, Ordering};
use std::time::Instant;
use crate::FileIdentity;
static CAPTURES: AtomicUsize = AtomicUsize::new(0);
static FSYNC_CALLS: AtomicU64 = AtomicU64::new(0);
static FSYNC_NS: AtomicU64 = AtomicU64::new(0);
static FSYNC_DIRECTORY_CALLS: AtomicU64 = AtomicU64::new(0);
#[derive(Debug)]
pub struct Observation(());
impl Observation {
#[must_use]
pub fn start() -> Self {
CAPTURES.fetch_add(1, Ordering::Relaxed);
Self(())
}
}
impl Drop for Observation {
fn drop(&mut self) {
CAPTURES.fetch_sub(1, Ordering::Relaxed);
}
}
static AUDITS: AtomicUsize = AtomicUsize::new(0);
static AUDIT: Mutex<AuditState> = Mutex::new(AuditState {
holders: 0,
log: Vec::new(),
});
struct AuditState {
holders: usize,
log: Vec<BarrierEntry>,
}
#[derive(Clone, Debug)]
enum BarrierEntry {
Record(BarrierRecord),
Unattributed(String),
}
fn audit() -> std::sync::MutexGuard<'static, AuditState> {
AUDIT
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct BarrierRecord {
pub identity: FileIdentity,
pub generation: u32,
pub directory: bool,
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct BarrierLog {
pub records: Vec<BarrierRecord>,
pub unattributed: Vec<String>,
}
#[derive(Debug)]
pub struct BarrierAudit {
start: usize,
}
impl BarrierAudit {
#[must_use]
pub fn start() -> Self {
let mut state = audit();
state.holders += 1;
AUDITS.store(state.holders, Ordering::SeqCst);
Self {
start: state.log.len(),
}
}
#[must_use]
pub fn finish(self) -> BarrierLog {
let state = audit();
let mut log = BarrierLog::default();
for entry in &state.log[self.start.min(state.log.len())..] {
match entry {
BarrierEntry::Record(record) => log.records.push(*record),
BarrierEntry::Unattributed(reason) => log.unattributed.push(reason.clone()),
}
}
log
}
}
impl Drop for BarrierAudit {
fn drop(&mut self) {
let mut state = audit();
state.holders -= 1;
AUDITS.store(state.holders, Ordering::SeqCst);
if state.holders == 0 {
state.log.clear();
}
}
}
#[must_use]
pub fn active() -> bool {
CAPTURES.load(Ordering::Relaxed) != 0 || AUDITS.load(Ordering::Relaxed) != 0
}
#[must_use]
pub fn fsync_totals() -> (u64, u64) {
(
FSYNC_CALLS.load(Ordering::Relaxed),
FSYNC_NS.load(Ordering::Relaxed),
)
}
#[must_use]
pub fn fsync_directory_calls() -> u64 {
FSYNC_DIRECTORY_CALLS.load(Ordering::Relaxed)
}
pub trait ObservedSync {
fn observed_sync_all(&self) -> io::Result<()>;
fn observed_sync_data(&self) -> io::Result<()>;
}
impl ObservedSync for File {
fn observed_sync_all(&self) -> io::Result<()> {
observe(self, || self.sync_all())
}
fn observed_sync_data(&self) -> io::Result<()> {
observe(self, || self.sync_data())
}
}
fn observe(file: &File, operation: impl FnOnce() -> io::Result<()>) -> io::Result<()> {
if !active() {
return operation();
}
let metadata = file.metadata();
let directory = metadata.as_ref().is_ok_and(std::fs::Metadata::is_dir);
if directory {
FSYNC_DIRECTORY_CALLS.fetch_add(1, Ordering::Relaxed);
}
if AUDITS.load(Ordering::SeqCst) != 0 {
let entry = match (
&metadata,
crate::file_identity(file),
crate::inode_generation(file),
) {
(Ok(_), Ok(identity), Ok(generation)) => BarrierEntry::Record(BarrierRecord {
identity,
generation,
directory,
}),
(metadata, identity, generation) => BarrierEntry::Unattributed(format!(
"metadata: {:?}; identity: {:?}; generation: {:?}",
metadata.as_ref().err(),
identity.err(),
generation.err()
)),
};
let mut state = audit();
if state.holders != 0 {
state.log.push(entry);
}
}
let started = Instant::now();
let result = operation();
FSYNC_NS.fetch_add(
u64::try_from(started.elapsed().as_nanos()).unwrap_or(u64::MAX),
Ordering::Relaxed,
);
FSYNC_CALLS.fetch_add(1, Ordering::Relaxed);
result
}
#[cfg(test)]
mod tests {
use super::*;
static TURNS: Mutex<()> = Mutex::new(());
fn take_turn() -> std::sync::MutexGuard<'static, ()> {
TURNS
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
}
fn barrier_on(file: &File) {
file.observed_sync_all().unwrap();
}
fn mine(log: &BarrierLog, identity: FileIdentity) -> usize {
if cfg!(target_os = "linux") {
assert!(log.unattributed.is_empty(), "{:?}", log.unattributed);
log.records
.iter()
.filter(|record| record.identity == identity)
.count()
} else {
assert!(log.records.is_empty(), "{:?}", log.records);
log.unattributed.len()
}
}
fn assert_mine(log: &BarrierLog, identity: FileIdentity, expected: usize, what: &str) {
let held = mine(log, identity);
if cfg!(target_os = "linux") {
assert_eq!(held, expected, "{what}");
} else {
assert!(held >= expected, "{what}: {held} < {expected}");
}
}
#[test]
fn overlapping_audits_keep_their_logs_until_the_last_ends() {
let _turn = take_turn();
let directory = tempfile::tempdir().unwrap();
let file = File::create(directory.path().join("file")).unwrap();
let identity = crate::file_identity(&file).unwrap();
let first = BarrierAudit::start();
barrier_on(&file);
let second = BarrierAudit::start();
barrier_on(&file);
assert_mine(&first.finish(), identity, 2, "first");
barrier_on(&file);
let second = second.finish();
assert_mine(&second, identity, 2, "second");
if cfg!(target_os = "linux") {
assert!(
second
.records
.iter()
.filter(|record| record.identity == identity)
.all(|record| !record.directory)
);
}
barrier_on(&file);
assert!(audit().log.is_empty());
assert_mine(&BarrierAudit::start().finish(), identity, 0, "later");
}
#[test]
fn audits_started_and_ended_concurrently_keep_their_own_slices() {
const THREADS: usize = 16;
const ROUNDS: usize = 60_000;
let _turn = take_turn();
let directory = tempfile::tempdir().unwrap();
std::thread::scope(|scope| {
for thread in 0..THREADS {
let directory = directory.path();
scope.spawn(move || {
let file = File::create(directory.join(format!("file-{thread}"))).unwrap();
let identity = crate::file_identity(&file).unwrap();
for round in 0..ROUNDS {
let audit = BarrierAudit::start();
let barriers = 1 + (round % 3);
for _ in 0..barriers {
barrier_on(&file);
}
if round % 4 == 3 {
drop(audit);
} else {
assert_mine(&audit.finish(), identity, barriers, "own slice");
}
}
});
}
});
assert!(audit().log.is_empty());
assert_eq!(audit().holders, 0);
}
#[cfg(target_os = "linux")]
#[test]
fn a_barrier_the_audit_cannot_name_is_reported() {
let _turn = take_turn();
let file = File::open("/proc/self/status").unwrap();
let audit = BarrierAudit::start();
let _ = file.observed_sync_all();
let log = audit.finish();
assert!(log.records.is_empty(), "{:?}", log.records);
assert_eq!(log.unattributed.len(), 1, "{:?}", log.unattributed);
assert!(log.unattributed[0].contains("generation"));
}
}