use std::collections::BTreeSet;
use std::fmt;
use std::fs;
use std::io;
use std::path::{Path, PathBuf};
use std::time::{SystemTime, UNIX_EPOCH};
pub const MAX_STABLE_READ_RETRIES: usize = 3;
pub const MAX_STABLE_READ_ATTEMPTS: usize = MAX_STABLE_READ_RETRIES + 1;
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct FileStamp {
pub size: u64,
pub modified_ns: Option<u128>,
#[cfg(unix)]
pub inode: u64,
#[cfg(unix)]
pub ctime_ns: i128,
}
impl FileStamp {
pub fn from_metadata(metadata: &fs::Metadata) -> Self {
Self {
size: metadata.len(),
modified_ns: system_time_ns(metadata.modified().ok()),
#[cfg(unix)]
inode: {
use std::os::unix::fs::MetadataExt;
metadata.ino()
},
#[cfg(unix)]
ctime_ns: {
use std::os::unix::fs::MetadataExt;
i128::from(metadata.ctime()) * 1_000_000_000 + i128::from(metadata.ctime_nsec())
},
}
}
#[cfg(test)]
fn synthetic(size: u64, modified_ns: u128) -> Self {
Self {
size,
modified_ns: Some(modified_ns),
#[cfg(unix)]
inode: 1,
#[cfg(unix)]
ctime_ns: 1,
}
}
}
fn system_time_ns(time: Option<SystemTime>) -> Option<u128> {
time.and_then(|time| time.duration_since(UNIX_EPOCH).ok())
.map(|duration| duration.as_nanos())
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct StableRead<T> {
pub value: T,
pub stamp: FileStamp,
pub attempts: usize,
}
#[derive(Debug)]
pub enum StableReadError {
Io(io::Error),
Unstable {
attempts: usize,
},
}
impl fmt::Display for StableReadError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Io(error) => write!(f, "stable read I/O error: {error}"),
Self::Unstable { attempts } => {
write!(
f,
"file changed around all {attempts} stable-read attempt(s)"
)
}
}
}
}
impl std::error::Error for StableReadError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::Io(error) => Some(error),
Self::Unstable { .. } => None,
}
}
}
impl From<io::Error> for StableReadError {
fn from(error: io::Error) -> Self {
Self::Io(error)
}
}
pub fn read_stable_file(path: &Path) -> Result<StableRead<Vec<u8>>, StableReadError> {
read_stable(
|| Ok(FileStamp::from_metadata(&fs::metadata(path)?)),
|| fs::read(path),
)
}
pub fn read_stable<T>(
mut stat: impl FnMut() -> io::Result<FileStamp>,
mut read: impl FnMut() -> io::Result<T>,
) -> Result<StableRead<T>, StableReadError> {
for retry in 0..=MAX_STABLE_READ_RETRIES {
let before = stat()?;
let value = read()?;
let after = stat()?;
if before == after {
return Ok(StableRead {
value,
stamp: after,
attempts: retry + 1,
});
}
}
Err(StableReadError::Unstable {
attempts: MAX_STABLE_READ_ATTEMPTS,
})
}
#[derive(Debug, Default)]
pub struct WatcherCollector {
invalidated_paths: BTreeSet<PathBuf>,
}
impl WatcherCollector {
pub fn invalidate(&mut self, path: impl Into<PathBuf>) {
self.invalidated_paths.insert(path.into());
}
pub fn take_collected(&mut self) -> Vec<PathBuf> {
std::mem::take(&mut self.invalidated_paths)
.into_iter()
.collect()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn stat_mismatch_gets_three_retries_then_is_left_unstable() {
let mut stat_calls = 0;
let mut read_calls = 0;
let result = read_stable(
|| {
stat_calls += 1;
let stamp = if stat_calls % 2 == 1 {
FileStamp::synthetic(1, stat_calls as u128)
} else {
FileStamp::synthetic(2, stat_calls as u128)
};
Ok(stamp)
},
|| {
read_calls += 1;
Ok::<_, io::Error>(b"read".to_vec())
},
);
assert!(matches!(
result,
Err(StableReadError::Unstable {
attempts: MAX_STABLE_READ_ATTEMPTS
})
));
assert_eq!(read_calls, MAX_STABLE_READ_ATTEMPTS);
assert_eq!(stat_calls, MAX_STABLE_READ_ATTEMPTS * 2);
}
#[test]
fn third_retry_can_produce_a_stable_snapshot() {
let stable = FileStamp::synthetic(9, 12);
let changing = FileStamp::synthetic(9, 13);
let mut stamps = vec![
stable.clone(),
changing.clone(),
stable.clone(),
changing.clone(),
stable.clone(),
changing,
stable.clone(),
stable.clone(),
]
.into_iter();
let result = read_stable(
|| Ok(stamps.next().expect("one stamp per stat")),
|| Ok::<_, io::Error>(b"stable bytes".to_vec()),
)
.expect("third retry succeeds");
assert_eq!(result.value, b"stable bytes");
assert_eq!(result.attempts, MAX_STABLE_READ_ATTEMPTS);
assert_eq!(result.stamp, stable);
}
#[test]
fn watcher_collector_only_deduplicates_and_drains_paths() {
let mut collector = WatcherCollector::default();
collector.invalidate("src/z.rs");
collector.invalidate("src/a.rs");
collector.invalidate("src/z.rs");
assert_eq!(
collector.take_collected(),
vec![PathBuf::from("src/a.rs"), PathBuf::from("src/z.rs")]
);
assert!(collector.take_collected().is_empty());
}
}