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        self.open_shared_with_access(root, identity, HandleAccess::read_write())
147    }
148
149    /// Attach with an explicit per-handle read-only or read-write restriction.
150    pub fn open_shared_with_access(
151        self: &'static DatabaseManager,
152        root: impl AsRef<std::path::Path>,
153        identity: OpenIdentity,
154        access: HandleAccess,
155    ) -> Result<DatabaseHandle> {
156        let identity_key = DatabaseFileIdentity::for_path(root.as_ref())?;
157        let core = loop {
158            let mut entries = self.entries.lock();
159            match entries.get(&identity_key) {
160                Some(CoreEntry::Open(weak)) => {
161                    if let Some(core) = weak.upgrade() {
162                        if core.is_open() {
163                            break core;
164                        }
165                        // A shutdown transitioned this core out of `Open`; the
166                        // entry is on its way out. If the lock is still held
167                        // the re-open below fails fast with `DatabaseLocked`.
168                        entries.remove(&identity_key);
169                    } else {
170                        // Stale weak: the last handle dropped and storage
171                        // closed with it. Re-initialize.
172                        entries.remove(&identity_key);
173                    }
174                }
175                Some(CoreEntry::Opening(cell)) => {
176                    let cell = cell.clone();
177                    drop(entries);
178                    cell.wait();
179                }
180                Some(CoreEntry::Closing) => {
181                    entries.remove(&identity_key);
182                }
183                None => {
184                    let cell = OpenWaitCell::new();
185                    entries.insert(identity_key.clone(), CoreEntry::Opening(cell.clone()));
186                    drop(entries);
187                    let mut guard = OpeningGuard {
188                        manager: self,
189                        identity: identity_key.clone(),
190                        cell: cell.clone(),
191                        armed: true,
192                    };
193                    // The one initialization for this core: recovery, WAL
194                    // opening, open-generation advancement, table mounting.
195                    let core = Database::open_for_shared_core(root.as_ref())?.into_core();
196                    core.set_registry(CoreRegistration {
197                        identity: identity_key.clone(),
198                        manager: self,
199                    })?;
200                    self.entries
201                        .lock()
202                        .insert(identity_key.clone(), CoreEntry::Open(Arc::downgrade(&core)));
203                    guard.disarm();
204                    cell.fire();
205                    break core;
206                }
207            }
208        };
209        let unbound = Database::from_core(Arc::clone(&core), None, true);
210        let (handle_identity, principal, service_permissions) = match identity {
211            OpenIdentity::Credentialless => {
212                if unbound.require_auth_enabled() {
213                    return Err(crate::error::MongrelError::AuthRequired);
214                }
215                (crate::handle::HandleIdentity::Credentialless, None, None)
216            }
217            OpenIdentity::ServicePrincipal { principal_id } => {
218                let permissions = Vec::new();
219                let principal = service_principal(principal_id, permissions.clone());
220                (
221                    crate::handle::HandleIdentity::ServicePrincipal { principal_id },
222                    Some(principal),
223                    Some(permissions),
224                )
225            }
226            OpenIdentity::ScopedServicePrincipal {
227                principal_id,
228                permissions,
229            } => {
230                let principal = service_principal(principal_id, permissions.clone());
231                (
232                    crate::handle::HandleIdentity::ServicePrincipal { principal_id },
233                    Some(principal),
234                    Some(permissions),
235                )
236            }
237            OpenIdentity::CatalogCredentials { username, password } => {
238                if !unbound.require_auth_enabled() {
239                    return Err(crate::error::MongrelError::AuthNotRequired);
240                }
241                let principal = unbound
242                    .authenticate_principal(&username, password.expose())?
243                    .ok_or_else(|| crate::error::MongrelError::InvalidCredentials {
244                        username: username.clone(),
245                    })?;
246                let identity = crate::handle::HandleIdentity::CatalogUser {
247                    username: principal.username.clone(),
248                    user_id: principal.user_id,
249                    created_version: principal.created_epoch,
250                };
251                (identity, Some(principal), None)
252            }
253        };
254        let facade = Database::from_core(core, principal, true);
255        Ok(DatabaseHandle::new(
256            facade,
257            handle_identity,
258            access,
259            service_permissions,
260        ))
261    }
262
263    /// Number of registry entries (all states). Diagnostics and tests.
264    pub fn registered_entries(&self) -> usize {
265        self.entries.lock().len()
266    }
267
268    /// S1A-004 shutdown step: move the entry for `core` to `Closing` so new
269    /// attaches do not rendezvous on a core that is going away.
270    pub(crate) fn mark_closing(&self, identity: &DatabaseFileIdentity, core: &Arc<DatabaseCore>) {
271        let mut entries = self.entries.lock();
272        if entries.get(identity).is_some_and(
273            |entry| matches!(entry, CoreEntry::Open(weak) if weak.as_ptr() == Arc::as_ptr(core)),
274        ) {
275            entries.insert(identity.clone(), CoreEntry::Closing);
276        }
277    }
278
279    /// S1A-004 shutdown step: remove the entry for `core` once the file lock
280    /// is released. The next attach re-initializes a fresh core.
281    pub(crate) fn entry_closed(&self, identity: &DatabaseFileIdentity, core: &Arc<DatabaseCore>) {
282        let mut entries = self.entries.lock();
283        let owned = match entries.get(identity) {
284            Some(CoreEntry::Closing) => true,
285            Some(CoreEntry::Open(weak)) => weak.as_ptr() == Arc::as_ptr(core),
286            _ => false,
287        };
288        if owned {
289            entries.remove(identity);
290        }
291    }
292}
293
294fn service_principal(
295    principal_id: [u8; 16],
296    permissions: Vec<crate::auth::Permission>,
297) -> crate::auth::Principal {
298    crate::auth::Principal {
299        user_id: 0,
300        created_epoch: 0,
301        username: format!("service:{principal_id:02x?}"),
302        is_admin: false,
303        roles: Vec::new(),
304        permissions,
305    }
306}
307
308#[cfg(test)]
309mod tests {
310    use super::*;
311
312    #[test]
313    fn wait_cell_wakes_all_waiters() {
314        let cell = OpenWaitCell::new();
315        let waiter = {
316            let cell = cell.clone();
317            std::thread::spawn(move || cell.wait())
318        };
319        std::thread::sleep(std::time::Duration::from_millis(10));
320        cell.fire();
321        waiter.join().unwrap();
322        // Fired cells return immediately.
323        cell.wait();
324    }
325
326    #[test]
327    fn global_is_a_singleton() {
328        assert!(std::ptr::eq(
329            DatabaseManager::global(),
330            DatabaseManager::global()
331        ));
332    }
333}