1use std::fs::{File, OpenOptions, TryLockError};
4use std::path::{Path, PathBuf};
5
6use crate::files::file_lock::{FileLock, LockMode};
7use crate::files::{atomic_file, settings, FileError, SettingsError, StoreSettings};
8use crate::id_source::StoreId;
9
10use super::lock_registry as registry;
11use registry::LockKind;
12pub use registry::StoreClosingGuard;
13pub(super) use registry::{bootstrap_running, LockInstance, RegisteredLock};
14
15#[derive(Debug)]
18pub struct StoreLock {
19 store: LockedStore<RegisteredLock>,
20}
21
22#[derive(Debug)]
23struct LockedStore<L> {
24 _file: L,
25 directory: PathBuf,
26 id: StoreId,
27}
28
29impl StoreLock {
30 pub fn closing_guard(&self) -> StoreClosingGuard {
33 self.store._file.closing_guard()
34 }
35
36 pub fn settings(&self) -> Result<StoreSettings, SettingsError> {
38 settings::read(&self.store.directory, self.store.id)
39 }
40
41 pub(super) fn directory(&self) -> &Path {
43 &self.store.directory
44 }
45
46 pub(crate) fn verify(&self, directory: &Path, id: StoreId) -> Result<(), StoreLockError> {
47 let path = std::fs::canonicalize(directory)
48 .map_err(|source| FileError::at("resolve locked store", directory, source))?;
49 if path != self.store.directory || id != self.store.id {
50 return Err(StoreLockError::WrongDirectory(id));
51 }
52 Ok(())
53 }
54}
55
56#[derive(Debug)]
58pub struct StoreReadLock {
59 _file: RegisteredLock,
60}
61
62impl StoreReadLock {
63 pub fn closing_guard(&self) -> StoreClosingGuard {
67 self._file.closing_guard()
68 }
69}
70
71#[derive(Debug)]
76pub struct ClosedStoreLock {
77 _file: RegisteredLock,
78}
79
80#[derive(Debug)]
83pub struct CacheWriteLock {
84 _file: FileLock,
85}
86
87const CACHE_WRITES: &str = ".coven-cache-writes.lock";
90
91pub(super) fn cache_writes(store: &Path, shared: bool) -> Result<CacheWriteLock, FileError> {
94 let path = store.join(CACHE_WRITES);
95 let mode = if shared { LockMode::Shared } else { LockMode::Exclusive };
96 Ok(CacheWriteLock { _file: wait_for_lock(&path, mode, "lock cache writes")? })
97}
98
99fn wait_for_lock(path: &Path, mode: LockMode, what: &'static str) -> Result<FileLock, FileError> {
102 let file = open_lock(path)?;
103 let attempt = file.try_clone().map_err(|source| FileError::at(what, path, source))?;
104 match FileLock::try_lock(attempt, mode) {
105 Ok(file) => return Ok(file),
106 Err(TryLockError::WouldBlock) => {
107 #[cfg(any(test, feature = "test-utils"))]
108 super::reached::reached(path);
109 }
110 Err(TryLockError::Error(source)) => return Err(FileError::at(what, path, source)),
111 }
112 FileLock::lock(file, mode).map_err(|source| FileError::at(what, path, source))
113}
114
115#[cfg(any(test, feature = "test-utils"))]
118pub fn cache_writes_lock(store: &Path) -> std::path::PathBuf {
119 store.join(CACHE_WRITES)
120}
121
122#[derive(Debug)]
128pub struct StoreDeletionLock {
129 store: LockedStore<FileLock>,
130 _readers: FileLock,
131 bootstrap: HeldBootstrap,
132}
133
134#[derive(Debug)]
136enum HeldBootstrap {
137 Locked(FileLock),
139 Leased(RegisteredLock),
141}
142
143impl HeldBootstrap {
144 fn release(self) {
145 match self {
146 Self::Locked(file) => drop(file),
147 Self::Leased(lock) => drop(lock),
148 }
149 }
150}
151
152impl StoreDeletionLock {
153 pub fn unpublish(&mut self) -> Result<(), FileError> {
159 let _layout = lock_layout(self.store.directory.parent().expect("store parent"))?;
160 self.unpublish_locked()
161 }
162
163 fn unpublish_locked(&mut self) -> Result<(), FileError> {
165 let destination = deletion_path(&self.store.directory, self.store.id);
166 if self.store.directory != destination && inspect_directory(&self.store.directory)? {
167 crate::files::atomic_file::rename_new(&self.store.directory, &destination).map_err(
168 |source| FileError::at("unpublish store", &self.store.directory, source),
169 )?;
170 self.store.directory = destination;
171 #[cfg(unix)]
172 sync_removal(&self.store.directory)?;
173 }
174 Ok(())
175 }
176
177 pub fn remove_directory(mut self) -> Result<(), FileError> {
182 let root = self.store.directory.parent().expect("store parent");
183 let _layout = lock_layout(root)?;
188 let paths = deletion_lock_paths(&self.store.directory, self.store.id);
189 self.unpublish_locked()?;
190 if inspect_directory(&self.store.directory)? {
191 std::fs::remove_dir_all(&self.store.directory).map_err(|source| {
192 FileError::at("remove store directory", &self.store.directory, source)
193 })?;
194 }
195 #[cfg(unix)]
196 sync_removal(&self.store.directory)?;
197 let Self { store, _readers, bootstrap } = self;
203 drop((store, _readers));
204 bootstrap.release();
205 for path in paths {
206 atomic_file::remove(&path)?;
207 }
208 Ok(())
209 }
210}
211
212#[cfg(unix)]
213fn sync_removal(directory: &Path) -> Result<(), FileError> {
214 crate::files::atomic_file::sync_directory(directory.parent().expect("store parent")).map_err(
215 |source| FileError::AfterRemove { path: directory.to_owned(), source: Box::new(source) },
216 )
217}
218
219#[derive(Debug, thiserror::Error)]
221pub enum StoreLockError {
222 #[error("store {0} is already open")]
225 AlreadyOpen(StoreId),
226 #[error("store {0} requires restore or join to finish bootstrap")]
230 BootstrapPending(StoreId),
231 #[error("store {store} requires {by} to finish replacing this device's installation")]
236 ReplacementUnfinished {
237 store: StoreId,
239 by: super::ReplacedBy,
241 },
242 #[error("store {0} is being deleted from this device")]
248 DeletionUnfinished(StoreId),
249 #[error("lock does not protect store {0}")]
251 WrongDirectory(StoreId),
252 #[error("store lock: {0}")]
255 File(FileError),
256 #[error(transparent)]
259 Bug(#[from] crate::task::Bug),
260}
261
262impl From<FileError> for StoreLockError {
263 fn from(error: FileError) -> Self {
264 match error {
265 FileError::Bug(bug) => Self::Bug(bug),
266 error => Self::File(error),
267 }
268 }
269}
270
271impl crate::task::CarriesBug for StoreLockError {
272 fn bug(&self) -> Option<&crate::task::Bug> {
273 match self {
274 Self::Bug(bug) => Some(bug),
275 Self::File(error) => error.bug(),
276 _ => None,
277 }
278 }
279}
280
281fn open_lock(path: &Path) -> Result<File, FileError> {
282 OpenOptions::new()
283 .read(true)
284 .write(true)
285 .create(true)
286 .truncate(false)
287 .open(path)
288 .map_err(|source| FileError::at("open store lock", path, source))
289}
290
291pub(crate) fn try_lock(path: &Path, id: StoreId, shared: bool) -> Result<FileLock, StoreLockError> {
292 let mode = if shared { LockMode::Shared } else { LockMode::Exclusive };
293 match FileLock::try_lock(open_lock(path)?, mode) {
294 Ok(file) => Ok(file),
295 Err(TryLockError::WouldBlock) => Err(StoreLockError::AlreadyOpen(id)),
296 Err(TryLockError::Error(source)) => Err(FileError::at("lock store", path, source).into()),
297 }
298}
299
300pub(crate) fn lock_layout(root: &Path) -> Result<FileLock, FileError> {
304 wait_for_lock(&layout_lock(root), LockMode::Exclusive, "lock store layout")
305}
306
307pub(crate) fn layout_lock(root: &Path) -> PathBuf {
310 root.join(".coven-stores.lock")
311}
312
313#[cfg(any(test, feature = "test-utils"))]
314pub(crate) fn acquire(directory: &Path, id: StoreId) -> Result<StoreLock, StoreLockError> {
315 let directory = resolve_directory(directory)?;
316 let file = registry::try_acquire(&directory, id, LockKind::Writer)?;
317 Ok(StoreLock { store: LockedStore { _file: file, directory, id } })
318}
319
320pub(crate) async fn acquire_for_open(
321 directory: &Path,
322 id: StoreId,
323 bugs: &crate::task::BugSinkRef,
324) -> Result<StoreLock, StoreLockError> {
325 let directory = resolve_directory(directory)?;
326 let file = registry::acquire_registered(&directory, id, LockKind::Writer, bugs).await?;
327 Ok(StoreLock { store: LockedStore { _file: file, directory, id } })
328}
329
330pub(crate) async fn acquire_closed(
334 directory: &Path,
335 id: StoreId,
336 bugs: &crate::task::BugSinkRef,
337) -> Result<ClosedStoreLock, StoreLockError> {
338 let directory = resolve_directory(directory)?;
339 Ok(ClosedStoreLock { _file: registry::acquire_closing_writer(&directory, id, bugs).await? })
340}
341
342pub(crate) async fn closed(directory: &Path, id: StoreId) -> Result<(), FileError> {
345 let directory = match std::fs::canonicalize(directory) {
346 Ok(directory) => directory,
347 Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(()),
348 Err(source) => return Err(FileError::at("resolve store directory", directory, source)),
349 };
350 registry::closed(&directory, id).await;
351 Ok(())
352}
353
354pub(super) async fn acquire_bootstrap(
355 directory: &Path,
356 id: StoreId,
357 bugs: &crate::task::BugSinkRef,
358) -> Result<RegisteredLock, StoreLockError> {
359 registry::acquire_registered(directory, id, LockKind::Bootstrap, bugs).await
360}
361
362fn resolve_directory(directory: &Path) -> Result<PathBuf, FileError> {
363 std::fs::canonicalize(directory)
364 .map_err(|source| FileError::at("resolve store directory", directory, source))
365}
366
367pub(super) fn exclude_readers(directory: &Path, id: StoreId) -> Result<FileLock, StoreLockError> {
368 let _layout = lock_layout(directory.parent().expect("store parent"))?;
369 try_lock(&lock_paths(directory, id)[1], id, false)
370}
371
372pub(crate) fn acquire_reader(
373 directory: &Path,
374 id: StoreId,
375) -> Result<StoreReadLock, StoreLockError> {
376 let directory = resolve_directory(directory)?;
377 Ok(StoreReadLock { _file: registry::acquire_reader(&directory, id)? })
378}
379
380pub(super) fn lock_existing(
381 directory: &Path,
382 id: StoreId,
383 shared: bool,
384) -> Result<(PathBuf, FileLock), StoreLockError> {
385 let directory = std::fs::canonicalize(directory)
386 .map_err(|source| FileError::at("resolve store directory", directory, source))?;
387 let _layout = lock_layout(directory.parent().expect("store parent"))?;
388 std::fs::canonicalize(&directory)
390 .map_err(|source| FileError::at("resolve store directory", &directory, source))?;
391 let [writer, readers] = lock_paths(&directory, id);
392 let path = if shared { readers } else { writer };
393 let file = try_lock(&path, id, shared)?;
394 Ok((directory, file))
395}
396
397pub(crate) fn lock_paths(directory: &Path, id: StoreId) -> [PathBuf; 2] {
398 [
399 directory.with_file_name(format!(".{id}.lock")),
400 directory.with_file_name(format!(".{id}.readers.lock")),
401 ]
402}
403
404fn bootstrap_lock_path(directory: &Path, id: StoreId) -> PathBuf {
407 directory.with_file_name(format!(".coven-bootstrap-{id}.lock"))
408}
409
410#[cfg(any(test, feature = "test-utils"))]
414pub fn writer_lock(directory: &Path, id: StoreId) -> PathBuf {
415 let [writer, _] = lock_paths(directory, id);
416 writer
417}
418
419#[cfg(any(test, feature = "test-utils"))]
422pub fn bootstrap_lock(directory: &Path, id: StoreId) -> PathBuf {
423 bootstrap_lock_path(directory, id)
424}
425
426#[cfg(any(test, feature = "test-utils"))]
428pub(super) fn kind_lock(directory: &Path, id: StoreId, kind: LockKind) -> PathBuf {
429 match kind {
430 LockKind::Writer => writer_lock(directory, id),
431 LockKind::Reader => lock_paths(directory, id)[1].clone(),
432 LockKind::Bootstrap => bootstrap_lock(directory, id),
433 }
434}
435
436pub(super) fn lock_bootstrap(directory: &Path, id: StoreId) -> Result<FileLock, StoreLockError> {
439 let _layout = lock_layout(directory.parent().expect("store parent"))?;
440 try_lock(&bootstrap_lock_path(directory, id), id, false)
441}
442
443fn deletion_lock_paths(directory: &Path, id: StoreId) -> [PathBuf; 3] {
447 let [writer, readers] = lock_paths(directory, id);
448 [bootstrap_lock_path(directory, id), writer, readers]
449}
450
451pub(crate) fn deletion_path(directory: &Path, id: StoreId) -> PathBuf {
452 directory.with_file_name(format!(".coven-delete-{id}"))
453}
454
455fn inspect_directory(directory: &Path) -> Result<bool, FileError> {
456 match std::fs::symlink_metadata(directory) {
457 Ok(metadata) if metadata.file_type().is_dir() => Ok(true),
458 Ok(_) => Err(FileError::at(
459 "inspect deletion directory",
460 directory,
461 std::io::Error::new(std::io::ErrorKind::InvalidInput, "store is not a directory"),
462 )),
463 Err(error) if error.kind() == std::io::ErrorKind::NotFound => {
464 tracing::debug!(path = %directory.display(), "store directory is absent");
465 Ok(false)
466 }
467 Err(source) => Err(FileError::at("inspect store directory", directory, source)),
468 }
469}
470
471pub(super) fn for_deletion(
476 directory: &Path,
477 id: StoreId,
478 lease: Option<&mut Option<RegisteredLock>>,
479) -> Result<Option<StoreDeletionLock>, StoreLockError> {
480 let root = directory.parent().expect("store parent");
481 let root = match std::fs::canonicalize(root) {
482 Ok(root) => root,
483 Err(error) if error.kind() == std::io::ErrorKind::NotFound => {
484 tracing::debug!(path = %root.display(), "store layout is absent");
485 return Ok(None);
486 }
487 Err(source) => return Err(FileError::at("resolve store layout", root, source).into()),
488 };
489 let _layout = lock_layout(&root)?;
490 let published = root.join(id.to_string());
491 let directory =
492 if inspect_directory(&published)? { published } else { deletion_path(&published, id) };
493 let paths = deletion_lock_paths(&directory, id);
494 if !inspect_directory(&directory)? {
495 let mut remaining = false;
496 for path in &paths {
497 remaining |= atomic_file::exists("inspect store lock", path)?;
498 }
499 if !remaining {
500 #[cfg(unix)]
503 sync_removal(&directory)?;
504 tracing::debug!(store = %id, "store and its locks are absent");
505 return Ok(None);
506 }
507 }
508 let [bootstrap, writer, readers] = paths;
509 let others =
510 || Ok::<_, StoreLockError>((try_lock(&writer, id, false)?, try_lock(&readers, id, false)?));
511 let (bootstrap, (writer, readers)) = match lease {
512 None => (HeldBootstrap::Locked(try_lock(&bootstrap, id, false)?), others()?),
515 Some(lease) => {
517 let others = others()?;
518 let held = lease.take().expect("a bootstrap hands over the bootstrap lock it holds");
519 (HeldBootstrap::Leased(held), others)
520 }
521 };
522 Ok(Some(StoreDeletionLock {
523 store: LockedStore { _file: writer, directory, id },
524 _readers: readers,
525 bootstrap,
526 }))
527}
528
529#[cfg(test)]
531pub(crate) fn store_entries(root: &Path) -> usize {
532 std::fs::read_dir(root)
533 .unwrap()
534 .filter(|entry| entry.as_ref().unwrap().file_name() != ".coven-stores.lock")
535 .count()
536}
537
538#[cfg(test)]
539#[path = "lock_tests.rs"]
540mod tests;