Skip to main content

mongreldb_server/
storage_runtime.rs

1//! Authoritative server storage runtime (P0.2 / ADR-0001).
2//!
3//! The daemon holds exactly one of:
4//! - [`ServerStorageRuntime::Standalone`] — ordinary single-node data plane
5//!   over a local [`mongreldb_core::Database`]
6//! - [`ServerStorageRuntime::Cluster`] — consensus/tablet data plane via
7//!   [`crate::cluster_runtime::ClusterRuntimeHandle`]; ordinary public writes
8//!   must not use a standalone WAL bypass
9//!
10//! Dual-root ownership (standalone user database + live cluster runtime as
11//! peer data planes) is refused.
12//!
13//! The standalone types (`StandaloneRuntime`, `ServerStorageRuntime`,
14//! `StorageRuntimeError`) are always compiled — the hot SQL path is typed over
15//! them. The cluster pieces (`ClusterGatewayRuntime`, the `Cluster` variant,
16//! and the cluster accessors) exist only with the `cluster` feature.
17
18use std::path::{Path, PathBuf};
19use std::sync::Arc;
20
21use mongreldb_core::Database;
22#[cfg(feature = "cluster")]
23use mongreldb_query::ai_retrieval::RemoteAiEndpoint;
24#[cfg(feature = "cluster")]
25use mongreldb_query::distributed::RemoteFragmentEndpoint;
26
27#[cfg(feature = "cluster")]
28use crate::cluster_runtime::ClusterRuntimeHandle;
29
30/// Error when code asks the standalone data plane for a cluster process (or
31/// the reverse).
32#[derive(Debug, Clone, PartialEq, Eq)]
33pub struct StorageRuntimeError {
34    message: String,
35}
36
37impl StorageRuntimeError {
38    /// Build a fail-closed error.
39    pub fn new(message: impl Into<String>) -> Self {
40        Self {
41            message: message.into(),
42        }
43    }
44
45    /// Stable message for the cluster standalone-bypass refusal.
46    pub fn cluster_refuses_standalone_bypass() -> Self {
47        Self::new(
48            "cluster mode refuses standalone AppState.db data-plane access; \
49             public data operations are owned by consensus/tablet state",
50        )
51    }
52}
53
54impl std::fmt::Display for StorageRuntimeError {
55    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
56        f.write_str(&self.message)
57    }
58}
59
60impl std::error::Error for StorageRuntimeError {}
61
62impl From<StorageRuntimeError> for mongreldb_core::MongrelError {
63    fn from(error: StorageRuntimeError) -> Self {
64        mongreldb_core::MongrelError::Other(error.message)
65    }
66}
67
68/// Standalone (single-node server) data plane.
69#[derive(Clone)]
70pub struct StandaloneRuntime {
71    db: Arc<Database>,
72}
73
74impl StandaloneRuntime {
75    /// Wrap an already-opened local database.
76    pub fn new(db: Arc<Database>) -> Self {
77        Self { db }
78    }
79
80    /// Local engine handle.
81    pub fn db(&self) -> &Arc<Database> {
82        &self.db
83    }
84
85    /// Durable root path for this standalone core.
86    pub fn root(&self) -> &Path {
87        self.db.root()
88    }
89}
90
91/// Cluster gateway / node data plane.
92///
93/// Holds the live [`ClusterRuntimeHandle`] and optional fragment/AI worker
94/// endpoints installed on the authenticated internal RPC transport. Public
95/// SQL/Kit/native user writes do not open a peer standalone WAL here.
96#[cfg(feature = "cluster")]
97#[derive(Clone)]
98pub struct ClusterGatewayRuntime {
99    handle: ClusterRuntimeHandle,
100    fragment_endpoint: Option<Arc<RemoteFragmentEndpoint>>,
101    ai_endpoint: Option<Arc<RemoteAiEndpoint>>,
102}
103
104#[cfg(feature = "cluster")]
105impl ClusterGatewayRuntime {
106    /// Wrap a started node runtime (workers may be installed later).
107    pub fn new(handle: ClusterRuntimeHandle) -> Self {
108        Self {
109            handle,
110            fragment_endpoint: None,
111            ai_endpoint: None,
112        }
113    }
114
115    /// Wrap a started node runtime that already has workers installed.
116    pub fn with_workers(
117        handle: ClusterRuntimeHandle,
118        fragment_endpoint: Arc<RemoteFragmentEndpoint>,
119        ai_endpoint: Arc<RemoteAiEndpoint>,
120    ) -> Self {
121        Self {
122            handle,
123            fragment_endpoint: Some(fragment_endpoint),
124            ai_endpoint: Some(ai_endpoint),
125        }
126    }
127
128    /// Live node runtime handle.
129    pub fn handle(&self) -> &ClusterRuntimeHandle {
130        &self.handle
131    }
132
133    /// Node data directory (identity, cluster-meta, tablet roots).
134    pub fn node_data(&self) -> &Path {
135        self.handle.node_data()
136    }
137
138    /// Installed fragment worker endpoint, when production start completed.
139    pub fn fragment_endpoint(&self) -> Option<&Arc<RemoteFragmentEndpoint>> {
140        self.fragment_endpoint.as_ref()
141    }
142
143    /// Installed AI worker endpoint, when production start completed.
144    pub fn ai_endpoint(&self) -> Option<&Arc<RemoteAiEndpoint>> {
145        self.ai_endpoint.as_ref()
146    }
147
148    /// Record worker endpoints after install (tests / staged startup).
149    pub fn set_workers(
150        &mut self,
151        fragment_endpoint: Arc<RemoteFragmentEndpoint>,
152        ai_endpoint: Arc<RemoteAiEndpoint>,
153    ) {
154        self.fragment_endpoint = Some(fragment_endpoint);
155        self.ai_endpoint = Some(ai_endpoint);
156    }
157
158    /// Whether both production workers are installed.
159    pub fn workers_installed(&self) -> bool {
160        self.fragment_endpoint.is_some() && self.ai_endpoint.is_some()
161    }
162}
163
164/// Single authoritative storage runtime for the HTTP/native daemon (P0.2).
165#[derive(Clone)]
166pub enum ServerStorageRuntime {
167    /// Single-node server-owned standalone core.
168    Standalone(StandaloneRuntime),
169    /// Cluster node: public data owned by consensus/tablet state.
170    #[cfg(feature = "cluster")]
171    Cluster(ClusterGatewayRuntime),
172}
173
174impl ServerStorageRuntime {
175    /// Construct the standalone variant.
176    pub fn standalone(db: Arc<Database>) -> Self {
177        Self::Standalone(StandaloneRuntime::new(db))
178    }
179
180    /// Construct the cluster variant (workers optional until installed).
181    #[cfg(feature = "cluster")]
182    pub fn cluster(handle: ClusterRuntimeHandle) -> Self {
183        Self::Cluster(ClusterGatewayRuntime::new(handle))
184    }
185
186    /// Construct the cluster variant with workers already installed.
187    #[cfg(feature = "cluster")]
188    pub fn cluster_with_workers(
189        handle: ClusterRuntimeHandle,
190        fragment_endpoint: Arc<RemoteFragmentEndpoint>,
191        ai_endpoint: Arc<RemoteAiEndpoint>,
192    ) -> Self {
193        Self::Cluster(ClusterGatewayRuntime::with_workers(
194            handle,
195            fragment_endpoint,
196            ai_endpoint,
197        ))
198    }
199
200    /// `true` when this process is a cluster node data plane. Always `false`
201    /// in builds without the `cluster` feature.
202    pub fn is_cluster(&self) -> bool {
203        #[cfg(feature = "cluster")]
204        {
205            matches!(self, Self::Cluster(_))
206        }
207        #[cfg(not(feature = "cluster"))]
208        {
209            false
210        }
211    }
212
213    /// `true` when this process is standalone.
214    pub fn is_standalone(&self) -> bool {
215        matches!(self, Self::Standalone(_))
216    }
217
218    /// Local standalone engine, when this is standalone mode.
219    pub fn standalone_db(&self) -> Option<&Arc<Database>> {
220        match self {
221            Self::Standalone(rt) => Some(rt.db()),
222            #[cfg(feature = "cluster")]
223            Self::Cluster(_) => None,
224        }
225    }
226
227    /// Require the standalone engine; fail closed in cluster mode so ordinary
228    /// public writes cannot bypass Raft through `AppState.db`.
229    pub fn require_standalone_db(&self) -> Result<&Arc<Database>, StorageRuntimeError> {
230        self.standalone_db()
231            .ok_or_else(StorageRuntimeError::cluster_refuses_standalone_bypass)
232    }
233
234    /// Live cluster handle, when this is cluster mode.
235    #[cfg(feature = "cluster")]
236    pub fn cluster_handle(&self) -> Option<&ClusterRuntimeHandle> {
237        match self {
238            Self::Cluster(rt) => Some(rt.handle()),
239            Self::Standalone(_) => None,
240        }
241    }
242
243    /// Cluster gateway pieces, when this is cluster mode.
244    #[cfg(feature = "cluster")]
245    pub fn cluster_gateway(&self) -> Option<&ClusterGatewayRuntime> {
246        match self {
247            Self::Cluster(rt) => Some(rt),
248            Self::Standalone(_) => None,
249        }
250    }
251
252    /// Mutable cluster gateway (worker install bookkeeping).
253    #[cfg(feature = "cluster")]
254    pub fn cluster_gateway_mut(&mut self) -> Option<&mut ClusterGatewayRuntime> {
255        match self {
256            Self::Cluster(rt) => Some(rt),
257            Self::Standalone(_) => None,
258        }
259    }
260
261    /// Durable path used for process-local server state (idempotency, pid
262    /// hints). Standalone uses the database root; cluster uses node data.
263    pub fn durable_path(&self) -> PathBuf {
264        match self {
265            Self::Standalone(rt) => rt.root().to_path_buf(),
266            #[cfg(feature = "cluster")]
267            Self::Cluster(rt) => rt.node_data().to_path_buf(),
268        }
269    }
270
271    /// Mode name for health / capabilities surfaces.
272    pub fn mode_name(&self) -> &'static str {
273        match self {
274            Self::Standalone(_) => "standalone",
275            #[cfg(feature = "cluster")]
276            Self::Cluster(_) => "cluster",
277        }
278    }
279}
280
281#[cfg(test)]
282mod tests {
283    use super::*;
284    use tempfile::tempdir;
285
286    #[test]
287    fn standalone_exposes_db_cluster_refuses() {
288        let dir = tempdir().unwrap();
289        let db = Arc::new(Database::create(dir.path()).unwrap());
290        let standalone = ServerStorageRuntime::standalone(Arc::clone(&db));
291        assert!(standalone.is_standalone());
292        assert!(!standalone.is_cluster());
293        assert!(Arc::ptr_eq(
294            standalone.require_standalone_db().unwrap(),
295            &db
296        ));
297        assert_eq!(standalone.mode_name(), "standalone");
298    }
299
300    #[test]
301    fn cluster_runtime_enum_exists_and_has_no_standalone_db() {
302        // Structural: Cluster variant holds gateway pieces without a peer
303        // standalone Database field. Construction with a live runtime is
304        // covered by cluster_runtime / storage integration tests.
305        let dir = tempdir().unwrap();
306        let _ = dir;
307        let err = StorageRuntimeError::cluster_refuses_standalone_bypass();
308        assert!(err.to_string().contains("cluster mode refuses"));
309        let converted: mongreldb_core::MongrelError = err.into();
310        assert!(converted.to_string().contains("cluster mode refuses"));
311    }
312}