use std::path::Path;
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
use mongreldb_types::ids::{ClusterId, DatabaseId, NodeId};
use crate::MongrelError;
pub const STORAGE_MODE_FILENAME: &str = "storage-mode";
const MAGIC: &[u8; 8] = b"MMODE001";
pub const STORAGE_MODE_FORMAT_VERSION: u16 = 1;
const TAG_STANDALONE: u8 = 0;
const TAG_SERVER_OWNED_STANDALONE: u8 = 1;
const TAG_CLUSTER_REPLICA: u8 = 2;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum StorageMode {
Standalone,
ServerOwnedStandalone,
ClusterReplica {
cluster_id: ClusterId,
node_id: NodeId,
database_id: DatabaseId,
},
}
impl StorageMode {
pub fn cluster_identity(&self) -> Option<(ClusterId, NodeId, DatabaseId)> {
match *self {
StorageMode::ClusterReplica {
cluster_id,
node_id,
database_id,
} => Some((cluster_id, node_id, database_id)),
_ => None,
}
}
fn body(&self) -> Vec<u8> {
let mut body = Vec::with_capacity(3 + 48);
body.extend_from_slice(&STORAGE_MODE_FORMAT_VERSION.to_le_bytes());
match self {
StorageMode::Standalone => body.push(TAG_STANDALONE),
StorageMode::ServerOwnedStandalone => body.push(TAG_SERVER_OWNED_STANDALONE),
StorageMode::ClusterReplica {
cluster_id,
node_id,
database_id,
} => {
body.push(TAG_CLUSTER_REPLICA);
body.extend_from_slice(cluster_id.as_bytes());
body.extend_from_slice(node_id.as_bytes());
body.extend_from_slice(database_id.as_bytes());
}
}
body
}
fn decode_body(body: &[u8]) -> Result<Self, StorageModeError> {
if body.len() < 3 {
return Err(StorageModeError::Corrupt {
reason: format!("marker body truncated: {} bytes", body.len()),
});
}
let version = u16::from_le_bytes([body[0], body[1]]);
if version != STORAGE_MODE_FORMAT_VERSION {
return Err(StorageModeError::UnsupportedVersion {
found: version,
supported: STORAGE_MODE_FORMAT_VERSION,
});
}
let expect_len = |len: usize, what: &str| -> Result<(), StorageModeError> {
if body.len() != len {
return Err(StorageModeError::Corrupt {
reason: format!(
"marker body for {what} must be {len} bytes, got {}",
body.len()
),
});
}
Ok(())
};
match body[2] {
TAG_STANDALONE => {
expect_len(3, "standalone")?;
Ok(StorageMode::Standalone)
}
TAG_SERVER_OWNED_STANDALONE => {
expect_len(3, "server-owned standalone")?;
Ok(StorageMode::ServerOwnedStandalone)
}
TAG_CLUSTER_REPLICA => {
expect_len(3 + 48, "cluster replica")?;
let id = |offset: usize| -> [u8; 16] {
body[3 + offset..3 + offset + 16]
.try_into()
.expect("length checked")
};
Ok(StorageMode::ClusterReplica {
cluster_id: ClusterId::from_bytes(id(0)),
node_id: NodeId::from_bytes(id(16)),
database_id: DatabaseId::from_bytes(id(32)),
})
}
tag => Err(StorageModeError::Corrupt {
reason: format!("unknown storage-mode tag {tag}"),
}),
}
}
fn frame(&self) -> Vec<u8> {
let body = self.body();
let hash = Sha256::digest(&body);
let mut out = Vec::with_capacity(8 + 32 + body.len());
out.extend_from_slice(MAGIC);
out.extend_from_slice(&hash);
out.extend_from_slice(&body);
out
}
fn deframe(bytes: &[u8]) -> Result<Self, StorageModeError> {
if bytes.len() < 8 + 32 + 3 || &bytes[..8] != MAGIC {
return Err(StorageModeError::Corrupt {
reason: "bad magic or truncated header".into(),
});
}
let (tag, body) = bytes[8..].split_at(32);
let calc = Sha256::digest(body);
if tag != calc.as_slice() {
return Err(StorageModeError::Corrupt {
reason: "checksum mismatch".into(),
});
}
Self::decode_body(body)
}
}
#[derive(Debug, thiserror::Error)]
pub enum StorageModeError {
#[error(
"database is a replica of cluster {cluster_id} node {node_id} database {database_id}: \
only the cluster node runtime may open it (Database::open_cluster_replica), or open it \
read-only with OpenOptions::offline_validation"
)]
ClusterReplicaRequiresClusterRuntime {
cluster_id: ClusterId,
node_id: NodeId,
database_id: DatabaseId,
},
#[error("storage-mode identity mismatch: {0}")]
IdentityMismatch(String),
#[error("corrupt storage-mode marker: {reason}")]
Corrupt {
reason: String,
},
#[error(
"unsupported storage-mode marker version {found}: this build supports version {supported} only"
)]
UnsupportedVersion {
found: u16,
supported: u16,
},
#[error("storage-mode conflict: {0}")]
Conflict(String),
#[error("io error: {0}")]
Io(#[from] std::io::Error),
}
impl From<StorageModeError> for MongrelError {
fn from(error: StorageModeError) -> Self {
match error {
StorageModeError::ClusterReplicaRequiresClusterRuntime { .. }
| StorageModeError::IdentityMismatch(_)
| StorageModeError::Conflict(_) => MongrelError::InvalidArgument(error.to_string()),
StorageModeError::Corrupt { reason } => MongrelError::CorruptWal {
offset: 0,
reason: format!("storage-mode marker: {reason}"),
},
StorageModeError::UnsupportedVersion { found, supported } => {
MongrelError::UnsupportedStorageVersion {
component: "storage-mode marker",
found,
supported,
}
}
StorageModeError::Io(error) => MongrelError::Io(error),
}
}
}
pub fn read(
durable_root: &crate::durable_file::DurableRoot,
) -> Result<Option<StorageMode>, StorageModeError> {
let relative = Path::new(crate::database::META_DIR).join(STORAGE_MODE_FILENAME);
match durable_root.entry_exists(&relative) {
Ok(true) => {}
Ok(false) => return Ok(None),
Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(None),
Err(error) => return Err(error.into()),
}
let mut file = durable_root.open_regular(&relative)?;
let mut bytes = Vec::new();
std::io::Read::read_to_end(&mut file, &mut bytes)?;
Ok(Some(StorageMode::deframe(&bytes)?))
}
pub fn read_at(root: impl AsRef<Path>) -> Result<Option<StorageMode>, StorageModeError> {
let path = root
.as_ref()
.join(crate::database::META_DIR)
.join(STORAGE_MODE_FILENAME);
let bytes = match std::fs::read(&path) {
Ok(bytes) => bytes,
Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(None),
Err(error) => return Err(error.into()),
};
Ok(Some(StorageMode::deframe(&bytes)?))
}
pub fn write(
durable_root: &crate::durable_file::DurableRoot,
mode: &StorageMode,
) -> Result<(), StorageModeError> {
if let Some(existing) = read(durable_root)? {
if existing == *mode {
return Ok(());
}
return Err(StorageModeError::Conflict(format!(
"existing marker {existing:?} disagrees with requested mode {mode:?}; \
in-place storage-mode conversion is not supported (spec 5.2)"
)));
}
durable_root.create_directory_all(crate::database::META_DIR)?;
durable_root.write_atomic(
Path::new(crate::database::META_DIR).join(STORAGE_MODE_FILENAME),
&mode.frame(),
)?;
Ok(())
}
pub(crate) fn rewrite(
durable_root: &crate::durable_file::DurableRoot,
mode: &StorageMode,
) -> Result<(), StorageModeError> {
durable_root.create_directory_all(crate::database::META_DIR)?;
durable_root.write_atomic(
Path::new(crate::database::META_DIR).join(STORAGE_MODE_FILENAME),
&mode.frame(),
)?;
Ok(())
}
pub(crate) fn check_open(
mode: Option<&StorageMode>,
offline_validation: bool,
) -> Result<(), StorageModeError> {
match mode {
Some(StorageMode::ClusterReplica {
cluster_id,
node_id,
database_id,
}) if !offline_validation => Err(StorageModeError::ClusterReplicaRequiresClusterRuntime {
cluster_id: *cluster_id,
node_id: *node_id,
database_id: *database_id,
}),
_ => Ok(()),
}
}
#[cfg(test)]
mod tests {
use super::*;
fn ids() -> (ClusterId, NodeId, DatabaseId) {
(
ClusterId::from_bytes([1; 16]),
NodeId::from_bytes([2; 16]),
DatabaseId::from_bytes([3; 16]),
)
}
#[test]
fn frame_round_trip_every_mode() {
let (cluster_id, node_id, database_id) = ids();
let modes = [
StorageMode::Standalone,
StorageMode::ServerOwnedStandalone,
StorageMode::ClusterReplica {
cluster_id,
node_id,
database_id,
},
];
for mode in modes {
let framed = mode.frame();
assert_eq!(StorageMode::deframe(&framed).unwrap(), mode);
}
}
#[test]
fn corrupt_frames_fail_closed() {
let mode = StorageMode::Standalone;
let framed = mode.frame();
assert!(StorageMode::deframe(&framed[..10]).is_err());
let mut bad_magic = framed.clone();
bad_magic[0] ^= 0x01;
assert!(matches!(
StorageMode::deframe(&bad_magic),
Err(StorageModeError::Corrupt { .. })
));
let mut bit_flip = framed;
let last = bit_flip.len() - 1;
bit_flip[last] ^= 0x01;
assert!(matches!(
StorageMode::deframe(&bit_flip),
Err(StorageModeError::Corrupt { .. })
));
}
#[test]
fn unknown_version_and_tag_fail_closed() {
let (cluster_id, node_id, database_id) = ids();
let mut body = StorageMode::ClusterReplica {
cluster_id,
node_id,
database_id,
}
.body();
body[0] = (STORAGE_MODE_FORMAT_VERSION + 1) as u8;
let hash = Sha256::digest(&body);
let mut framed = Vec::new();
framed.extend_from_slice(MAGIC);
framed.extend_from_slice(&hash);
framed.extend_from_slice(&body);
assert!(matches!(
StorageMode::deframe(&framed),
Err(StorageModeError::UnsupportedVersion { found, .. }) if found == STORAGE_MODE_FORMAT_VERSION + 1
));
let mut tagged = StorageMode::Standalone.frame();
let n = tagged.len();
tagged[n - 1] = 99;
let body = &tagged[40..];
let hash = Sha256::digest(body);
tagged[8..40].copy_from_slice(&hash);
assert!(matches!(
StorageMode::deframe(&tagged),
Err(StorageModeError::Corrupt { .. })
));
}
#[test]
fn durable_write_read_and_no_conversion() {
let tmp = tempfile::tempdir().unwrap();
let root = crate::durable_file::DurableRoot::open(tmp.path()).unwrap();
assert_eq!(read(&root).unwrap(), None);
write(&root, &StorageMode::Standalone).unwrap();
assert_eq!(read(&root).unwrap(), Some(StorageMode::Standalone));
write(&root, &StorageMode::Standalone).unwrap();
let (cluster_id, node_id, database_id) = ids();
let cluster = StorageMode::ClusterReplica {
cluster_id,
node_id,
database_id,
};
assert!(matches!(
write(&root, &cluster),
Err(StorageModeError::Conflict(_))
));
assert_eq!(read(&root).unwrap(), Some(StorageMode::Standalone));
rewrite(&root, &cluster).unwrap();
assert_eq!(read(&root).unwrap(), Some(cluster));
assert_eq!(read_at(tmp.path()).unwrap(), Some(cluster));
}
#[test]
fn open_gate_matrix() {
let (cluster_id, node_id, database_id) = ids();
let cluster = StorageMode::ClusterReplica {
cluster_id,
node_id,
database_id,
};
assert!(check_open(None, false).is_ok());
assert!(check_open(Some(&StorageMode::Standalone), false).is_ok());
assert!(check_open(Some(&StorageMode::ServerOwnedStandalone), false).is_ok());
assert!(matches!(
check_open(Some(&cluster), false),
Err(StorageModeError::ClusterReplicaRequiresClusterRuntime { .. })
));
assert!(check_open(Some(&cluster), true).is_ok());
assert!(check_open(Some(&StorageMode::Standalone), true).is_ok());
}
#[test]
fn error_display_names_cluster_runtime() {
let (cluster_id, node_id, database_id) = ids();
let error = StorageModeError::ClusterReplicaRequiresClusterRuntime {
cluster_id,
node_id,
database_id,
};
let display = error.to_string();
assert!(display.contains(&cluster_id.to_hex()), "{display}");
assert!(display.contains(&node_id.to_hex()), "{display}");
assert!(display.contains(&database_id.to_hex()), "{display}");
assert!(display.contains("cluster node runtime"), "{display}");
let mapped: MongrelError = error.into();
assert!(matches!(mapped, MongrelError::InvalidArgument(_)));
}
}