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 #[allow(dead_code)] pub(crate) fn acquire_shared(directory: &Path) -> Result<Self, StoreLockError> {
132 Self::shared_named(directory, false, false)
133 }
134
135 pub(crate) fn reader_lease(directory: &Path) -> Result<Self, StoreLockError> {
136 Self::shared_named(directory, true, true)
137 }
138
139 pub(crate) fn reader_lease_read_only(directory: &Path) -> Result<Option<Self>, StoreLockError> {
140 match Self::shared_named(directory, true, false) {
141 Ok(lock) => Ok(Some(lock)),
142 Err(StoreLockError::Io { source, .. })
143 if source.kind() == std::io::ErrorKind::NotFound =>
144 {
145 Ok(None)
146 }
147 Err(error) => Err(error),
148 }
149 }
150
151 fn shared_named(directory: &Path, lease: bool, create: bool) -> Result<Self, StoreLockError> {
152 let path = directory.join(if lease {
153 ".ze-readers.lock"
154 } else {
155 STORE_LOCK_FILE
156 });
157 let identity = store_identity(directory).map_err(|source| StoreLockError::Io {
158 path: directory.to_path_buf(),
159 source,
160 })?;
161 let registry_key = (identity.0, identity.1, lease);
162 let mut held = held_stores();
163 if let Some(entry) = held.get_mut(®istry_key) {
164 if entry.mode == LockMode::Exclusive {
165 return Err(StoreLockError::Io {
166 path,
167 source: std::io::Error::from(std::io::ErrorKind::WouldBlock),
168 });
169 }
170 let Some(next_count) = entry.shared_count.checked_add(1) else {
171 return Err(StoreLockError::Io {
172 path,
173 source: std::io::Error::other("shared store lock holder count overflow"),
174 });
175 };
176 entry.shared_count = next_count;
177 return Ok(Self {
178 registry_key,
179 mode: LockMode::Shared,
180 });
181 }
182
183 let file = (if create {
184 open_lock_file(&path, true)
185 } else {
186 open_shared_lock_file(&path)
187 })
188 .map_err(|source| StoreLockError::Io {
189 path: path.clone(),
190 source,
191 })?;
192 if let Err(source) = lock_shared(&file) {
193 drop(file);
194 return Err(StoreLockError::Io { path, source });
195 }
196 held.insert(
197 registry_key,
198 RegistryEntry {
199 _file: file,
200 mode: LockMode::Shared,
201 shared_count: 1,
202 },
203 );
204 Ok(Self {
205 registry_key,
206 mode: LockMode::Shared,
207 })
208 }
209}
210
211fn held_stores() -> MutexGuard<'static, BTreeMap<StoreKey, RegistryEntry>> {
212 match HELD_STORES.lock() {
213 Ok(held) => held,
214 Err(poisoned) => poisoned.into_inner(),
215 }
216}
217
218fn open_shared_lock_file(path: &Path) -> std::io::Result<File> {
220 let file = open_shared_lock_handle(path)?;
221 if file.metadata()?.is_file() {
222 Ok(file)
223 } else {
224 Err(std::io::Error::new(
225 std::io::ErrorKind::IsADirectory,
226 "store lock path is not a regular file",
227 ))
228 }
229}
230
231#[cfg(not(windows))]
233fn open_shared_lock_handle(path: &Path) -> std::io::Result<File> {
234 let mut options = OpenOptions::new();
235 options.read(true);
236 #[cfg(unix)]
237 {
238 use std::os::unix::fs::OpenOptionsExt as _;
239 options.custom_flags(libc::O_NOFOLLOW);
240 }
241 options.open(path)
242}
243
244#[cfg(windows)]
256fn open_shared_lock_handle(path: &Path) -> std::io::Result<File> {
257 use std::os::windows::fs::OpenOptionsExt as _;
259
260 OpenOptions::new()
261 .read(true)
262 .share_mode(crate::sys::windows::FILE_SHARE_READ | crate::sys::windows::FILE_SHARE_WRITE)
263 .open(path)
264}
265
266#[cfg(unix)]
268pub(super) fn store_identity(directory: &Path) -> std::io::Result<(u64, u64)> {
269 let metadata = std::fs::metadata(directory)?;
270 Ok((metadata.dev(), metadata.ino()))
271}
272
273#[cfg(windows)]
274pub(super) fn store_identity(directory: &Path) -> std::io::Result<(u64, u64)> {
275 crate::sys::windows::directory_identity(directory)
276}
277
278#[cfg(not(any(unix, windows)))]
279pub(super) fn store_identity(_directory: &Path) -> std::io::Result<(u64, u64)> {
280 Err(std::io::Error::new(
281 std::io::ErrorKind::Unsupported,
282 "store identity requires a supported platform",
283 ))
284}
285
286#[cfg(not(windows))]
288fn open_lock_file(path: &Path, create: bool) -> std::io::Result<File> {
289 let mut options = OpenOptions::new();
290 options
291 .create(create)
292 .truncate(false)
293 .read(true)
294 .write(true);
295 #[cfg(unix)]
296 {
297 use std::os::unix::fs::OpenOptionsExt as _;
298 options.custom_flags(libc::O_NOFOLLOW);
299 }
300 options.open(path)
301}
302
303#[cfg(windows)]
313fn open_lock_file(path: &Path, create: bool) -> std::io::Result<File> {
314 use std::os::windows::fs::OpenOptionsExt as _;
316
317 OpenOptions::new()
318 .create(create)
319 .truncate(false)
320 .read(true)
321 .write(true)
322 .share_mode(crate::sys::windows::FILE_SHARE_READ | crate::sys::windows::FILE_SHARE_WRITE)
323 .open(path)
324}
325
326#[cfg(unix)]
327fn lock_exclusive(file: &File) -> std::io::Result<()> {
328 let mut lock = unsafe {
329 std::mem::zeroed::<libc::flock>()
331 };
332 lock.l_type = libc::F_WRLCK as libc::c_short;
333 lock.l_whence = libc::SEEK_SET as libc::c_short;
334 lock.l_start = 0;
335 lock.l_len = 0;
336 let result = unsafe {
337 libc::fcntl(file.as_raw_fd(), libc::F_SETLK, &lock)
340 };
341 if result == -1 {
342 let error = std::io::Error::last_os_error();
343 if error
344 .raw_os_error()
345 .is_some_and(|errno| errno == libc::EAGAIN || errno == libc::EACCES)
346 {
347 Err(std::io::Error::from(std::io::ErrorKind::WouldBlock))
348 } else {
349 Err(error)
350 }
351 } else {
352 Ok(())
353 }
354}
355
356#[cfg(unix)]
357#[allow(dead_code)] fn lock_shared(file: &File) -> std::io::Result<()> {
359 let mut lock = unsafe {
360 std::mem::zeroed::<libc::flock>()
362 };
363 lock.l_type = libc::F_RDLCK as libc::c_short;
364 lock.l_whence = libc::SEEK_SET as libc::c_short;
365 lock.l_start = 0;
366 lock.l_len = 0;
367 let result = unsafe {
368 libc::fcntl(file.as_raw_fd(), libc::F_SETLK, &lock)
371 };
372 if result == -1 {
373 let error = std::io::Error::last_os_error();
374 if error
375 .raw_os_error()
376 .is_some_and(|errno| errno == libc::EAGAIN || errno == libc::EACCES)
377 {
378 Err(std::io::Error::from(std::io::ErrorKind::WouldBlock))
379 } else {
380 Err(error)
381 }
382 } else {
383 Ok(())
384 }
385}
386
387#[cfg(windows)]
394#[allow(dead_code)] fn lock_shared(file: &File) -> std::io::Result<()> {
396 crate::sys::windows::lock_file_shared(file)
397}
398
399#[cfg(not(any(unix, windows)))]
400#[allow(dead_code)] fn lock_shared(_: &File) -> std::io::Result<()> {
402 Err(std::io::Error::new(
403 std::io::ErrorKind::Unsupported,
404 "shared store locks require a supported file-locking platform",
405 ))
406}
407
408#[cfg(windows)]
414fn lock_exclusive(file: &File) -> std::io::Result<()> {
415 crate::sys::windows::lock_file_exclusive(file)
416}
417
418#[cfg(not(any(unix, windows)))]
419fn lock_exclusive(_: &File) -> std::io::Result<()> {
420 Err(std::io::Error::new(
421 std::io::ErrorKind::Unsupported,
422 "store locks require a supported file-locking platform",
423 ))
424}
425
426impl Drop for StoreLock {
427 fn drop(&mut self) {
428 let mut held = held_stores();
429 let remove = match held.get_mut(&self.registry_key) {
430 Some(entry) if entry.mode == LockMode::Shared && self.mode == LockMode::Shared => {
431 if entry.shared_count > 1 {
432 entry.shared_count -= 1;
433 false
434 } else {
435 true
436 }
437 }
438 Some(entry)
439 if entry.mode == LockMode::Exclusive && self.mode == LockMode::Exclusive =>
440 {
441 true
442 }
443 _ => false,
444 };
445 if remove {
446 drop(held.remove(&self.registry_key));
450 }
451 }
452}
453
454#[derive(Debug)]
456pub enum StoreLockError {
457 Io {
459 path: PathBuf,
461 source: std::io::Error,
463 },
464}
465
466impl std::fmt::Display for StoreLockError {
467 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
468 match self {
469 Self::Io { path, source } => {
470 write!(formatter, "store lock {}: {source}", path.display())
471 }
472 }
473 }
474}
475
476impl std::error::Error for StoreLockError {
477 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
478 match self {
479 Self::Io { source, .. } => Some(source),
480 }
481 }
482}