1use std::collections::BTreeSet;
24use std::fmt;
25use std::fs::{self, File, OpenOptions};
26use std::io::{self, Read, Write};
27use std::path::{Path, PathBuf};
28use std::str::FromStr;
29use std::sync::atomic::{AtomicU64, Ordering};
30use std::time::{SystemTime, UNIX_EPOCH};
31
32use mongreldb_types::hlc::HlcTimestamp;
33use mongreldb_types::ids::{ClusterId, NodeId};
34use serde::{Deserialize, Serialize};
35
36pub const NODE_IDENTITY_FORMAT_VERSION: u32 = 1;
38pub const MIN_SUPPORTED_NODE_IDENTITY_FORMAT_VERSION: u32 = 1;
40pub const CLUSTER_META_DIR: &str = "cluster-meta";
42pub const IDENTITY_FILENAME: &str = "identity.json";
44pub(crate) const MAX_META_BYTES: u64 = 16 * 1024 * 1024;
46
47pub type Csprng<'a> = &'a mut dyn FnMut(&mut [u8]) -> Result<(), getrandom::Error>;
51
52#[derive(Debug, thiserror::Error)]
54pub enum ClusterError {
55 #[error(
59 "cluster identity mismatch: persisted identity belongs to cluster {persisted}, \
60 cannot bootstrap or join cluster {requested}; wipe the node identity to reprovision"
61 )]
62 ClusterIdentityMismatch {
63 persisted: ClusterId,
65 requested: ClusterId,
67 },
68 #[error("unsupported format version {found} in {file} (supported {min}..={max})")]
71 UnsupportedFormatVersion {
72 file: &'static str,
74 found: u32,
76 min: u32,
78 max: u32,
80 },
81 #[error("cluster metadata file {file} failed verification: {detail}")]
84 CorruptMetadata {
85 file: &'static str,
87 detail: String,
89 },
90 #[error("cluster metadata I/O error: {0}")]
92 Io(#[from] std::io::Error),
93 #[error("operating-system CSPRNG failed: {0}")]
95 Rng(String),
96 #[error("unsupported operation: {0}")]
98 Unsupported(&'static str),
99 #[error("invalid trust material: {0}")]
101 InvalidTrustMaterial(&'static str),
102 #[error("invalid join invite: {0}")]
104 InvalidInvite(&'static str),
105 #[error(
107 "cluster is already bootstrapped on this node (cluster {cluster_id}); \
108 wipe the node identity to reprovision"
109 )]
110 AlreadyBootstrapped {
111 cluster_id: ClusterId,
113 },
114 #[error(
116 "cluster metadata is not initialized in this directory; run cluster init or join first"
117 )]
118 NotInitialized,
119 #[error(
125 "another bootstrap workflow holds the lock {0}; if no init/join/drain is running, \
126 remove that lock file (it holds pid+timestamp) and retry"
127 )]
128 BootstrapInProgress(PathBuf),
129 #[error("node {node} is not present in the cluster membership record")]
131 NodeNotFound {
132 node: NodeId,
134 },
135 #[error("invalid node state transition for node {node}: {from} -> {to}")]
137 InvalidNodeStateTransition {
138 node: NodeId,
140 from: NodeState,
142 to: NodeState,
144 },
145 #[error("invalid node-removal confirmation token")]
147 InvalidConfirmationToken,
148}
149
150#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
156#[serde(deny_unknown_fields)]
157pub struct NodeIdentity {
158 pub cluster_id: ClusterId,
160 pub node_id: NodeId,
162 pub created_at: HlcTimestamp,
164 pub format_version: u32,
166}
167
168impl NodeIdentity {
169 pub fn load(node_data: &Path) -> Result<Option<Self>, ClusterError> {
175 let path = cluster_meta_dir(node_data).join(IDENTITY_FILENAME);
176 let Some(bytes) = read_meta_file(&path)? else {
177 return Ok(None);
178 };
179 let identity: Self = decode_json(IDENTITY_FILENAME, &bytes)?;
180 if identity.format_version < MIN_SUPPORTED_NODE_IDENTITY_FORMAT_VERSION
181 || identity.format_version > NODE_IDENTITY_FORMAT_VERSION
182 {
183 return Err(ClusterError::UnsupportedFormatVersion {
184 file: IDENTITY_FILENAME,
185 found: identity.format_version,
186 min: MIN_SUPPORTED_NODE_IDENTITY_FORMAT_VERSION,
187 max: NODE_IDENTITY_FORMAT_VERSION,
188 });
189 }
190 if identity.cluster_id == ClusterId::ZERO || identity.node_id == NodeId::ZERO {
191 return Err(ClusterError::CorruptMetadata {
192 file: IDENTITY_FILENAME,
193 detail: "reserved all-zero identifier".to_owned(),
194 });
195 }
196 Ok(Some(identity))
197 }
198
199 pub fn load_or_create(node_data: &Path, csprng: Csprng<'_>) -> Result<Self, ClusterError> {
206 if let Some(identity) = Self::load(node_data)? {
207 return Ok(identity);
208 }
209 let cluster_id = ClusterId::from_bytes(mint_id(csprng)?);
210 match Self::create(node_data, cluster_id, csprng)? {
211 CreateOutcome::Created(identity) => Ok(identity),
212 CreateOutcome::AlreadyExists => {
213 Self::load(node_data)?.ok_or(ClusterError::CorruptMetadata {
214 file: IDENTITY_FILENAME,
215 detail: "identity vanished after create race".to_owned(),
216 })
217 }
218 }
219 }
220
221 pub(crate) fn provision(
229 node_data: &Path,
230 cluster_id: ClusterId,
231 csprng: Csprng<'_>,
232 ) -> Result<Self, ClusterError> {
233 match Self::create(node_data, cluster_id, csprng)? {
234 CreateOutcome::Created(identity) => Ok(identity),
235 CreateOutcome::AlreadyExists => {
236 let identity = Self::load(node_data)?.ok_or(ClusterError::CorruptMetadata {
237 file: IDENTITY_FILENAME,
238 detail: "identity vanished after create race".to_owned(),
239 })?;
240 if identity.cluster_id != cluster_id {
241 return Err(ClusterError::ClusterIdentityMismatch {
242 persisted: identity.cluster_id,
243 requested: cluster_id,
244 });
245 }
246 Ok(identity)
247 }
248 }
249 }
250
251 fn create(
252 node_data: &Path,
253 cluster_id: ClusterId,
254 csprng: Csprng<'_>,
255 ) -> Result<CreateOutcome, ClusterError> {
256 let identity = Self {
257 cluster_id,
258 node_id: NodeId::from_bytes(mint_id(csprng)?),
259 created_at: wall_clock_now(),
260 format_version: NODE_IDENTITY_FORMAT_VERSION,
261 };
262 let bytes = encode_json(IDENTITY_FILENAME, &identity)?;
263 let meta_dir = cluster_meta_dir(node_data);
264 fs::create_dir_all(&meta_dir)?;
265 Ok(
266 match create_meta_file(&meta_dir, IDENTITY_FILENAME, &bytes)? {
267 true => CreateOutcome::Created(identity),
268 false => CreateOutcome::AlreadyExists,
269 },
270 )
271 }
272}
273
274enum CreateOutcome {
275 Created(NodeIdentity),
276 AlreadyExists,
277}
278
279#[derive(Clone, Debug, PartialEq, Eq)]
282pub struct WipedMarker {
283 pub wiped_cluster_id: ClusterId,
285 pub wiped_node_id: NodeId,
287 pub wiped_at: HlcTimestamp,
289 pub removed_files: Vec<PathBuf>,
291}
292
293pub fn wipe_identity(node_data: &Path) -> Result<Option<WipedMarker>, ClusterError> {
300 let Some(identity) = NodeIdentity::load(node_data)? else {
301 return Ok(None);
302 };
303 let meta_dir = cluster_meta_dir(node_data);
304 let mut removed_files = Vec::new();
305 for filename in [
306 IDENTITY_FILENAME,
307 crate::bootstrap::CLUSTER_RECORD_FILENAME,
308 crate::bootstrap::TRUST_FILENAME,
309 crate::bootstrap::JOIN_RECORD_FILENAME,
310 ] {
311 let path = meta_dir.join(filename);
312 match fs::remove_file(&path) {
313 Ok(()) => removed_files.push(path),
314 Err(error) if error.kind() == io::ErrorKind::NotFound => {}
315 Err(error) => return Err(error.into()),
316 }
317 }
318 sync_dir(&meta_dir)?;
319 Ok(Some(WipedMarker {
320 wiped_cluster_id: identity.cluster_id,
321 wiped_node_id: identity.node_id,
322 wiped_at: wall_clock_now(),
323 removed_files,
324 }))
325}
326
327#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
330pub enum NodeState {
331 Bootstrapping,
333 Up,
335 Draining,
337 Decommissioned,
339 Down,
341}
342
343impl fmt::Display for NodeState {
344 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
345 let name = match self {
346 Self::Bootstrapping => "Bootstrapping",
347 Self::Up => "Up",
348 Self::Draining => "Draining",
349 Self::Decommissioned => "Decommissioned",
350 Self::Down => "Down",
351 };
352 f.write_str(name)
353 }
354}
355
356#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
358pub enum LocalityParseError {
359 #[error("invalid locality tier `{0}`: expected `key=value`")]
361 InvalidTier(String),
362 #[error("locality tier `{0}` has an empty key or value")]
364 EmptyComponent(String),
365 #[error("duplicate locality key `{0}`")]
367 DuplicateKey(String),
368}
369
370#[derive(Clone, Debug, Default, PartialEq, Eq)]
378pub struct Locality {
379 tiers: Vec<(String, String)>,
380}
381
382impl Locality {
383 pub fn tiers(&self) -> &[(String, String)] {
385 &self.tiers
386 }
387
388 pub fn get(&self, key: &str) -> Option<&str> {
390 self.tiers
391 .iter()
392 .find(|(k, _)| k == key)
393 .map(|(_, v)| v.as_str())
394 }
395}
396
397impl FromStr for Locality {
398 type Err = LocalityParseError;
399
400 fn from_str(text: &str) -> Result<Self, Self::Err> {
401 let mut tiers = Vec::new();
402 for tier in text.split(',') {
403 let tier = tier.trim();
404 if tier.is_empty() {
405 continue;
406 }
407 let (key, value) = tier
408 .split_once('=')
409 .ok_or_else(|| LocalityParseError::InvalidTier(tier.to_owned()))?;
410 let key = key.trim();
411 let value = value.trim();
412 if key.is_empty() || value.is_empty() {
413 return Err(LocalityParseError::EmptyComponent(tier.to_owned()));
414 }
415 if tiers.iter().any(|(k, _): &(String, String)| k == key) {
416 return Err(LocalityParseError::DuplicateKey(key.to_owned()));
417 }
418 tiers.push((key.to_owned(), value.to_owned()));
419 }
420 Ok(Self { tiers })
421 }
422}
423
424impl fmt::Display for Locality {
425 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
426 for (index, (key, value)) in self.tiers.iter().enumerate() {
427 if index > 0 {
428 f.write_str(",")?;
429 }
430 write!(f, "{key}={value}")?;
431 }
432 Ok(())
433 }
434}
435
436impl Serialize for Locality {
437 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
439 serializer.serialize_str(&self.to_string())
440 }
441}
442
443impl<'de> Deserialize<'de> for Locality {
444 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
445 let text = String::deserialize(deserializer)?;
446 text.parse().map_err(serde::de::Error::custom)
447 }
448}
449
450#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
452#[serde(deny_unknown_fields)]
453pub struct NodeCapacity {
454 pub cpu: u32,
456 pub memory_bytes: u64,
458 pub disk_bytes: u64,
460}
461
462#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
464#[serde(deny_unknown_fields)]
465pub struct BuildVersion {
466 pub version: String,
468 pub git_sha: Option<String>,
470}
471
472impl BuildVersion {
473 pub fn current() -> Self {
475 Self {
476 version: env!("CARGO_PKG_VERSION").to_owned(),
477 git_sha: option_env!("MONGRELDB_GIT_SHA").map(str::to_owned),
478 }
479 }
480}
481
482pub const PROTOCOL_VERSION_MIN: u32 = 1;
490pub const PROTOCOL_VERSION_MAX: u32 = 1;
495pub const LOG_FORMAT_VERSION_MIN: u32 = mongreldb_log::envelope::MIN_SUPPORTED_FORMAT_VERSION;
498pub const LOG_FORMAT_VERSION_MAX: u32 = mongreldb_log::envelope::COMMAND_ENVELOPE_FORMAT_VERSION;
501pub const SNAPSHOT_FORMAT_VERSION_MIN: u32 = 1;
509pub const SNAPSHOT_FORMAT_VERSION_MAX: u32 = 1;
516
517#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
525#[serde(deny_unknown_fields)]
526pub struct VersionInfo {
527 pub binary_version: String,
529 pub protocol_min: u32,
531 pub protocol_max: u32,
533 pub log_format_min: u32,
535 pub log_format_max: u32,
537 pub snapshot_format_min: u32,
539 pub snapshot_format_max: u32,
541 pub feature_set: BTreeSet<String>,
544}
545
546impl VersionInfo {
547 pub fn current() -> Self {
550 Self {
551 binary_version: env!("CARGO_PKG_VERSION").to_owned(),
552 protocol_min: PROTOCOL_VERSION_MIN,
553 protocol_max: PROTOCOL_VERSION_MAX,
554 log_format_min: LOG_FORMAT_VERSION_MIN,
555 log_format_max: LOG_FORMAT_VERSION_MAX,
556 snapshot_format_min: SNAPSHOT_FORMAT_VERSION_MIN,
557 snapshot_format_max: SNAPSHOT_FORMAT_VERSION_MAX,
558 feature_set: crate::meta::FeatureRegistry::current().feature_names(),
559 }
560 }
561
562 pub fn is_compatible_with(&self, other: &Self) -> Result<(), Incompatibility> {
570 if !ranges_overlap(
571 self.protocol_min,
572 self.protocol_max,
573 other.protocol_min,
574 other.protocol_max,
575 ) {
576 return Err(Incompatibility::ProtocolVersion {
577 ours: (self.protocol_min, self.protocol_max),
578 theirs: (other.protocol_min, other.protocol_max),
579 });
580 }
581 if !ranges_overlap(
582 self.log_format_min,
583 self.log_format_max,
584 other.log_format_min,
585 other.log_format_max,
586 ) {
587 return Err(Incompatibility::LogFormat {
588 ours: (self.log_format_min, self.log_format_max),
589 theirs: (other.log_format_min, other.log_format_max),
590 });
591 }
592 if !ranges_overlap(
593 self.snapshot_format_min,
594 self.snapshot_format_max,
595 other.snapshot_format_min,
596 other.snapshot_format_max,
597 ) {
598 return Err(Incompatibility::SnapshotFormat {
599 ours: (self.snapshot_format_min, self.snapshot_format_max),
600 theirs: (other.snapshot_format_min, other.snapshot_format_max),
601 });
602 }
603 Ok(())
604 }
605}
606
607fn ranges_overlap(a_min: u32, a_max: u32, b_min: u32, b_max: u32) -> bool {
610 a_min <= a_max && b_min <= b_max && a_min <= b_max && b_min <= a_max
611}
612
613#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
616pub enum Incompatibility {
617 #[error(
619 "wire-protocol version ranges do not overlap: {ours:?} vs {theirs:?} (min, max inclusive)"
620 )]
621 ProtocolVersion {
622 ours: (u32, u32),
624 theirs: (u32, u32),
626 },
627 #[error("log format ranges do not overlap: {ours:?} vs {theirs:?} (min, max inclusive)")]
629 LogFormat {
630 ours: (u32, u32),
632 theirs: (u32, u32),
634 },
635 #[error("snapshot format ranges do not overlap: {ours:?} vs {theirs:?} (min, max inclusive)")]
637 SnapshotFormat {
638 ours: (u32, u32),
640 theirs: (u32, u32),
642 },
643}
644
645#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
649#[serde(deny_unknown_fields)]
650pub struct NodeDescriptor {
651 pub node_id: NodeId,
653 pub rpc_address: String,
655 pub locality: Locality,
657 pub capacity: NodeCapacity,
659 pub state: NodeState,
661 pub version: BuildVersion,
663 #[serde(default)]
669 pub version_info: VersionInfo,
670}
671
672pub(crate) fn cluster_meta_dir(node_data: &Path) -> PathBuf {
674 node_data.join(CLUSTER_META_DIR)
675}
676
677pub(crate) fn wall_clock_now() -> HlcTimestamp {
681 let physical_micros = SystemTime::now()
682 .duration_since(UNIX_EPOCH)
683 .map(|duration| u64::try_from(duration.as_micros()).unwrap_or(u64::MAX))
684 .unwrap_or(0);
685 HlcTimestamp {
686 physical_micros,
687 logical: 0,
688 node_tiebreaker: 0,
689 }
690}
691
692pub(crate) fn mint_id(csprng: Csprng<'_>) -> Result<[u8; 16], ClusterError> {
695 loop {
696 let mut bytes = [0u8; 16];
697 csprng(&mut bytes).map_err(|error| ClusterError::Rng(error.to_string()))?;
698 if bytes != [0u8; 16] {
699 return Ok(bytes);
700 }
701 }
702}
703
704pub(crate) fn encode_json<T: Serialize>(
706 file: &'static str,
707 value: &T,
708) -> Result<Vec<u8>, ClusterError> {
709 serde_json::to_vec_pretty(value).map_err(|error| ClusterError::CorruptMetadata {
710 file,
711 detail: format!("encode: {error}"),
712 })
713}
714
715pub(crate) fn decode_json<T: for<'de> Deserialize<'de>>(
718 file: &'static str,
719 bytes: &[u8],
720) -> Result<T, ClusterError> {
721 serde_json::from_slice(bytes).map_err(|error| ClusterError::CorruptMetadata {
722 file,
723 detail: format!("decode: {error}"),
724 })
725}
726
727pub(crate) fn read_meta_file(path: &Path) -> Result<Option<Vec<u8>>, ClusterError> {
730 let file = match File::open(path) {
731 Ok(file) => file,
732 Err(error) if error.kind() == io::ErrorKind::NotFound => return Ok(None),
733 Err(error) => return Err(error.into()),
734 };
735 let length = file.metadata()?.len();
736 if length > MAX_META_BYTES {
737 return Err(ClusterError::CorruptMetadata {
738 file: "cluster-meta",
739 detail: format!(
740 "{} exceeds the {} byte limit",
741 path.display(),
742 MAX_META_BYTES
743 ),
744 });
745 }
746 let mut bytes = Vec::with_capacity(length as usize);
747 file.take(MAX_META_BYTES + 1).read_to_end(&mut bytes)?;
748 if bytes.len() as u64 != length {
749 return Err(ClusterError::CorruptMetadata {
750 file: "cluster-meta",
751 detail: format!("{} changed while reading", path.display()),
752 });
753 }
754 Ok(Some(bytes))
755}
756
757pub(crate) fn write_meta_atomic(dir: &Path, filename: &str, bytes: &[u8]) -> io::Result<()> {
760 let temporary = write_temp_file(dir, filename, bytes)?;
761 let result = fs::rename(&temporary, dir.join(filename));
762 match result {
763 Ok(()) => sync_dir(dir),
764 Err(error) => {
765 let _ = fs::remove_file(&temporary);
766 Err(error)
767 }
768 }
769}
770
771pub(crate) fn create_meta_file(dir: &Path, filename: &str, bytes: &[u8]) -> io::Result<bool> {
775 let temporary = write_temp_file(dir, filename, bytes)?;
776 let result = fs::hard_link(&temporary, dir.join(filename));
777 let _ = fs::remove_file(&temporary);
778 match result {
779 Ok(()) => {
780 sync_dir(dir)?;
781 Ok(true)
782 }
783 Err(error) if error.kind() == io::ErrorKind::AlreadyExists => Ok(false),
784 Err(error) => Err(error),
785 }
786}
787
788fn write_temp_file(dir: &Path, filename: &str, bytes: &[u8]) -> io::Result<PathBuf> {
790 static TEMP_COUNTER: AtomicU64 = AtomicU64::new(0);
791 loop {
792 let unique = TEMP_COUNTER.fetch_add(1, Ordering::Relaxed);
793 let temporary = dir.join(format!(".{filename}.tmp-{}-{unique}", std::process::id()));
794 let mut file = match OpenOptions::new()
795 .write(true)
796 .create_new(true)
797 .open(&temporary)
798 {
799 Ok(file) => file,
800 Err(error) if error.kind() == io::ErrorKind::AlreadyExists => continue,
802 Err(error) => return Err(error),
803 };
804 let result = file.write_all(bytes).and_then(|()| file.sync_all());
805 drop(file);
806 match result {
807 Ok(()) => return Ok(temporary),
808 Err(error) => {
809 let _ = fs::remove_file(&temporary);
810 return Err(error);
811 }
812 }
813 }
814}
815
816pub(crate) fn sync_dir(dir: &Path) -> io::Result<()> {
818 mongreldb_types::durability::sync_directory(dir)
819}
820
821#[cfg(test)]
822mod tests {
823 use super::*;
824
825 fn test_csprng() -> impl FnMut(&mut [u8]) -> Result<(), getrandom::Error> {
826 let mut counter = 0u64;
827 move |buf: &mut [u8]| {
828 for chunk in buf.chunks_mut(8) {
829 counter += 1;
830 let bytes = counter.to_le_bytes();
831 chunk.copy_from_slice(&bytes[..chunk.len()]);
832 }
833 Ok(())
834 }
835 }
836
837 fn minted_identity(node_data: &Path) -> NodeIdentity {
838 NodeIdentity::load_or_create(node_data, &mut test_csprng()).expect("mint identity")
839 }
840
841 #[test]
842 fn first_boot_mints_and_persists_identity() {
843 let dir = tempfile::tempdir().unwrap();
844 let identity = minted_identity(dir.path());
845 assert_ne!(identity.cluster_id, ClusterId::ZERO);
846 assert_ne!(identity.node_id, NodeId::ZERO);
847 assert_eq!(identity.format_version, NODE_IDENTITY_FORMAT_VERSION);
848 assert!(dir
849 .path()
850 .join(CLUSTER_META_DIR)
851 .join(IDENTITY_FILENAME)
852 .is_file());
853 }
854
855 #[test]
856 fn reload_returns_the_same_identity() {
857 let dir = tempfile::tempdir().unwrap();
858 let first = minted_identity(dir.path());
859 let second = NodeIdentity::load_or_create(dir.path(), &mut test_csprng())
860 .expect("second boot loads, never re-mints");
861 assert_eq!(first, second);
862 assert_eq!(NodeIdentity::load(dir.path()).unwrap(), Some(first));
863 }
864
865 #[test]
866 fn minted_cluster_and_node_ids_differ() {
867 let dir = tempfile::tempdir().unwrap();
868 let identity = minted_identity(dir.path());
869 assert_ne!(identity.cluster_id.as_bytes(), identity.node_id.as_bytes());
870 }
871
872 #[test]
873 fn load_on_empty_dir_returns_none() {
874 let dir = tempfile::tempdir().unwrap();
875 assert_eq!(NodeIdentity::load(dir.path()).unwrap(), None);
876 }
877
878 #[test]
879 fn unknown_format_version_fails_closed() {
880 let dir = tempfile::tempdir().unwrap();
881 minted_identity(dir.path());
882 let mut value: serde_json::Value = serde_json::from_slice(
883 &std::fs::read(dir.path().join(CLUSTER_META_DIR).join(IDENTITY_FILENAME)).unwrap(),
884 )
885 .unwrap();
886 value["format_version"] = serde_json::json!(99);
887 std::fs::write(
888 dir.path().join(CLUSTER_META_DIR).join(IDENTITY_FILENAME),
889 serde_json::to_vec(&value).unwrap(),
890 )
891 .unwrap();
892 let error = NodeIdentity::load(dir.path()).unwrap_err();
893 assert!(
894 matches!(
895 error,
896 ClusterError::UnsupportedFormatVersion {
897 file: IDENTITY_FILENAME,
898 found: 99,
899 ..
900 }
901 ),
902 "unexpected error: {error}"
903 );
904 assert!(NodeIdentity::load_or_create(dir.path(), &mut test_csprng()).is_err());
906 }
907
908 #[test]
909 fn corrupt_payload_fails_closed() {
910 let dir = tempfile::tempdir().unwrap();
911 let meta = dir.path().join(CLUSTER_META_DIR);
912 std::fs::create_dir_all(&meta).unwrap();
913 std::fs::write(meta.join(IDENTITY_FILENAME), b"{ not json").unwrap();
914 let error = NodeIdentity::load(dir.path()).unwrap_err();
915 assert!(matches!(
916 error,
917 ClusterError::CorruptMetadata {
918 file: IDENTITY_FILENAME,
919 ..
920 }
921 ));
922 }
923
924 #[test]
925 fn unknown_fields_fail_closed() {
926 let dir = tempfile::tempdir().unwrap();
927 minted_identity(dir.path());
928 let path = dir.path().join(CLUSTER_META_DIR).join(IDENTITY_FILENAME);
929 let mut value: serde_json::Value =
930 serde_json::from_slice(&std::fs::read(&path).unwrap()).unwrap();
931 value["unexpected"] = serde_json::json!(1);
932 std::fs::write(&path, serde_json::to_vec(&value).unwrap()).unwrap();
933 let error = NodeIdentity::load(dir.path()).unwrap_err();
934 assert!(matches!(
935 error,
936 ClusterError::CorruptMetadata {
937 file: IDENTITY_FILENAME,
938 ..
939 }
940 ));
941 }
942
943 #[test]
944 fn reserved_zero_identifiers_fail_closed() {
945 let dir = tempfile::tempdir().unwrap();
946 let meta = dir.path().join(CLUSTER_META_DIR);
947 std::fs::create_dir_all(&meta).unwrap();
948 let identity = NodeIdentity {
949 cluster_id: ClusterId::ZERO,
950 node_id: NodeId::new_random(),
951 created_at: HlcTimestamp::ZERO,
952 format_version: NODE_IDENTITY_FORMAT_VERSION,
953 };
954 std::fs::write(
955 meta.join(IDENTITY_FILENAME),
956 serde_json::to_vec(&identity).unwrap(),
957 )
958 .unwrap();
959 let error = NodeIdentity::load(dir.path()).unwrap_err();
960 assert!(matches!(
961 error,
962 ClusterError::CorruptMetadata {
963 file: IDENTITY_FILENAME,
964 ..
965 }
966 ));
967 }
968
969 #[test]
970 fn provision_rejects_a_different_cluster() {
971 let dir = tempfile::tempdir().unwrap();
972 let cluster_a = ClusterId::new_random();
973 let identity = NodeIdentity::provision(dir.path(), cluster_a, &mut test_csprng()).unwrap();
974 assert_eq!(identity.cluster_id, cluster_a);
975 let again = NodeIdentity::provision(dir.path(), cluster_a, &mut test_csprng()).unwrap();
977 assert_eq!(again, identity);
978 let error =
980 NodeIdentity::provision(dir.path(), ClusterId::new_random(), &mut test_csprng())
981 .unwrap_err();
982 assert!(
983 matches!(
984 error,
985 ClusterError::ClusterIdentityMismatch { persisted, .. } if persisted == cluster_a
986 ),
987 "unexpected error: {error}"
988 );
989 }
990
991 #[test]
992 fn wipe_is_the_only_reset() {
993 let dir = tempfile::tempdir().unwrap();
994 let cluster_a = ClusterId::new_random();
995 NodeIdentity::provision(dir.path(), cluster_a, &mut test_csprng()).unwrap();
996 let marker = wipe_identity(dir.path())
997 .unwrap()
998 .expect("identity was persisted");
999 assert_eq!(marker.wiped_cluster_id, cluster_a);
1000 assert!(marker
1001 .removed_files
1002 .iter()
1003 .any(|path| path.ends_with(IDENTITY_FILENAME)));
1004 assert_eq!(NodeIdentity::load(dir.path()).unwrap(), None);
1005 assert_eq!(wipe_identity(dir.path()).unwrap(), None);
1007 let cluster_b = ClusterId::new_random();
1009 let identity = NodeIdentity::provision(dir.path(), cluster_b, &mut test_csprng()).unwrap();
1010 assert_eq!(identity.cluster_id, cluster_b);
1011 }
1012
1013 #[test]
1014 fn locality_round_trips_canonical_text() {
1015 let locality: Locality = "region=us-central,zone=a".parse().unwrap();
1016 assert_eq!(locality.tiers().len(), 2);
1017 assert_eq!(locality.get("region"), Some("us-central"));
1018 assert_eq!(locality.get("zone"), Some("a"));
1019 assert_eq!(locality.get("rack"), None);
1020 assert_eq!(locality.to_string(), "region=us-central,zone=a");
1021 let full: Locality = " region=r1 , zone=z , rack=rk , node=n ".parse().unwrap();
1023 assert_eq!(full.to_string(), "region=r1,zone=z,rack=rk,node=n");
1024 assert!("".parse::<Locality>().unwrap().tiers().is_empty());
1026 }
1027
1028 #[test]
1029 fn locality_rejects_malformed_input() {
1030 assert!(matches!(
1031 "region".parse::<Locality>(),
1032 Err(LocalityParseError::InvalidTier(_))
1033 ));
1034 assert!(matches!(
1035 "region=".parse::<Locality>(),
1036 Err(LocalityParseError::EmptyComponent(_))
1037 ));
1038 assert!(matches!(
1039 "=a".parse::<Locality>(),
1040 Err(LocalityParseError::EmptyComponent(_))
1041 ));
1042 assert!(matches!(
1043 "region=a,region=b".parse::<Locality>(),
1044 Err(LocalityParseError::DuplicateKey(_))
1045 ));
1046 }
1047
1048 #[test]
1049 fn locality_serializes_as_canonical_string() {
1050 let locality: Locality = "region=us-central,zone=a".parse().unwrap();
1051 let json = serde_json::to_string(&locality).unwrap();
1052 assert_eq!(json, "\"region=us-central,zone=a\"");
1053 let back: Locality = serde_json::from_str(&json).unwrap();
1054 assert_eq!(back, locality);
1055 }
1056
1057 #[test]
1058 fn build_version_comes_from_the_build_environment() {
1059 let version = BuildVersion::current();
1060 assert_eq!(version.version, env!("CARGO_PKG_VERSION"));
1061 }
1062
1063 #[test]
1064 fn node_state_serializes_by_stable_variant_name() {
1065 assert_eq!(serde_json::to_string(&NodeState::Up).unwrap(), "\"Up\"");
1066 assert_eq!(
1067 serde_json::to_string(&NodeState::Decommissioned).unwrap(),
1068 "\"Decommissioned\""
1069 );
1070 let back: NodeState = serde_json::from_str("\"Draining\"").unwrap();
1071 assert_eq!(back, NodeState::Draining);
1072 }
1073
1074 #[test]
1075 fn concurrent_first_boots_agree_on_one_identity() {
1076 let dir = tempfile::tempdir().unwrap();
1077 let path = dir.path().to_path_buf();
1078 let barrier = std::sync::Barrier::new(4);
1079 std::thread::scope(|scope| {
1080 let handles: Vec<_> = (0..4)
1081 .map(|_| {
1082 scope.spawn(|| {
1083 barrier.wait();
1084 NodeIdentity::load_or_create(&path, &mut test_csprng())
1085 })
1086 })
1087 .collect();
1088 let identities: Vec<_> = handles.into_iter().map(|h| h.join().unwrap()).collect();
1089 let first = identities[0].as_ref().unwrap().clone();
1090 for identity in &identities {
1091 assert_eq!(identity.as_ref().unwrap(), &first);
1092 }
1093 });
1094 }
1095
1096 fn version_info(protocol: (u32, u32), log: (u32, u32), snapshot: (u32, u32)) -> VersionInfo {
1097 VersionInfo {
1098 binary_version: "test".to_owned(),
1099 protocol_min: protocol.0,
1100 protocol_max: protocol.1,
1101 log_format_min: log.0,
1102 log_format_max: log.1,
1103 snapshot_format_min: snapshot.0,
1104 snapshot_format_max: snapshot.1,
1105 feature_set: BTreeSet::new(),
1106 }
1107 }
1108
1109 fn sample_descriptor() -> NodeDescriptor {
1110 NodeDescriptor {
1111 node_id: NodeId::new_random(),
1112 rpc_address: "127.0.0.1:7100".to_owned(),
1113 locality: "region=us-central,zone=a".parse().unwrap(),
1114 capacity: NodeCapacity::default(),
1115 state: NodeState::Up,
1116 version: BuildVersion::current(),
1117 version_info: VersionInfo::current(),
1118 }
1119 }
1120
1121 #[test]
1122 fn current_version_info_advertises_a_self_compatible_window() {
1123 let current = VersionInfo::current();
1124 assert_eq!(current.binary_version, env!("CARGO_PKG_VERSION"));
1125 assert!(current.protocol_min <= current.protocol_max);
1126 assert!(current.log_format_min <= current.log_format_max);
1127 assert!(current.snapshot_format_min <= current.snapshot_format_max);
1128 assert_eq!(
1130 current.log_format_min,
1131 mongreldb_log::envelope::MIN_SUPPORTED_FORMAT_VERSION
1132 );
1133 assert_eq!(
1134 current.log_format_max,
1135 mongreldb_log::envelope::COMMAND_ENVELOPE_FORMAT_VERSION
1136 );
1137 current.is_compatible_with(¤t).unwrap();
1138 }
1139
1140 #[test]
1141 fn compatibility_is_range_overlap_in_every_dimension() {
1142 let base = version_info((1, 2), (1, 2), (1, 2));
1143 base.is_compatible_with(&base.clone()).unwrap();
1145 base.is_compatible_with(&version_info((2, 2), (1, 1), (1, 2)))
1146 .unwrap();
1147 let error = base
1149 .is_compatible_with(&version_info((3, 4), (1, 2), (1, 2)))
1150 .unwrap_err();
1151 assert!(matches!(error, Incompatibility::ProtocolVersion { .. }));
1152 let error = base
1153 .is_compatible_with(&version_info((1, 2), (3, 4), (1, 2)))
1154 .unwrap_err();
1155 assert!(matches!(error, Incompatibility::LogFormat { .. }));
1156 let error = base
1157 .is_compatible_with(&version_info((1, 2), (1, 2), (3, 4)))
1158 .unwrap_err();
1159 assert!(matches!(error, Incompatibility::SnapshotFormat { .. }));
1160 let error = base
1162 .is_compatible_with(&version_info((2, 1), (1, 2), (1, 2)))
1163 .unwrap_err();
1164 assert!(matches!(error, Incompatibility::ProtocolVersion { .. }));
1165 }
1166
1167 #[test]
1168 fn n_and_n_minus_1_interoperate_but_n_plus_1_skew_does_not() {
1169 let n_minus_1 = version_info((1, 1), (1, 1), (1, 1));
1172 let n = version_info((1, 2), (1, 2), (1, 2));
1173 n.is_compatible_with(&n_minus_1).unwrap();
1174 n_minus_1.is_compatible_with(&n).unwrap();
1175 let n_plus_1 = version_info((2, 3), (2, 3), (2, 3));
1177 assert!(matches!(
1178 n_minus_1.is_compatible_with(&n_plus_1).unwrap_err(),
1179 Incompatibility::ProtocolVersion { .. }
1180 ));
1181 }
1182
1183 #[test]
1184 fn missing_advertisement_fails_closed_against_a_real_one() {
1185 let legacy = VersionInfo::default();
1188 let error = legacy
1189 .is_compatible_with(&VersionInfo::current())
1190 .unwrap_err();
1191 assert!(matches!(error, Incompatibility::ProtocolVersion { .. }));
1192 legacy.is_compatible_with(&VersionInfo::default()).unwrap();
1195 }
1196
1197 #[test]
1198 fn descriptor_without_version_info_decodes_with_a_default() {
1199 let descriptor = sample_descriptor();
1200 let mut value = serde_json::to_value(&descriptor).unwrap();
1201 value.as_object_mut().unwrap().remove("version_info");
1203 let decoded: NodeDescriptor = serde_json::from_value(value).unwrap();
1204 assert_eq!(decoded.version_info, VersionInfo::default());
1205 assert_eq!(decoded.node_id, descriptor.node_id);
1206 }
1207
1208 #[test]
1209 fn descriptor_with_version_info_round_trips() {
1210 let descriptor = sample_descriptor();
1211 let json = serde_json::to_vec(&descriptor).unwrap();
1212 let decoded: NodeDescriptor = serde_json::from_slice(&json).unwrap();
1213 assert_eq!(decoded, descriptor);
1214 }
1215
1216 #[test]
1217 fn descriptor_still_fails_closed_on_unknown_fields() {
1218 let descriptor = sample_descriptor();
1219 let mut value = serde_json::to_value(&descriptor).unwrap();
1220 value["unexpected"] = serde_json::json!(1);
1221 assert!(serde_json::from_value::<NodeDescriptor>(value).is_err());
1222 }
1223}