use std::fs::File;
use std::io::{self, Read as _, Seek as _, SeekFrom, Write as _};
use std::sync::atomic::{AtomicBool, Ordering};
const CLEAN: &[u8; 17] = b"BCSTHEALTH1:CLEAN";
const DIRTY: &[u8; 17] = b"BCSTHEALTH1:DIRTY";
pub struct StorageTrackingFence {
file: File,
failed: AtomicBool,
}
impl StorageTrackingFence {
pub fn begin(mut file: File) -> io::Result<Self> {
file.try_lock().map_err(io::Error::from)?;
let length = file.metadata()?.len();
if length != 0 {
if length != CLEAN.len() as u64 {
return Err(unavailable());
}
file.seek(SeekFrom::Start(0))?;
let mut bytes = [0; CLEAN.len()];
file.read_exact(&mut bytes)?;
if &bytes != CLEAN {
return Err(unavailable());
}
}
file.seek(SeekFrom::Start(0))?;
file.write_all(DIRTY)?;
file.sync_all()?;
Ok(Self {
file,
failed: AtomicBool::new(false),
})
}
pub fn fail(&self) {
self.failed.store(true, Ordering::SeqCst);
}
#[must_use]
pub fn healthy(&self) -> bool {
!self.failed.load(Ordering::SeqCst)
}
pub fn finish(mut self) -> io::Result<()> {
if !self.healthy() {
return Err(unavailable());
}
self.file.seek(SeekFrom::Start(0))?;
self.file.write_all(CLEAN)?;
self.file.sync_all()?;
self.file.unlock()
}
}
fn unavailable() -> io::Error {
io::Error::new(
io::ErrorKind::InvalidData,
"storage access tracking requires explicit maintenance",
)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn crash_child() {
let Ok(path) = std::env::var("BCODE_TRACKING_FENCE_CRASH_FILE") else {
return;
};
let file = std::fs::OpenOptions::new()
.read(true)
.write(true)
.open(path)
.expect("file");
let _fence = StorageTrackingFence::begin(file).expect("begin");
std::process::exit(93);
}
#[test]
fn process_death_releases_lock_but_retains_dirty_authority() {
let file = tempfile::NamedTempFile::new().expect("file");
let status = std::process::Command::new(std::env::current_exe().expect("executable"))
.args(["--exact", "storage_tracking_fence::tests::crash_child"])
.env("BCODE_TRACKING_FENCE_CRASH_FILE", file.path())
.status()
.expect("child");
assert_eq!(status.code(), Some(93));
let reopened = file.reopen().expect("open");
reopened.try_lock().expect("dead owner released OS lock");
reopened.unlock().expect("unlock");
assert!(StorageTrackingFence::begin(reopened).is_err());
assert_eq!(std::fs::read(file.path()).expect("durable dirty"), DIRTY);
}
#[test]
fn clean_shutdown_can_restart_but_failed_epoch_cannot() {
let file = tempfile::NamedTempFile::new().expect("file");
let fence = StorageTrackingFence::begin(file.reopen().expect("open")).expect("begin");
assert!(fence.healthy());
assert!(StorageTrackingFence::begin(file.reopen().expect("contender")).is_err());
fence.finish().expect("clean");
let fence = StorageTrackingFence::begin(file.reopen().expect("restart")).expect("restart");
fence.fail();
assert!(!fence.healthy());
assert!(fence.finish().is_err());
assert!(StorageTrackingFence::begin(file.reopen().expect("later restart")).is_err());
assert_eq!(std::fs::read(file.path()).expect("dirty"), DIRTY);
}
#[test]
fn abandoned_epoch_and_unknown_versions_are_not_repaired() {
let file = tempfile::NamedTempFile::new().expect("file");
drop(StorageTrackingFence::begin(file.reopen().expect("open")).expect("begin"));
assert!(StorageTrackingFence::begin(file.reopen().expect("reopen")).is_err());
for bytes in [b"future format".as_slice(), b"", b"BCSTHEALTH2:CLEAN"] {
if bytes.is_empty() {
continue;
}
std::fs::write(file.path(), bytes).expect("fixture");
assert!(StorageTrackingFence::begin(file.reopen().expect("open")).is_err());
assert_eq!(std::fs::read(file.path()).expect("unchanged"), bytes);
}
}
#[test]
fn failure_requires_no_further_durable_write() {
let file = tempfile::NamedTempFile::new().expect("file");
let mut fence = StorageTrackingFence::begin(file.reopen().expect("open")).expect("begin");
let mut bytes = [0; DIRTY.len()];
fence.file.seek(SeekFrom::Start(0)).expect("rewind");
fence.file.read_exact(&mut bytes).expect("pre-failure");
assert_eq!(&bytes, DIRTY);
fence.fail();
fence.file.seek(SeekFrom::Start(0)).expect("rewind");
fence.file.read_exact(&mut bytes).expect("post-failure");
assert_eq!(&bytes, DIRTY);
drop(fence);
assert!(StorageTrackingFence::begin(file.reopen().expect("restart")).is_err());
}
}