#![allow(unsafe_code)]
use std::collections::BTreeMap;
use std::path::PathBuf;
use std::sync::Arc;
use parking_lot::Mutex;
use crate::Result;
use crate::errors::PagedbError;
#[cfg(any(unix, windows))]
use super::blocking::offload;
#[derive(Debug, Clone, Copy)]
enum LockState {
Free,
Exclusive,
Shared(u32),
}
#[derive(Debug, Clone, Copy)]
pub(crate) enum LockKind {
Exclusive,
Shared,
}
struct InProcLockEntry {
state: Mutex<LockState>,
}
impl InProcLockEntry {
fn try_enter(&self, kind: LockKind) -> Result<()> {
let mut state = self.state.lock();
match (kind, *state) {
(LockKind::Exclusive, LockState::Free) => *state = LockState::Exclusive,
(LockKind::Shared, LockState::Free) => *state = LockState::Shared(1),
(LockKind::Shared, LockState::Shared(n)) => *state = LockState::Shared(n + 1),
_ => return Err(PagedbError::AlreadyLocked),
}
Ok(())
}
fn leave(&self, kind: LockKind) {
let mut state = self.state.lock();
match (kind, *state) {
(LockKind::Exclusive, LockState::Exclusive)
| (LockKind::Shared, LockState::Shared(1)) => *state = LockState::Free,
(LockKind::Shared, LockState::Shared(n)) if n > 1 => *state = LockState::Shared(n - 1),
_ => {}
}
}
}
pub(crate) struct LockTable {
entries: Mutex<BTreeMap<String, Arc<InProcLockEntry>>>,
}
impl LockTable {
pub(crate) fn new() -> Self {
Self {
entries: Mutex::new(BTreeMap::new()),
}
}
fn entry(&self, path: &str) -> Arc<InProcLockEntry> {
let mut entries = self.entries.lock();
entries
.entry(path.to_string())
.or_insert_with(|| {
Arc::new(InProcLockEntry {
state: Mutex::new(LockState::Free),
})
})
.clone()
}
}
#[cfg(unix)]
struct OsFcntlHandle {
_file: std::fs::File,
}
#[cfg(unix)]
impl OsFcntlHandle {
fn try_acquire(path: &std::path::Path, kind: LockKind) -> Result<Self> {
use std::os::unix::io::AsRawFd;
let file = std::fs::OpenOptions::new()
.create(true)
.truncate(false)
.read(true)
.write(true)
.open(path)
.map_err(PagedbError::Io)?;
let fd = file.as_raw_fd();
#[allow(clippy::cast_possible_truncation)]
let l_type = match kind {
LockKind::Exclusive => libc::F_WRLCK as libc::c_short,
LockKind::Shared => libc::F_RDLCK as libc::c_short,
};
#[allow(clippy::cast_possible_truncation)]
let flock = libc::flock {
l_type,
l_whence: libc::SEEK_SET as libc::c_short,
l_start: 0,
l_len: 0,
l_pid: 0,
};
#[cfg(target_os = "linux")]
let cmd = libc::F_OFD_SETLK;
#[cfg(not(target_os = "linux"))]
let cmd = libc::F_SETLK;
let rc = unsafe { libc::fcntl(fd, cmd, &flock) };
if rc == -1 {
let err = std::io::Error::last_os_error();
let raw = err.raw_os_error().unwrap_or(0);
if raw == libc::EAGAIN || raw == libc::EACCES {
return Err(PagedbError::AlreadyLocked);
}
return Err(PagedbError::Io(err));
}
Ok(Self { _file: file })
}
}
#[cfg(unix)]
unsafe impl Send for OsFcntlHandle {}
#[cfg(windows)]
struct OsLockFileExHandle {
file: std::fs::File,
}
#[cfg(windows)]
impl OsLockFileExHandle {
fn try_acquire(path: &std::path::Path, kind: LockKind) -> Result<Self> {
use std::os::windows::fs::OpenOptionsExt;
use std::os::windows::io::AsRawHandle;
use windows_sys::Win32::Foundation::{ERROR_IO_PENDING, ERROR_LOCK_VIOLATION};
use windows_sys::Win32::Storage::FileSystem::LockFileEx;
use windows_sys::Win32::Storage::FileSystem::{
LOCKFILE_EXCLUSIVE_LOCK, LOCKFILE_FAIL_IMMEDIATELY,
};
use windows_sys::Win32::System::IO::OVERLAPPED;
const FILE_SHARE_READ_WRITE: u32 = 0x0000_0003;
let file = std::fs::OpenOptions::new()
.create(true)
.truncate(false)
.read(true)
.write(true)
.share_mode(FILE_SHARE_READ_WRITE)
.open(path)
.map_err(PagedbError::Io)?;
let handle = file.as_raw_handle() as windows_sys::Win32::Foundation::HANDLE;
let flags = match kind {
LockKind::Exclusive => LOCKFILE_EXCLUSIVE_LOCK | LOCKFILE_FAIL_IMMEDIATELY,
LockKind::Shared => LOCKFILE_FAIL_IMMEDIATELY,
};
let mut overlapped: OVERLAPPED = unsafe { std::mem::zeroed() };
let rc = unsafe { LockFileEx(handle, flags, 0, u32::MAX, u32::MAX, &mut overlapped) };
if rc == 0 {
let err_code = unsafe { windows_sys::Win32::Foundation::GetLastError() };
if err_code == ERROR_LOCK_VIOLATION || err_code == ERROR_IO_PENDING {
return Err(PagedbError::AlreadyLocked);
}
return Err(PagedbError::Io(std::io::Error::last_os_error()));
}
Ok(Self { file })
}
}
#[cfg(windows)]
impl Drop for OsLockFileExHandle {
fn drop(&mut self) {
use std::os::windows::io::AsRawHandle;
use windows_sys::Win32::Storage::FileSystem::UnlockFileEx;
use windows_sys::Win32::System::IO::OVERLAPPED;
let handle = self.file.as_raw_handle() as windows_sys::Win32::Foundation::HANDLE;
let mut overlapped: OVERLAPPED = unsafe { std::mem::zeroed() };
let _ = unsafe { UnlockFileEx(handle, 0, u32::MAX, u32::MAX, &mut overlapped) };
}
}
#[cfg(windows)]
unsafe impl Send for OsLockFileExHandle {}
#[cfg(unix)]
type OsLock = OsFcntlHandle;
#[cfg(windows)]
type OsLock = OsLockFileExHandle;
pub struct NativeLockHandle {
entry: Arc<InProcLockEntry>,
kind: LockKind,
#[cfg(any(unix, windows))]
_os_lock: OsLock,
}
impl Drop for NativeLockHandle {
fn drop(&mut self) {
self.entry.leave(self.kind);
}
}
pub(crate) async fn acquire(
table: &LockTable,
logical_path: &str,
lock_path: PathBuf,
kind: LockKind,
) -> Result<NativeLockHandle> {
let entry = table.entry(logical_path);
entry.try_enter(kind)?;
#[cfg(any(unix, windows))]
{
let acquired = offload(move || {
if let Some(parent) = lock_path.parent() {
std::fs::create_dir_all(parent).map_err(PagedbError::Io)?;
}
OsLock::try_acquire(&lock_path, kind)
})
.await;
match acquired {
Ok(os_lock) => Ok(NativeLockHandle {
entry,
kind,
_os_lock: os_lock,
}),
Err(error) => {
entry.leave(kind);
Err(error)
}
}
}
#[cfg(not(any(unix, windows)))]
{
let _ = lock_path;
Ok(NativeLockHandle { entry, kind })
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn an_exclusive_entry_excludes_every_other_kind() {
let table = LockTable::new();
let entry = table.entry("/db");
entry.try_enter(LockKind::Exclusive).unwrap();
assert!(matches!(
entry.try_enter(LockKind::Exclusive),
Err(PagedbError::AlreadyLocked)
));
assert!(matches!(
entry.try_enter(LockKind::Shared),
Err(PagedbError::AlreadyLocked)
));
}
#[test]
fn shared_entries_stack_and_only_the_last_release_frees_the_domain() {
let table = LockTable::new();
let entry = table.entry("/db");
entry.try_enter(LockKind::Shared).unwrap();
entry.try_enter(LockKind::Shared).unwrap();
entry.leave(LockKind::Shared);
assert!(
matches!(
entry.try_enter(LockKind::Exclusive),
Err(PagedbError::AlreadyLocked)
),
"one shared holder remains, so exclusive must still be refused"
);
entry.leave(LockKind::Shared);
entry.try_enter(LockKind::Exclusive).unwrap();
}
#[test]
fn the_same_logical_path_always_maps_to_one_entry() {
let table = LockTable::new();
let first = table.entry("/db");
let second = table.entry("/db");
assert!(Arc::ptr_eq(&first, &second));
assert!(!Arc::ptr_eq(&first, &table.entry("/other")));
}
}