163 lines
5 KiB
Rust
163 lines
5 KiB
Rust
use anyhow::Result;
|
|
use anyhow::*;
|
|
use fail::fail_point;
|
|
use std::{
|
|
fs::{remove_file, File},
|
|
io::Write,
|
|
path::{Path, PathBuf},
|
|
time::Instant,
|
|
};
|
|
use uuid::Uuid;
|
|
use walkdir::WalkDir;
|
|
|
|
use rand::{rngs::OsRng, RngCore};
|
|
use std::{thread, time};
|
|
|
|
pub struct Lock {
|
|
path: PathBuf,
|
|
}
|
|
|
|
const MAX_TIMEOUT_MILLIS: u16 = 8192;
|
|
const FILE_EXTENSION: &str = ".lock";
|
|
|
|
impl Lock {
|
|
#[allow(clippy::self_named_constructors)]
|
|
pub fn lock(index_directory: &Path) -> Result<Self> {
|
|
Lock::lock_with_timeout(index_directory, MAX_TIMEOUT_MILLIS)
|
|
}
|
|
|
|
pub fn lock_with_timeout(index_directory: &Path, max_timeout_millis: u16) -> Result<Self> {
|
|
let mut buffer = [0u8; 16];
|
|
OsRng.fill_bytes(&mut buffer);
|
|
let id = Uuid::from_bytes(buffer);
|
|
Lock::wait_to_have_sole_lock(id, index_directory, max_timeout_millis)?;
|
|
let path = Lock::lock_file_path(index_directory, id)?;
|
|
Ok(Lock { path })
|
|
}
|
|
|
|
pub fn release(self) -> Result<()> {
|
|
self.delete_lock_file()?;
|
|
Ok(())
|
|
}
|
|
|
|
fn delete_lock_file(&self) -> Result<()> {
|
|
if self.path.exists() {
|
|
remove_file(&self.path)?;
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn wait_to_have_sole_lock(lock_id: Uuid, index_directory: &Path, max_timeout_millis: u16) -> Result<()> {
|
|
let start_time = Instant::now();
|
|
let _ = Lock::create_lock_file(lock_id, index_directory);
|
|
while !Lock::sole_lock(lock_id, index_directory)? {
|
|
let path = Lock::lock_file_path(index_directory, lock_id)?;
|
|
if path.exists() {
|
|
remove_file(path)?;
|
|
}
|
|
let sleep_duration = time::Duration::from_millis((OsRng.next_u32() % 64).into());
|
|
thread::sleep(sleep_duration);
|
|
|
|
// timeout will take care of permanent errors
|
|
let _ = Lock::create_lock_file(lock_id, index_directory);
|
|
|
|
if start_time.elapsed().as_millis() > max_timeout_millis.into() {
|
|
return Err(anyhow!("timed out waiting on lock"));
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn sole_lock(lock_id: Uuid, index_directory: &Path) -> Result<bool> {
|
|
let my_lock_file_path = Lock::lock_file_path(index_directory, lock_id)?;
|
|
|
|
let walker = WalkDir::new(index_directory);
|
|
let all_locks: Vec<_> = walker
|
|
.into_iter()
|
|
.filter_map(|e| e.ok())
|
|
.filter(|e| e.file_name().to_string_lossy().ends_with(FILE_EXTENSION))
|
|
.collect();
|
|
if all_locks.len() != 1 {
|
|
return Ok(false);
|
|
}
|
|
let walker = WalkDir::new(index_directory);
|
|
let my_locks: Vec<_> = walker
|
|
.into_iter()
|
|
.filter_map(|e| e.ok())
|
|
.filter(|e| e.path() == my_lock_file_path)
|
|
.collect();
|
|
if my_locks.len() != 1 {
|
|
return Ok(false);
|
|
}
|
|
let result = all_locks.first().unwrap().path() == my_locks.first().unwrap().path();
|
|
Ok(result)
|
|
}
|
|
|
|
fn create_lock_file(lock_id: Uuid, index_directory: &Path) -> Result<()> {
|
|
let lock_file_path = Lock::lock_file_path(index_directory, lock_id)?;
|
|
fail_point!("create-lock-file", |e: Option<String>| Err(anyhow!(e.unwrap())));
|
|
let mut file = File::create(lock_file_path)?;
|
|
let lock_id_text = lock_id.to_hyphenated().to_string();
|
|
let lock_id_bytes = lock_id_text.as_bytes();
|
|
Ok(file.write_all(lock_id_bytes)?)
|
|
}
|
|
|
|
fn lock_file_path(path: &Path, lock_id: Uuid) -> Result<PathBuf> {
|
|
let file_name = format!("{}{}", lock_id, FILE_EXTENSION);
|
|
Ok(path.join(&file_name))
|
|
}
|
|
}
|
|
|
|
impl Drop for Lock {
|
|
fn drop(&mut self) {
|
|
let _ = self.delete_lock_file();
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod must {
|
|
use super::Lock;
|
|
use anyhow::Result;
|
|
use pretty_assertions::assert_eq;
|
|
|
|
use tempfile::tempdir;
|
|
#[cfg(feature = "failpoints")]
|
|
use two_rusty_forks::rusty_fork_test;
|
|
|
|
#[test]
|
|
fn be_released_when_dropped() -> Result<()> {
|
|
let temp_dir = tempdir()?;
|
|
let initial_number_of_entries = temp_dir.path().read_dir()?.count();
|
|
{
|
|
let _lock = Lock::lock(temp_dir.path())?;
|
|
}
|
|
let entries = temp_dir.path().read_dir()?.count();
|
|
|
|
assert_eq!(entries, initial_number_of_entries);
|
|
Ok(())
|
|
}
|
|
|
|
#[cfg(feature = "failpoints")]
|
|
rusty_fork_test! {
|
|
#[test]
|
|
fn be_able_to_lock_when_creating_lock_file_fails_sometimes() {
|
|
fail::cfg("create-lock-file", "90%10*return(some lock file creation error)->off").unwrap();
|
|
let temp_dir = tempdir().unwrap();
|
|
|
|
let lock = Lock::lock(temp_dir.path()).unwrap();
|
|
lock.release().unwrap();
|
|
}
|
|
}
|
|
|
|
#[cfg(feature = "failpoints")]
|
|
rusty_fork_test! {
|
|
#[test]
|
|
fn know_to_give_up_when_creating_lock_file_always_fails() {
|
|
fail::cfg("create-lock-file", "return(persistent lock file creation error)").unwrap();
|
|
let temp_dir = tempdir().unwrap();
|
|
|
|
assert!(Lock::lock_with_timeout(temp_dir.path(), 1).is_err());
|
|
}
|
|
}
|
|
}
|