use std::{
collections::HashMap,
fs::{File, OpenOptions},
path::{Path, PathBuf},
sync::{Arc, LazyLock, Mutex, PoisonError},
};
use fs4::FileExt;
use crate::client::PimdirError;
const OWNER: &str = "owner.lock";
const OBJECTS: &str = "objects.lock";
static OWNED: LazyLock<Mutex<HashMap<PathBuf, Owned>>> =
LazyLock::new(|| Mutex::new(HashMap::new()));
struct Owned {
_file: File,
handles: usize,
}
pub struct PimdirLock {
_file: Option<File>,
registered: Option<PathBuf>,
}
impl PimdirLock {
pub fn own(dir: &Path) -> Result<Arc<Self>, PimdirError> {
let key = dir.canonicalize()?;
let mut owned = OWNED.lock().unwrap_or_else(PoisonError::into_inner);
match owned.get_mut(&key) {
Some(entry) => entry.handles += 1,
None => {
let file = open(&dir.join(OWNER))?;
FileExt::try_lock(&file).map_err(|_| PimdirError::Owned(dir.to_path_buf()))?;
owned.insert(
key.clone(),
Owned {
_file: file,
handles: 1,
},
);
}
}
Ok(Arc::new(Self {
_file: None,
registered: Some(key),
}))
}
pub fn collect(dir: &Path) -> Result<Self, PimdirError> {
let file = open(&dir.join(OBJECTS))?;
FileExt::try_lock(&file).map_err(|_| PimdirError::Staging(dir.to_path_buf()))?;
Ok(Self {
_file: Some(file),
registered: None,
})
}
pub fn stage(dir: &Path) -> Result<Self, PimdirError> {
let file = open(&dir.join(OBJECTS))?;
FileExt::lock_shared(&file)?;
Ok(Self {
_file: Some(file),
registered: None,
})
}
}
impl Drop for PimdirLock {
fn drop(&mut self) {
let Some(key) = &self.registered else {
return;
};
let mut owned = OWNED.lock().unwrap_or_else(PoisonError::into_inner);
let Some(entry) = owned.get_mut(key) else {
return;
};
entry.handles -= 1;
if entry.handles == 0 {
owned.remove(key);
}
}
}
fn open(path: &Path) -> Result<File, PimdirError> {
Ok(OpenOptions::new()
.create(true)
.read(true)
.write(true)
.truncate(false)
.open(path)?)
}
#[cfg(test)]
mod tests {
use alloc::{format, string::String, vec::Vec};
use std::{sync::Arc, thread};
use super::PimdirLock;
#[test]
fn a_handover_within_one_process_is_never_refused() {
let dir = tempfile::tempdir().unwrap();
let path = Arc::new(dir.path().to_path_buf());
let threads: Vec<_> = (0..4)
.map(|_| {
let path = Arc::clone(&path);
thread::spawn(move || {
for _ in 0..20_000 {
PimdirLock::own(&path).map_err(|err| format!("{err}"))?;
}
Ok::<(), String>(())
})
})
.collect();
for thread in threads {
thread.join().unwrap().expect("a handover was refused");
}
}
}