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 #[cfg(feature = "graph-cypher")]
73 pub(crate) fn acquire_existing(directory: &Path) -> Result<Self, StoreLockError> {
74 Self::acquire_exclusive(directory, false)
75 }
76
77 fn acquire_exclusive(directory: &Path, create: bool) -> Result<Self, StoreLockError> {
78 Self::exclusive_named(directory, create, false)
79 }
80
81 pub(crate) fn reclaim_exclusive(directory: &Path) -> Result<Self, StoreLockError> {
82 Self::exclusive_named(directory, true, true)
83 }
84
85 fn exclusive_named(
86 directory: &Path,
87 create: bool,
88 lease: bool,
89 ) -> Result<Self, StoreLockError> {
90 let path = directory.join(if lease {
91 ".ze-readers.lock"
92 } else {
93 STORE_LOCK_FILE
94 });
95 let identity = store_identity(directory).map_err(|source| StoreLockError::Io {
96 path: directory.to_path_buf(),
97 source,
98 })?;
99 let registry_key = (identity.0, identity.1, lease);
100 let mut held = held_stores();
101 if held.contains_key(®istry_key) {
102 return Err(StoreLockError::Io {
103 path,
104 source: std::io::Error::from(std::io::ErrorKind::WouldBlock),
105 });
106 }
107 let file = open_lock_file(&path, create).map_err(|source| StoreLockError::Io {
108 path: path.clone(),
109 source,
110 })?;
111 if let Err(source) = lock_exclusive(&file) {
112 drop(file);
113 return Err(StoreLockError::Io { path, source });
114 }
115 held.insert(
116 registry_key,
117 RegistryEntry {
118 _file: file,
119 mode: LockMode::Exclusive,
120 shared_count: 0,
121 },
122 );
123 Ok(Self {
124 registry_key,
125 mode: LockMode::Exclusive,
126 })
127 }
128
129 #[cfg(feature = "graph-cypher")]
136 #[allow(dead_code)] pub(crate) fn acquire_shared(directory: &Path) -> Result<Self, StoreLockError> {
138 Self::shared_named(directory, false, false)
139 }
140
141 pub(crate) fn reader_lease(directory: &Path) -> Result<Self, StoreLockError> {
142 Self::shared_named(directory, true, true)
143 }
144
145 pub(crate) fn reader_lease_read_only(directory: &Path) -> Result<Option<Self>, StoreLockError> {
146 match Self::shared_named(directory, true, false) {
147 Ok(lock) => Ok(Some(lock)),
148 Err(StoreLockError::Io { source, .. })
149 if source.kind() == std::io::ErrorKind::NotFound =>
150 {
151 Ok(None)
152 }
153 Err(error) => Err(error),
154 }
155 }
156
157 fn shared_named(directory: &Path, lease: bool, create: bool) -> Result<Self, StoreLockError> {
158 let path = directory.join(if lease {
159 ".ze-readers.lock"
160 } else {
161 STORE_LOCK_FILE
162 });
163 let identity = store_identity(directory).map_err(|source| StoreLockError::Io {
164 path: directory.to_path_buf(),
165 source,
166 })?;
167 let registry_key = (identity.0, identity.1, lease);
168 let mut held = held_stores();
169 if let Some(entry) = held.get_mut(®istry_key) {
170 if entry.mode == LockMode::Exclusive {
171 return Err(StoreLockError::Io {
172 path,
173 source: std::io::Error::from(std::io::ErrorKind::WouldBlock),
174 });
175 }
176 let Some(next_count) = entry.shared_count.checked_add(1) else {
177 return Err(StoreLockError::Io {
178 path,
179 source: std::io::Error::other("shared store lock holder count overflow"),
180 });
181 };
182 entry.shared_count = next_count;
183 return Ok(Self {
184 registry_key,
185 mode: LockMode::Shared,
186 });
187 }
188
189 let file = (if create {
190 open_lock_file(&path, true)
191 } else {
192 open_shared_lock_file(&path)
193 })
194 .map_err(|source| StoreLockError::Io {
195 path: path.clone(),
196 source,
197 })?;
198 if let Err(source) = lock_shared(&file) {
199 drop(file);
200 return Err(StoreLockError::Io { path, source });
201 }
202 held.insert(
203 registry_key,
204 RegistryEntry {
205 _file: file,
206 mode: LockMode::Shared,
207 shared_count: 1,
208 },
209 );
210 Ok(Self {
211 registry_key,
212 mode: LockMode::Shared,
213 })
214 }
215}
216
217fn held_stores() -> MutexGuard<'static, BTreeMap<StoreKey, RegistryEntry>> {
218 match HELD_STORES.lock() {
219 Ok(held) => held,
220 Err(poisoned) => poisoned.into_inner(),
221 }
222}
223
224fn open_shared_lock_file(path: &Path) -> std::io::Result<File> {
226 let file = open_shared_lock_handle(path)?;
227 if file.metadata()?.is_file() {
228 Ok(file)
229 } else {
230 Err(std::io::Error::new(
231 std::io::ErrorKind::IsADirectory,
232 "store lock path is not a regular file",
233 ))
234 }
235}
236
237#[cfg(not(windows))]
239fn open_shared_lock_handle(path: &Path) -> std::io::Result<File> {
240 let mut options = OpenOptions::new();
241 options.read(true);
242 #[cfg(unix)]
243 {
244 use std::os::unix::fs::OpenOptionsExt as _;
245 options.custom_flags(libc::O_NOFOLLOW);
246 }
247 options.open(path)
248}
249
250#[cfg(windows)]
262fn open_shared_lock_handle(path: &Path) -> std::io::Result<File> {
263 use std::os::windows::fs::OpenOptionsExt as _;
265
266 OpenOptions::new()
267 .read(true)
268 .share_mode(crate::sys::windows::FILE_SHARE_READ | crate::sys::windows::FILE_SHARE_WRITE)
269 .open(path)
270}
271
272#[cfg(unix)]
274pub(super) fn store_identity(directory: &Path) -> std::io::Result<(u64, u64)> {
275 let metadata = std::fs::metadata(directory)?;
276 Ok((metadata.dev(), metadata.ino()))
277}
278
279#[cfg(windows)]
280pub(super) fn store_identity(directory: &Path) -> std::io::Result<(u64, u64)> {
281 crate::sys::windows::directory_identity(directory)
282}
283
284#[cfg(not(any(unix, windows)))]
285pub(super) fn store_identity(_directory: &Path) -> std::io::Result<(u64, u64)> {
286 Err(std::io::Error::new(
287 std::io::ErrorKind::Unsupported,
288 "store identity requires a supported platform",
289 ))
290}
291
292#[cfg(not(windows))]
294fn open_lock_file(path: &Path, create: bool) -> std::io::Result<File> {
295 let mut options = OpenOptions::new();
296 options
297 .create(create)
298 .truncate(false)
299 .read(true)
300 .write(true);
301 #[cfg(unix)]
302 {
303 use std::os::unix::fs::OpenOptionsExt as _;
304 options.custom_flags(libc::O_NOFOLLOW);
305 }
306 options.open(path)
307}
308
309#[cfg(windows)]
319fn open_lock_file(path: &Path, create: bool) -> std::io::Result<File> {
320 use std::os::windows::fs::OpenOptionsExt as _;
322
323 OpenOptions::new()
324 .create(create)
325 .truncate(false)
326 .read(true)
327 .write(true)
328 .share_mode(crate::sys::windows::FILE_SHARE_READ | crate::sys::windows::FILE_SHARE_WRITE)
329 .open(path)
330}
331
332#[cfg(unix)]
333fn lock_exclusive(file: &File) -> std::io::Result<()> {
334 let mut lock = unsafe {
335 std::mem::zeroed::<libc::flock>()
337 };
338 lock.l_type = libc::F_WRLCK as libc::c_short;
339 lock.l_whence = libc::SEEK_SET as libc::c_short;
340 lock.l_start = 0;
341 lock.l_len = 0;
342 let result = unsafe {
343 libc::fcntl(file.as_raw_fd(), libc::F_SETLK, &lock)
346 };
347 if result == -1 {
348 let error = std::io::Error::last_os_error();
349 if error
350 .raw_os_error()
351 .is_some_and(|errno| errno == libc::EAGAIN || errno == libc::EACCES)
352 {
353 Err(std::io::Error::from(std::io::ErrorKind::WouldBlock))
354 } else {
355 Err(error)
356 }
357 } else {
358 Ok(())
359 }
360}
361
362#[cfg(unix)]
363#[allow(dead_code)] fn lock_shared(file: &File) -> std::io::Result<()> {
365 let mut lock = unsafe {
366 std::mem::zeroed::<libc::flock>()
368 };
369 lock.l_type = libc::F_RDLCK as libc::c_short;
370 lock.l_whence = libc::SEEK_SET as libc::c_short;
371 lock.l_start = 0;
372 lock.l_len = 0;
373 let result = unsafe {
374 libc::fcntl(file.as_raw_fd(), libc::F_SETLK, &lock)
377 };
378 if result == -1 {
379 let error = std::io::Error::last_os_error();
380 if error
381 .raw_os_error()
382 .is_some_and(|errno| errno == libc::EAGAIN || errno == libc::EACCES)
383 {
384 Err(std::io::Error::from(std::io::ErrorKind::WouldBlock))
385 } else {
386 Err(error)
387 }
388 } else {
389 Ok(())
390 }
391}
392
393#[cfg(windows)]
400#[allow(dead_code)] fn lock_shared(file: &File) -> std::io::Result<()> {
402 crate::sys::windows::lock_file_shared(file)
403}
404
405#[cfg(not(any(unix, windows)))]
406#[allow(dead_code)] fn lock_shared(_: &File) -> std::io::Result<()> {
408 Err(std::io::Error::new(
409 std::io::ErrorKind::Unsupported,
410 "shared store locks require a supported file-locking platform",
411 ))
412}
413
414#[cfg(windows)]
420fn lock_exclusive(file: &File) -> std::io::Result<()> {
421 crate::sys::windows::lock_file_exclusive(file)
422}
423
424#[cfg(not(any(unix, windows)))]
425fn lock_exclusive(_: &File) -> std::io::Result<()> {
426 Err(std::io::Error::new(
427 std::io::ErrorKind::Unsupported,
428 "store locks require a supported file-locking platform",
429 ))
430}
431
432impl Drop for StoreLock {
433 fn drop(&mut self) {
434 let mut held = held_stores();
435 let remove = match held.get_mut(&self.registry_key) {
436 Some(entry) if entry.mode == LockMode::Shared && self.mode == LockMode::Shared => {
437 if entry.shared_count > 1 {
438 entry.shared_count -= 1;
439 false
440 } else {
441 true
442 }
443 }
444 Some(entry)
445 if entry.mode == LockMode::Exclusive && self.mode == LockMode::Exclusive =>
446 {
447 true
448 }
449 _ => false,
450 };
451 if remove {
452 drop(held.remove(&self.registry_key));
456 }
457 }
458}
459
460#[derive(Debug)]
462pub enum StoreLockError {
463 Io {
465 path: PathBuf,
467 source: std::io::Error,
469 },
470}
471
472impl std::fmt::Display for StoreLockError {
473 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
474 match self {
475 Self::Io { path, source } => {
476 write!(formatter, "store lock {}: {source}", path.display())
477 }
478 }
479 }
480}
481
482impl std::error::Error for StoreLockError {
483 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
484 match self {
485 Self::Io { source, .. } => Some(source),
486 }
487 }
488}