zeppelin_embed/lifecycle/
lock.rs1#[cfg(all(test, feature = "graph-cypher"))]
24mod native_tests;
25
26use std::collections::BTreeMap;
27use std::fs::{File, OpenOptions};
28#[cfg(unix)]
29use std::os::fd::AsRawFd;
30#[cfg(unix)]
31use std::os::unix::fs::MetadataExt as _;
32use std::path::{Path, PathBuf};
33use std::sync::{Mutex, MutexGuard};
34
35pub const STORE_LOCK_FILE: &str = "writer.lock";
37
38type StoreKey = (u64, u64, bool);
40
41#[derive(Clone, Copy, Debug, Eq, PartialEq)]
42enum LockMode {
43 Exclusive,
44 Shared,
45}
46
47struct RegistryEntry {
48 _file: File,
49 mode: LockMode,
50 shared_count: u32,
51}
52
53static HELD_STORES: Mutex<BTreeMap<StoreKey, RegistryEntry>> = Mutex::new(BTreeMap::new());
54
55#[derive(Debug)]
57pub struct StoreLock {
58 registry_key: StoreKey,
59 mode: LockMode,
60}
61
62impl StoreLock {
63 pub fn acquire(directory: &Path) -> Result<Self, StoreLockError> {
68 Self::acquire_exclusive(directory, true)
69 }
70
71 fn acquire_exclusive(directory: &Path, create: bool) -> Result<Self, StoreLockError> {
72 Self::exclusive_named(directory, create, false)
73 }
74
75 pub(crate) fn reclaim_exclusive(directory: &Path) -> Result<Self, StoreLockError> {
76 Self::exclusive_named(directory, true, true)
77 }
78
79 fn exclusive_named(
80 directory: &Path,
81 create: bool,
82 lease: bool,
83 ) -> Result<Self, StoreLockError> {
84 let path = directory.join(if lease {
85 ".ze-readers.lock"
86 } else {
87 STORE_LOCK_FILE
88 });
89 let identity = store_identity(directory).map_err(|source| StoreLockError::Io {
90 path: directory.to_path_buf(),
91 source,
92 })?;
93 let registry_key = (identity.0, identity.1, lease);
94 let mut held = held_stores();
95 if held.contains_key(®istry_key) {
96 return Err(StoreLockError::Io {
97 path,
98 source: std::io::Error::from(std::io::ErrorKind::WouldBlock),
99 });
100 }
101 let file = open_lock_file(&path, create).map_err(|source| StoreLockError::Io {
102 path: path.clone(),
103 source,
104 })?;
105 if let Err(source) = lock_exclusive(&file) {
106 drop(file);
107 return Err(StoreLockError::Io { path, source });
108 }
109 held.insert(
110 registry_key,
111 RegistryEntry {
112 _file: file,
113 mode: LockMode::Exclusive,
114 shared_count: 0,
115 },
116 );
117 Ok(Self {
118 registry_key,
119 mode: LockMode::Exclusive,
120 })
121 }
122
123 #[cfg(feature = "graph-cypher")]
130 pub(crate) fn acquire_shared(directory: &Path) -> Result<Self, StoreLockError> {
131 Self::shared_named(directory, false, false)
132 }
133
134 pub(crate) fn reader_lease(directory: &Path) -> Result<Self, StoreLockError> {
135 Self::shared_named(directory, true, true)
136 }
137
138 pub(crate) fn reader_lease_read_only(directory: &Path) -> Result<Option<Self>, StoreLockError> {
139 match Self::shared_named(directory, true, false) {
140 Ok(lock) => Ok(Some(lock)),
141 Err(StoreLockError::Io { source, .. })
142 if source.kind() == std::io::ErrorKind::NotFound =>
143 {
144 Ok(None)
145 }
146 Err(error) => Err(error),
147 }
148 }
149
150 fn shared_named(directory: &Path, lease: bool, create: bool) -> Result<Self, StoreLockError> {
151 let path = directory.join(if lease {
152 ".ze-readers.lock"
153 } else {
154 STORE_LOCK_FILE
155 });
156 let identity = store_identity(directory).map_err(|source| StoreLockError::Io {
157 path: directory.to_path_buf(),
158 source,
159 })?;
160 let registry_key = (identity.0, identity.1, lease);
161 let mut held = held_stores();
162 if let Some(entry) = held.get_mut(®istry_key) {
163 if entry.mode == LockMode::Exclusive {
164 return Err(StoreLockError::Io {
165 path,
166 source: std::io::Error::from(std::io::ErrorKind::WouldBlock),
167 });
168 }
169 let Some(next_count) = entry.shared_count.checked_add(1) else {
170 return Err(StoreLockError::Io {
171 path,
172 source: std::io::Error::other("shared store lock holder count overflow"),
173 });
174 };
175 entry.shared_count = next_count;
176 return Ok(Self {
177 registry_key,
178 mode: LockMode::Shared,
179 });
180 }
181
182 let file = (if create {
183 open_lock_file(&path, true)
184 } else {
185 open_shared_lock_file(&path)
186 })
187 .map_err(|source| StoreLockError::Io {
188 path: path.clone(),
189 source,
190 })?;
191 if let Err(source) = lock_shared(&file) {
192 drop(file);
193 return Err(StoreLockError::Io { path, source });
194 }
195 held.insert(
196 registry_key,
197 RegistryEntry {
198 _file: file,
199 mode: LockMode::Shared,
200 shared_count: 1,
201 },
202 );
203 Ok(Self {
204 registry_key,
205 mode: LockMode::Shared,
206 })
207 }
208}
209
210fn held_stores() -> MutexGuard<'static, BTreeMap<StoreKey, RegistryEntry>> {
211 match HELD_STORES.lock() {
212 Ok(held) => held,
213 Err(poisoned) => poisoned.into_inner(),
214 }
215}
216
217fn open_shared_lock_file(path: &Path) -> std::io::Result<File> {
219 let file = open_shared_lock_handle(path)?;
220 if file.metadata()?.is_file() {
221 Ok(file)
222 } else {
223 Err(std::io::Error::new(
224 std::io::ErrorKind::IsADirectory,
225 "store lock path is not a regular file",
226 ))
227 }
228}
229
230#[cfg(not(windows))]
232fn open_shared_lock_handle(path: &Path) -> std::io::Result<File> {
233 let mut options = OpenOptions::new();
234 options.read(true);
235 #[cfg(unix)]
236 {
237 use std::os::unix::fs::OpenOptionsExt as _;
238 options.custom_flags(libc::O_NOFOLLOW);
239 }
240 options.open(path)
241}
242
243#[cfg(windows)]
255fn open_shared_lock_handle(path: &Path) -> std::io::Result<File> {
256 use std::os::windows::fs::OpenOptionsExt as _;
258
259 OpenOptions::new()
260 .read(true)
261 .share_mode(crate::sys::windows::FILE_SHARE_READ | crate::sys::windows::FILE_SHARE_WRITE)
262 .open(path)
263}
264
265#[cfg(unix)]
267pub(super) fn store_identity(directory: &Path) -> std::io::Result<(u64, u64)> {
268 let metadata = std::fs::metadata(directory)?;
269 Ok((metadata.dev(), metadata.ino()))
270}
271
272#[cfg(windows)]
273pub(super) fn store_identity(directory: &Path) -> std::io::Result<(u64, u64)> {
274 crate::sys::windows::directory_identity(directory)
275}
276
277#[cfg(not(any(unix, windows)))]
278pub(super) fn store_identity(_directory: &Path) -> std::io::Result<(u64, u64)> {
279 Err(std::io::Error::new(
280 std::io::ErrorKind::Unsupported,
281 "store identity requires a supported platform",
282 ))
283}
284
285#[cfg(not(windows))]
287fn open_lock_file(path: &Path, create: bool) -> std::io::Result<File> {
288 let mut options = OpenOptions::new();
289 options
290 .create(create)
291 .truncate(false)
292 .read(true)
293 .write(true);
294 #[cfg(unix)]
295 {
296 use std::os::unix::fs::OpenOptionsExt as _;
297 options.custom_flags(libc::O_NOFOLLOW);
298 }
299 options.open(path)
300}
301
302#[cfg(windows)]
312fn open_lock_file(path: &Path, create: bool) -> std::io::Result<File> {
313 use std::os::windows::fs::OpenOptionsExt as _;
315
316 OpenOptions::new()
317 .create(create)
318 .truncate(false)
319 .read(true)
320 .write(true)
321 .share_mode(crate::sys::windows::FILE_SHARE_READ | crate::sys::windows::FILE_SHARE_WRITE)
322 .open(path)
323}
324
325#[cfg(unix)]
326fn lock_exclusive(file: &File) -> std::io::Result<()> {
327 let mut lock = unsafe {
328 std::mem::zeroed::<libc::flock>()
330 };
331 lock.l_type = libc::F_WRLCK as libc::c_short;
332 lock.l_whence = libc::SEEK_SET as libc::c_short;
333 lock.l_start = 0;
334 lock.l_len = 0;
335 let result = unsafe {
336 libc::fcntl(file.as_raw_fd(), libc::F_SETLK, &lock)
339 };
340 if result == -1 {
341 let error = std::io::Error::last_os_error();
342 if error
343 .raw_os_error()
344 .is_some_and(|errno| errno == libc::EAGAIN || errno == libc::EACCES)
345 {
346 Err(std::io::Error::from(std::io::ErrorKind::WouldBlock))
347 } else {
348 Err(error)
349 }
350 } else {
351 Ok(())
352 }
353}
354
355#[cfg(unix)]
356fn lock_shared(file: &File) -> std::io::Result<()> {
357 let mut lock = unsafe {
358 std::mem::zeroed::<libc::flock>()
360 };
361 lock.l_type = libc::F_RDLCK as libc::c_short;
362 lock.l_whence = libc::SEEK_SET as libc::c_short;
363 lock.l_start = 0;
364 lock.l_len = 0;
365 let result = unsafe {
366 libc::fcntl(file.as_raw_fd(), libc::F_SETLK, &lock)
369 };
370 if result == -1 {
371 let error = std::io::Error::last_os_error();
372 if error
373 .raw_os_error()
374 .is_some_and(|errno| errno == libc::EAGAIN || errno == libc::EACCES)
375 {
376 Err(std::io::Error::from(std::io::ErrorKind::WouldBlock))
377 } else {
378 Err(error)
379 }
380 } else {
381 Ok(())
382 }
383}
384
385#[cfg(windows)]
392fn lock_shared(file: &File) -> std::io::Result<()> {
393 crate::sys::windows::lock_file_shared(file)
394}
395
396#[cfg(not(any(unix, windows)))]
397fn lock_shared(_: &File) -> std::io::Result<()> {
398 Err(std::io::Error::new(
399 std::io::ErrorKind::Unsupported,
400 "shared store locks require a supported file-locking platform",
401 ))
402}
403
404#[cfg(windows)]
410fn lock_exclusive(file: &File) -> std::io::Result<()> {
411 crate::sys::windows::lock_file_exclusive(file)
412}
413
414#[cfg(not(any(unix, windows)))]
415fn lock_exclusive(_: &File) -> std::io::Result<()> {
416 Err(std::io::Error::new(
417 std::io::ErrorKind::Unsupported,
418 "store locks require a supported file-locking platform",
419 ))
420}
421
422impl Drop for StoreLock {
423 fn drop(&mut self) {
424 let mut held = held_stores();
425 let remove = match held.get_mut(&self.registry_key) {
426 Some(entry) if entry.mode == LockMode::Shared && self.mode == LockMode::Shared => {
427 if entry.shared_count > 1 {
428 entry.shared_count -= 1;
429 false
430 } else {
431 true
432 }
433 }
434 Some(entry)
435 if entry.mode == LockMode::Exclusive && self.mode == LockMode::Exclusive =>
436 {
437 true
438 }
439 _ => false,
440 };
441 if remove {
442 drop(held.remove(&self.registry_key));
446 }
447 }
448}
449
450#[derive(Debug)]
452pub enum StoreLockError {
453 Io {
455 path: PathBuf,
457 source: std::io::Error,
459 },
460}
461
462impl std::fmt::Display for StoreLockError {
463 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
464 match self {
465 Self::Io { path, source } => {
466 write!(formatter, "store lock {}: {source}", path.display())
467 }
468 }
469 }
470}
471
472impl std::error::Error for StoreLockError {
473 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
474 match self {
475 Self::Io { source, .. } => Some(source),
476 }
477 }
478}