Skip to main content

mongreldb_core/
storage_mode.rs

1//! Durable storage-mode marker (spec section 5.3, Stage 2E).
2//!
3//! Every database root carries `_meta/storage-mode`: a versioned, checksummed
4//! marker declaring which runtime owns the directory. The marker is written
5//! atomically through the pinned [`crate::durable_file::DurableRoot`] (temp +
6//! rename + fsync, mirroring the `_meta/generation` idiom in
7//! [`crate::catalog`]).
8//!
9//! # Rules (spec section 5.3)
10//!
11//! - [`StorageMode::Standalone`] may be opened embedded.
12//! - [`StorageMode::ServerOwnedStandalone`] is functionally standalone; the
13//!   server owns the lock (the existing `_meta/.lock` arbitration enforces
14//!   that; embedded opens succeed when the lock is free).
15//! - [`StorageMode::ClusterReplica`] may be opened only by the cluster node
16//!   runtime (`Database::open_cluster_replica`); every other open path rejects
17//!   it with [`StorageModeError::ClusterReplicaRequiresClusterRuntime`].
18//! - A backup validator may open any mode read-only through the special
19//!   offline-validation API (`OpenOptions::offline_validation`).
20//!
21//! Databases created before the marker existed have no marker file: they open
22//! as [`StorageMode::Standalone`] and the marker is written on first open (the
23//! on-disk format is otherwise unchanged — the marker is purely additive).
24//!
25//! Mode transitions are never rewritten in place (spec section 5.2: no
26//! in-place "magic conversion"); [`write`] fails closed when the existing
27//! marker disagrees with the requested mode.
28
29use std::path::Path;
30
31use serde::{Deserialize, Serialize};
32use sha2::{Digest, Sha256};
33
34use mongreldb_types::ids::{ClusterId, DatabaseId, NodeId};
35
36use crate::MongrelError;
37
38/// Marker file name under `_meta/`.
39pub const STORAGE_MODE_FILENAME: &str = "storage-mode";
40
41const MAGIC: &[u8; 8] = b"MMODE001";
42/// The only marker format version this build reads and writes.
43pub const STORAGE_MODE_FORMAT_VERSION: u16 = 1;
44
45const TAG_STANDALONE: u8 = 0;
46const TAG_SERVER_OWNED_STANDALONE: u8 = 1;
47const TAG_CLUSTER_REPLICA: u8 = 2;
48
49/// Which runtime owns a database directory (spec section 5.3).
50#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
51pub enum StorageMode {
52    /// Openable embedded and in single-node-server mode.
53    Standalone,
54    /// Functionally standalone, but the owning server holds the lock.
55    ServerOwnedStandalone,
56    /// Owned by the cluster node runtime; rejected by every normal open.
57    ClusterReplica {
58        /// The owning cluster.
59        cluster_id: ClusterId,
60        /// The node this replica belongs to.
61        node_id: NodeId,
62        /// The replicated logical database.
63        database_id: DatabaseId,
64    },
65}
66
67impl StorageMode {
68    /// The cluster identity of a [`StorageMode::ClusterReplica`], `None` for
69    /// standalone modes.
70    pub fn cluster_identity(&self) -> Option<(ClusterId, NodeId, DatabaseId)> {
71        match *self {
72            StorageMode::ClusterReplica {
73                cluster_id,
74                node_id,
75                database_id,
76            } => Some((cluster_id, node_id, database_id)),
77            _ => None,
78        }
79    }
80
81    fn body(&self) -> Vec<u8> {
82        let mut body = Vec::with_capacity(3 + 48);
83        body.extend_from_slice(&STORAGE_MODE_FORMAT_VERSION.to_le_bytes());
84        match self {
85            StorageMode::Standalone => body.push(TAG_STANDALONE),
86            StorageMode::ServerOwnedStandalone => body.push(TAG_SERVER_OWNED_STANDALONE),
87            StorageMode::ClusterReplica {
88                cluster_id,
89                node_id,
90                database_id,
91            } => {
92                body.push(TAG_CLUSTER_REPLICA);
93                body.extend_from_slice(cluster_id.as_bytes());
94                body.extend_from_slice(node_id.as_bytes());
95                body.extend_from_slice(database_id.as_bytes());
96            }
97        }
98        body
99    }
100
101    fn decode_body(body: &[u8]) -> Result<Self, StorageModeError> {
102        if body.len() < 3 {
103            return Err(StorageModeError::Corrupt {
104                reason: format!("marker body truncated: {} bytes", body.len()),
105            });
106        }
107        let version = u16::from_le_bytes([body[0], body[1]]);
108        if version != STORAGE_MODE_FORMAT_VERSION {
109            return Err(StorageModeError::UnsupportedVersion {
110                found: version,
111                supported: STORAGE_MODE_FORMAT_VERSION,
112            });
113        }
114        let expect_len = |len: usize, what: &str| -> Result<(), StorageModeError> {
115            if body.len() != len {
116                return Err(StorageModeError::Corrupt {
117                    reason: format!(
118                        "marker body for {what} must be {len} bytes, got {}",
119                        body.len()
120                    ),
121                });
122            }
123            Ok(())
124        };
125        match body[2] {
126            TAG_STANDALONE => {
127                expect_len(3, "standalone")?;
128                Ok(StorageMode::Standalone)
129            }
130            TAG_SERVER_OWNED_STANDALONE => {
131                expect_len(3, "server-owned standalone")?;
132                Ok(StorageMode::ServerOwnedStandalone)
133            }
134            TAG_CLUSTER_REPLICA => {
135                expect_len(3 + 48, "cluster replica")?;
136                let id = |offset: usize| -> [u8; 16] {
137                    body[3 + offset..3 + offset + 16]
138                        .try_into()
139                        .expect("length checked")
140                };
141                Ok(StorageMode::ClusterReplica {
142                    cluster_id: ClusterId::from_bytes(id(0)),
143                    node_id: NodeId::from_bytes(id(16)),
144                    database_id: DatabaseId::from_bytes(id(32)),
145                })
146            }
147            tag => Err(StorageModeError::Corrupt {
148                reason: format!("unknown storage-mode tag {tag}"),
149            }),
150        }
151    }
152
153    /// The framed on-disk form: `MAGIC | sha256(body) | body`.
154    fn frame(&self) -> Vec<u8> {
155        let body = self.body();
156        let hash = Sha256::digest(&body);
157        let mut out = Vec::with_capacity(8 + 32 + body.len());
158        out.extend_from_slice(MAGIC);
159        out.extend_from_slice(&hash);
160        out.extend_from_slice(&body);
161        out
162    }
163
164    fn deframe(bytes: &[u8]) -> Result<Self, StorageModeError> {
165        if bytes.len() < 8 + 32 + 3 || &bytes[..8] != MAGIC {
166            return Err(StorageModeError::Corrupt {
167                reason: "bad magic or truncated header".into(),
168            });
169        }
170        let (tag, body) = bytes[8..].split_at(32);
171        let calc = Sha256::digest(body);
172        if tag != calc.as_slice() {
173            return Err(StorageModeError::Corrupt {
174                reason: "checksum mismatch".into(),
175            });
176        }
177        Self::decode_body(body)
178    }
179}
180
181/// Typed errors of the storage-mode marker (spec section 5.3). Mirrors the
182/// subsystem-error idiom of [`crate::locks::LockError`];
183/// [`From<StorageModeError> for MongrelError`] maps onto the closest existing
184/// engine variants.
185#[derive(Debug, thiserror::Error)]
186pub enum StorageModeError {
187    /// A normal open path touched a cluster-replica database.
188    #[error(
189        "database is a replica of cluster {cluster_id} node {node_id} database {database_id}: \
190         only the cluster node runtime may open it (Database::open_cluster_replica), or open it \
191         read-only with OpenOptions::offline_validation"
192    )]
193    ClusterReplicaRequiresClusterRuntime {
194        /// Owning cluster.
195        cluster_id: ClusterId,
196        /// Owning node.
197        node_id: NodeId,
198        /// Replicated database.
199        database_id: DatabaseId,
200    },
201    /// The cluster runtime offered an identity that disagrees with the marker
202    /// (fail closed: opening the wrong replica would corrupt two clusters).
203    #[error("storage-mode identity mismatch: {0}")]
204    IdentityMismatch(String),
205    /// The marker failed validation (fails closed).
206    #[error("corrupt storage-mode marker: {reason}")]
207    Corrupt {
208        /// Why the marker failed validation.
209        reason: String,
210    },
211    /// The marker was written by a newer format than this build supports.
212    #[error(
213        "unsupported storage-mode marker version {found}: this build supports version {supported} only"
214    )]
215    UnsupportedVersion {
216        /// Version found on disk.
217        found: u16,
218        /// Version this build writes.
219        supported: u16,
220    },
221    /// A mode change was requested (spec section 5.2 forbids in-place
222    /// conversion).
223    #[error("storage-mode conflict: {0}")]
224    Conflict(String),
225    /// Underlying I/O failure.
226    #[error("io error: {0}")]
227    Io(#[from] std::io::Error),
228}
229
230impl From<StorageModeError> for MongrelError {
231    fn from(error: StorageModeError) -> Self {
232        match error {
233            // The caller used the wrong open contract for this directory
234            // (mirrors the "database already exists; use Database::open()"
235            // guidance style of `InvalidArgument`).
236            StorageModeError::ClusterReplicaRequiresClusterRuntime { .. }
237            | StorageModeError::IdentityMismatch(_)
238            | StorageModeError::Conflict(_) => MongrelError::InvalidArgument(error.to_string()),
239            StorageModeError::Corrupt { reason } => MongrelError::CorruptWal {
240                offset: 0,
241                reason: format!("storage-mode marker: {reason}"),
242            },
243            StorageModeError::UnsupportedVersion { found, supported } => {
244                MongrelError::UnsupportedStorageVersion {
245                    component: "storage-mode marker",
246                    found,
247                    supported,
248                }
249            }
250            StorageModeError::Io(error) => MongrelError::Io(error),
251        }
252    }
253}
254
255/// Reads the marker from `_meta/storage-mode`. Returns `None` for databases
256/// created before the marker existed (they are [`StorageMode::Standalone`]).
257/// Corrupt or unsupported markers fail closed.
258pub fn read(
259    durable_root: &crate::durable_file::DurableRoot,
260) -> Result<Option<StorageMode>, StorageModeError> {
261    let relative = Path::new(crate::database::META_DIR).join(STORAGE_MODE_FILENAME);
262    match durable_root.entry_exists(&relative) {
263        Ok(true) => {}
264        Ok(false) => return Ok(None),
265        // The `_meta` directory itself may not exist yet (fresh roots).
266        Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(None),
267        Err(error) => return Err(error.into()),
268    }
269    let mut file = durable_root.open_regular(&relative)?;
270    let mut bytes = Vec::new();
271    std::io::Read::read_to_end(&mut file, &mut bytes)?;
272    Ok(Some(StorageMode::deframe(&bytes)?))
273}
274
275/// Reads the marker from a filesystem path (no pinned root). Same contract as
276/// [`read`]; used before a durable root exists.
277pub fn read_at(root: impl AsRef<Path>) -> Result<Option<StorageMode>, StorageModeError> {
278    let path = root
279        .as_ref()
280        .join(crate::database::META_DIR)
281        .join(STORAGE_MODE_FILENAME);
282    let bytes = match std::fs::read(&path) {
283        Ok(bytes) => bytes,
284        Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(None),
285        Err(error) => return Err(error.into()),
286    };
287    Ok(Some(StorageMode::deframe(&bytes)?))
288}
289
290/// Writes the marker atomically (temp + rename + fsync through the pinned
291/// root). Fails closed when a different mode is already recorded: spec section
292/// 5.2 forbids in-place conversion for the first cluster release.
293pub fn write(
294    durable_root: &crate::durable_file::DurableRoot,
295    mode: &StorageMode,
296) -> Result<(), StorageModeError> {
297    if let Some(existing) = read(durable_root)? {
298        if existing == *mode {
299            return Ok(());
300        }
301        return Err(StorageModeError::Conflict(format!(
302            "existing marker {existing:?} disagrees with requested mode {mode:?}; \
303             in-place storage-mode conversion is not supported (spec 5.2)"
304        )));
305    }
306    durable_root.create_directory_all(crate::database::META_DIR)?;
307    durable_root.write_atomic(
308        Path::new(crate::database::META_DIR).join(STORAGE_MODE_FILENAME),
309        &mode.frame(),
310    )?;
311    Ok(())
312}
313
314/// Overwrites the marker atomically with `mode`. Restricted to the cluster
315/// snapshot-install path, which rewrites the marker's node identity when a
316/// snapshot built by a peer is staged locally (spec section 11.5: the staged
317/// image is validated before it replaces anything).
318pub(crate) fn rewrite(
319    durable_root: &crate::durable_file::DurableRoot,
320    mode: &StorageMode,
321) -> Result<(), StorageModeError> {
322    durable_root.create_directory_all(crate::database::META_DIR)?;
323    durable_root.write_atomic(
324        Path::new(crate::database::META_DIR).join(STORAGE_MODE_FILENAME),
325        &mode.frame(),
326    )?;
327    Ok(())
328}
329
330/// The open-path gate (spec section 5.3). `offline_validation` is the special
331/// offline validator: it may open any mode, and the caller forces the opened
332/// core read-only.
333pub(crate) fn check_open(
334    mode: Option<&StorageMode>,
335    offline_validation: bool,
336) -> Result<(), StorageModeError> {
337    match mode {
338        Some(StorageMode::ClusterReplica {
339            cluster_id,
340            node_id,
341            database_id,
342        }) if !offline_validation => Err(StorageModeError::ClusterReplicaRequiresClusterRuntime {
343            cluster_id: *cluster_id,
344            node_id: *node_id,
345            database_id: *database_id,
346        }),
347        _ => Ok(()),
348    }
349}
350
351#[cfg(test)]
352mod tests {
353    use super::*;
354
355    fn ids() -> (ClusterId, NodeId, DatabaseId) {
356        (
357            ClusterId::from_bytes([1; 16]),
358            NodeId::from_bytes([2; 16]),
359            DatabaseId::from_bytes([3; 16]),
360        )
361    }
362
363    #[test]
364    fn frame_round_trip_every_mode() {
365        let (cluster_id, node_id, database_id) = ids();
366        let modes = [
367            StorageMode::Standalone,
368            StorageMode::ServerOwnedStandalone,
369            StorageMode::ClusterReplica {
370                cluster_id,
371                node_id,
372                database_id,
373            },
374        ];
375        for mode in modes {
376            let framed = mode.frame();
377            assert_eq!(StorageMode::deframe(&framed).unwrap(), mode);
378        }
379    }
380
381    #[test]
382    fn corrupt_frames_fail_closed() {
383        let mode = StorageMode::Standalone;
384        let framed = mode.frame();
385        // Truncated.
386        assert!(StorageMode::deframe(&framed[..10]).is_err());
387        // Bad magic.
388        let mut bad_magic = framed.clone();
389        bad_magic[0] ^= 0x01;
390        assert!(matches!(
391            StorageMode::deframe(&bad_magic),
392            Err(StorageModeError::Corrupt { .. })
393        ));
394        // Checksum mismatch.
395        let mut bit_flip = framed;
396        let last = bit_flip.len() - 1;
397        bit_flip[last] ^= 0x01;
398        assert!(matches!(
399            StorageMode::deframe(&bit_flip),
400            Err(StorageModeError::Corrupt { .. })
401        ));
402    }
403
404    #[test]
405    fn unknown_version_and_tag_fail_closed() {
406        let (cluster_id, node_id, database_id) = ids();
407        let mut body = StorageMode::ClusterReplica {
408            cluster_id,
409            node_id,
410            database_id,
411        }
412        .body();
413        body[0] = (STORAGE_MODE_FORMAT_VERSION + 1) as u8;
414        let hash = Sha256::digest(&body);
415        let mut framed = Vec::new();
416        framed.extend_from_slice(MAGIC);
417        framed.extend_from_slice(&hash);
418        framed.extend_from_slice(&body);
419        assert!(matches!(
420            StorageMode::deframe(&framed),
421            Err(StorageModeError::UnsupportedVersion { found, .. }) if found == STORAGE_MODE_FORMAT_VERSION + 1
422        ));
423
424        let mut tagged = StorageMode::Standalone.frame();
425        let n = tagged.len();
426        tagged[n - 1] = 99;
427        let body = &tagged[40..];
428        let hash = Sha256::digest(body);
429        tagged[8..40].copy_from_slice(&hash);
430        assert!(matches!(
431            StorageMode::deframe(&tagged),
432            Err(StorageModeError::Corrupt { .. })
433        ));
434    }
435
436    #[test]
437    fn durable_write_read_and_no_conversion() {
438        let tmp = tempfile::tempdir().unwrap();
439        let root = crate::durable_file::DurableRoot::open(tmp.path()).unwrap();
440        assert_eq!(read(&root).unwrap(), None);
441
442        write(&root, &StorageMode::Standalone).unwrap();
443        assert_eq!(read(&root).unwrap(), Some(StorageMode::Standalone));
444        // Idempotent rewrite of the same mode.
445        write(&root, &StorageMode::Standalone).unwrap();
446
447        let (cluster_id, node_id, database_id) = ids();
448        let cluster = StorageMode::ClusterReplica {
449            cluster_id,
450            node_id,
451            database_id,
452        };
453        assert!(matches!(
454            write(&root, &cluster),
455            Err(StorageModeError::Conflict(_))
456        ));
457        assert_eq!(read(&root).unwrap(), Some(StorageMode::Standalone));
458
459        rewrite(&root, &cluster).unwrap();
460        assert_eq!(read(&root).unwrap(), Some(cluster));
461        assert_eq!(read_at(tmp.path()).unwrap(), Some(cluster));
462    }
463
464    #[test]
465    fn open_gate_matrix() {
466        let (cluster_id, node_id, database_id) = ids();
467        let cluster = StorageMode::ClusterReplica {
468            cluster_id,
469            node_id,
470            database_id,
471        };
472        // Normal opens.
473        assert!(check_open(None, false).is_ok());
474        assert!(check_open(Some(&StorageMode::Standalone), false).is_ok());
475        assert!(check_open(Some(&StorageMode::ServerOwnedStandalone), false).is_ok());
476        assert!(matches!(
477            check_open(Some(&cluster), false),
478            Err(StorageModeError::ClusterReplicaRequiresClusterRuntime { .. })
479        ));
480        // Offline validation opens any mode.
481        assert!(check_open(Some(&cluster), true).is_ok());
482        assert!(check_open(Some(&StorageMode::Standalone), true).is_ok());
483    }
484
485    #[test]
486    fn error_display_names_cluster_runtime() {
487        let (cluster_id, node_id, database_id) = ids();
488        let error = StorageModeError::ClusterReplicaRequiresClusterRuntime {
489            cluster_id,
490            node_id,
491            database_id,
492        };
493        let display = error.to_string();
494        assert!(display.contains(&cluster_id.to_hex()), "{display}");
495        assert!(display.contains(&node_id.to_hex()), "{display}");
496        assert!(display.contains(&database_id.to_hex()), "{display}");
497        assert!(display.contains("cluster node runtime"), "{display}");
498        let mapped: MongrelError = error.into();
499        assert!(matches!(mapped, MongrelError::InvalidArgument(_)));
500    }
501}