1use std::fs::File;
26use std::io;
27
28#[derive(Debug, Clone, Copy, PartialEq, Eq)]
31pub enum LockMode {
32 Shared,
35 Exclusive,
38}
39
40#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
58pub enum FileLocking {
59 #[default]
62 Enabled,
63 Disabled,
66 BestEffort,
72}
73
74impl FileLocking {
75 pub fn from_env() -> Self {
78 Self::parse_env_value(std::env::var("HDF5_USE_FILE_LOCKING").ok().as_deref())
79 }
80
81 pub fn from_env_or(default: FileLocking) -> Self {
83 match std::env::var("HDF5_USE_FILE_LOCKING").ok().as_deref() {
84 None => default,
85 Some(v) => Self::parse_env_value(Some(v)),
86 }
87 }
88
89 pub(crate) fn parse_env_value(value: Option<&str>) -> Self {
90 match value {
91 None => FileLocking::Enabled,
92 Some(v) => {
93 let trimmed = v.trim();
94 if trimmed.eq_ignore_ascii_case("FALSE")
95 || trimmed == "0"
96 || trimmed.eq_ignore_ascii_case("OFF")
97 || trimmed.eq_ignore_ascii_case("NO")
98 {
99 FileLocking::Disabled
100 } else if trimmed.eq_ignore_ascii_case("BEST_EFFORT")
101 || trimmed.eq_ignore_ascii_case("BEST-EFFORT")
102 || trimmed.eq_ignore_ascii_case("BESTEFFORT")
103 {
104 FileLocking::BestEffort
105 } else {
106 FileLocking::Enabled
108 }
109 }
110 }
111 }
112}
113
114pub fn try_acquire(file: &File, mode: LockMode, policy: FileLocking) -> io::Result<bool> {
128 if matches!(policy, FileLocking::Disabled) {
129 return Ok(false);
130 }
131
132 const RETRY_ATTEMPTS: u32 = 10;
133 const RETRY_SLEEP: std::time::Duration = std::time::Duration::from_millis(10);
134
135 let mut attempt = match mode {
136 LockMode::Shared => file.try_lock_shared(),
137 LockMode::Exclusive => file.try_lock(),
138 };
139 for _ in 0..RETRY_ATTEMPTS {
140 if !matches!(attempt, Err(std::fs::TryLockError::WouldBlock)) {
141 break;
142 }
143 std::thread::sleep(RETRY_SLEEP);
144 attempt = match mode {
145 LockMode::Shared => file.try_lock_shared(),
146 LockMode::Exclusive => file.try_lock(),
147 };
148 }
149
150 match attempt {
151 Ok(()) => Ok(true),
152 Err(std::fs::TryLockError::WouldBlock) => match policy {
153 FileLocking::Enabled => Err(io::Error::new(
154 io::ErrorKind::WouldBlock,
155 "unable to lock file: another process holds a conflicting lock",
156 )),
157 FileLocking::BestEffort => Ok(false),
158 FileLocking::Disabled => unreachable!(),
159 },
160 Err(std::fs::TryLockError::Error(e)) => match policy {
161 FileLocking::Enabled => Err(e),
162 FileLocking::BestEffort => Ok(false),
163 FileLocking::Disabled => unreachable!(),
164 },
165 }
166}
167
168pub fn release(file: &File) -> io::Result<()> {
172 file.unlock()
173}
174
175#[cfg(test)]
176mod tests {
177 use super::*;
178
179 #[test]
180 fn parse_env_value_defaults_to_enabled() {
181 assert_eq!(FileLocking::parse_env_value(None), FileLocking::Enabled);
182 }
183
184 #[test]
185 fn parse_env_value_recognizes_disabled() {
186 for v in ["FALSE", "false", "0", "off", "no"] {
187 assert_eq!(
188 FileLocking::parse_env_value(Some(v)),
189 FileLocking::Disabled,
190 "value: {v}",
191 );
192 }
193 }
194
195 #[test]
196 fn parse_env_value_recognizes_best_effort() {
197 for v in ["BEST_EFFORT", "best_effort", "best-effort", "BestEffort"] {
198 assert_eq!(
199 FileLocking::parse_env_value(Some(v)),
200 FileLocking::BestEffort,
201 "value: {v}",
202 );
203 }
204 }
205
206 #[test]
207 fn parse_env_value_recognizes_enabled() {
208 for v in ["TRUE", "true", "1", "on", "yes", "garbage"] {
209 assert_eq!(
210 FileLocking::parse_env_value(Some(v)),
211 FileLocking::Enabled,
212 "value: {v}",
213 );
214 }
215 }
216
217 #[test]
218 fn try_acquire_disabled_is_noop() {
219 let dir =
220 std::env::temp_dir().join(format!("rust_hdf5_lock_disabled_{}", std::process::id()));
221 std::fs::create_dir_all(&dir).unwrap();
222 let path = dir.join("noop.bin");
223 let f = std::fs::File::create(&path).unwrap();
224 let acquired = try_acquire(&f, LockMode::Exclusive, FileLocking::Disabled).unwrap();
225 assert!(!acquired, "Disabled policy must not acquire a lock");
226 let _ = std::fs::remove_dir_all(&dir);
227 }
228
229 #[test]
230 fn try_acquire_exclusive_then_shared_fails() {
231 let dir = std::env::temp_dir().join(format!("rust_hdf5_lock_excl_{}", std::process::id()));
232 std::fs::create_dir_all(&dir).unwrap();
233 let path = dir.join("conflict.bin");
234 let f1 = std::fs::File::create(&path).unwrap();
235 assert!(try_acquire(&f1, LockMode::Exclusive, FileLocking::Enabled).unwrap());
236 let f2 = std::fs::OpenOptions::new().read(true).open(&path).unwrap();
237 let res = try_acquire(&f2, LockMode::Shared, FileLocking::Enabled);
238 assert!(res.is_err(), "expected lock conflict");
239 let res2 = try_acquire(&f2, LockMode::Shared, FileLocking::BestEffort).unwrap();
241 assert!(!res2, "best-effort must report unsuccessful lock as false");
242 release(&f1).unwrap();
243 let _ = std::fs::remove_dir_all(&dir);
244 }
245
246 #[test]
247 fn shared_locks_coexist() {
248 let dir =
249 std::env::temp_dir().join(format!("rust_hdf5_lock_shared_{}", std::process::id()));
250 std::fs::create_dir_all(&dir).unwrap();
251 let path = dir.join("shared.bin");
252 std::fs::File::create(&path).unwrap();
253 let f1 = std::fs::OpenOptions::new().read(true).open(&path).unwrap();
254 let f2 = std::fs::OpenOptions::new().read(true).open(&path).unwrap();
255 assert!(try_acquire(&f1, LockMode::Shared, FileLocking::Enabled).unwrap());
256 assert!(try_acquire(&f2, LockMode::Shared, FileLocking::Enabled).unwrap());
257 release(&f1).unwrap();
258 release(&f2).unwrap();
259 let _ = std::fs::remove_dir_all(&dir);
260 }
261
262 #[test]
263 fn release_then_relock_works() {
264 let dir =
265 std::env::temp_dir().join(format!("rust_hdf5_lock_release_{}", std::process::id()));
266 std::fs::create_dir_all(&dir).unwrap();
267 let path = dir.join("release.bin");
268 let f1 = std::fs::File::create(&path).unwrap();
269 assert!(try_acquire(&f1, LockMode::Exclusive, FileLocking::Enabled).unwrap());
270 release(&f1).unwrap();
272
273 let f2 = std::fs::OpenOptions::new()
274 .read(true)
275 .write(true)
276 .open(&path)
277 .unwrap();
278 assert!(try_acquire(&f2, LockMode::Exclusive, FileLocking::Enabled).unwrap());
279
280 release(&f2).unwrap();
281 let _ = std::fs::remove_dir_all(&dir);
282 }
283}