use std::fs::File;
use std::io;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LockMode {
Shared,
Exclusive,
}
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
pub enum FileLocking {
#[default]
Enabled,
Disabled,
BestEffort,
}
impl FileLocking {
pub fn from_env() -> Self {
Self::parse_env_value(std::env::var("HDF5_USE_FILE_LOCKING").ok().as_deref())
}
pub fn from_env_or(default: FileLocking) -> Self {
match std::env::var("HDF5_USE_FILE_LOCKING").ok().as_deref() {
None => default,
Some(v) => Self::parse_env_value(Some(v)),
}
}
pub(crate) fn parse_env_value(value: Option<&str>) -> Self {
match value {
None => FileLocking::Enabled,
Some(v) => {
let trimmed = v.trim();
if trimmed.eq_ignore_ascii_case("FALSE")
|| trimmed == "0"
|| trimmed.eq_ignore_ascii_case("OFF")
|| trimmed.eq_ignore_ascii_case("NO")
{
FileLocking::Disabled
} else if trimmed.eq_ignore_ascii_case("BEST_EFFORT")
|| trimmed.eq_ignore_ascii_case("BEST-EFFORT")
|| trimmed.eq_ignore_ascii_case("BESTEFFORT")
{
FileLocking::BestEffort
} else {
FileLocking::Enabled
}
}
}
}
}
pub fn try_acquire(file: &File, mode: LockMode, policy: FileLocking) -> io::Result<bool> {
if matches!(policy, FileLocking::Disabled) {
return Ok(false);
}
const RETRY_ATTEMPTS: u32 = 10;
const RETRY_SLEEP: std::time::Duration = std::time::Duration::from_millis(10);
let mut attempt = match mode {
LockMode::Shared => file.try_lock_shared(),
LockMode::Exclusive => file.try_lock(),
};
for _ in 0..RETRY_ATTEMPTS {
if !matches!(attempt, Err(std::fs::TryLockError::WouldBlock)) {
break;
}
std::thread::sleep(RETRY_SLEEP);
attempt = match mode {
LockMode::Shared => file.try_lock_shared(),
LockMode::Exclusive => file.try_lock(),
};
}
match attempt {
Ok(()) => Ok(true),
Err(std::fs::TryLockError::WouldBlock) => match policy {
FileLocking::Enabled => Err(io::Error::new(
io::ErrorKind::WouldBlock,
"unable to lock file: another process holds a conflicting lock",
)),
FileLocking::BestEffort => Ok(false),
FileLocking::Disabled => unreachable!(),
},
Err(std::fs::TryLockError::Error(e)) => match policy {
FileLocking::Enabled => Err(e),
FileLocking::BestEffort => Ok(false),
FileLocking::Disabled => unreachable!(),
},
}
}
pub fn release(file: &File) -> io::Result<()> {
file.unlock()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_env_value_defaults_to_enabled() {
assert_eq!(FileLocking::parse_env_value(None), FileLocking::Enabled);
}
#[test]
fn parse_env_value_recognizes_disabled() {
for v in ["FALSE", "false", "0", "off", "no"] {
assert_eq!(
FileLocking::parse_env_value(Some(v)),
FileLocking::Disabled,
"value: {v}",
);
}
}
#[test]
fn parse_env_value_recognizes_best_effort() {
for v in ["BEST_EFFORT", "best_effort", "best-effort", "BestEffort"] {
assert_eq!(
FileLocking::parse_env_value(Some(v)),
FileLocking::BestEffort,
"value: {v}",
);
}
}
#[test]
fn parse_env_value_recognizes_enabled() {
for v in ["TRUE", "true", "1", "on", "yes", "garbage"] {
assert_eq!(
FileLocking::parse_env_value(Some(v)),
FileLocking::Enabled,
"value: {v}",
);
}
}
#[test]
fn try_acquire_disabled_is_noop() {
let dir =
std::env::temp_dir().join(format!("rust_hdf5_lock_disabled_{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
let path = dir.join("noop.bin");
let f = std::fs::File::create(&path).unwrap();
let acquired = try_acquire(&f, LockMode::Exclusive, FileLocking::Disabled).unwrap();
assert!(!acquired, "Disabled policy must not acquire a lock");
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn try_acquire_exclusive_then_shared_fails() {
let dir = std::env::temp_dir().join(format!("rust_hdf5_lock_excl_{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
let path = dir.join("conflict.bin");
let f1 = std::fs::File::create(&path).unwrap();
assert!(try_acquire(&f1, LockMode::Exclusive, FileLocking::Enabled).unwrap());
let f2 = std::fs::OpenOptions::new().read(true).open(&path).unwrap();
let res = try_acquire(&f2, LockMode::Shared, FileLocking::Enabled);
assert!(res.is_err(), "expected lock conflict");
let res2 = try_acquire(&f2, LockMode::Shared, FileLocking::BestEffort).unwrap();
assert!(!res2, "best-effort must report unsuccessful lock as false");
release(&f1).unwrap();
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn shared_locks_coexist() {
let dir =
std::env::temp_dir().join(format!("rust_hdf5_lock_shared_{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
let path = dir.join("shared.bin");
std::fs::File::create(&path).unwrap();
let f1 = std::fs::OpenOptions::new().read(true).open(&path).unwrap();
let f2 = std::fs::OpenOptions::new().read(true).open(&path).unwrap();
assert!(try_acquire(&f1, LockMode::Shared, FileLocking::Enabled).unwrap());
assert!(try_acquire(&f2, LockMode::Shared, FileLocking::Enabled).unwrap());
release(&f1).unwrap();
release(&f2).unwrap();
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn release_then_relock_works() {
let dir =
std::env::temp_dir().join(format!("rust_hdf5_lock_release_{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
let path = dir.join("release.bin");
let f1 = std::fs::File::create(&path).unwrap();
assert!(try_acquire(&f1, LockMode::Exclusive, FileLocking::Enabled).unwrap());
release(&f1).unwrap();
let f2 = std::fs::OpenOptions::new()
.read(true)
.write(true)
.open(&path)
.unwrap();
assert!(try_acquire(&f2, LockMode::Exclusive, FileLocking::Enabled).unwrap());
release(&f2).unwrap();
let _ = std::fs::remove_dir_all(&dir);
}
}