use std::cell::RefCell;
use std::fs::{self, File, OpenOptions};
use std::path::{Path, PathBuf};
use crate::error::{Error, Result};
thread_local! {
static HELD: RefCell<Vec<PathBuf>> = const { RefCell::new(Vec::new()) };
}
pub(crate) fn lock_path_for(store_path: &Path) -> PathBuf {
let mut os = store_path.as_os_str().to_os_string();
os.push(".lock");
PathBuf::from(os)
}
#[derive(Debug)]
pub(crate) enum StoreLock {
Owner { file: File, key: PathBuf },
Reentrant,
}
impl StoreLock {
pub(crate) fn acquire(store_path: &Path) -> Result<Self> {
let path = lock_path_for(&super::atomic::resolve(store_path));
let key = fs::canonicalize(&path).unwrap_or_else(|_| path.clone());
if HELD.with(|held| held.borrow().contains(&key)) {
return Ok(Self::Reentrant);
}
let mut opts = OpenOptions::new();
opts.read(true).write(true).create(true).truncate(false);
#[cfg(unix)]
{
use std::os::unix::fs::OpenOptionsExt;
opts.mode(0o600);
}
let file = opts.open(&path).map_err(|e| {
Error::io(format!(
"cannot open the store lock {}: {e}",
path.display()
))
.with_source(e)
})?;
file.lock().map_err(|e| {
Error::io(format!("cannot lock {}: {e}", path.display())).with_source(e)
})?;
let key = fs::canonicalize(&path).unwrap_or(key);
HELD.with(|held| held.borrow_mut().push(key.clone()));
Ok(Self::Owner { file, key })
}
pub(crate) fn is_reentrant(&self) -> bool {
matches!(self, Self::Reentrant)
}
}
impl Drop for StoreLock {
fn drop(&mut self) {
if let Self::Owner { file, key } = self {
let _ = file.unlock();
HELD.with(|held| held.borrow_mut().retain(|held_key| held_key != key));
}
}
}
pub(crate) fn refresh_lock_path_for(store_path: &Path) -> PathBuf {
let mut os = store_path.as_os_str().to_os_string();
os.push(".refresh.lock");
PathBuf::from(os)
}
#[derive(Debug)]
pub(crate) struct RefreshLock {
file: File,
}
impl RefreshLock {
pub(crate) fn acquire(store_path: &Path) -> Result<Self> {
let path = refresh_lock_path_for(&super::atomic::resolve(store_path));
let mut opts = OpenOptions::new();
opts.read(true).write(true).create(true).truncate(false);
#[cfg(unix)]
{
use std::os::unix::fs::OpenOptionsExt;
opts.mode(0o600);
}
let file = opts.open(&path).map_err(|e| {
Error::io(format!(
"cannot open the refresh lock {}: {e}",
path.display()
))
.with_source(e)
})?;
file.lock().map_err(|e| {
Error::io(format!("cannot lock {}: {e}", path.display())).with_source(e)
})?;
Ok(Self { file })
}
pub(crate) async fn acquire_off_runtime(store_path: PathBuf) -> Result<Self> {
tokio::task::spawn_blocking(move || Self::acquire(&store_path))
.await
.map_err(|e| Error::Internal(format!("refresh lock task failed: {e}")))?
}
}
impl Drop for RefreshLock {
fn drop(&mut self) {
let _ = self.file.unlock();
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn refresh_lock_path_sits_beside_the_store() {
assert_eq!(
refresh_lock_path_for(Path::new("/h/.xurl")),
PathBuf::from("/h/.xurl.refresh.lock")
);
}
#[test]
fn a_second_refresh_lock_waits_for_the_first_to_drop() {
let dir = tempfile::TempDir::new().unwrap();
let store = dir.path().join(".xurl");
let first = RefreshLock::acquire(&store).unwrap();
let (acquired, on_acquire) = std::sync::mpsc::channel();
let waiter = std::thread::spawn({
let store = store.clone();
move || {
let _second = RefreshLock::acquire(&store).unwrap();
acquired.send(()).unwrap();
}
});
assert!(
on_acquire
.recv_timeout(std::time::Duration::from_millis(200))
.is_err(),
"the second acquire completed while the first was held"
);
drop(first);
on_acquire
.recv_timeout(std::time::Duration::from_secs(5))
.expect("the second acquire completes once the first drops");
waiter.join().unwrap();
}
#[test]
fn lock_path_sits_beside_the_store() {
assert_eq!(
lock_path_for(Path::new("/h/.xurl")),
PathBuf::from("/h/.xurl.lock")
);
}
#[test]
fn a_missing_parent_directory_is_an_error() {
let dir = tempfile::TempDir::new().unwrap();
let store = dir.path().join("absent").join(".xurl");
assert!(StoreLock::acquire(&store).is_err());
}
#[test]
fn a_second_acquire_on_the_same_thread_is_reentrant_until_the_owner_drops() {
let dir = tempfile::TempDir::new().unwrap();
let store = dir.path().join(".xurl");
let owner = StoreLock::acquire(&store).unwrap();
assert!(!owner.is_reentrant());
assert!(StoreLock::acquire(&store).unwrap().is_reentrant());
drop(owner);
assert!(!StoreLock::acquire(&store).unwrap().is_reentrant());
}
}