Skip to main content

mongreldb_core/
manager.rs

1//! Process-local shared-core registry: `DatabaseManager` (spec §10.1,
2//! S1A-002/S1A-003).
3//!
4//! One process owns one storage core per durable root (spec §4.1). The
5//! manager keys cores by their stable [`DatabaseFileIdentity`] — never by
6//! path text — and hands out lightweight [`DatabaseHandle`]s that all
7//! reference the same `Arc<DatabaseCore>`. Recovery, WAL opening,
8//! open-generation advancement, and table mounting happen exactly once, on
9//! the first `open_shared`; concurrent attaches rendezvous on an
10//! [`OpenWaitCell`] instead of racing a second open.
11//!
12//! Exclusivity is enforced both ways (spec §2.6 applies to independent
13//! writers only): a shared core holds the same `ExclusiveDatabaseLease` an
14//! exclusive `Database::open` would take, so `Database::open` on the same
15//! root is rejected while shared handles exist — and `open_shared` fails the
16//! same way while an exclusive owner holds the root. Dropping one handle
17//! never closes storage; the last drop closes it (the lease and the file lock
18//! release with the core), and a stale `Weak` is re-initialized on the next
19//! attach.
20
21use std::collections::HashMap;
22use std::sync::{Arc, OnceLock, Weak};
23
24use parking_lot::{Condvar, Mutex};
25
26use crate::core::DatabaseFileIdentity;
27use crate::database::{Database, DatabaseCore};
28use crate::error::Result;
29use crate::handle::{DatabaseHandle, HandleAccess, OpenIdentity};
30
31/// The process-local shared-core registry (spec §10.1, S1A-002).
32pub struct DatabaseManager {
33    entries: Mutex<HashMap<DatabaseFileIdentity, CoreEntry>>,
34}
35
36/// One registry slot per durable root (spec §10.1, S1A-002).
37enum CoreEntry {
38    /// Initialization is in progress on another thread; waiters block on the
39    /// cell and re-check the map when it fires.
40    Opening(OpenWaitCell),
41    /// The core is initialized. The registry never keeps a core alive — the
42    /// handles do; when the last one drops the `Weak` goes stale and the next
43    /// attach re-initializes.
44    Open(Weak<DatabaseCore>),
45    /// `shutdown()` has begun on this core (S1A-004). Transient: the shutdown
46    /// removes the entry once the file lock is released.
47    Closing,
48}
49
50/// Rendezvous for concurrent `open_shared` calls: exactly one caller
51/// initializes; the rest wait here and then re-read the registry.
52#[derive(Clone)]
53struct OpenWaitCell {
54    inner: Arc<(Mutex<bool>, Condvar)>,
55}
56
57impl OpenWaitCell {
58    fn new() -> Self {
59        Self {
60            inner: Arc::new((Mutex::new(false), Condvar::new())),
61        }
62    }
63
64    fn wait(&self) {
65        let (lock, condvar) = &*self.inner;
66        let mut ready = lock.lock();
67        while !*ready {
68            condvar.wait(&mut ready);
69        }
70    }
71
72    fn fire(&self) {
73        let (lock, condvar) = &*self.inner;
74        *lock.lock() = true;
75        condvar.notify_all();
76    }
77}
78
79/// Removes a still-`Opening` entry and wakes its waiters if initialization
80/// ends without installing a core (failure or panic), so attachers never
81/// block forever.
82struct OpeningGuard<'a> {
83    manager: &'a DatabaseManager,
84    identity: DatabaseFileIdentity,
85    cell: OpenWaitCell,
86    armed: bool,
87}
88
89impl OpeningGuard<'_> {
90    fn disarm(&mut self) {
91        self.armed = false;
92    }
93}
94
95impl Drop for OpeningGuard<'_> {
96    fn drop(&mut self) {
97        if !self.armed {
98            return;
99        }
100        let mut entries = self.manager.entries.lock();
101        if entries.get(&self.identity).is_some_and(
102            |entry| matches!(entry, CoreEntry::Opening(cell) if cell.same_as(&self.cell)),
103        ) {
104            entries.remove(&self.identity);
105        }
106        drop(entries);
107        self.cell.fire();
108    }
109}
110
111impl OpenWaitCell {
112    fn same_as(&self, other: &OpenWaitCell) -> bool {
113        Arc::ptr_eq(&self.inner, &other.inner)
114    }
115}
116
117/// Back-link installed on a manager-initialized core so `shutdown()` can move
118/// the registry entry through `Closing` to removal (S1A-004).
119pub(crate) struct CoreRegistration {
120    pub identity: DatabaseFileIdentity,
121    pub manager: &'static DatabaseManager,
122}
123
124impl DatabaseManager {
125    /// The process-global registry (spec §10.1, S1A-002).
126    pub fn global() -> &'static DatabaseManager {
127        static MANAGER: OnceLock<DatabaseManager> = OnceLock::new();
128        MANAGER.get_or_init(|| DatabaseManager {
129            entries: Mutex::new(HashMap::new()),
130        })
131    }
132
133    /// Attach to the one shared core for `root`, initializing it on first use
134    /// (spec §10.1, S1A-002).
135    ///
136    /// Concurrent attaches initialize exactly once: the first caller runs the
137    /// full open path (recovery, WAL opening, open-generation advancement,
138    /// table mounting) while the rest wait, then every caller receives a
139    /// handle over the same core. Fails with `DatabaseLocked` while an
140    /// exclusive `Database` owner holds the root, and vice versa.
141    pub fn open_shared(
142        self: &'static DatabaseManager,
143        root: impl AsRef<std::path::Path>,
144        identity: OpenIdentity,
145    ) -> Result<DatabaseHandle> {
146        let identity_key = DatabaseFileIdentity::for_path(root.as_ref())?;
147        let core = loop {
148            let mut entries = self.entries.lock();
149            match entries.get(&identity_key) {
150                Some(CoreEntry::Open(weak)) => {
151                    if let Some(core) = weak.upgrade() {
152                        if core.is_open() {
153                            break core;
154                        }
155                        // A shutdown transitioned this core out of `Open`; the
156                        // entry is on its way out. If the lock is still held
157                        // the re-open below fails fast with `DatabaseLocked`.
158                        entries.remove(&identity_key);
159                    } else {
160                        // Stale weak: the last handle dropped and storage
161                        // closed with it. Re-initialize.
162                        entries.remove(&identity_key);
163                    }
164                }
165                Some(CoreEntry::Opening(cell)) => {
166                    let cell = cell.clone();
167                    drop(entries);
168                    cell.wait();
169                }
170                Some(CoreEntry::Closing) => {
171                    entries.remove(&identity_key);
172                }
173                None => {
174                    let cell = OpenWaitCell::new();
175                    entries.insert(identity_key.clone(), CoreEntry::Opening(cell.clone()));
176                    drop(entries);
177                    let mut guard = OpeningGuard {
178                        manager: self,
179                        identity: identity_key.clone(),
180                        cell: cell.clone(),
181                        armed: true,
182                    };
183                    // The one initialization for this core: recovery, WAL
184                    // opening, open-generation advancement, table mounting.
185                    let core = Database::open_for_shared_core(root.as_ref())?.into_core();
186                    core.set_registry(CoreRegistration {
187                        identity: identity_key.clone(),
188                        manager: self,
189                    })?;
190                    self.entries
191                        .lock()
192                        .insert(identity_key.clone(), CoreEntry::Open(Arc::downgrade(&core)));
193                    guard.disarm();
194                    cell.fire();
195                    break core;
196                }
197            }
198        };
199        let facade = Database::from_core(core, None, true);
200        Ok(DatabaseHandle::new(
201            facade,
202            identity.handle_identity(),
203            HandleAccess::default(),
204        ))
205    }
206
207    /// Number of registry entries (all states). Diagnostics and tests.
208    pub fn registered_entries(&self) -> usize {
209        self.entries.lock().len()
210    }
211
212    /// S1A-004 shutdown step: move the entry for `core` to `Closing` so new
213    /// attaches do not rendezvous on a core that is going away.
214    pub(crate) fn mark_closing(&self, identity: &DatabaseFileIdentity, core: &Arc<DatabaseCore>) {
215        let mut entries = self.entries.lock();
216        if entries.get(identity).is_some_and(
217            |entry| matches!(entry, CoreEntry::Open(weak) if weak.as_ptr() == Arc::as_ptr(core)),
218        ) {
219            entries.insert(identity.clone(), CoreEntry::Closing);
220        }
221    }
222
223    /// S1A-004 shutdown step: remove the entry for `core` once the file lock
224    /// is released. The next attach re-initializes a fresh core.
225    pub(crate) fn entry_closed(&self, identity: &DatabaseFileIdentity, core: &Arc<DatabaseCore>) {
226        let mut entries = self.entries.lock();
227        let owned = match entries.get(identity) {
228            Some(CoreEntry::Closing) => true,
229            Some(CoreEntry::Open(weak)) => weak.as_ptr() == Arc::as_ptr(core),
230            _ => false,
231        };
232        if owned {
233            entries.remove(identity);
234        }
235    }
236}
237
238#[cfg(test)]
239mod tests {
240    use super::*;
241
242    #[test]
243    fn wait_cell_wakes_all_waiters() {
244        let cell = OpenWaitCell::new();
245        let waiter = {
246            let cell = cell.clone();
247            std::thread::spawn(move || cell.wait())
248        };
249        std::thread::sleep(std::time::Duration::from_millis(10));
250        cell.fire();
251        waiter.join().unwrap();
252        // Fired cells return immediately.
253        cell.wait();
254    }
255
256    #[test]
257    fn global_is_a_singleton() {
258        assert!(std::ptr::eq(
259            DatabaseManager::global(),
260            DatabaseManager::global()
261        ));
262    }
263}