use std::fs::File;
use std::fs::OpenOptions;
use std::io;
use std::path::Path;
use std::path::PathBuf;
use std::time::Duration;
use std::time::Instant;
use crate::error::Result;
use crate::error::SnapshotError;
pub(crate) const LOCK_BUDGET: Duration = Duration::from_secs(5);
pub(crate) fn dir_in(partition: &Path) -> PathBuf {
partition.join("locks")
}
#[derive(Debug)]
pub(crate) struct SessionGuard {
_file: Option<File>,
}
impl SessionGuard {
#[allow(dead_code)]
pub(crate) fn is_enforced(&self) -> bool {
self._file.is_some()
}
fn held(file: File) -> Self {
Self { _file: Some(file) }
}
fn unenforced() -> Self {
Self { _file: None }
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum Disposition {
Contended,
Transient,
Unsupported,
Fatal,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum Site {
Open,
Lock,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum Platform {
Unix,
Windows,
}
impl Platform {
pub(crate) const CURRENT: Self = if cfg!(windows) {
Self::Windows
} else {
Self::Unix
};
}
const UNSUPPORTED_UNIX: [i32; 4] = [
95, 38, 37, 45, ];
const UNSUPPORTED_WINDOWS: [i32; 2] = [
1, 50, ];
const WINDOWS_SHARING_VIOLATION: i32 = 32;
pub(crate) fn classify(
kind: io::ErrorKind,
raw: Option<i32>,
site: Site,
platform: Platform,
) -> Disposition {
if let Some(code) = raw {
let unsupported = match platform {
Platform::Unix => UNSUPPORTED_UNIX.contains(&code),
Platform::Windows => UNSUPPORTED_WINDOWS.contains(&code),
};
if unsupported {
return Disposition::Unsupported;
}
if platform == Platform::Windows && code == WINDOWS_SHARING_VIOLATION && site == Site::Open
{
return Disposition::Contended;
}
}
match kind {
io::ErrorKind::WouldBlock => Disposition::Contended,
io::ErrorKind::StaleNetworkFileHandle | io::ErrorKind::Interrupted => {
Disposition::Transient
}
io::ErrorKind::Unsupported => Disposition::Unsupported,
_ => Disposition::Fatal,
}
}
pub(crate) fn acquire(
partition: &Path,
session_id: &str,
budget: Duration,
) -> Result<Option<SessionGuard>> {
crate::id::validate_stored("session id", session_id)?;
let dir = dir_in(partition);
std::fs::create_dir_all(&dir).map_err(|e| SnapshotError::io(&dir, e))?;
let path = dir.join(format!("{session_id}.lock"));
let deadline = Instant::now() + budget;
let mut backoff = Duration::from_millis(2);
loop {
match OpenOptions::new()
.read(true)
.write(true)
.create(true)
.truncate(false)
.open(&path)
{
Ok(file) => match file.try_lock() {
Ok(()) => return Ok(Some(SessionGuard::held(file))),
Err(std::fs::TryLockError::WouldBlock) => {}
Err(std::fs::TryLockError::Error(err)) => {
match classify(
err.kind(),
err.raw_os_error(),
Site::Lock,
Platform::CURRENT,
) {
Disposition::Unsupported => {
return Ok(Some(SessionGuard::unenforced()));
}
Disposition::Contended | Disposition::Transient => {}
Disposition::Fatal => return Err(SnapshotError::io(&path, err)),
}
}
},
Err(err) => match classify(
err.kind(),
err.raw_os_error(),
Site::Open,
Platform::CURRENT,
) {
Disposition::Unsupported => return Ok(Some(SessionGuard::unenforced())),
Disposition::Contended | Disposition::Transient => {}
Disposition::Fatal => return Err(SnapshotError::io(&path, err)),
},
}
if Instant::now() >= deadline {
return Ok(None);
}
std::thread::sleep(backoff);
backoff = (backoff * 2).min(Duration::from_millis(100));
}
}
#[cfg(test)]
#[path = "lock_tests.rs"]
mod tests;