#[cfg(all(test, feature = "graph-cypher"))]
mod native_tests;
use std::collections::BTreeMap;
use std::fs::{File, OpenOptions};
#[cfg(unix)]
use std::os::fd::AsRawFd;
#[cfg(unix)]
use std::os::unix::fs::MetadataExt as _;
use std::path::{Path, PathBuf};
use std::sync::{Mutex, MutexGuard};
pub const STORE_LOCK_FILE: &str = "writer.lock";
type StoreKey = (u64, u64, bool);
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum LockMode {
Exclusive,
Shared,
}
struct RegistryEntry {
_file: File,
mode: LockMode,
shared_count: u32,
}
static HELD_STORES: Mutex<BTreeMap<StoreKey, RegistryEntry>> = Mutex::new(BTreeMap::new());
#[derive(Debug)]
pub struct StoreLock {
registry_key: StoreKey,
mode: LockMode,
}
impl StoreLock {
pub fn acquire(directory: &Path) -> Result<Self, StoreLockError> {
Self::acquire_exclusive(directory, true)
}
fn acquire_exclusive(directory: &Path, create: bool) -> Result<Self, StoreLockError> {
Self::exclusive_named(directory, create, false)
}
pub(crate) fn reclaim_exclusive(directory: &Path) -> Result<Self, StoreLockError> {
Self::exclusive_named(directory, true, true)
}
fn exclusive_named(
directory: &Path,
create: bool,
lease: bool,
) -> Result<Self, StoreLockError> {
let path = directory.join(if lease {
".ze-readers.lock"
} else {
STORE_LOCK_FILE
});
let identity = store_identity(directory).map_err(|source| StoreLockError::Io {
path: directory.to_path_buf(),
source,
})?;
let registry_key = (identity.0, identity.1, lease);
let mut held = held_stores();
if held.contains_key(®istry_key) {
return Err(StoreLockError::Io {
path,
source: std::io::Error::from(std::io::ErrorKind::WouldBlock),
});
}
let file = open_lock_file(&path, create).map_err(|source| StoreLockError::Io {
path: path.clone(),
source,
})?;
if let Err(source) = lock_exclusive(&file) {
drop(file);
return Err(StoreLockError::Io { path, source });
}
held.insert(
registry_key,
RegistryEntry {
_file: file,
mode: LockMode::Exclusive,
shared_count: 0,
},
);
Ok(Self {
registry_key,
mode: LockMode::Exclusive,
})
}
#[cfg(feature = "graph-cypher")]
pub(crate) fn acquire_shared(directory: &Path) -> Result<Self, StoreLockError> {
Self::shared_named(directory, false, false)
}
pub(crate) fn reader_lease(directory: &Path) -> Result<Self, StoreLockError> {
Self::shared_named(directory, true, true)
}
pub(crate) fn reader_lease_read_only(directory: &Path) -> Result<Option<Self>, StoreLockError> {
match Self::shared_named(directory, true, false) {
Ok(lock) => Ok(Some(lock)),
Err(StoreLockError::Io { source, .. })
if source.kind() == std::io::ErrorKind::NotFound =>
{
Ok(None)
}
Err(error) => Err(error),
}
}
fn shared_named(directory: &Path, lease: bool, create: bool) -> Result<Self, StoreLockError> {
let path = directory.join(if lease {
".ze-readers.lock"
} else {
STORE_LOCK_FILE
});
let identity = store_identity(directory).map_err(|source| StoreLockError::Io {
path: directory.to_path_buf(),
source,
})?;
let registry_key = (identity.0, identity.1, lease);
let mut held = held_stores();
if let Some(entry) = held.get_mut(®istry_key) {
if entry.mode == LockMode::Exclusive {
return Err(StoreLockError::Io {
path,
source: std::io::Error::from(std::io::ErrorKind::WouldBlock),
});
}
let Some(next_count) = entry.shared_count.checked_add(1) else {
return Err(StoreLockError::Io {
path,
source: std::io::Error::other("shared store lock holder count overflow"),
});
};
entry.shared_count = next_count;
return Ok(Self {
registry_key,
mode: LockMode::Shared,
});
}
let file = (if create {
open_lock_file(&path, true)
} else {
open_shared_lock_file(&path)
})
.map_err(|source| StoreLockError::Io {
path: path.clone(),
source,
})?;
if let Err(source) = lock_shared(&file) {
drop(file);
return Err(StoreLockError::Io { path, source });
}
held.insert(
registry_key,
RegistryEntry {
_file: file,
mode: LockMode::Shared,
shared_count: 1,
},
);
Ok(Self {
registry_key,
mode: LockMode::Shared,
})
}
}
fn held_stores() -> MutexGuard<'static, BTreeMap<StoreKey, RegistryEntry>> {
match HELD_STORES.lock() {
Ok(held) => held,
Err(poisoned) => poisoned.into_inner(),
}
}
fn open_shared_lock_file(path: &Path) -> std::io::Result<File> {
let file = open_shared_lock_handle(path)?;
if file.metadata()?.is_file() {
Ok(file)
} else {
Err(std::io::Error::new(
std::io::ErrorKind::IsADirectory,
"store lock path is not a regular file",
))
}
}
#[cfg(not(windows))]
fn open_shared_lock_handle(path: &Path) -> std::io::Result<File> {
let mut options = OpenOptions::new();
options.read(true);
#[cfg(unix)]
{
use std::os::unix::fs::OpenOptionsExt as _;
options.custom_flags(libc::O_NOFOLLOW);
}
options.open(path)
}
#[cfg(windows)]
fn open_shared_lock_handle(path: &Path) -> std::io::Result<File> {
use std::os::windows::fs::OpenOptionsExt as _;
OpenOptions::new()
.read(true)
.share_mode(crate::sys::windows::FILE_SHARE_READ | crate::sys::windows::FILE_SHARE_WRITE)
.open(path)
}
#[cfg(unix)]
pub(super) fn store_identity(directory: &Path) -> std::io::Result<(u64, u64)> {
let metadata = std::fs::metadata(directory)?;
Ok((metadata.dev(), metadata.ino()))
}
#[cfg(windows)]
pub(super) fn store_identity(directory: &Path) -> std::io::Result<(u64, u64)> {
crate::sys::windows::directory_identity(directory)
}
#[cfg(not(any(unix, windows)))]
pub(super) fn store_identity(_directory: &Path) -> std::io::Result<(u64, u64)> {
Err(std::io::Error::new(
std::io::ErrorKind::Unsupported,
"store identity requires a supported platform",
))
}
#[cfg(not(windows))]
fn open_lock_file(path: &Path, create: bool) -> std::io::Result<File> {
let mut options = OpenOptions::new();
options
.create(create)
.truncate(false)
.read(true)
.write(true);
#[cfg(unix)]
{
use std::os::unix::fs::OpenOptionsExt as _;
options.custom_flags(libc::O_NOFOLLOW);
}
options.open(path)
}
#[cfg(windows)]
fn open_lock_file(path: &Path, create: bool) -> std::io::Result<File> {
use std::os::windows::fs::OpenOptionsExt as _;
OpenOptions::new()
.create(create)
.truncate(false)
.read(true)
.write(true)
.share_mode(crate::sys::windows::FILE_SHARE_READ | crate::sys::windows::FILE_SHARE_WRITE)
.open(path)
}
#[cfg(unix)]
fn lock_exclusive(file: &File) -> std::io::Result<()> {
let mut lock = unsafe {
std::mem::zeroed::<libc::flock>()
};
lock.l_type = libc::F_WRLCK as libc::c_short;
lock.l_whence = libc::SEEK_SET as libc::c_short;
lock.l_start = 0;
lock.l_len = 0;
let result = unsafe {
libc::fcntl(file.as_raw_fd(), libc::F_SETLK, &lock)
};
if result == -1 {
let error = std::io::Error::last_os_error();
if error
.raw_os_error()
.is_some_and(|errno| errno == libc::EAGAIN || errno == libc::EACCES)
{
Err(std::io::Error::from(std::io::ErrorKind::WouldBlock))
} else {
Err(error)
}
} else {
Ok(())
}
}
#[cfg(unix)]
fn lock_shared(file: &File) -> std::io::Result<()> {
let mut lock = unsafe {
std::mem::zeroed::<libc::flock>()
};
lock.l_type = libc::F_RDLCK as libc::c_short;
lock.l_whence = libc::SEEK_SET as libc::c_short;
lock.l_start = 0;
lock.l_len = 0;
let result = unsafe {
libc::fcntl(file.as_raw_fd(), libc::F_SETLK, &lock)
};
if result == -1 {
let error = std::io::Error::last_os_error();
if error
.raw_os_error()
.is_some_and(|errno| errno == libc::EAGAIN || errno == libc::EACCES)
{
Err(std::io::Error::from(std::io::ErrorKind::WouldBlock))
} else {
Err(error)
}
} else {
Ok(())
}
}
#[cfg(windows)]
fn lock_shared(file: &File) -> std::io::Result<()> {
crate::sys::windows::lock_file_shared(file)
}
#[cfg(not(any(unix, windows)))]
fn lock_shared(_: &File) -> std::io::Result<()> {
Err(std::io::Error::new(
std::io::ErrorKind::Unsupported,
"shared store locks require a supported file-locking platform",
))
}
#[cfg(windows)]
fn lock_exclusive(file: &File) -> std::io::Result<()> {
crate::sys::windows::lock_file_exclusive(file)
}
#[cfg(not(any(unix, windows)))]
fn lock_exclusive(_: &File) -> std::io::Result<()> {
Err(std::io::Error::new(
std::io::ErrorKind::Unsupported,
"store locks require a supported file-locking platform",
))
}
impl Drop for StoreLock {
fn drop(&mut self) {
let mut held = held_stores();
let remove = match held.get_mut(&self.registry_key) {
Some(entry) if entry.mode == LockMode::Shared && self.mode == LockMode::Shared => {
if entry.shared_count > 1 {
entry.shared_count -= 1;
false
} else {
true
}
}
Some(entry)
if entry.mode == LockMode::Exclusive && self.mode == LockMode::Exclusive =>
{
true
}
_ => false,
};
if remove {
drop(held.remove(&self.registry_key));
}
}
}
#[derive(Debug)]
pub enum StoreLockError {
Io {
path: PathBuf,
source: std::io::Error,
},
}
impl std::fmt::Display for StoreLockError {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Io { path, source } => {
write!(formatter, "store lock {}: {source}", path.display())
}
}
}
}
impl std::error::Error for StoreLockError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::Io { source, .. } => Some(source),
}
}
}