#[cfg(any(target_os = "macos", target_os = "linux"))]
mod registry;
#[cfg(any(target_os = "macos", target_os = "linux"))]
pub use registry::{
AcknowledgedStorageMaintenance, OwnedStorageMaintenance, StorageAdmissionRegistry,
UnacknowledgedStorageDaemon,
};
use std::fs::File;
use std::io::{self, Read as _, Seek as _, Write as _};
const CLEAN: &[u8; 17] = b"BCSTREADV1:CLEAN!";
const DIRTY: &[u8; 17] = b"BCSTREADV1:DIRTY!";
fn invalid() -> io::Error {
io::Error::new(
io::ErrorKind::InvalidData,
"storage tracking participant is not clean",
)
}
fn read_state(file: &mut File, permit_empty: bool) -> io::Result<()> {
let length = file.metadata()?.len();
if permit_empty && length == 0 {
return Ok(());
}
if length != CLEAN.len() as u64 {
return Err(invalid());
}
file.rewind()?;
let mut bytes = [0; CLEAN.len()];
file.read_exact(&mut bytes)?;
if &bytes == CLEAN {
Ok(())
} else {
Err(invalid())
}
}
fn write_state(file: &mut File, bytes: &[u8]) -> io::Result<()> {
file.rewind()?;
file.write_all(bytes)?;
file.sync_all()
}
pub struct StorageReadAdmission {
gate: File,
participant: File,
may_clean: bool,
}
impl StorageReadAdmission {
pub fn begin(gate: File, mut participant: File) -> io::Result<Self> {
if !gate.metadata()?.is_file() || !participant.metadata()?.is_file() {
return Err(invalid());
}
gate.try_lock_shared().map_err(io::Error::from)?;
participant.try_lock().map_err(io::Error::from)?;
let state = read_state(&mut participant, true);
let may_clean = match state {
Ok(()) => true,
Err(error) if error.kind() == io::ErrorKind::InvalidData => false,
Err(error) => return Err(error),
};
if may_clean {
write_state(&mut participant, DIRTY)?;
}
Ok(Self {
gate,
participant,
may_clean,
})
}
pub fn complete(mut self) -> io::Result<()> {
if !self.may_clean {
return Err(invalid());
}
write_state(&mut self.participant, CLEAN)?;
self.participant.unlock()?;
self.gate.unlock()
}
}
pub struct StorageMaintenanceAdmission {
_gate: File,
}
impl StorageMaintenanceAdmission {
pub fn begin(
gate: File,
participants: impl IntoIterator<Item = io::Result<File>>,
) -> io::Result<Self> {
if !gate.metadata()?.is_file() {
return Err(invalid());
}
gate.try_lock().map_err(io::Error::from)?;
for file in participants {
let mut file = file?;
if !file.metadata()?.is_file() {
return Err(invalid());
}
file.try_lock_shared().map_err(io::Error::from)?;
read_state(&mut file, false)?;
file.unlock()?;
}
Ok(Self { _gate: gate })
}
}
#[cfg(test)]
mod tests {
use super::*;
fn open(file: &tempfile::NamedTempFile) -> File {
file.reopen().expect("open")
}
#[test]
fn crash_child() {
let Ok(gate_path) = std::env::var("BCODE_ADMISSION_CRASH_GATE") else {
return;
};
let participant_path =
std::env::var("BCODE_ADMISSION_CRASH_PARTICIPANT").expect("participant path");
let gate = std::fs::OpenOptions::new()
.read(true)
.write(true)
.open(gate_path)
.expect("gate");
let participant = std::fs::OpenOptions::new()
.read(true)
.write(true)
.open(participant_path)
.expect("participant");
let _admission = StorageReadAdmission::begin(gate, participant).expect("admission");
std::process::exit(94);
}
#[test]
fn process_crash_releases_admission_lock_but_preserves_ineligible_participant() {
let gate = tempfile::NamedTempFile::new().expect("gate");
let participant = tempfile::NamedTempFile::new().expect("participant");
let status = std::process::Command::new(std::env::current_exe().expect("test executable"))
.args(["--exact", "storage_admission::tests::crash_child"])
.env("BCODE_ADMISSION_CRASH_GATE", gate.path())
.env("BCODE_ADMISSION_CRASH_PARTICIPANT", participant.path())
.status()
.expect("child");
assert_eq!(status.code(), Some(94));
let released_gate = open(&gate);
released_gate.try_lock().expect("OS released dead owner");
released_gate.unlock().expect("unlock");
assert!(
StorageMaintenanceAdmission::begin(released_gate, [Ok(open(&participant))]).is_err()
);
assert_eq!(
std::fs::read(participant.path()).expect("dirty record"),
DIRTY
);
}
#[test]
fn independent_readers_coexist_and_exclude_maintenance_until_all_complete() {
let gate = tempfile::NamedTempFile::new().expect("gate");
let first = tempfile::NamedTempFile::new().expect("first");
let second = tempfile::NamedTempFile::new().expect("second");
let a = StorageReadAdmission::begin(open(&gate), open(&first)).expect("first read");
let b = StorageReadAdmission::begin(open(&gate), open(&second)).expect("second read");
assert!(
StorageMaintenanceAdmission::begin(open(&gate), [Ok(open(&first)), Ok(open(&second))])
.is_err()
);
a.complete().expect("first complete");
assert!(
StorageMaintenanceAdmission::begin(open(&gate), [Ok(open(&first)), Ok(open(&second))])
.is_err()
);
b.complete().expect("second complete");
let maintenance =
StorageMaintenanceAdmission::begin(open(&gate), [Ok(open(&first)), Ok(open(&second))])
.expect("maintenance");
assert!(StorageReadAdmission::begin(open(&gate), open(&first)).is_err());
drop(maintenance);
StorageReadAdmission::begin(open(&gate), open(&first))
.expect("read again")
.complete()
.expect("complete");
}
#[test]
fn abandoned_reader_blocks_maintenance_but_not_other_readers() {
let gate = tempfile::NamedTempFile::new().expect("gate");
let first = tempfile::NamedTempFile::new().expect("first");
let second = tempfile::NamedTempFile::new().expect("second");
drop(StorageReadAdmission::begin(open(&gate), open(&first)).expect("read"));
StorageReadAdmission::begin(open(&gate), open(&second))
.expect("unrelated read")
.complete()
.expect("tracked");
assert!(
StorageMaintenanceAdmission::begin(open(&gate), [Ok(open(&first)), Ok(open(&second))])
.is_err()
);
assert!(
StorageReadAdmission::begin(open(&gate), open(&first))
.expect("damaged participant can read")
.complete()
.is_err()
);
assert_eq!(std::fs::read(first.path()).expect("dirty"), DIRTY);
}
#[test]
fn unknown_state_and_incomplete_registry_are_preserved_and_rejected() {
let gate = tempfile::NamedTempFile::new().expect("gate");
let participant = tempfile::NamedTempFile::new().expect("participant");
for bytes in [b"future-version".as_slice(), b"truncated"] {
std::fs::write(participant.path(), bytes).expect("fixture");
assert!(
StorageMaintenanceAdmission::begin(open(&gate), [Ok(open(&participant))]).is_err()
);
assert!(
StorageReadAdmission::begin(open(&gate), open(&participant))
.expect("read")
.complete()
.is_err()
);
assert_eq!(std::fs::read(participant.path()).expect("preserved"), bytes);
}
assert!(
StorageMaintenanceAdmission::begin(
open(&gate),
[Err(io::Error::other("incomplete discovery"))]
)
.is_err()
);
}
}