use std::fmt;
use std::fs::{self, File, OpenOptions, TryLockError};
use std::io;
use std::path::{Path, PathBuf};
use super::DatabaseError;
pub(super) const LOCK_FILE: &str = "writer.lock";
pub struct DataDirLock {
file: File,
lock_path: PathBuf,
created_anchor: bool,
}
impl fmt::Debug for DataDirLock {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("DataDirLock")
.field("lock_path", &self.lock_path)
.field("file", &self.file)
.field("created_anchor", &self.created_anchor)
.finish()
}
}
impl DataDirLock {
pub fn acquire(data_dir: &Path) -> Result<Self, LockError> {
let lock_path = data_dir.join(LOCK_FILE);
let mut attempts = 0_u32;
let (file, created_anchor) = loop {
attempts += 1;
if attempts > 4 {
return Err(LockError::Io {
lock_path,
error: io::Error::other(
"writer.lock flapped between present and absent across repeated \
attempts; another process is churning the anchor",
),
created_anchor: false,
});
}
match fs::symlink_metadata(&lock_path) {
Ok(meta) if meta.file_type().is_file() => {
match OpenOptions::new().read(true).write(true).open(&lock_path) {
Ok(file) => break (file, false),
Err(error) if error.kind() == io::ErrorKind::NotFound => {}
Err(error) => {
return Err(LockError::Io {
lock_path,
error,
created_anchor: false,
});
}
}
}
Ok(_meta) => return Err(LockError::AnchorNotARegularFile { lock_path }),
Err(error) if error.kind() == io::ErrorKind::NotFound => {
match OpenOptions::new()
.read(true)
.write(true)
.create_new(true)
.open(&lock_path)
{
Ok(file) => break (file, true),
Err(error) if error.kind() == io::ErrorKind::AlreadyExists => {}
Err(error) => {
return Err(LockError::Io {
lock_path,
error,
created_anchor: false,
});
}
}
}
Err(error) => {
return Err(LockError::Io {
lock_path,
error,
created_anchor: false,
});
}
}
};
#[cfg(test)]
if FAIL_NEXT_TRY_LOCK.with(|flag| flag.replace(false)) {
return Err(LockError::Io {
lock_path,
error: io::Error::other("injected post-open lock failure (test seam)"),
created_anchor,
});
}
#[cfg(test)]
if CONTEND_NEXT_TRY_LOCK.with(|flag| flag.replace(false)) {
if let Ok(competitor) = OpenOptions::new().read(true).write(true).open(&lock_path)
&& competitor.try_lock().is_ok()
{
CONTENDERS.with(|held| held.borrow_mut().push(competitor));
}
}
match file.try_lock() {
Ok(()) => Ok(Self {
file,
lock_path,
created_anchor,
}),
Err(TryLockError::WouldBlock) => Err(LockError::AlreadyLocked {
lock_path,
created_anchor,
}),
Err(error) => Err(LockError::Io {
lock_path,
error: error.into(),
created_anchor,
}),
}
}
pub const fn created_anchor(&self) -> bool {
self.created_anchor
}
}
#[cfg(test)]
thread_local! {
static FAIL_NEXT_TRY_LOCK: std::cell::Cell<bool> = const { std::cell::Cell::new(false) };
static CONTEND_NEXT_TRY_LOCK: std::cell::Cell<bool> = const { std::cell::Cell::new(false) };
static CONTENDERS: std::cell::RefCell<Vec<File>> = const { std::cell::RefCell::new(Vec::new()) };
}
#[cfg(test)]
pub(super) fn fail_next_try_lock() {
FAIL_NEXT_TRY_LOCK.with(|flag| flag.set(true));
}
#[cfg(test)]
pub(super) fn contend_next_try_lock() {
CONTEND_NEXT_TRY_LOCK.with(|flag| flag.set(true));
}
#[cfg(test)]
pub(super) fn release_contenders() {
CONTENDERS.with(|held| held.borrow_mut().clear());
}
#[derive(Debug)]
pub enum LockError {
AlreadyLocked {
lock_path: PathBuf,
created_anchor: bool,
},
AnchorNotARegularFile { lock_path: PathBuf },
Io {
lock_path: PathBuf,
error: io::Error,
created_anchor: bool,
},
}
impl fmt::Display for LockError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::AlreadyLocked { lock_path, .. } => write!(
formatter,
"data dir writer lock at {} is held by another live writer",
lock_path.display()
),
Self::AnchorNotARegularFile { lock_path } => write!(
formatter,
"writer.lock at {} is not a regular file (symlink or special entry); the \
advisory anchor must be a plain file in the data dir — remove or replace \
it with a plain empty file, then re-run",
lock_path.display()
),
Self::Io {
lock_path, error, ..
} => write!(
formatter,
"failed to acquire data dir writer lock at {}: {error}",
lock_path.display()
),
}
}
}
impl std::error::Error for LockError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::Io { error, .. } => Some(error),
Self::AlreadyLocked { .. } | Self::AnchorNotARegularFile { .. } => None,
}
}
}
impl From<LockError> for DatabaseError {
fn from(error: LockError) -> Self {
match error {
LockError::AlreadyLocked { lock_path, .. } => Self::DataDirLocked { lock_path },
LockError::AnchorNotARegularFile { lock_path } => Self::LockFileIo {
error: io::Error::other(
"writer.lock is not a regular file (symlink or special entry); the \
advisory anchor must be a plain file in the data dir",
),
lock_path,
},
LockError::Io {
lock_path, error, ..
} => Self::LockFileIo { lock_path, error },
}
}
}