use std::fs::{File, OpenOptions, TryLockError};
use std::io::{Read, Seek, SeekFrom, Write};
use std::path::Path;
use crate::error::{DbError, DbResult};
pub(crate) const LOCK_FILE_NAME: &str = "db.lock";
pub(crate) fn acquire(dir: &Path) -> DbResult<File> {
let path = dir.join(LOCK_FILE_NAME);
let mut file = OpenOptions::new()
.create(true)
.read(true)
.write(true)
.truncate(false)
.open(&path)?;
match file.try_lock() {
Ok(()) => {}
Err(TryLockError::WouldBlock) => {
return Err(DbError::Locked {
path: dir.to_path_buf(),
pid: read_pid(&mut file),
});
}
Err(TryLockError::Error(e)) => return Err(DbError::Io(e)),
}
if let Err(e) = write_pid(&mut file) {
tracing::warn!(?path, error = %e, "could not record pid in the lock file");
}
Ok(file)
}
fn read_pid(file: &mut File) -> Option<u32> {
let mut buf = String::new();
file.seek(SeekFrom::Start(0)).ok()?;
file.take(20).read_to_string(&mut buf).ok()?;
buf.trim().parse().ok()
}
fn write_pid(file: &mut File) -> std::io::Result<()> {
file.set_len(0)?;
file.seek(SeekFrom::Start(0))?;
writeln!(file, "{}", std::process::id())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::error::DbError;
#[test]
fn second_acquire_reports_locked_with_the_holder_pid() {
let dir = tempfile::tempdir().unwrap();
let _held = acquire(dir.path()).expect("first acquire must succeed");
let err = acquire(dir.path()).expect_err("second acquire must fail");
match err {
DbError::Locked { path, pid } => {
assert_eq!(path, dir.path());
assert_eq!(pid, Some(std::process::id()), "holder pid must be recorded");
}
other => panic!("expected DbError::Locked, got {other:?}"),
}
}
#[test]
fn lock_is_released_when_the_file_is_dropped() {
let dir = tempfile::tempdir().unwrap();
let held = acquire(dir.path()).expect("first acquire");
drop(held);
acquire(dir.path()).expect("reacquire after drop must succeed");
}
#[test]
fn unreadable_pid_yields_no_hint_rather_than_an_error() {
let dir = tempfile::tempdir().unwrap();
let _held = acquire(dir.path()).expect("first acquire");
std::fs::write(dir.path().join(LOCK_FILE_NAME), b"not-a-pid").unwrap();
let err = acquire(dir.path()).expect_err("second acquire must still fail");
match err {
DbError::Locked { pid, .. } => {
assert_eq!(pid, None, "garbage pid must degrade to None")
}
other => panic!("expected DbError::Locked, got {other:?}"),
}
}
}