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}