| use lazy_static::lazy_static; |
| use std::fs::{self, File, OpenOptions}; |
| use std::io; |
| use std::path::{Path, PathBuf}; |
| use std::sync::atomic::{AtomicBool, Ordering}; |
| use std::sync::{Arc, Mutex, MutexGuard, PoisonError}; |
| use std::thread; |
| use std::time::{Duration, SystemTime}; |
| |
| lazy_static! { |
| static ref LOCK: Mutex<()> = Mutex::new(()); |
| } |
| |
| pub struct Lock { |
| intraprocess_guard: Guard, |
| lockfile: FileLock, |
| } |
| |
| // High-quality lock to coordinate different #[test] functions within the *same* |
| // integration test crate. |
| enum Guard { |
| NotLocked, |
| Locked(MutexGuard<'static, ()>), |
| } |
| |
| // Best-effort filesystem lock to coordinate different #[test] functions across |
| // *different* integration tests. |
| enum FileLock { |
| NotLocked, |
| Locked { |
| path: PathBuf, |
| done: Arc<AtomicBool>, |
| }, |
| } |
| |
| impl Lock { |
| pub fn acquire(path: impl AsRef<Path>) -> Self { |
| Lock { |
| intraprocess_guard: Guard::acquire(), |
| lockfile: FileLock::acquire(path), |
| } |
| } |
| } |
| |
| impl Guard { |
| fn acquire() -> Self { |
| Guard::Locked(LOCK.lock().unwrap_or_else(PoisonError::into_inner)) |
| } |
| } |
| |
| impl FileLock { |
| fn acquire(path: impl AsRef<Path>) -> Self { |
| let path = path.as_ref().to_owned(); |
| let lockfile = match create(&path) { |
| None => return FileLock::NotLocked, |
| Some(lockfile) => lockfile, |
| }; |
| let done = Arc::new(AtomicBool::new(false)); |
| thread::spawn({ |
| let done = Arc::clone(&done); |
| move || poll(lockfile, done) |
| }); |
| FileLock::Locked { path, done } |
| } |
| } |
| |
| impl Drop for Lock { |
| fn drop(&mut self) { |
| let Lock { |
| intraprocess_guard, |
| lockfile, |
| } = self; |
| // Unlock file lock first. |
| *lockfile = FileLock::NotLocked; |
| *intraprocess_guard = Guard::NotLocked; |
| } |
| } |
| |
| impl Drop for FileLock { |
| fn drop(&mut self) { |
| match self { |
| FileLock::NotLocked => {} |
| FileLock::Locked { path, done } => { |
| done.store(true, Ordering::Release); |
| let _ = fs::remove_file(path); |
| } |
| } |
| } |
| } |
| |
| fn create(path: &Path) -> Option<File> { |
| loop { |
| match OpenOptions::new().write(true).create_new(true).open(path) { |
| // Acquired lock by creating lockfile. |
| Ok(lockfile) => return Some(lockfile), |
| Err(io_error) => match io_error.kind() { |
| // Lock is already held by another test. |
| io::ErrorKind::AlreadyExists => {} |
| // File based locking isn't going to work for some reason. |
| _ => return None, |
| }, |
| } |
| |
| // Check whether it's okay to bust the lock. |
| let metadata = match fs::metadata(path) { |
| Ok(metadata) => metadata, |
| Err(io_error) => match io_error.kind() { |
| // Other holder of the lock finished. Retry. |
| io::ErrorKind::NotFound => continue, |
| _ => return None, |
| }, |
| }; |
| |
| let modified = match metadata.modified() { |
| Ok(modified) => modified, |
| Err(_) => return None, |
| }; |
| |
| let now = SystemTime::now(); |
| let considered_stale = now - Duration::from_millis(1500); |
| let considered_future = now + Duration::from_millis(1500); |
| if modified < considered_stale || considered_future < modified { |
| return File::create(path).ok(); |
| } |
| |
| // Try again shortly. |
| thread::sleep(Duration::from_millis(500)); |
| } |
| } |
| |
| // Bump mtime periodically while test directory is in use. |
| fn poll(lockfile: File, done: Arc<AtomicBool>) { |
| loop { |
| thread::sleep(Duration::from_millis(500)); |
| if done.load(Ordering::Acquire) || lockfile.set_len(0).is_err() { |
| return; |
| } |
| } |
| } |