khive_db/stores/blob/
gc_locks.rs1use std::collections::HashMap;
2use std::fs;
3use std::path::{Path, PathBuf};
4use std::sync::{Arc, Mutex as StdMutex, OnceLock};
5
6use khive_storage::{SqlAccess, StorageCapability, StorageError, StorageResult};
7
8use super::{map_io_err, DATABASE_GC_LOCK_SUFFIX};
9
10pub(super) fn database_gc_lock_path(database_path: &Path) -> PathBuf {
11 let mut lock_path = database_path.as_os_str().to_os_string();
12 lock_path.push(DATABASE_GC_LOCK_SUFFIX);
13 PathBuf::from(lock_path)
14}
15
16pub(super) fn acquire_database_gc_lock(
17 database_path: Option<&Path>,
18) -> StorageResult<Option<fs::File>> {
19 let Some(database_path) = database_path else {
20 return Ok(None);
23 };
24 let lock_path = database_gc_lock_path(database_path);
25 let lock_file = fs::OpenOptions::new()
26 .read(true)
27 .write(true)
28 .create(true)
29 .truncate(false)
30 .open(&lock_path)
31 .map_err(|e| map_io_err(e, "database_gc_lock_open"))?;
32 fs4::FileExt::lock(&lock_file).map_err(|e| map_io_err(e, "database_gc_lock_acquire"))?;
33 Ok(Some(lock_file))
34}
35
36type SweepLockMap = HashMap<Option<PathBuf>, Arc<DatabaseGcProcessLock>>;
43
44#[derive(Debug, Default)]
45pub(super) struct DatabaseGcProcessLock {
46 held: StdMutex<bool>,
47 released: std::sync::Condvar,
48 #[cfg(test)]
49 waiters: std::sync::atomic::AtomicUsize,
50}
51
52impl DatabaseGcProcessLock {
53 pub(super) fn acquire(self: &Arc<Self>) -> DatabaseGcProcessGuard {
54 let mut held = self
55 .held
56 .lock()
57 .unwrap_or_else(std::sync::PoisonError::into_inner);
58 while *held {
59 #[cfg(test)]
60 self.waiters
61 .fetch_add(1, std::sync::atomic::Ordering::SeqCst);
62 held = self
63 .released
64 .wait(held)
65 .unwrap_or_else(std::sync::PoisonError::into_inner);
66 #[cfg(test)]
67 self.waiters
68 .fetch_sub(1, std::sync::atomic::Ordering::SeqCst);
69 }
70 *held = true;
71 DatabaseGcProcessGuard {
72 lock: Arc::clone(self),
73 }
74 }
75
76 pub(super) fn try_acquire(self: &Arc<Self>) -> Option<DatabaseGcProcessGuard> {
77 let mut held = self
78 .held
79 .lock()
80 .unwrap_or_else(std::sync::PoisonError::into_inner);
81 if *held {
82 return None;
83 }
84 *held = true;
85 Some(DatabaseGcProcessGuard {
86 lock: Arc::clone(self),
87 })
88 }
89}
90
91#[derive(Debug)]
92pub(super) struct DatabaseGcProcessGuard {
93 lock: Arc<DatabaseGcProcessLock>,
94}
95
96impl Drop for DatabaseGcProcessGuard {
97 fn drop(&mut self) {
98 let mut held = self
99 .lock
100 .held
101 .lock()
102 .unwrap_or_else(std::sync::PoisonError::into_inner);
103 debug_assert!(*held, "database GC process owner released twice");
104 *held = false;
105 self.lock.released.notify_one();
106 }
107}
108
109fn database_sweep_locks() -> &'static StdMutex<SweepLockMap> {
110 static REGISTRY: OnceLock<StdMutex<SweepLockMap>> = OnceLock::new();
111 REGISTRY.get_or_init(|| StdMutex::new(HashMap::new()))
112}
113
114pub(super) fn sweep_lock_for_database(database_path: Option<&Path>) -> Arc<DatabaseGcProcessLock> {
115 let key = database_path.map(Path::to_path_buf);
116 let mut locks = database_sweep_locks()
117 .lock()
118 .unwrap_or_else(std::sync::PoisonError::into_inner);
119 locks
120 .entry(key)
121 .or_insert_with(|| Arc::new(DatabaseGcProcessLock::default()))
122 .clone()
123}
124
125#[cfg(test)]
126pub(crate) fn database_gc_waiter_count(database_path: Option<&Path>) -> usize {
127 sweep_lock_for_database(database_path)
128 .waiters
129 .load(std::sync::atomic::Ordering::SeqCst)
130}
131
132pub struct DatabaseGcOwnerGuard {
140 _process_guard: DatabaseGcProcessGuard,
141 _advisory_guard: Option<fs::File>,
142 database_path: Option<PathBuf>,
143}
144
145pub(crate) fn acquire_database_gc_owner_for_path_blocking(
146 database_path: Option<PathBuf>,
147) -> StorageResult<DatabaseGcOwnerGuard> {
148 let process_guard = sweep_lock_for_database(database_path.as_deref()).acquire();
149 let advisory_guard = acquire_database_gc_lock(database_path.as_deref())?;
150 Ok(DatabaseGcOwnerGuard {
151 _process_guard: process_guard,
152 _advisory_guard: advisory_guard,
153 database_path,
154 })
155}
156
157pub(crate) fn try_acquire_database_gc_owner_for_path(
165 database_path: PathBuf,
166) -> StorageResult<DatabaseGcOwnerGuard> {
167 let process_guard = sweep_lock_for_database(Some(&database_path))
168 .try_acquire()
169 .ok_or_else(|| {
170 StorageError::Internal(format!(
171 "database GC owner for {} is already held; retry schema migration through the \
172 coordinated backend boot path",
173 database_path.display()
174 ))
175 })?;
176 let lock_path = database_gc_lock_path(&database_path);
177 let advisory_guard = fs::OpenOptions::new()
178 .read(true)
179 .write(true)
180 .create(true)
181 .truncate(false)
182 .open(&lock_path)
183 .map_err(|error| map_io_err(error, "database_gc_lock_open"))?;
184 fs4::FileExt::try_lock(&advisory_guard)
185 .map_err(|error| map_io_err(error.into(), "database_gc_lock_try_acquire"))?;
186 Ok(DatabaseGcOwnerGuard {
187 _process_guard: process_guard,
188 _advisory_guard: Some(advisory_guard),
189 database_path: Some(database_path),
190 })
191}
192
193impl std::fmt::Debug for DatabaseGcOwnerGuard {
194 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
195 formatter
196 .debug_struct("DatabaseGcOwnerGuard")
197 .field("database_path", &self.database_path)
198 .finish_non_exhaustive()
199 }
200}
201
202impl DatabaseGcOwnerGuard {
203 pub fn database_path(&self) -> Option<&Path> {
206 self.database_path.as_deref()
207 }
208}
209
210pub async fn acquire_database_gc_owner(sql: &dyn SqlAccess) -> StorageResult<DatabaseGcOwnerGuard> {
214 let database_path = sql.database_path();
215 tokio::task::spawn_blocking(move || acquire_database_gc_owner_for_path_blocking(database_path))
216 .await
217 .map_err(|error| {
218 StorageError::driver(StorageCapability::Blob, "acquire_database_gc_owner", error)
219 })?
220}