1use std::path::{Path, PathBuf};
17use std::sync::Arc;
18use std::time::{Duration, Instant};
19
20use parking_lot::{Condvar, Mutex};
21
22use crate::error::{MongrelError, Result};
23
24#[derive(Clone, Debug)]
31pub struct DatabaseFileIdentity {
32 stable: crate::durable_file::DurableFileIdentity,
33 canonical_path: PathBuf,
34}
35
36impl DatabaseFileIdentity {
37 pub fn for_path(root: impl AsRef<Path>) -> Result<Self> {
42 let root = root.as_ref();
43 let canonical_path = root.canonicalize().map_err(|error| {
44 if error.kind() == std::io::ErrorKind::NotFound {
45 MongrelError::NotFound(format!("database root {}: {error}", root.display()))
46 } else {
47 MongrelError::Io(error)
48 }
49 })?;
50 let durable_root = crate::durable_file::DurableRoot::open(&canonical_path)?;
51 let stable = durable_root.file_identity()?;
52 Ok(Self {
53 stable,
54 canonical_path,
55 })
56 }
57
58 pub fn from_durable_root(root: &crate::durable_file::DurableRoot) -> Result<Self> {
60 Ok(Self {
61 stable: root.file_identity()?,
62 canonical_path: root.canonical_path().to_path_buf(),
63 })
64 }
65
66 pub fn canonical_path(&self) -> &Path {
69 &self.canonical_path
70 }
71}
72
73impl PartialEq for DatabaseFileIdentity {
74 fn eq(&self, other: &Self) -> bool {
75 self.stable == other.stable
76 }
77}
78
79impl Eq for DatabaseFileIdentity {}
80
81impl std::hash::Hash for DatabaseFileIdentity {
82 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
83 self.stable.hash(state);
84 }
85}
86
87impl std::fmt::Display for DatabaseFileIdentity {
88 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
89 write!(formatter, "{}", self.canonical_path.display())
90 }
91}
92
93#[derive(Clone, Copy, Debug, PartialEq, Eq)]
95pub enum LifecycleState {
96 Opening,
99 Open,
101 Draining,
104 Closing,
107 Closed,
109 Poisoned,
112}
113
114impl LifecycleState {
115 pub fn admits_operations(self) -> bool {
117 matches!(self, LifecycleState::Open)
118 }
119}
120
121impl std::fmt::Display for LifecycleState {
122 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
123 let name = match self {
124 LifecycleState::Opening => "opening",
125 LifecycleState::Open => "open",
126 LifecycleState::Draining => "draining",
127 LifecycleState::Closing => "closing",
128 LifecycleState::Closed => "closed",
129 LifecycleState::Poisoned => "poisoned",
130 };
131 formatter.write_str(name)
132 }
133}
134
135#[derive(Debug)]
143pub struct LifecycleController {
144 inner: Mutex<LifecycleInner>,
145 drained: Condvar,
146}
147
148#[derive(Debug)]
149struct LifecycleInner {
150 state: LifecycleState,
151 active_operations: u64,
152}
153
154impl Default for LifecycleController {
155 fn default() -> Self {
156 Self::new()
157 }
158}
159
160impl LifecycleController {
161 pub fn new() -> Self {
164 Self {
165 inner: Mutex::new(LifecycleInner {
166 state: LifecycleState::Opening,
167 active_operations: 0,
168 }),
169 drained: Condvar::new(),
170 }
171 }
172
173 pub fn state(&self) -> LifecycleState {
175 self.inner.lock().state
176 }
177
178 pub fn is_open(&self) -> bool {
180 self.state().admits_operations()
181 }
182
183 pub fn mark_open(&self) {
187 let mut inner = self.inner.lock();
188 if matches!(inner.state, LifecycleState::Opening) {
189 inner.state = LifecycleState::Open;
190 }
191 }
192
193 pub fn begin_shutdown(&self) -> bool {
198 let mut inner = self.inner.lock();
199 if matches!(inner.state, LifecycleState::Open) {
200 inner.state = LifecycleState::Draining;
201 return true;
202 }
203 false
204 }
205
206 pub fn wait_drained(&self, deadline: Duration) -> Result<()> {
210 let started = Instant::now();
211 let mut inner = self.inner.lock();
212 loop {
213 if inner.active_operations == 0 {
214 return Ok(());
215 }
216 let elapsed = started.elapsed();
217 if elapsed >= deadline {
218 return Err(MongrelError::DatabaseBusy {
219 strong_handles: inner.active_operations as usize,
220 });
221 }
222 let remaining = deadline - elapsed;
223 if self
224 .drained
225 .wait_for(&mut inner, remaining.min(Duration::from_millis(50)))
226 .timed_out()
227 && started.elapsed() >= deadline
228 {
229 return Err(MongrelError::DatabaseBusy {
230 strong_handles: inner.active_operations as usize,
231 });
232 }
233 }
234 }
235
236 pub fn mark_closing(&self) {
239 let mut inner = self.inner.lock();
240 if matches!(inner.state, LifecycleState::Draining) {
241 inner.state = LifecycleState::Closing;
242 }
243 }
244
245 pub fn mark_closed(&self) {
247 let mut inner = self.inner.lock();
248 if matches!(
249 inner.state,
250 LifecycleState::Closing | LifecycleState::Draining
251 ) {
252 inner.state = LifecycleState::Closed;
253 }
254 self.drained.notify_all();
255 }
256
257 pub fn poison(&self) {
260 let mut inner = self.inner.lock();
261 inner.state = LifecycleState::Poisoned;
262 drop(inner);
263 self.drained.notify_all();
264 }
265
266 pub fn begin_operation(self: &Arc<Self>) -> Result<OperationGuard> {
270 let mut inner = self.inner.lock();
271 if !inner.state.admits_operations() {
272 return Err(MongrelError::Conflict(format!(
273 "database core is not open (lifecycle state: {})",
274 inner.state
275 )));
276 }
277 inner.active_operations = inner
278 .active_operations
279 .checked_add(1)
280 .ok_or_else(|| MongrelError::Full("operation counter exhausted".into()))?;
281 Ok(OperationGuard {
282 lifecycle: Arc::clone(self),
283 })
284 }
285
286 pub fn active_operations(&self) -> u64 {
288 self.inner.lock().active_operations
289 }
290
291 fn end_operation(&self) {
292 let mut inner = self.inner.lock();
293 inner.active_operations = inner.active_operations.saturating_sub(1);
294 if inner.active_operations == 0 {
295 drop(inner);
296 self.drained.notify_all();
297 }
298 }
299}
300
301#[derive(Debug)]
305pub struct OperationGuard {
306 lifecycle: Arc<LifecycleController>,
307}
308
309impl OperationGuard {
310 pub fn lifecycle(&self) -> &Arc<LifecycleController> {
313 &self.lifecycle
314 }
315}
316
317impl Drop for OperationGuard {
318 fn drop(&mut self) {
319 self.lifecycle.end_operation();
320 }
321}
322
323#[cfg(test)]
324mod tests {
325 use super::*;
326
327 #[test]
328 fn file_identity_ignores_path_spelling() {
329 let dir = std::env::temp_dir().join(format!(
330 "mongreldb-core-identity-{}-{}",
331 std::process::id(),
332 std::time::SystemTime::now()
333 .duration_since(std::time::UNIX_EPOCH)
334 .unwrap()
335 .as_nanos()
336 ));
337 std::fs::create_dir_all(dir.join("sub")).unwrap();
338 let direct = DatabaseFileIdentity::for_path(dir.join("sub")).unwrap();
339 let aliased =
340 DatabaseFileIdentity::for_path(dir.join("sub").join("..").join("sub")).unwrap();
341 assert_eq!(direct, aliased);
342 let other = DatabaseFileIdentity::for_path(&dir).unwrap();
343 assert_ne!(direct, other);
344 std::fs::remove_dir_all(&dir).unwrap();
345 }
346
347 #[test]
348 fn lifecycle_rejects_operations_unless_open() {
349 let lifecycle = Arc::new(LifecycleController::new());
350 assert_eq!(lifecycle.state(), LifecycleState::Opening);
351 assert!(lifecycle.begin_operation().is_err());
352 lifecycle.mark_open();
353 let guard = lifecycle.begin_operation().unwrap();
354 assert_eq!(lifecycle.active_operations(), 1);
355 drop(guard);
356 assert_eq!(lifecycle.active_operations(), 0);
357 }
358
359 #[test]
360 fn shutdown_drains_then_closes() {
361 let lifecycle = Arc::new(LifecycleController::new());
362 lifecycle.mark_open();
363 let guard = lifecycle.begin_operation().unwrap();
364 assert!(lifecycle.begin_shutdown());
365 assert!(lifecycle.begin_operation().is_err());
367 assert!(!lifecycle.begin_shutdown());
369 assert!(
370 lifecycle.wait_drained(Duration::from_millis(10)).is_err(),
371 "drain must time out while a guard is held"
372 );
373 drop(guard);
374 lifecycle.wait_drained(Duration::from_secs(1)).unwrap();
375 lifecycle.mark_closing();
376 assert_eq!(lifecycle.state(), LifecycleState::Closing);
377 lifecycle.mark_closed();
378 assert_eq!(lifecycle.state(), LifecycleState::Closed);
379 assert!(lifecycle.begin_operation().is_err());
380 }
381
382 #[test]
383 fn poison_is_terminal() {
384 let lifecycle = Arc::new(LifecycleController::new());
385 lifecycle.mark_open();
386 lifecycle.poison();
387 assert_eq!(lifecycle.state(), LifecycleState::Poisoned);
388 assert!(lifecycle.begin_operation().is_err());
389 lifecycle.mark_open();
390 assert_eq!(lifecycle.state(), LifecycleState::Poisoned);
391 assert!(!lifecycle.begin_shutdown());
392 }
393}