1use 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
38pub const STORAGE_MODE_FILENAME: &str = "storage-mode";
40
41const MAGIC: &[u8; 8] = b"MMODE001";
42pub 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
51pub enum StorageMode {
52 Standalone,
54 ServerOwnedStandalone,
56 ClusterReplica {
58 cluster_id: ClusterId,
60 node_id: NodeId,
62 database_id: DatabaseId,
64 },
65}
66
67impl StorageMode {
68 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 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#[derive(Debug, thiserror::Error)]
186pub enum StorageModeError {
187 #[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 cluster_id: ClusterId,
196 node_id: NodeId,
198 database_id: DatabaseId,
200 },
201 #[error("storage-mode identity mismatch: {0}")]
204 IdentityMismatch(String),
205 #[error("corrupt storage-mode marker: {reason}")]
207 Corrupt {
208 reason: String,
210 },
211 #[error(
213 "unsupported storage-mode marker version {found}: this build supports version {supported} only"
214 )]
215 UnsupportedVersion {
216 found: u16,
218 supported: u16,
220 },
221 #[error("storage-mode conflict: {0}")]
224 Conflict(String),
225 #[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 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
255pub 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 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
275pub 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
290pub 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
314pub(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
330pub(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 assert!(StorageMode::deframe(&framed[..10]).is_err());
387 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 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 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 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 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}