1use std::collections::{BTreeMap, BTreeSet, VecDeque};
63use std::fmt;
64use std::path::{Path, PathBuf};
65use std::sync::{Arc, Mutex};
66
67use mongreldb_consensus::error::ConsensusError;
68use mongreldb_consensus::group::{ConsensusGroup, GroupCommitReceipt, GroupConfig};
69use mongreldb_consensus::identity::{raft_node_id, CommandKind, RaftNodeId, ReplicatedCommand};
70use mongreldb_consensus::network::RaftTransport;
71use mongreldb_consensus::state_machine::{AppliedCommand, ApplySink, StateMachineError};
72use mongreldb_consensus::storage::StorageConfig;
73use mongreldb_log::commit_log::{ExecutionControl, LogPosition};
74use mongreldb_log::envelope::CommandEnvelope;
75use mongreldb_types::hlc::HlcTimestamp;
76use mongreldb_types::ids::{
77 DatabaseId, MetadataVersion, NodeId, RaftGroupId, SchemaVersion, TableId, TabletId,
78};
79use serde::{Deserialize, Serialize};
80
81use crate::merge::MergePublishCommand;
82use crate::node::{Incompatibility, NodeDescriptor, NodeState, VersionInfo};
83use crate::split::SplitPublishCommand;
84use crate::tablet::{Bound, Key};
85
86#[derive(
93 Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize,
94)]
95pub struct ClusterFeatureLevel(pub u64);
96
97impl ClusterFeatureLevel {
98 pub const ZERO: Self = Self(0);
100}
101
102impl fmt::Display for ClusterFeatureLevel {
103 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
104 self.0.fmt(f)
105 }
106}
107
108#[derive(Clone, Debug, Default, PartialEq, Eq)]
117pub struct FeatureRegistry {
118 required_level: BTreeMap<String, u64>,
119}
120
121impl FeatureRegistry {
122 pub fn current() -> Self {
124 Self::default()
125 }
126
127 pub fn declare(&mut self, feature: impl Into<String>, level: ClusterFeatureLevel) {
129 self.required_level.insert(feature.into(), level.0);
130 }
131
132 pub fn required_level(&self, feature: &str) -> Option<ClusterFeatureLevel> {
135 self.required_level
136 .get(feature)
137 .copied()
138 .map(ClusterFeatureLevel)
139 }
140
141 pub fn feature_supported(&self, level: ClusterFeatureLevel, feature: &str) -> bool {
143 self.required_level(feature)
144 .is_some_and(|required| level >= required)
145 }
146
147 pub fn feature_names(&self) -> BTreeSet<String> {
150 self.required_level.keys().cloned().collect()
151 }
152}
153
154#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
162#[serde(deny_unknown_fields)]
163pub struct FeatureActivation {
164 pub feature: String,
166 pub level: ClusterFeatureLevel,
168 pub activated_at: HlcTimestamp,
171 pub activated_by: NodeId,
173}
174
175#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, thiserror::Error)]
181pub enum FeatureActivationError {
182 #[error("feature `{feature}` is not declared in the feature registry")]
184 UnknownFeature {
185 feature: String,
187 },
188 #[error(
190 "feature `{feature}` requires cluster feature level {required}; \
191 activation attempted at {attempted}"
192 )]
193 LevelBelowRequirement {
194 feature: String,
196 required: ClusterFeatureLevel,
198 attempted: ClusterFeatureLevel,
200 },
201 #[error(
204 "cluster feature level never lowers: current level {current}, \
205 activation attempted at {attempted}"
206 )]
207 LevelRegression {
208 current: ClusterFeatureLevel,
210 attempted: ClusterFeatureLevel,
212 },
213 #[error("feature `{feature}` cannot activate: voter {node} does not support it")]
217 UnsupportedByVoter {
218 feature: String,
220 node: NodeId,
222 },
223 #[error("feature activation requires at least one voter")]
225 NoVoters,
226}
227
228impl FeatureActivation {
229 pub fn validate(
237 &self,
238 registry: &FeatureRegistry,
239 current_level: ClusterFeatureLevel,
240 voters: &[NodeDescriptor],
241 ) -> Result<(), FeatureActivationError> {
242 let required = registry.required_level(&self.feature).ok_or_else(|| {
243 FeatureActivationError::UnknownFeature {
244 feature: self.feature.clone(),
245 }
246 })?;
247 if self.level < required {
248 return Err(FeatureActivationError::LevelBelowRequirement {
249 feature: self.feature.clone(),
250 required,
251 attempted: self.level,
252 });
253 }
254 if self.level < current_level {
255 return Err(FeatureActivationError::LevelRegression {
256 current: current_level,
257 attempted: self.level,
258 });
259 }
260 if voters.is_empty() {
261 return Err(FeatureActivationError::NoVoters);
262 }
263 for voter in voters {
264 if !voter.version_info.feature_set.contains(&self.feature) {
265 return Err(FeatureActivationError::UnsupportedByVoter {
266 feature: self.feature.clone(),
267 node: voter.node_id,
268 });
269 }
270 }
271 Ok(())
272 }
273}
274
275#[derive(Clone, Copy, Debug, PartialEq, Eq)]
278pub enum UpgradeStep {
279 UpgradeFollower {
282 node_id: NodeId,
284 },
285 TransferLeadership {
288 from: NodeId,
290 },
291 UpgradeFormerLeader {
293 node_id: NodeId,
295 },
296 EnableNewFeatures,
301}
302
303#[derive(Clone, Debug, PartialEq, Eq)]
306pub struct UpgradePlan {
307 pub target: VersionInfo,
309 pub steps: Vec<UpgradeStep>,
311}
312
313#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
316pub enum UpgradePlanError {
317 #[error("cannot plan a rolling upgrade for an empty membership")]
319 EmptyMembership,
320 #[error("current leader {leader} is not present in the supplied membership")]
322 LeaderNotInMembership {
323 leader: NodeId,
325 },
326 #[error("node {node} appears more than once in the supplied membership")]
328 DuplicateNode {
329 node: NodeId,
331 },
332 #[error("node {node} is not compatible with the upgrade target: {incompatibility}")]
335 IncompatibleNode {
336 node: NodeId,
338 incompatibility: Incompatibility,
340 },
341}
342
343pub fn plan_rolling_upgrade(
355 nodes: &[NodeDescriptor],
356 current_leader: NodeId,
357 target: &VersionInfo,
358) -> Result<UpgradePlan, UpgradePlanError> {
359 if nodes.is_empty() {
360 return Err(UpgradePlanError::EmptyMembership);
361 }
362 for (index, node) in nodes.iter().enumerate() {
363 if nodes[..index]
364 .iter()
365 .any(|prior| prior.node_id == node.node_id)
366 {
367 return Err(UpgradePlanError::DuplicateNode { node: node.node_id });
368 }
369 }
370 if !nodes.iter().any(|node| node.node_id == current_leader) {
371 return Err(UpgradePlanError::LeaderNotInMembership {
372 leader: current_leader,
373 });
374 }
375 for node in nodes {
376 if let Err(incompatibility) = node.version_info.is_compatible_with(target) {
377 return Err(UpgradePlanError::IncompatibleNode {
378 node: node.node_id,
379 incompatibility,
380 });
381 }
382 }
383 let mut steps = Vec::with_capacity(nodes.len() + 2);
384 for node in nodes {
385 if node.node_id != current_leader {
386 steps.push(UpgradeStep::UpgradeFollower {
387 node_id: node.node_id,
388 });
389 }
390 }
391 if nodes.len() > 1 {
392 steps.push(UpgradeStep::TransferLeadership {
393 from: current_leader,
394 });
395 }
396 steps.push(UpgradeStep::UpgradeFormerLeader {
397 node_id: current_leader,
398 });
399 steps.push(UpgradeStep::EnableNewFeatures);
400 Ok(UpgradePlan {
401 target: target.clone(),
402 steps,
403 })
404}
405
406#[derive(Clone, Copy, Debug, PartialEq, Eq)]
409pub enum RollbackPath {
410 BinaryDowngrade,
416 RestoreFromBackup,
421}
422
423#[derive(Clone, Debug, PartialEq, Eq)]
425pub struct RollbackAssessment {
426 pub path: RollbackPath,
428 pub activated_features: Vec<String>,
431}
432
433pub fn assess_rollback(activations: &[FeatureActivation]) -> RollbackAssessment {
439 let activated_features: Vec<String> = activations
440 .iter()
441 .map(|activation| activation.feature.clone())
442 .collect();
443 let path = if activated_features.is_empty() {
444 RollbackPath::BinaryDowngrade
445 } else {
446 RollbackPath::RestoreFromBackup
447 };
448 RollbackAssessment {
449 path,
450 activated_features,
451 }
452}
453
454pub const META_COMMAND_FORMAT_VERSION: u32 = 2;
483pub const MIN_SUPPORTED_META_COMMAND_FORMAT_VERSION: u32 = 1;
485pub const META_STATE_FORMAT_VERSION: u32 = 2;
488pub const MIN_SUPPORTED_META_STATE_FORMAT_VERSION: u32 = 1;
490pub const COMMAND_TYPE_META_COMMAND: u32 = 4;
497pub const META_REJECTION_LIMIT: usize = 256;
504pub const FIRST_RAFT_NODE_ID: u64 = 1;
511pub const MAX_RAFT_NODE_ID_ALLOCATION: u32 = 4096;
513pub const RAFT_ID_ALLOCATION_RECORD_LIMIT: usize = 1024;
516pub const RAFT_ID_ALLOCATION_SCAN_LIMIT: u64 = 1 << 20;
520pub const SECRET_SETTING_KEY_DENYLIST: &[&str] = &[
527 "secret",
528 "password",
529 "passwd",
530 "private_key",
531 "api_key",
532 "token",
533 "credential",
534];
535
536pub const KNOWN_SETTING_KEYS: &[&str] = &[
540 "history_retention_epochs",
541 "backup.enabled",
542 "backup.interval_seconds",
543 "backup.retention_count",
544 "default_consistency",
545 "ai.max_concurrent_requests",
546 "ai.max_memory_bytes",
547 "jobs.max_concurrent",
548];
549
550pub const DEFAULT_MAX_CONCURRENT_JOBS: u32 = 2;
553pub const DEFAULT_BACKUP_INTERVAL_SECONDS: u64 = 86_400;
555pub const DEFAULT_BACKUP_RETENTION_COUNT: u32 = 7;
557pub const DEFAULT_AI_MAX_CONCURRENT_REQUESTS: u32 = 16;
559pub const DEFAULT_AI_MAX_MEMORY_BYTES: u64 = 512 * 1024 * 1024;
561
562fn zero_metadata_version() -> MetadataVersion {
563 MetadataVersion::ZERO
564}
565
566fn hex_encode(bytes: &[u8]) -> String {
569 const HEX: &[u8; 16] = b"0123456789abcdef";
570 let mut out = String::with_capacity(bytes.len() * 2);
571 for byte in bytes {
572 out.push(HEX[(byte >> 4) as usize] as char);
573 out.push(HEX[(byte & 0x0f) as usize] as char);
574 }
575 out
576}
577
578#[derive(Debug, thiserror::Error)]
581pub enum MetaError {
582 #[error(transparent)]
585 Consensus(#[from] ConsensusError),
586 #[error("meta command encoding failed: {0}")]
588 Encode(String),
589 #[error("meta command refused at apply: {0}")]
592 Rejected(MetaRejectionReason),
593 #[error("invalid meta group request: {0}")]
596 InvalidRequest(String),
597 #[error("meta group I/O error: {0}")]
599 Io(#[from] std::io::Error),
600 #[error("operating-system CSPRNG failed: {0}")]
602 Rng(String),
603 #[error("corrupt meta state checkpoint: {0}")]
606 CorruptCheckpoint(String),
607}
608
609#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
612pub enum MetaDecodeError {
613 #[error("meta command decode failed: {0}")]
615 Malformed(String),
616 #[error("unsupported meta command format version {found} (supported {min}..={max})")]
618 UnsupportedVersion {
619 found: u32,
621 min: u32,
623 max: u32,
625 },
626}
627
628#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
634pub enum DatabaseState {
635 Online,
637 Dropping,
639}
640
641#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
643pub struct DatabaseDescriptor {
644 pub database_id: DatabaseId,
646 pub name: String,
648 pub created_at: HlcTimestamp,
650 pub state: DatabaseState,
652 #[serde(default = "zero_metadata_version")]
654 pub metadata_version: MetadataVersion,
655}
656
657#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
660pub struct TableSchemaRecord {
661 pub table_id: TableId,
663 pub database_id: DatabaseId,
665 pub schema_version: SchemaVersion,
667 pub schema: serde_json::Value,
670 #[serde(default = "zero_metadata_version")]
672 pub metadata_version: MetadataVersion,
673}
674
675pub use crate::placement::LocalityConstraint;
681pub use crate::placement::PlacementPolicy;
683pub use crate::tablet::PartitionBounds;
685pub use crate::tablet::ReplicaDescriptor;
687pub use crate::tablet::ReplicaRole;
689pub use crate::tablet::TabletDescriptor;
691pub use crate::tablet::TabletState;
693
694#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
698pub struct ReplicaPlacement {
699 pub raft_group_id: RaftGroupId,
701 pub replicas: Vec<ReplicaDescriptor>,
703 #[serde(default = "zero_metadata_version")]
705 pub metadata_version: MetadataVersion,
706}
707
708#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
713pub struct TabletRecord {
714 pub descriptor: TabletDescriptor,
716 #[serde(default = "zero_metadata_version")]
718 pub metadata_version: MetadataVersion,
719}
720
721#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
724pub struct PlacementPolicyRecord {
725 pub policy: PlacementPolicy,
727 #[serde(default = "zero_metadata_version")]
729 pub metadata_version: MetadataVersion,
730}
731
732#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
736pub enum SchemaJobKind {
737 IndexBuild,
739 ColumnBackfill,
741 SchemaValidation,
743}
744
745#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
748pub enum SchemaJobState {
749 Pending,
751 Running,
753 Paused,
755 Cancelling,
757 Succeeded,
759 Failed,
761 RollingBack,
763}
764
765impl SchemaJobState {
766 pub fn is_terminal(self) -> bool {
768 matches!(self, Self::Succeeded | Self::Failed)
769 }
770
771 pub fn can_transition(self, next: Self) -> bool {
774 use SchemaJobState::{
775 Cancelling, Failed, Paused, Pending, RollingBack, Running, Succeeded,
776 };
777 matches!(
778 (self, next),
779 (Pending, Running)
780 | (Pending, Cancelling)
781 | (Running, Paused)
782 | (Running, Cancelling)
783 | (Running, RollingBack)
784 | (Running, Succeeded)
785 | (Paused, Pending)
786 | (Paused, Cancelling)
787 | (Cancelling, RollingBack)
788 | (Cancelling, Failed)
789 | (RollingBack, Failed)
790 )
791 }
792}
793
794#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
797pub struct SchemaJobRecord {
798 pub job_id: u64,
800 pub database_id: DatabaseId,
802 pub table_id: TableId,
804 pub kind: SchemaJobKind,
806 pub state: SchemaJobState,
808 pub submitted_at: HlcTimestamp,
810 pub updated_at: HlcTimestamp,
812 pub error: Option<String>,
814 #[serde(default = "zero_metadata_version")]
816 pub metadata_version: MetadataVersion,
817}
818
819#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
823pub struct TxnStatusPartition {
824 pub partition_id: u32,
827 pub home_raft_group: RaftGroupId,
829}
830
831#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
838#[serde(default)]
839pub struct ResourceGroupSetting {
840 pub max_memory_bytes: u64,
842 pub max_concurrent_queries: u32,
844 pub temp_disk_budget_bytes: u64,
846}
847
848#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
852#[serde(default)]
853pub struct BackupPolicy {
854 pub enabled: bool,
856 pub interval_seconds: u64,
858 pub retention_count: u32,
860}
861
862impl Default for BackupPolicy {
863 fn default() -> Self {
864 Self {
865 enabled: false,
866 interval_seconds: DEFAULT_BACKUP_INTERVAL_SECONDS,
867 retention_count: DEFAULT_BACKUP_RETENTION_COUNT,
868 }
869 }
870}
871
872#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
875#[serde(default)]
876pub struct AiLimits {
877 pub max_concurrent_requests: u32,
879 pub max_memory_bytes: u64,
881}
882
883impl Default for AiLimits {
884 fn default() -> Self {
885 Self {
886 max_concurrent_requests: DEFAULT_AI_MAX_CONCURRENT_REQUESTS,
887 max_memory_bytes: DEFAULT_AI_MAX_MEMORY_BYTES,
888 }
889 }
890}
891
892#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
896pub enum DefaultConsistency {
897 #[default]
899 Linearizable,
900 ReadYourWrites,
902 Snapshot,
904 BoundedStaleness {
906 max_lag_ms: u64,
908 },
909 Eventual,
911}
912
913#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
920#[serde(default)]
921pub struct ClusterSettings {
922 pub resource_groups: BTreeMap<String, ResourceGroupSetting>,
924 pub history_retention_epochs: u64,
927 pub backup: BackupPolicy,
929 pub default_consistency: DefaultConsistency,
931 pub ai: AiLimits,
933 pub max_concurrent_jobs: u32,
935}
936
937impl ClusterSettings {
938 fn apply(&mut self, key: &str, value: &serde_json::Value) -> Result<(), MetaRejectionReason> {
942 let lowered = key.to_ascii_lowercase();
943 if SECRET_SETTING_KEY_DENYLIST
944 .iter()
945 .any(|needle| lowered.contains(needle))
946 {
947 return Err(MetaRejectionReason::SecretSettingKey {
948 key: key.to_owned(),
949 });
950 }
951 fn invalid(key: &str, reason: impl Into<String>) -> MetaRejectionReason {
952 MetaRejectionReason::InvalidSettingValue {
953 key: key.to_owned(),
954 reason: reason.into(),
955 }
956 }
957 match key {
958 "history_retention_epochs" => {
959 self.history_retention_epochs = value
960 .as_u64()
961 .ok_or_else(|| invalid(key, "expected an unsigned integer"))?;
962 }
963 "backup.enabled" => {
964 self.backup.enabled = value
965 .as_bool()
966 .ok_or_else(|| invalid(key, "expected a boolean"))?;
967 }
968 "backup.interval_seconds" => {
969 let interval = value
970 .as_u64()
971 .ok_or_else(|| invalid(key, "expected an unsigned integer"))?;
972 if interval == 0 {
973 return Err(invalid(key, "interval must be positive"));
974 }
975 self.backup.interval_seconds = interval;
976 }
977 "backup.retention_count" => {
978 let retention = value
979 .as_u64()
980 .ok_or_else(|| invalid(key, "expected an unsigned integer"))?;
981 self.backup.retention_count = u32::try_from(retention)
982 .map_err(|_| invalid(key, "retention count exceeds u32"))?;
983 }
984 "default_consistency" => {
985 self.default_consistency =
986 serde_json::from_value(value.clone()).map_err(|error| {
987 invalid(
988 key,
989 format!(
990 "expected one of \"Linearizable\", \"ReadYourWrites\", \
991 \"Snapshot\", \"Eventual\", or \
992 {{\"BoundedStaleness\": {{\"max_lag_ms\": ..}}}}: {error}"
993 ),
994 )
995 })?;
996 }
997 "ai.max_concurrent_requests" => {
998 let cap = value
999 .as_u64()
1000 .ok_or_else(|| invalid(key, "expected an unsigned integer"))?;
1001 self.ai.max_concurrent_requests =
1002 u32::try_from(cap).map_err(|_| invalid(key, "cap exceeds u32"))?;
1003 }
1004 "ai.max_memory_bytes" => {
1005 self.ai.max_memory_bytes = value
1006 .as_u64()
1007 .ok_or_else(|| invalid(key, "expected an unsigned integer"))?;
1008 }
1009 "jobs.max_concurrent" => {
1010 let cap = value
1011 .as_u64()
1012 .ok_or_else(|| invalid(key, "expected an unsigned integer"))?;
1013 if cap == 0 {
1014 return Err(invalid(key, "job concurrency must be positive"));
1015 }
1016 self.max_concurrent_jobs =
1017 u32::try_from(cap).map_err(|_| invalid(key, "cap exceeds u32"))?;
1018 }
1019 _ => {
1020 let Some(name) = key.strip_prefix("resource_groups.") else {
1021 return Err(MetaRejectionReason::UnknownSettingKey {
1022 key: key.to_owned(),
1023 });
1024 };
1025 if name.is_empty() {
1026 return Err(MetaRejectionReason::UnknownSettingKey {
1027 key: key.to_owned(),
1028 });
1029 }
1030 if value.is_null() {
1031 self.resource_groups.remove(name);
1032 } else {
1033 let group: ResourceGroupSetting = serde_json::from_value(value.clone())
1034 .map_err(|error| {
1035 invalid(key, format!("expected a resource-group object: {error}"))
1036 })?;
1037 self.resource_groups.insert(name.to_owned(), group);
1038 }
1039 }
1040 }
1041 Ok(())
1042 }
1043}
1044
1045#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, thiserror::Error)]
1055pub enum MetaRejectionReason {
1056 #[error(transparent)]
1059 FeatureActivation(#[from] FeatureActivationError),
1060 #[error(
1062 "cluster setting key `{key}` is denied: secrets are never stored as \
1063 plaintext cluster settings"
1064 )]
1065 SecretSettingKey {
1066 key: String,
1068 },
1069 #[error("unknown cluster setting key `{key}`")]
1071 UnknownSettingKey {
1072 key: String,
1074 },
1075 #[error("invalid value for cluster setting `{key}`: {reason}")]
1077 InvalidSettingValue {
1078 key: String,
1080 reason: String,
1082 },
1083 #[error("stale write to {resource}: current version {current}, attempted {attempted}")]
1085 StaleWrite {
1086 resource: String,
1088 current: MetadataVersion,
1090 attempted: MetadataVersion,
1092 },
1093 #[error("conflicting {resource}: {reason}")]
1095 Conflict {
1096 resource: String,
1098 reason: String,
1100 },
1101 #[error("{resource} not found")]
1103 NotFound {
1104 resource: String,
1106 },
1107 #[error("invalid meta command: {reason}")]
1109 Invalid {
1110 reason: String,
1112 },
1113 #[error("meta proposal failed: {reason}")]
1121 ProposalFailed {
1122 reason: String,
1124 },
1125 #[error("not the meta leader (current leader: {leader:?})")]
1131 NotLeader {
1132 leader: Option<u64>,
1134 },
1135}
1136
1137#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
1139pub struct MetaRejection {
1140 pub command_id: Option<[u8; 16]>,
1143 pub reason: MetaRejectionReason,
1145}
1146
1147#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1159pub enum MetaCommand {
1160 RegisterNode {
1163 descriptor: NodeDescriptor,
1165 },
1166 UpdateNodeState {
1168 node_id: NodeId,
1170 state: NodeState,
1172 expected_version: Option<MetadataVersion>,
1175 },
1176 RemoveNode {
1179 node_id: NodeId,
1181 },
1182 CreateDatabase {
1184 descriptor: DatabaseDescriptor,
1186 },
1187 DropDatabase {
1190 database_id: DatabaseId,
1192 },
1193 SetTableSchema {
1195 record: TableSchemaRecord,
1197 },
1198 SetTabletDescriptor {
1207 descriptor: TabletDescriptor,
1209 },
1210 RemoveTabletDescriptor {
1212 tablet_id: TabletId,
1214 generation: u64,
1217 },
1218 SetReplicaPlacement {
1220 placement: ReplicaPlacement,
1222 },
1223 SetPlacementPolicy {
1225 name: String,
1227 policy: PlacementPolicy,
1229 },
1230 SubmitSchemaJob {
1232 job: SchemaJobRecord,
1234 },
1235 UpdateSchemaJob {
1237 job_id: u64,
1239 state: SchemaJobState,
1241 updated_at: HlcTimestamp,
1243 error: Option<String>,
1245 expected_version: Option<MetadataVersion>,
1248 },
1249 SetClusterSetting {
1252 key: String,
1254 value: serde_json::Value,
1256 },
1257 ActivateFeature {
1261 activation: FeatureActivation,
1263 },
1264 SetTxnStatusPartition {
1266 partition: TxnStatusPartition,
1268 },
1269 PublishSplit {
1280 command: SplitPublishCommand,
1282 },
1283 PublishMerge {
1293 command: MergePublishCommand,
1295 },
1296 AllocateRaftNodeIds {
1306 count: u32,
1309 },
1310}
1311
1312#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1316pub struct MetaCommandRecord {
1317 pub format_version: u32,
1319 pub command: MetaCommand,
1321}
1322
1323impl MetaCommandRecord {
1324 pub fn new(command: MetaCommand) -> Self {
1326 Self {
1327 format_version: META_COMMAND_FORMAT_VERSION,
1328 command,
1329 }
1330 }
1331
1332 pub fn encode(&self) -> Result<Vec<u8>, MetaError> {
1335 serde_json::to_vec(self).map_err(|error| MetaError::Encode(error.to_string()))
1336 }
1337
1338 pub fn decode(payload: &[u8]) -> Result<Self, MetaDecodeError> {
1342 let probe: FormatVersionProbe = serde_json::from_slice(payload)
1343 .map_err(|error| MetaDecodeError::Malformed(error.to_string()))?;
1344 if probe.format_version < MIN_SUPPORTED_META_COMMAND_FORMAT_VERSION
1345 || probe.format_version > META_COMMAND_FORMAT_VERSION
1346 {
1347 return Err(MetaDecodeError::UnsupportedVersion {
1348 found: probe.format_version,
1349 min: MIN_SUPPORTED_META_COMMAND_FORMAT_VERSION,
1350 max: META_COMMAND_FORMAT_VERSION,
1351 });
1352 }
1353 if probe.format_version == 1 {
1354 let record: v1::MetaCommandRecord = serde_json::from_slice(payload)
1355 .map_err(|error| MetaDecodeError::Malformed(error.to_string()))?;
1356 if record.format_version != probe.format_version {
1357 return Err(MetaDecodeError::Malformed(
1358 "inconsistent format version".to_owned(),
1359 ));
1360 }
1361 return Ok(Self {
1362 format_version: META_COMMAND_FORMAT_VERSION,
1363 command: migrate_command(record.command),
1364 });
1365 }
1366 serde_json::from_slice(payload)
1367 .map_err(|error| MetaDecodeError::Malformed(error.to_string()))
1368 }
1369}
1370
1371#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
1379pub struct NodeRecord {
1380 pub descriptor: NodeDescriptor,
1382 #[serde(default = "zero_metadata_version")]
1384 pub metadata_version: MetadataVersion,
1385}
1386
1387#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1395#[serde(default)]
1396pub struct MetaState {
1397 pub format_version: u32,
1399 pub metadata_version: MetadataVersion,
1401 pub nodes: BTreeMap<NodeId, NodeRecord>,
1403 pub databases: BTreeMap<DatabaseId, DatabaseDescriptor>,
1405 pub tables: BTreeMap<TableId, TableSchemaRecord>,
1407 pub tablets: BTreeMap<TabletId, TabletRecord>,
1409 pub placements: BTreeMap<RaftGroupId, ReplicaPlacement>,
1411 pub placement_policies: BTreeMap<String, PlacementPolicyRecord>,
1413 pub schema_jobs: BTreeMap<u64, SchemaJobRecord>,
1415 pub txn_status_partitions: BTreeMap<u32, TxnStatusPartition>,
1417 pub settings: ClusterSettings,
1419 pub feature_level: ClusterFeatureLevel,
1421 pub feature_activations: Vec<FeatureActivation>,
1423 pub rejections: VecDeque<MetaRejection>,
1426 pub next_raft_node_id: u64,
1430 pub raft_id_allocations: BTreeMap<String, u64>,
1436 pub raft_id_allocation_order: VecDeque<String>,
1438}
1439
1440impl Default for MetaState {
1441 fn default() -> Self {
1442 Self {
1443 format_version: META_STATE_FORMAT_VERSION,
1444 metadata_version: MetadataVersion::ZERO,
1445 nodes: BTreeMap::new(),
1446 databases: BTreeMap::new(),
1447 tables: BTreeMap::new(),
1448 tablets: BTreeMap::new(),
1449 placements: BTreeMap::new(),
1450 placement_policies: BTreeMap::new(),
1451 schema_jobs: BTreeMap::new(),
1452 txn_status_partitions: BTreeMap::new(),
1453 settings: ClusterSettings::default(),
1454 feature_level: ClusterFeatureLevel::ZERO,
1455 feature_activations: Vec::new(),
1456 rejections: VecDeque::new(),
1457 next_raft_node_id: FIRST_RAFT_NODE_ID,
1458 raft_id_allocations: BTreeMap::new(),
1459 raft_id_allocation_order: VecDeque::new(),
1460 }
1461 }
1462}
1463
1464impl MetaState {
1465 pub fn apply(
1474 &mut self,
1475 command: &MetaCommand,
1476 command_id: Option<[u8; 16]>,
1477 commit_ts: HlcTimestamp,
1478 registry: &FeatureRegistry,
1479 ) -> Result<(), MetaRejectionReason> {
1480 self.metadata_version = MetadataVersion(self.metadata_version.get() + 1);
1481 let version = self.metadata_version;
1482 let result = self.dispatch(command, command_id, commit_ts, registry, version);
1483 if let Err(reason) = &result {
1484 self.rejections.push_back(MetaRejection {
1485 command_id,
1486 reason: reason.clone(),
1487 });
1488 while self.rejections.len() > META_REJECTION_LIMIT {
1489 self.rejections.pop_front();
1490 }
1491 }
1492 result
1493 }
1494
1495 fn dispatch(
1496 &mut self,
1497 command: &MetaCommand,
1498 command_id: Option<[u8; 16]>,
1499 commit_ts: HlcTimestamp,
1500 registry: &FeatureRegistry,
1501 version: MetadataVersion,
1502 ) -> Result<(), MetaRejectionReason> {
1503 match command {
1504 MetaCommand::RegisterNode { descriptor } => {
1505 if descriptor.node_id == NodeId::ZERO {
1506 return Err(MetaRejectionReason::Invalid {
1507 reason: "reserved all-zero node id".to_owned(),
1508 });
1509 }
1510 match self.nodes.get(&descriptor.node_id) {
1511 Some(existing) if existing.descriptor == *descriptor => Ok(()),
1512 _ => {
1513 let projected = raft_node_id(&descriptor.node_id);
1520 let assigned_to_replica = self.tablets.values().any(|record| {
1521 record
1522 .descriptor
1523 .replicas
1524 .iter()
1525 .any(|replica| replica.raft_node_id == projected)
1526 }) || self.placements.values().any(|placement| {
1527 placement
1528 .replicas
1529 .iter()
1530 .any(|replica| replica.raft_node_id == projected)
1531 });
1532 if assigned_to_replica {
1533 return Err(MetaRejectionReason::Conflict {
1534 resource: format!("node {}", descriptor.node_id),
1535 reason: format!(
1536 "raft id projection {projected} is already assigned to a \
1537 tablet or placement replica; re-mint the node id"
1538 ),
1539 });
1540 }
1541 self.nodes.insert(
1542 descriptor.node_id,
1543 NodeRecord {
1544 descriptor: descriptor.clone(),
1545 metadata_version: version,
1546 },
1547 );
1548 Ok(())
1549 }
1550 }
1551 }
1552 MetaCommand::UpdateNodeState {
1553 node_id,
1554 state,
1555 expected_version,
1556 } => {
1557 let Some(record) = self.nodes.get_mut(node_id) else {
1558 return Err(MetaRejectionReason::NotFound {
1559 resource: format!("node {node_id}"),
1560 });
1561 };
1562 if let Some(expected) = expected_version {
1563 if *expected != record.metadata_version {
1564 return Err(MetaRejectionReason::StaleWrite {
1565 resource: format!("node {node_id}"),
1566 current: record.metadata_version,
1567 attempted: *expected,
1568 });
1569 }
1570 }
1571 if record.descriptor.state == NodeState::Decommissioned {
1572 return Err(MetaRejectionReason::Conflict {
1573 resource: format!("node {node_id}"),
1574 reason: "decommissioned is terminal".to_owned(),
1575 });
1576 }
1577 if record.descriptor.state == *state {
1578 return Ok(());
1579 }
1580 record.descriptor.state = *state;
1581 record.metadata_version = version;
1582 Ok(())
1583 }
1584 MetaCommand::RemoveNode { node_id } => {
1585 if !self.nodes.contains_key(node_id) {
1586 return Ok(());
1587 }
1588 let referenced_by_placement = self
1589 .placements
1590 .values()
1591 .any(|placement| placement.replicas.iter().any(|r| r.node_id == *node_id));
1592 let referenced_by_tablet = self.tablets.values().any(|record| {
1593 record
1594 .descriptor
1595 .replicas
1596 .iter()
1597 .any(|r| r.node_id == *node_id)
1598 || record.descriptor.leader_hint == Some(*node_id)
1599 });
1600 if referenced_by_placement || referenced_by_tablet {
1601 return Err(MetaRejectionReason::Conflict {
1602 resource: format!("node {node_id}"),
1603 reason: "node still hosts a tablet or placement replica".to_owned(),
1604 });
1605 }
1606 self.nodes.remove(node_id);
1607 Ok(())
1608 }
1609 MetaCommand::CreateDatabase { descriptor } => {
1610 if descriptor.database_id == DatabaseId::ZERO {
1611 return Err(MetaRejectionReason::Invalid {
1612 reason: "reserved all-zero database id".to_owned(),
1613 });
1614 }
1615 if descriptor.name.trim().is_empty() {
1616 return Err(MetaRejectionReason::Invalid {
1617 reason: "database name is empty".to_owned(),
1618 });
1619 }
1620 if let Some(existing) = self.databases.get(&descriptor.database_id) {
1621 return if existing.name == descriptor.name
1622 && existing.state == descriptor.state
1623 && existing.created_at == descriptor.created_at
1624 {
1625 Ok(())
1626 } else {
1627 Err(MetaRejectionReason::Conflict {
1628 resource: format!("database {}", descriptor.database_id),
1629 reason: "database id already exists with different content".to_owned(),
1630 })
1631 };
1632 }
1633 if self
1634 .databases
1635 .values()
1636 .any(|database| database.name == descriptor.name)
1637 {
1638 return Err(MetaRejectionReason::Conflict {
1639 resource: format!("database `{}`", descriptor.name),
1640 reason: "database name already taken".to_owned(),
1641 });
1642 }
1643 let mut descriptor = descriptor.clone();
1644 descriptor.metadata_version = version;
1645 self.databases.insert(descriptor.database_id, descriptor);
1646 Ok(())
1647 }
1648 MetaCommand::DropDatabase { database_id } => {
1649 if !self.databases.contains_key(database_id) {
1650 return Ok(());
1651 }
1652 if self
1653 .tables
1654 .values()
1655 .any(|table| table.database_id == *database_id)
1656 {
1657 return Err(MetaRejectionReason::Conflict {
1658 resource: format!("database {database_id}"),
1659 reason: "database still has tables".to_owned(),
1660 });
1661 }
1662 self.databases.remove(database_id);
1663 Ok(())
1664 }
1665 MetaCommand::SetTableSchema { record } => {
1666 if !self.databases.contains_key(&record.database_id) {
1667 return Err(MetaRejectionReason::NotFound {
1668 resource: format!("database {}", record.database_id),
1669 });
1670 }
1671 if record.schema_version == SchemaVersion::ZERO {
1672 return Err(MetaRejectionReason::Invalid {
1673 reason: "reserved zero schema version".to_owned(),
1674 });
1675 }
1676 match self.tables.get(&record.table_id) {
1677 Some(existing) => {
1678 if record.schema_version > existing.schema_version {
1679 let mut record = record.clone();
1680 record.metadata_version = version;
1681 self.tables.insert(record.table_id, record);
1682 Ok(())
1683 } else if record.schema_version == existing.schema_version {
1684 if existing.database_id == record.database_id
1685 && existing.schema == record.schema
1686 {
1687 Ok(())
1688 } else {
1689 Err(MetaRejectionReason::Conflict {
1690 resource: format!("table {}", record.table_id),
1691 reason: "schema version already used for different content"
1692 .to_owned(),
1693 })
1694 }
1695 } else {
1696 Err(MetaRejectionReason::StaleWrite {
1697 resource: format!("table {}", record.table_id),
1698 current: MetadataVersion(existing.schema_version.get()),
1699 attempted: MetadataVersion(record.schema_version.get()),
1700 })
1701 }
1702 }
1703 None => {
1704 let mut record = record.clone();
1705 record.metadata_version = version;
1706 self.tables.insert(record.table_id, record);
1707 Ok(())
1708 }
1709 }
1710 }
1711 MetaCommand::SetTabletDescriptor { descriptor } => {
1712 if let Err(error) = descriptor.validate() {
1713 return Err(MetaRejectionReason::Invalid {
1714 reason: error.to_string(),
1715 });
1716 }
1717 if !self.tables.contains_key(&descriptor.table_id) {
1718 return Err(MetaRejectionReason::NotFound {
1719 resource: format!("table {}", descriptor.table_id),
1720 });
1721 }
1722 match self.tablets.get(&descriptor.tablet_id) {
1723 Some(existing) => {
1724 if descriptor.generation > existing.descriptor.generation {
1725 self.tablets.insert(
1726 descriptor.tablet_id,
1727 TabletRecord {
1728 descriptor: descriptor.clone(),
1729 metadata_version: version,
1730 },
1731 );
1732 Ok(())
1733 } else if descriptor.generation == existing.descriptor.generation {
1734 if existing.descriptor == *descriptor {
1735 Ok(())
1736 } else {
1737 Err(MetaRejectionReason::Conflict {
1738 resource: format!("tablet {}", descriptor.tablet_id),
1739 reason: "generation already used for different content"
1740 .to_owned(),
1741 })
1742 }
1743 } else {
1744 Err(MetaRejectionReason::StaleWrite {
1745 resource: format!("tablet {}", descriptor.tablet_id),
1746 current: MetadataVersion(existing.descriptor.generation),
1747 attempted: MetadataVersion(descriptor.generation),
1748 })
1749 }
1750 }
1751 None => {
1752 self.tablets.insert(
1753 descriptor.tablet_id,
1754 TabletRecord {
1755 descriptor: descriptor.clone(),
1756 metadata_version: version,
1757 },
1758 );
1759 Ok(())
1760 }
1761 }
1762 }
1763 MetaCommand::RemoveTabletDescriptor {
1764 tablet_id,
1765 generation,
1766 } => match self.tablets.get(tablet_id) {
1767 None => Ok(()),
1768 Some(existing) => {
1769 if *generation >= existing.descriptor.generation {
1770 self.tablets.remove(tablet_id);
1771 Ok(())
1772 } else {
1773 Err(MetaRejectionReason::StaleWrite {
1774 resource: format!("tablet {tablet_id}"),
1775 current: MetadataVersion(existing.descriptor.generation),
1776 attempted: MetadataVersion(*generation),
1777 })
1778 }
1779 }
1780 },
1781 MetaCommand::SetReplicaPlacement { placement } => {
1782 if placement.replicas.is_empty() {
1783 return Err(MetaRejectionReason::Invalid {
1784 reason: "placement has no replicas".to_owned(),
1785 });
1786 }
1787 for (index, replica) in placement.replicas.iter().enumerate() {
1788 if !self.nodes.contains_key(&replica.node_id) {
1789 return Err(MetaRejectionReason::NotFound {
1790 resource: format!("node {}", replica.node_id),
1791 });
1792 }
1793 if placement.replicas[..index]
1794 .iter()
1795 .any(|prior| prior.node_id == replica.node_id)
1796 {
1797 return Err(MetaRejectionReason::Conflict {
1798 resource: format!("raft group {}", placement.raft_group_id),
1799 reason: format!("node {} appears twice", replica.node_id),
1800 });
1801 }
1802 }
1803 match self.placements.get(&placement.raft_group_id) {
1804 Some(existing) if existing.replicas == placement.replicas => Ok(()),
1805 _ => {
1806 let mut placement = placement.clone();
1807 placement.metadata_version = version;
1808 self.placements.insert(placement.raft_group_id, placement);
1809 Ok(())
1810 }
1811 }
1812 }
1813 MetaCommand::SetPlacementPolicy { name, policy } => {
1814 if name.trim().is_empty() {
1815 return Err(MetaRejectionReason::Invalid {
1816 reason: "placement policy name is empty".to_owned(),
1817 });
1818 }
1819 if policy.replicas == 0 {
1820 return Err(MetaRejectionReason::Invalid {
1821 reason: format!("placement policy `{name}` requests zero replicas"),
1822 });
1823 }
1824 match self.placement_policies.get(name) {
1825 Some(existing) if existing.policy == *policy => Ok(()),
1826 _ => {
1827 self.placement_policies.insert(
1828 name.clone(),
1829 PlacementPolicyRecord {
1830 policy: policy.clone(),
1831 metadata_version: version,
1832 },
1833 );
1834 Ok(())
1835 }
1836 }
1837 }
1838 MetaCommand::SubmitSchemaJob { job } => {
1839 if job.job_id == 0 {
1840 return Err(MetaRejectionReason::Invalid {
1841 reason: "reserved zero job id".to_owned(),
1842 });
1843 }
1844 if !self.databases.contains_key(&job.database_id) {
1845 return Err(MetaRejectionReason::NotFound {
1846 resource: format!("database {}", job.database_id),
1847 });
1848 }
1849 if !self.tables.contains_key(&job.table_id) {
1850 return Err(MetaRejectionReason::NotFound {
1851 resource: format!("table {}", job.table_id),
1852 });
1853 }
1854 if job.state != SchemaJobState::Pending {
1855 return Err(MetaRejectionReason::Invalid {
1856 reason: "submitted jobs start Pending".to_owned(),
1857 });
1858 }
1859 match self.schema_jobs.get(&job.job_id) {
1860 Some(existing) => {
1861 let mut comparable = existing.clone();
1862 comparable.metadata_version = job.metadata_version;
1863 if comparable == *job {
1864 Ok(())
1865 } else {
1866 Err(MetaRejectionReason::Conflict {
1867 resource: format!("schema job {}", job.job_id),
1868 reason: "job id already exists with different content".to_owned(),
1869 })
1870 }
1871 }
1872 None => {
1873 let mut job = job.clone();
1874 job.metadata_version = version;
1875 self.schema_jobs.insert(job.job_id, job);
1876 Ok(())
1877 }
1878 }
1879 }
1880 MetaCommand::UpdateSchemaJob {
1881 job_id,
1882 state,
1883 updated_at,
1884 error,
1885 expected_version,
1886 } => {
1887 let Some(record) = self.schema_jobs.get_mut(job_id) else {
1888 return Err(MetaRejectionReason::NotFound {
1889 resource: format!("schema job {job_id}"),
1890 });
1891 };
1892 if let Some(expected) = expected_version {
1893 if *expected != record.metadata_version {
1894 return Err(MetaRejectionReason::StaleWrite {
1895 resource: format!("schema job {job_id}"),
1896 current: record.metadata_version,
1897 attempted: *expected,
1898 });
1899 }
1900 }
1901 if record.state.is_terminal() {
1902 return Err(MetaRejectionReason::Conflict {
1903 resource: format!("schema job {job_id}"),
1904 reason: format!("terminal state {:?}", record.state),
1905 });
1906 }
1907 if record.state != *state && !record.state.can_transition(*state) {
1908 return Err(MetaRejectionReason::Conflict {
1909 resource: format!("schema job {job_id}"),
1910 reason: format!("illegal transition {:?} -> {:?}", record.state, state),
1911 });
1912 }
1913 if record.state == *state && record.error == *error {
1914 return Ok(());
1915 }
1916 record.state = *state;
1917 record.updated_at = *updated_at;
1918 record.error = error.clone();
1919 record.metadata_version = version;
1920 Ok(())
1921 }
1922 MetaCommand::SetClusterSetting { key, value } => self.settings.apply(key, value),
1923 MetaCommand::ActivateFeature { activation } => {
1924 let voters: Vec<NodeDescriptor> = self
1930 .nodes
1931 .values()
1932 .filter(|record| record.descriptor.state != NodeState::Decommissioned)
1933 .map(|record| record.descriptor.clone())
1934 .collect();
1935 activation.validate(registry, self.feature_level, &voters)?;
1936 let already = self.feature_activations.iter().any(|applied| {
1937 applied.feature == activation.feature && applied.level == activation.level
1938 });
1939 if already {
1940 return Ok(());
1941 }
1942 if activation.level > self.feature_level {
1943 self.feature_level = activation.level;
1944 }
1945 let mut applied = activation.clone();
1946 if applied.activated_at == HlcTimestamp::ZERO {
1947 applied.activated_at = commit_ts;
1948 }
1949 self.feature_activations.push(applied);
1950 Ok(())
1951 }
1952 MetaCommand::SetTxnStatusPartition { partition } => {
1953 match self.txn_status_partitions.get(&partition.partition_id) {
1954 Some(existing) if existing == partition => Ok(()),
1955 _ => {
1956 self.txn_status_partitions
1957 .insert(partition.partition_id, partition.clone());
1958 Ok(())
1959 }
1960 }
1961 }
1962 MetaCommand::PublishSplit { command } => self.apply_split_publish(command, version),
1963 MetaCommand::PublishMerge { command } => self.apply_merge_publish(command, version),
1964 MetaCommand::AllocateRaftNodeIds { count } => {
1965 self.apply_allocate_raft_node_ids(*count, command_id)
1966 }
1967 }
1968 }
1969
1970 fn apply_split_publish(
1975 &mut self,
1976 command: &SplitPublishCommand,
1977 version: MetadataVersion,
1978 ) -> Result<(), MetaRejectionReason> {
1979 command
1980 .validate()
1981 .map_err(|error| MetaRejectionReason::Invalid {
1982 reason: error.to_string(),
1983 })?;
1984 if !self.tables.contains_key(&command.source.table_id) {
1985 return Err(MetaRejectionReason::NotFound {
1986 resource: format!("table {}", command.source.table_id),
1987 });
1988 }
1989 let publish = command.publish_generation();
1990 let replay = self.tablet(command.source.tablet_id) == Some(&command.source)
1993 && command
1994 .children
1995 .iter()
1996 .all(|child| self.tablet(child.tablet_id) == Some(child));
1997 if replay {
1998 return Ok(());
1999 }
2000 let precursor_generation =
2001 publish
2002 .checked_sub(1)
2003 .ok_or_else(|| MetaRejectionReason::Invalid {
2004 reason: "publication generation is zero".to_owned(),
2005 })?;
2006 self.require_stored_precursor(
2010 &command.source,
2011 TabletState::Splitting,
2012 precursor_generation,
2013 false,
2014 )?;
2015 for child in &command.children {
2018 self.require_stored_precursor(
2019 child,
2020 TabletState::Creating,
2021 precursor_generation,
2022 true,
2023 )?;
2024 }
2025 let participants: BTreeSet<TabletId> = command
2029 .children
2030 .iter()
2031 .map(|child| child.tablet_id)
2032 .chain([command.source.tablet_id])
2033 .collect();
2034 for child in &command.children {
2035 self.require_no_routable_overlap(child, &participants)?;
2036 }
2037 for child in &command.children {
2038 self.tablets.insert(
2039 child.tablet_id,
2040 TabletRecord {
2041 descriptor: child.clone(),
2042 metadata_version: version,
2043 },
2044 );
2045 }
2046 self.tablets.insert(
2047 command.source.tablet_id,
2048 TabletRecord {
2049 descriptor: command.source.clone(),
2050 metadata_version: version,
2051 },
2052 );
2053 Ok(())
2054 }
2055
2056 fn apply_merge_publish(
2061 &mut self,
2062 command: &MergePublishCommand,
2063 version: MetadataVersion,
2064 ) -> Result<(), MetaRejectionReason> {
2065 command
2066 .validate()
2067 .map_err(|error| MetaRejectionReason::Invalid {
2068 reason: error.to_string(),
2069 })?;
2070 if !self.tables.contains_key(&command.replacement.table_id) {
2071 return Err(MetaRejectionReason::NotFound {
2072 resource: format!("table {}", command.replacement.table_id),
2073 });
2074 }
2075 let publish = command.publish_generation();
2076 let replay = self.tablet(command.replacement.tablet_id) == Some(&command.replacement)
2077 && command
2078 .sources
2079 .iter()
2080 .all(|source| self.tablet(source.tablet_id) == Some(source));
2081 if replay {
2082 return Ok(());
2083 }
2084 let precursor_generation =
2085 publish
2086 .checked_sub(1)
2087 .ok_or_else(|| MetaRejectionReason::Invalid {
2088 reason: "publication generation is zero".to_owned(),
2089 })?;
2090 self.require_stored_precursor(
2095 &command.replacement,
2096 TabletState::Creating,
2097 precursor_generation,
2098 true,
2099 )?;
2100 for source in &command.sources {
2104 let stored = self.tablet(source.tablet_id).cloned().ok_or_else(|| {
2105 MetaRejectionReason::NotFound {
2106 resource: format!("tablet {}", source.tablet_id),
2107 }
2108 })?;
2109 let mut precursor = stored.clone();
2110 precursor.state = TabletState::Retiring;
2111 precursor.generation = publish;
2112 if stored.state != TabletState::Merging
2113 || stored.generation >= publish
2114 || precursor != *source
2115 {
2116 return Err(MetaRejectionReason::Conflict {
2117 resource: format!("tablet {}", source.tablet_id),
2118 reason: format!(
2119 "stored descriptor (state {}, generation {}) is not the Merging \
2120 precursor of the publication (generation {publish})",
2121 stored.state, stored.generation
2122 ),
2123 });
2124 }
2125 }
2126 let participants: BTreeSet<TabletId> = command
2127 .sources
2128 .iter()
2129 .map(|source| source.tablet_id)
2130 .chain([command.replacement.tablet_id])
2131 .collect();
2132 self.require_no_routable_overlap(&command.replacement, &participants)?;
2133 self.tablets.insert(
2134 command.replacement.tablet_id,
2135 TabletRecord {
2136 descriptor: command.replacement.clone(),
2137 metadata_version: version,
2138 },
2139 );
2140 for source in &command.sources {
2141 self.tablets.insert(
2142 source.tablet_id,
2143 TabletRecord {
2144 descriptor: source.clone(),
2145 metadata_version: version,
2146 },
2147 );
2148 }
2149 Ok(())
2150 }
2151
2152 fn require_stored_precursor(
2157 &self,
2158 published: &TabletDescriptor,
2159 precursor_state: TabletState,
2160 precursor_generation: u64,
2161 learners_promoted: bool,
2162 ) -> Result<TabletDescriptor, MetaRejectionReason> {
2163 let stored = self.tablet(published.tablet_id).cloned().ok_or_else(|| {
2164 MetaRejectionReason::NotFound {
2165 resource: format!("tablet {}", published.tablet_id),
2166 }
2167 })?;
2168 let mut precursor = stored.clone();
2169 precursor.state = published.state;
2170 precursor.generation = published.generation;
2171 if learners_promoted {
2172 for replica in &mut precursor.replicas {
2173 replica.role = ReplicaRole::Voter;
2174 }
2175 }
2176 if stored.state != precursor_state
2177 || stored.generation != precursor_generation
2178 || precursor != *published
2179 {
2180 return Err(MetaRejectionReason::Conflict {
2181 resource: format!("tablet {}", published.tablet_id),
2182 reason: format!(
2183 "stored descriptor (state {}, generation {}) is not the {} precursor of \
2184 the publication (state {}, generation {})",
2185 stored.state,
2186 stored.generation,
2187 precursor_state,
2188 published.state,
2189 published.generation
2190 ),
2191 });
2192 }
2193 Ok(stored)
2194 }
2195
2196 fn require_no_routable_overlap(
2200 &self,
2201 published: &TabletDescriptor,
2202 participants: &BTreeSet<TabletId>,
2203 ) -> Result<(), MetaRejectionReason> {
2204 let overlapping =
2205 self.tablets
2206 .values()
2207 .map(|record| &record.descriptor)
2208 .find(|descriptor| {
2209 !participants.contains(&descriptor.tablet_id)
2210 && descriptor.table_id == published.table_id
2211 && descriptor.state.is_routable()
2212 && descriptor.partition.overlaps(&published.partition)
2213 });
2214 if let Some(other) = overlapping {
2215 return Err(MetaRejectionReason::Conflict {
2216 resource: format!("tablet {}", published.tablet_id),
2217 reason: format!("bounds overlap routable tablet {}", other.tablet_id),
2218 });
2219 }
2220 Ok(())
2221 }
2222
2223 fn apply_allocate_raft_node_ids(
2228 &mut self,
2229 count: u32,
2230 command_id: Option<[u8; 16]>,
2231 ) -> Result<(), MetaRejectionReason> {
2232 if count == 0 || count > MAX_RAFT_NODE_ID_ALLOCATION {
2233 return Err(MetaRejectionReason::Invalid {
2234 reason: format!(
2235 "raft node id allocation count {count} is outside \
2236 1..={MAX_RAFT_NODE_ID_ALLOCATION}"
2237 ),
2238 });
2239 }
2240 if let Some(id) = command_id {
2241 if self.raft_id_allocations.contains_key(&hex_encode(&id)) {
2242 return Ok(());
2244 }
2245 }
2246 let mut used = BTreeSet::new();
2247 for record in self.nodes.values() {
2248 used.insert(raft_node_id(&record.descriptor.node_id));
2249 }
2250 for record in self.tablets.values() {
2251 for replica in &record.descriptor.replicas {
2252 used.insert(replica.raft_node_id);
2253 }
2254 }
2255 for placement in self.placements.values() {
2256 for replica in &placement.replicas {
2257 used.insert(replica.raft_node_id);
2258 }
2259 }
2260 let count = u64::from(count);
2261 let mut base = self.next_raft_node_id.max(FIRST_RAFT_NODE_ID);
2262 let mut skips = 0u64;
2263 loop {
2264 let end = base
2265 .checked_add(count)
2266 .ok_or_else(|| MetaRejectionReason::Conflict {
2267 resource: "raft node id allocator".to_owned(),
2268 reason: "allocation overflows u64".to_owned(),
2269 })?;
2270 match used.range(base..end).next() {
2271 None => break,
2272 Some(conflicting) => {
2273 skips += 1;
2274 if skips > RAFT_ID_ALLOCATION_SCAN_LIMIT {
2275 return Err(MetaRejectionReason::Conflict {
2276 resource: "raft node id allocator".to_owned(),
2277 reason: format!(
2278 "collision skip scan exceeded {RAFT_ID_ALLOCATION_SCAN_LIMIT} \
2279 allocated ids"
2280 ),
2281 });
2282 }
2283 base = conflicting + 1;
2284 }
2285 }
2286 }
2287 let end = base + count;
2288 self.next_raft_node_id = end;
2289 if let Some(id) = command_id {
2290 let key = hex_encode(&id);
2291 self.raft_id_allocations.insert(key.clone(), base);
2292 self.raft_id_allocation_order.push_back(key);
2293 while self.raft_id_allocation_order.len() > RAFT_ID_ALLOCATION_RECORD_LIMIT {
2294 if let Some(oldest) = self.raft_id_allocation_order.pop_front() {
2295 self.raft_id_allocations.remove(&oldest);
2296 }
2297 }
2298 }
2299 Ok(())
2300 }
2301
2302 pub fn node(&self, node_id: NodeId) -> Option<&NodeDescriptor> {
2304 self.nodes.get(&node_id).map(|record| &record.descriptor)
2305 }
2306
2307 pub fn node_record(&self, node_id: NodeId) -> Option<&NodeRecord> {
2309 self.nodes.get(&node_id)
2310 }
2311
2312 pub fn node_descriptors(&self) -> Vec<NodeDescriptor> {
2314 self.nodes
2315 .values()
2316 .map(|record| record.descriptor.clone())
2317 .collect()
2318 }
2319
2320 pub fn database(&self, database_id: DatabaseId) -> Option<&DatabaseDescriptor> {
2322 self.databases.get(&database_id)
2323 }
2324
2325 pub fn database_by_name(&self, name: &str) -> Option<&DatabaseDescriptor> {
2327 self.databases
2328 .values()
2329 .find(|database| database.name == name)
2330 }
2331
2332 pub fn table(&self, table_id: TableId) -> Option<&TableSchemaRecord> {
2334 self.tables.get(&table_id)
2335 }
2336
2337 pub fn tablet(&self, tablet_id: TabletId) -> Option<&TabletDescriptor> {
2339 self.tablets
2340 .get(&tablet_id)
2341 .map(|record| &record.descriptor)
2342 }
2343
2344 pub fn tablet_record(&self, tablet_id: TabletId) -> Option<&TabletRecord> {
2346 self.tablets.get(&tablet_id)
2347 }
2348
2349 pub fn placement(&self, raft_group_id: RaftGroupId) -> Option<&ReplicaPlacement> {
2351 self.placements.get(&raft_group_id)
2352 }
2353
2354 pub fn placement_policy(&self, name: &str) -> Option<&PlacementPolicy> {
2356 self.placement_policies
2357 .get(name)
2358 .map(|record| &record.policy)
2359 }
2360
2361 pub fn placement_policy_record(&self, name: &str) -> Option<&PlacementPolicyRecord> {
2363 self.placement_policies.get(name)
2364 }
2365
2366 pub fn schema_job(&self, job_id: u64) -> Option<&SchemaJobRecord> {
2368 self.schema_jobs.get(&job_id)
2369 }
2370
2371 pub fn txn_status_partition(&self, partition_id: u32) -> Option<&TxnStatusPartition> {
2373 self.txn_status_partitions.get(&partition_id)
2374 }
2375
2376 pub fn settings(&self) -> &ClusterSettings {
2378 &self.settings
2379 }
2380
2381 pub fn feature_level(&self) -> ClusterFeatureLevel {
2383 self.feature_level
2384 }
2385
2386 pub fn feature_active(&self, registry: &FeatureRegistry, feature: &str) -> bool {
2388 registry.feature_supported(self.feature_level, feature)
2389 }
2390
2391 pub fn feature_activations(&self) -> &[FeatureActivation] {
2393 &self.feature_activations
2394 }
2395
2396 pub fn rejections(&self) -> &VecDeque<MetaRejection> {
2398 &self.rejections
2399 }
2400
2401 pub fn next_raft_node_id(&self) -> u64 {
2403 self.next_raft_node_id
2404 }
2405
2406 pub fn raft_id_allocation(&self, command_id: &[u8; 16]) -> Option<u64> {
2410 self.raft_id_allocations
2411 .get(&hex_encode(command_id))
2412 .copied()
2413 }
2414}
2415
2416mod v1 {
2427 use super::*;
2428
2429 #[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
2430 pub enum ReplicaRole {
2431 Voter,
2432 Learner,
2433 }
2434
2435 #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
2436 pub struct ReplicaDescriptor {
2437 pub node_id: NodeId,
2438 pub role: ReplicaRole,
2439 }
2440
2441 #[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
2442 pub struct PartitionBounds {
2443 pub start: Option<Vec<u8>>,
2444 pub end: Option<Vec<u8>>,
2445 }
2446
2447 #[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
2448 pub enum TabletState {
2449 Creating,
2450 Online,
2451 Offline,
2452 }
2453
2454 fn zero_database_id() -> DatabaseId {
2455 DatabaseId::ZERO
2456 }
2457
2458 #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
2459 pub struct TabletDescriptor {
2460 pub tablet_id: TabletId,
2461 pub table_id: TableId,
2462 #[serde(default = "zero_database_id")]
2463 pub database_id: DatabaseId,
2464 pub raft_group_id: RaftGroupId,
2465 pub partition: PartitionBounds,
2466 pub replicas: Vec<ReplicaDescriptor>,
2467 pub leader_hint: Option<NodeId>,
2468 pub generation: u64,
2469 pub state: TabletState,
2470 #[serde(default = "zero_metadata_version")]
2471 pub metadata_version: MetadataVersion,
2472 }
2473
2474 #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
2475 pub struct ReplicaPlacement {
2476 pub raft_group_id: RaftGroupId,
2477 pub replicas: Vec<ReplicaDescriptor>,
2478 #[serde(default = "zero_metadata_version")]
2479 pub metadata_version: MetadataVersion,
2480 }
2481
2482 #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
2483 pub struct LocalityConstraint {
2484 pub key: String,
2485 pub value: String,
2486 }
2487
2488 #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
2489 pub struct PlacementPolicy {
2490 pub replicas: u8,
2491 pub voter_constraints: Vec<LocalityConstraint>,
2492 pub leader_preferences: Vec<LocalityConstraint>,
2493 pub prohibited_nodes: Vec<NodeId>,
2494 #[serde(default = "zero_metadata_version")]
2495 pub metadata_version: MetadataVersion,
2496 }
2497
2498 #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
2501 pub enum MetaCommand {
2502 RegisterNode {
2503 descriptor: NodeDescriptor,
2504 },
2505 UpdateNodeState {
2506 node_id: NodeId,
2507 state: NodeState,
2508 expected_version: Option<MetadataVersion>,
2509 },
2510 RemoveNode {
2511 node_id: NodeId,
2512 },
2513 CreateDatabase {
2514 descriptor: DatabaseDescriptor,
2515 },
2516 DropDatabase {
2517 database_id: DatabaseId,
2518 },
2519 SetTableSchema {
2520 record: TableSchemaRecord,
2521 },
2522 SetTabletDescriptor {
2523 descriptor: TabletDescriptor,
2524 },
2525 RemoveTabletDescriptor {
2526 tablet_id: TabletId,
2527 generation: u64,
2528 },
2529 SetReplicaPlacement {
2530 placement: ReplicaPlacement,
2531 },
2532 SetPlacementPolicy {
2533 name: String,
2534 policy: PlacementPolicy,
2535 },
2536 SubmitSchemaJob {
2537 job: SchemaJobRecord,
2538 },
2539 UpdateSchemaJob {
2540 job_id: u64,
2541 state: SchemaJobState,
2542 updated_at: HlcTimestamp,
2543 error: Option<String>,
2544 expected_version: Option<MetadataVersion>,
2545 },
2546 SetClusterSetting {
2547 key: String,
2548 value: serde_json::Value,
2549 },
2550 ActivateFeature {
2551 activation: FeatureActivation,
2552 },
2553 SetTxnStatusPartition {
2554 partition: TxnStatusPartition,
2555 },
2556 }
2557
2558 #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
2559 pub struct MetaCommandRecord {
2560 pub format_version: u32,
2561 pub command: MetaCommand,
2562 }
2563
2564 #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
2565 #[serde(default)]
2566 pub struct MetaState {
2567 pub format_version: u32,
2568 pub metadata_version: MetadataVersion,
2569 pub nodes: BTreeMap<NodeId, NodeRecord>,
2570 pub databases: BTreeMap<DatabaseId, DatabaseDescriptor>,
2571 pub tables: BTreeMap<TableId, TableSchemaRecord>,
2572 pub tablets: BTreeMap<TabletId, TabletDescriptor>,
2573 pub placements: BTreeMap<RaftGroupId, ReplicaPlacement>,
2574 pub placement_policies: BTreeMap<String, PlacementPolicy>,
2575 pub schema_jobs: BTreeMap<u64, SchemaJobRecord>,
2576 pub txn_status_partitions: BTreeMap<u32, TxnStatusPartition>,
2577 pub settings: ClusterSettings,
2578 pub feature_level: ClusterFeatureLevel,
2579 pub feature_activations: Vec<FeatureActivation>,
2580 pub rejections: VecDeque<MetaRejection>,
2581 }
2582
2583 impl Default for MetaState {
2584 fn default() -> Self {
2585 Self {
2586 format_version: 1,
2587 metadata_version: MetadataVersion::ZERO,
2588 nodes: BTreeMap::new(),
2589 databases: BTreeMap::new(),
2590 tables: BTreeMap::new(),
2591 tablets: BTreeMap::new(),
2592 placements: BTreeMap::new(),
2593 placement_policies: BTreeMap::new(),
2594 schema_jobs: BTreeMap::new(),
2595 txn_status_partitions: BTreeMap::new(),
2596 settings: ClusterSettings::default(),
2597 feature_level: ClusterFeatureLevel::ZERO,
2598 feature_activations: Vec::new(),
2599 rejections: VecDeque::new(),
2600 }
2601 }
2602 }
2603
2604 #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
2605 pub struct MetaStateCheckpoint {
2606 pub format_version: u32,
2607 pub position: LogPosition,
2608 pub command_id: Option<[u8; 16]>,
2609 pub state: MetaState,
2610 }
2611}
2612
2613impl From<v1::ReplicaRole> for ReplicaRole {
2614 fn from(role: v1::ReplicaRole) -> Self {
2615 match role {
2616 v1::ReplicaRole::Voter => Self::Voter,
2617 v1::ReplicaRole::Learner => Self::Learner,
2618 }
2619 }
2620}
2621
2622fn migrate_replica(replica: v1::ReplicaDescriptor) -> ReplicaDescriptor {
2625 ReplicaDescriptor {
2626 node_id: replica.node_id,
2627 role: replica.role.into(),
2628 raft_node_id: raft_node_id(&replica.node_id),
2629 }
2630}
2631
2632fn migrate_bounds(bounds: v1::PartitionBounds) -> PartitionBounds {
2635 PartitionBounds {
2636 low: match bounds.start {
2637 None => Bound::Unbounded,
2638 Some(start) => Bound::Included(Key::from_bytes(start)),
2639 },
2640 high: match bounds.end {
2641 None => Bound::Unbounded,
2642 Some(end) => Bound::Excluded(Key::from_bytes(end)),
2643 },
2644 }
2645}
2646
2647fn migrate_tablet_state(state: v1::TabletState) -> TabletState {
2648 match state {
2649 v1::TabletState::Creating => TabletState::Creating,
2650 v1::TabletState::Online => TabletState::Active,
2651 v1::TabletState::Offline => TabletState::Retiring,
2654 }
2655}
2656
2657fn migrate_tablet(tablet: v1::TabletDescriptor) -> TabletRecord {
2658 TabletRecord {
2659 descriptor: TabletDescriptor {
2660 tablet_id: tablet.tablet_id,
2661 table_id: tablet.table_id,
2662 database_id: tablet.database_id,
2663 raft_group_id: tablet.raft_group_id,
2664 partition: migrate_bounds(tablet.partition),
2665 replicas: tablet.replicas.into_iter().map(migrate_replica).collect(),
2666 leader_hint: tablet.leader_hint,
2667 generation: tablet.generation,
2668 state: migrate_tablet_state(tablet.state),
2669 },
2670 metadata_version: tablet.metadata_version,
2671 }
2672}
2673
2674fn migrate_placement(placement: v1::ReplicaPlacement) -> ReplicaPlacement {
2675 ReplicaPlacement {
2676 raft_group_id: placement.raft_group_id,
2677 replicas: placement
2678 .replicas
2679 .into_iter()
2680 .map(migrate_replica)
2681 .collect(),
2682 metadata_version: placement.metadata_version,
2683 }
2684}
2685
2686fn migrate_policy(policy: v1::PlacementPolicy) -> PlacementPolicyRecord {
2689 PlacementPolicyRecord {
2690 policy: PlacementPolicy {
2691 replicas: policy.replicas,
2692 voter_constraints: policy
2693 .voter_constraints
2694 .into_iter()
2695 .map(|constraint| LocalityConstraint {
2696 key: constraint.key,
2697 value: constraint.value,
2698 required: true,
2699 })
2700 .collect(),
2701 leader_preferences: policy
2702 .leader_preferences
2703 .into_iter()
2704 .map(|constraint| LocalityConstraint {
2705 key: constraint.key,
2706 value: constraint.value,
2707 required: false,
2708 })
2709 .collect(),
2710 prohibited_nodes: policy.prohibited_nodes,
2711 },
2712 metadata_version: policy.metadata_version,
2713 }
2714}
2715
2716fn migrate_command(command: v1::MetaCommand) -> MetaCommand {
2717 match command {
2718 v1::MetaCommand::RegisterNode { descriptor } => MetaCommand::RegisterNode { descriptor },
2719 v1::MetaCommand::UpdateNodeState {
2720 node_id,
2721 state,
2722 expected_version,
2723 } => MetaCommand::UpdateNodeState {
2724 node_id,
2725 state,
2726 expected_version,
2727 },
2728 v1::MetaCommand::RemoveNode { node_id } => MetaCommand::RemoveNode { node_id },
2729 v1::MetaCommand::CreateDatabase { descriptor } => {
2730 MetaCommand::CreateDatabase { descriptor }
2731 }
2732 v1::MetaCommand::DropDatabase { database_id } => MetaCommand::DropDatabase { database_id },
2733 v1::MetaCommand::SetTableSchema { record } => MetaCommand::SetTableSchema { record },
2734 v1::MetaCommand::SetTabletDescriptor { descriptor } => MetaCommand::SetTabletDescriptor {
2735 descriptor: migrate_tablet(descriptor).descriptor,
2736 },
2737 v1::MetaCommand::RemoveTabletDescriptor {
2738 tablet_id,
2739 generation,
2740 } => MetaCommand::RemoveTabletDescriptor {
2741 tablet_id,
2742 generation,
2743 },
2744 v1::MetaCommand::SetReplicaPlacement { placement } => MetaCommand::SetReplicaPlacement {
2745 placement: migrate_placement(placement),
2746 },
2747 v1::MetaCommand::SetPlacementPolicy { name, policy } => MetaCommand::SetPlacementPolicy {
2748 name,
2749 policy: migrate_policy(policy).policy,
2750 },
2751 v1::MetaCommand::SubmitSchemaJob { job } => MetaCommand::SubmitSchemaJob { job },
2752 v1::MetaCommand::UpdateSchemaJob {
2753 job_id,
2754 state,
2755 updated_at,
2756 error,
2757 expected_version,
2758 } => MetaCommand::UpdateSchemaJob {
2759 job_id,
2760 state,
2761 updated_at,
2762 error,
2763 expected_version,
2764 },
2765 v1::MetaCommand::SetClusterSetting { key, value } => {
2766 MetaCommand::SetClusterSetting { key, value }
2767 }
2768 v1::MetaCommand::ActivateFeature { activation } => {
2769 MetaCommand::ActivateFeature { activation }
2770 }
2771 v1::MetaCommand::SetTxnStatusPartition { partition } => {
2772 MetaCommand::SetTxnStatusPartition { partition }
2773 }
2774 }
2775}
2776
2777fn migrate_state(state: v1::MetaState) -> MetaState {
2778 MetaState {
2779 format_version: META_STATE_FORMAT_VERSION,
2780 metadata_version: state.metadata_version,
2781 nodes: state.nodes,
2782 databases: state.databases,
2783 tables: state.tables,
2784 tablets: state
2785 .tablets
2786 .into_iter()
2787 .map(|(tablet_id, tablet)| (tablet_id, migrate_tablet(tablet)))
2788 .collect(),
2789 placements: state
2790 .placements
2791 .into_iter()
2792 .map(|(group_id, placement)| (group_id, migrate_placement(placement)))
2793 .collect(),
2794 placement_policies: state
2795 .placement_policies
2796 .into_iter()
2797 .map(|(name, policy)| (name, migrate_policy(policy)))
2798 .collect(),
2799 schema_jobs: state.schema_jobs,
2800 txn_status_partitions: state.txn_status_partitions,
2801 settings: state.settings,
2802 feature_level: state.feature_level,
2803 feature_activations: state.feature_activations,
2804 rejections: state.rejections,
2805 next_raft_node_id: FIRST_RAFT_NODE_ID,
2809 raft_id_allocations: BTreeMap::new(),
2810 raft_id_allocation_order: VecDeque::new(),
2811 }
2812}
2813
2814fn migrate_checkpoint(checkpoint: v1::MetaStateCheckpoint) -> MetaStateCheckpoint {
2815 MetaStateCheckpoint {
2816 format_version: META_STATE_CHECKPOINT_FORMAT_VERSION,
2817 position: checkpoint.position,
2818 command_id: checkpoint.command_id,
2819 state: migrate_state(checkpoint.state),
2820 }
2821}
2822
2823#[derive(Deserialize)]
2826struct FormatVersionProbe {
2827 format_version: u32,
2828}
2829
2830fn decode_meta_checkpoint(bytes: &[u8]) -> Result<MetaStateCheckpoint, String> {
2834 let probe: FormatVersionProbe =
2835 serde_json::from_slice(bytes).map_err(|error| format!("decode: {error}"))?;
2836 if probe.format_version < MIN_SUPPORTED_META_STATE_CHECKPOINT_FORMAT_VERSION
2837 || probe.format_version > META_STATE_CHECKPOINT_FORMAT_VERSION
2838 {
2839 return Err(format!(
2840 "unsupported format version {} (supported \
2841 {MIN_SUPPORTED_META_STATE_CHECKPOINT_FORMAT_VERSION}..=\
2842 {META_STATE_CHECKPOINT_FORMAT_VERSION})",
2843 probe.format_version
2844 ));
2845 }
2846 if probe.format_version == 1 {
2847 let checkpoint: v1::MetaStateCheckpoint =
2848 serde_json::from_slice(bytes).map_err(|error| format!("decode v1: {error}"))?;
2849 if checkpoint.format_version != probe.format_version {
2850 return Err("inconsistent format version".to_owned());
2851 }
2852 return Ok(migrate_checkpoint(checkpoint));
2853 }
2854 serde_json::from_slice(bytes).map_err(|error| format!("decode: {error}"))
2855}
2856
2857pub const META_STATE_CHECKPOINT_FILENAME: &str = "meta-state.json";
2863pub const META_STATE_CHECKPOINT_FORMAT_VERSION: u32 = 2;
2866pub const MIN_SUPPORTED_META_STATE_CHECKPOINT_FORMAT_VERSION: u32 = 1;
2868
2869#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
2873pub struct MetaStateCheckpoint {
2874 pub format_version: u32,
2876 pub position: LogPosition,
2878 pub command_id: Option<[u8; 16]>,
2881 pub state: MetaState,
2883}
2884
2885pub struct MetaApplySink {
2912 state: MetaState,
2913 position: LogPosition,
2914 command_id: Option<[u8; 16]>,
2915 registry: FeatureRegistry,
2916 state_dir: PathBuf,
2918}
2919
2920impl MetaApplySink {
2921 pub fn open(group_dir: &Path, registry: FeatureRegistry) -> Result<Self, MetaError> {
2929 let state_dir = group_dir.join("raft").join("state");
2930 std::fs::create_dir_all(&state_dir).map_err(MetaError::Io)?;
2931 let checkpoint_path = state_dir.join(META_STATE_CHECKPOINT_FILENAME);
2932 let Some(bytes) =
2933 crate::node::read_meta_file(&checkpoint_path).map_err(|error| match error {
2934 crate::node::ClusterError::Io(error) => MetaError::Io(error),
2935 other => MetaError::CorruptCheckpoint(other.to_string()),
2936 })?
2937 else {
2938 return Ok(MetaApplySink {
2939 state: MetaState::default(),
2940 position: LogPosition::ZERO,
2941 command_id: None,
2942 registry,
2943 state_dir,
2944 });
2945 };
2946 let checkpoint = decode_meta_checkpoint(&bytes).map_err(MetaError::CorruptCheckpoint)?;
2947 if checkpoint.state.format_version < MIN_SUPPORTED_META_STATE_FORMAT_VERSION
2948 || checkpoint.state.format_version > META_STATE_FORMAT_VERSION
2949 {
2950 return Err(MetaError::CorruptCheckpoint(format!(
2951 "unsupported meta state format version {} (supported \
2952 {MIN_SUPPORTED_META_STATE_FORMAT_VERSION}..={META_STATE_FORMAT_VERSION})",
2953 checkpoint.state.format_version
2954 )));
2955 }
2956 Ok(MetaApplySink {
2957 state: checkpoint.state,
2958 position: checkpoint.position,
2959 command_id: checkpoint.command_id,
2960 registry,
2961 state_dir,
2962 })
2963 }
2964
2965 pub fn state(&self) -> &MetaState {
2967 &self.state
2968 }
2969
2970 pub fn metadata_version(&self) -> MetadataVersion {
2972 self.state.metadata_version
2973 }
2974
2975 pub fn applied_position(&self) -> LogPosition {
2978 self.position
2979 }
2980
2981 fn checkpoint(&self) -> MetaStateCheckpoint {
2982 MetaStateCheckpoint {
2983 format_version: META_STATE_CHECKPOINT_FORMAT_VERSION,
2984 position: self.position,
2985 command_id: self.command_id,
2986 state: self.state.clone(),
2987 }
2988 }
2989
2990 fn persist(&self) -> Result<(), StateMachineError> {
2991 let bytes = serde_json::to_vec(&self.checkpoint())
2992 .map_err(|error| StateMachineError::Sink(format!("meta checkpoint encode: {error}")))?;
2993 crate::node::write_meta_atomic(&self.state_dir, META_STATE_CHECKPOINT_FILENAME, &bytes)
2994 .map_err(|error| StateMachineError::Sink(format!("meta checkpoint write: {error}")))
2995 }
2996}
2997
2998impl ApplySink for MetaApplySink {
2999 fn apply(&mut self, command: &AppliedCommand) -> Result<(), StateMachineError> {
3000 if command.position.index <= self.position.index {
3003 return Ok(());
3004 }
3005 match &command.command {
3006 ReplicatedCommand::Catalog(catalog) => {
3007 catalog.envelope.verify().map_err(|error| {
3008 StateMachineError::Corrupt(format!("meta envelope: {error}"))
3009 })?;
3010 if catalog.envelope.command_type != COMMAND_TYPE_META_COMMAND {
3011 return Err(StateMachineError::Corrupt(format!(
3012 "meta command_type {} is not COMMAND_TYPE_META_COMMAND",
3013 catalog.envelope.command_type
3014 )));
3015 }
3016 let record = MetaCommandRecord::decode(&catalog.envelope.payload)
3017 .map_err(|error| StateMachineError::Corrupt(error.to_string()))?;
3018 let commit_ts = command.commit_ts().unwrap_or(HlcTimestamp::ZERO);
3019 let _ = self.state.apply(
3021 &record.command,
3022 command.command_id(),
3023 commit_ts,
3024 &self.registry,
3025 );
3026 }
3027 ReplicatedCommand::Maintenance(_) | ReplicatedCommand::Noop => {}
3030 ReplicatedCommand::Transaction(_) => {
3033 return Err(StateMachineError::Corrupt(
3034 "transaction command on the meta group: the meta group owns \
3035 control-plane state only (spec section 12.1)"
3036 .to_owned(),
3037 ));
3038 }
3039 }
3040 self.position = command.position;
3041 if let Some(command_id) = command.command_id() {
3042 self.command_id = Some(command_id);
3043 }
3044 self.persist()
3045 }
3046
3047 fn snapshot(&self) -> Result<Vec<u8>, StateMachineError> {
3048 serde_json::to_vec(&self.checkpoint())
3049 .map_err(|error| StateMachineError::Sink(format!("meta snapshot encode: {error}")))
3050 }
3051
3052 fn install(&mut self, data: &[u8]) -> Result<(), StateMachineError> {
3053 let checkpoint = decode_meta_checkpoint(data)
3054 .map_err(|error| StateMachineError::Corrupt(format!("meta snapshot {error}")))?;
3055 if checkpoint.state.format_version < MIN_SUPPORTED_META_STATE_FORMAT_VERSION
3056 || checkpoint.state.format_version > META_STATE_FORMAT_VERSION
3057 {
3058 return Err(StateMachineError::Corrupt(format!(
3059 "unsupported meta state format version {} (supported \
3060 {MIN_SUPPORTED_META_STATE_FORMAT_VERSION}..={META_STATE_FORMAT_VERSION})",
3061 checkpoint.state.format_version
3062 )));
3063 }
3064 self.state = checkpoint.state;
3065 self.position = checkpoint.position;
3066 self.command_id = checkpoint.command_id;
3067 self.persist()
3068 }
3069}
3070
3071impl fmt::Debug for MetaApplySink {
3072 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3073 f.debug_struct("MetaApplySink")
3074 .field("metadata_version", &self.state.metadata_version)
3075 .finish()
3076 }
3077}
3078
3079#[derive(Debug, Clone)]
3098pub struct MetaGroupConfig {
3099 pub node_data: PathBuf,
3101 pub meta_group_id: RaftGroupId,
3104 pub node_id: NodeId,
3106 pub registry: FeatureRegistry,
3109 pub storage: StorageConfig,
3111 pub idempotency_retention: usize,
3113}
3114
3115impl MetaGroupConfig {
3116 pub fn new(node_data: PathBuf, meta_group_id: RaftGroupId, node_id: NodeId) -> Self {
3119 MetaGroupConfig {
3120 node_data,
3121 meta_group_id,
3122 node_id,
3123 registry: FeatureRegistry::current(),
3124 storage: StorageConfig::default(),
3125 idempotency_retention:
3126 mongreldb_consensus::state_machine::DEFAULT_IDEMPOTENCY_RETENTION,
3127 }
3128 }
3129
3130 pub fn group_dir(&self) -> PathBuf {
3133 self.node_data
3134 .join("groups")
3135 .join(self.meta_group_id.to_hex())
3136 }
3137
3138 pub fn cluster_name(&self) -> String {
3140 format!("meta-{}", self.meta_group_id.to_hex())
3141 }
3142
3143 pub fn group_config(&self) -> GroupConfig {
3146 let mut config = GroupConfig::new(
3147 self.cluster_name(),
3148 raft_node_id(&self.node_id),
3149 self.group_dir(),
3150 );
3151 config.storage = self.storage.clone();
3152 config.idempotency_retention = self.idempotency_retention;
3153 config
3154 }
3155}
3156
3157#[derive(Debug, Clone)]
3159pub struct MetaCommandReceipt {
3160 pub receipt: GroupCommitReceipt,
3163 pub metadata_version: MetadataVersion,
3165}
3166
3167pub struct MetaGroup<T: RaftTransport> {
3171 group: ConsensusGroup<T>,
3172 sink: Arc<Mutex<MetaApplySink>>,
3173 config: MetaGroupConfig,
3174}
3175
3176fn basic_node<N>(address: &str) -> Result<N, MetaError>
3183where
3184 N: for<'de> Deserialize<'de>,
3185{
3186 serde_json::from_value(serde_json::json!({ "addr": address }))
3187 .map_err(|error| MetaError::InvalidRequest(format!("member address `{address}`: {error}")))
3188}
3189
3190pub(crate) fn new_command_id() -> Result<[u8; 16], MetaError> {
3192 let mut id = [0u8; 16];
3193 getrandom::getrandom(&mut id).map_err(|error| MetaError::Rng(error.to_string()))?;
3194 Ok(id)
3195}
3196
3197impl<T: RaftTransport> MetaGroup<T> {
3198 pub async fn create(
3203 config: MetaGroupConfig,
3204 group_config: GroupConfig,
3205 transport: Arc<T>,
3206 ) -> Result<Self, MetaError> {
3207 if group_config.node_id != raft_node_id(&config.node_id) {
3208 return Err(MetaError::InvalidRequest(format!(
3209 "group config raft id {} does not match the node id projection {}",
3210 group_config.node_id,
3211 raft_node_id(&config.node_id)
3212 )));
3213 }
3214 if group_config.dir != config.group_dir() {
3215 return Err(MetaError::InvalidRequest(format!(
3216 "group config dir {:?} is not the meta group dir {:?}",
3217 group_config.dir,
3218 config.group_dir()
3219 )));
3220 }
3221 let sink = Arc::new(Mutex::new(MetaApplySink::open(
3222 &config.group_dir(),
3223 config.registry.clone(),
3224 )?));
3225 let group = ConsensusGroup::create(
3226 group_config,
3227 transport,
3228 sink.clone() as Arc<Mutex<dyn ApplySink>>,
3229 )
3230 .await?;
3231 Ok(MetaGroup {
3232 group,
3233 sink,
3234 config,
3235 })
3236 }
3237
3238 pub fn group(&self) -> &ConsensusGroup<T> {
3241 &self.group
3242 }
3243
3244 pub fn node_id(&self) -> NodeId {
3246 self.config.node_id
3247 }
3248
3249 pub fn meta_group_id(&self) -> RaftGroupId {
3251 self.config.meta_group_id
3252 }
3253
3254 pub async fn bootstrap(&self, members: &[(NodeId, String)]) -> Result<(), MetaError> {
3261 let mut projected: BTreeMap<RaftNodeId, NodeId> = BTreeMap::new();
3262 let mut map = BTreeMap::new();
3263 for (node_id, address) in members {
3264 let raft_id = raft_node_id(node_id);
3265 if let Some(prior) = projected.insert(raft_id, *node_id) {
3266 if prior != *node_id {
3267 return Err(MetaError::InvalidRequest(format!(
3268 "node id projection collision: {prior} and {node_id} both project to \
3269 raft id {raft_id}; re-mint one of the node ids"
3270 )));
3271 }
3272 }
3273 map.insert(raft_id, basic_node(address)?);
3274 }
3275 self.group
3276 .bootstrap(map)
3277 .await
3278 .map_err(MetaError::Consensus)
3279 }
3280
3281 pub async fn is_initialized(&self) -> Result<bool, MetaError> {
3283 self.group
3284 .is_initialized()
3285 .await
3286 .map_err(MetaError::Consensus)
3287 }
3288
3289 pub async fn add_member(
3296 &self,
3297 descriptor: &NodeDescriptor,
3298 control: &ExecutionControl,
3299 ) -> Result<MetaCommandReceipt, MetaError> {
3300 let raft_id = raft_node_id(&descriptor.node_id);
3301 let (voters, learners) = self.group.members();
3302 if voters.contains(&raft_id) || learners.contains(&raft_id) {
3303 return Err(MetaError::InvalidRequest(format!(
3304 "node {} is already a meta group member",
3305 descriptor.node_id
3306 )));
3307 }
3308 self.group
3309 .add_learner(raft_id, basic_node(&descriptor.rpc_address)?)
3310 .await?;
3311 self.group.promote(raft_id).await?;
3312 self.propose(
3313 new_command_id()?,
3314 MetaCommand::RegisterNode {
3315 descriptor: descriptor.clone(),
3316 },
3317 control,
3318 )
3319 .await
3320 }
3321
3322 pub async fn remove_member(
3330 &self,
3331 node_id: NodeId,
3332 control: &ExecutionControl,
3333 ) -> Result<MetaCommandReceipt, MetaError> {
3334 let raft_id = raft_node_id(&node_id);
3335 let metrics = self.group.metrics();
3336 if metrics.current_leader == Some(raft_id) {
3337 return Err(MetaError::InvalidRequest(
3338 "transfer leadership off the node before removing it".to_owned(),
3339 ));
3340 }
3341 let receipt = self
3342 .propose(
3343 new_command_id()?,
3344 MetaCommand::RemoveNode { node_id },
3345 control,
3346 )
3347 .await?;
3348 self.group.remove(raft_id).await?;
3349 Ok(receipt)
3350 }
3351
3352 pub async fn propose(
3362 &self,
3363 command_id: [u8; 16],
3364 command: MetaCommand,
3365 control: &ExecutionControl,
3366 ) -> Result<MetaCommandReceipt, MetaError> {
3367 let payload = MetaCommandRecord::new(command).encode()?;
3368 let envelope = CommandEnvelope::new(COMMAND_TYPE_META_COMMAND, command_id, payload);
3369 let receipt = self
3370 .group
3371 .propose(CommandKind::Catalog, envelope, control)
3372 .await?;
3373 let (metadata_version, rejection) = {
3376 let sink = self
3377 .sink
3378 .lock()
3379 .map_err(|_| MetaError::InvalidRequest("meta sink lock poisoned".to_owned()))?;
3380 let rejection = sink
3381 .state()
3382 .rejections()
3383 .iter()
3384 .rev()
3385 .find(|entry| entry.command_id == Some(command_id))
3386 .map(|entry| entry.reason.clone());
3387 (sink.metadata_version(), rejection)
3388 };
3389 if let Some(reason) = rejection {
3390 return Err(MetaError::Rejected(reason));
3391 }
3392 Ok(MetaCommandReceipt {
3393 receipt,
3394 metadata_version,
3395 })
3396 }
3397
3398 pub fn state(&self) -> MetaState {
3402 self.sink
3403 .lock()
3404 .expect("meta sink lock poisoned")
3405 .state()
3406 .clone()
3407 }
3408
3409 pub async fn allocate_raft_node_ids(
3417 &self,
3418 count: u32,
3419 control: &ExecutionControl,
3420 ) -> Result<Vec<RaftNodeId>, MetaError> {
3421 let command_id = new_command_id()?;
3422 self.propose(
3423 command_id,
3424 MetaCommand::AllocateRaftNodeIds { count },
3425 control,
3426 )
3427 .await?;
3428 let state = self.state();
3429 let base = state.raft_id_allocation(&command_id).ok_or_else(|| {
3430 MetaError::InvalidRequest(
3431 "raft node id allocation record missing after commit".to_owned(),
3432 )
3433 })?;
3434 Ok((base..base + u64::from(count)).collect())
3435 }
3436
3437 pub fn metadata_version(&self) -> MetadataVersion {
3439 self.sink
3440 .lock()
3441 .expect("meta sink lock poisoned")
3442 .metadata_version()
3443 }
3444
3445 pub async fn shutdown(&self) -> Result<(), MetaError> {
3447 self.group.shutdown().await.map_err(MetaError::Consensus)
3448 }
3449
3450 pub async fn crash(self) {
3455 self.group.crash().await;
3456 }
3457}
3458
3459#[cfg(test)]
3460mod tests {
3461 use super::*;
3462 use crate::node::{BuildVersion, Locality, NodeCapacity, NodeState};
3463
3464 fn node_id(byte: u8) -> NodeId {
3465 NodeId::from_bytes([byte; 16])
3466 }
3467
3468 fn descriptor(byte: u8, features: &[&str]) -> NodeDescriptor {
3469 let mut version_info = VersionInfo::current();
3470 version_info.feature_set = features.iter().map(|feature| feature.to_string()).collect();
3471 NodeDescriptor {
3472 node_id: node_id(byte),
3473 rpc_address: format!("127.0.0.1:{}", 7000 + u16::from(byte)),
3474 locality: Locality::default(),
3475 capacity: NodeCapacity::default(),
3476 state: NodeState::Up,
3477 version: BuildVersion::current(),
3478 version_info,
3479 }
3480 }
3481
3482 fn registry_with(feature: &str, level: u64) -> FeatureRegistry {
3483 let mut registry = FeatureRegistry::current();
3484 registry.declare(feature, ClusterFeatureLevel(level));
3485 registry
3486 }
3487
3488 fn activation(feature: &str, level: u64) -> FeatureActivation {
3489 FeatureActivation {
3490 feature: feature.to_owned(),
3491 level: ClusterFeatureLevel(level),
3492 activated_at: HlcTimestamp::ZERO,
3493 activated_by: node_id(1),
3494 }
3495 }
3496
3497 #[test]
3498 fn feature_supported_only_at_or_above_registered_level() {
3499 let registry = registry_with("ann-v2", 7);
3500 assert!(!registry.feature_supported(ClusterFeatureLevel(6), "ann-v2"));
3501 assert!(registry.feature_supported(ClusterFeatureLevel(7), "ann-v2"));
3502 assert!(registry.feature_supported(ClusterFeatureLevel(8), "ann-v2"));
3503 assert!(!registry.feature_supported(ClusterFeatureLevel(u64::MAX), "nope"));
3505 }
3506
3507 #[test]
3508 fn activation_refused_until_every_voter_supports_the_feature() {
3509 let registry = registry_with("ann-v2", 7);
3510 let voters = vec![
3511 descriptor(1, &["ann-v2"]),
3512 descriptor(2, &[]),
3513 descriptor(3, &["ann-v2"]),
3514 ];
3515 let error = activation("ann-v2", 7)
3516 .validate(®istry, ClusterFeatureLevel::ZERO, &voters)
3517 .unwrap_err();
3518 assert_eq!(
3519 error,
3520 FeatureActivationError::UnsupportedByVoter {
3521 feature: "ann-v2".to_owned(),
3522 node: node_id(2),
3523 }
3524 );
3525 let voters = vec![
3527 descriptor(1, &["ann-v2"]),
3528 descriptor(2, &["ann-v2"]),
3529 descriptor(3, &["ann-v2"]),
3530 ];
3531 activation("ann-v2", 7)
3532 .validate(®istry, ClusterFeatureLevel::ZERO, &voters)
3533 .unwrap();
3534 }
3535
3536 #[test]
3537 fn activation_rejects_unknown_features() {
3538 let registry = FeatureRegistry::current();
3539 let voters = vec![descriptor(1, &["ann-v2"])];
3540 let error = activation("ann-v2", 7)
3541 .validate(®istry, ClusterFeatureLevel::ZERO, &voters)
3542 .unwrap_err();
3543 assert_eq!(
3544 error,
3545 FeatureActivationError::UnknownFeature {
3546 feature: "ann-v2".to_owned(),
3547 }
3548 );
3549 }
3550
3551 #[test]
3552 fn activation_rejects_a_level_below_the_registered_minimum() {
3553 let registry = registry_with("ann-v2", 7);
3554 let voters = vec![descriptor(1, &["ann-v2"])];
3555 let error = activation("ann-v2", 6)
3556 .validate(®istry, ClusterFeatureLevel::ZERO, &voters)
3557 .unwrap_err();
3558 assert_eq!(
3559 error,
3560 FeatureActivationError::LevelBelowRequirement {
3561 feature: "ann-v2".to_owned(),
3562 required: ClusterFeatureLevel(7),
3563 attempted: ClusterFeatureLevel(6),
3564 }
3565 );
3566 }
3567
3568 #[test]
3569 fn feature_level_never_regresses() {
3570 let registry = registry_with("ann-v2", 7);
3571 let voters = vec![descriptor(1, &["ann-v2"])];
3572 let error = activation("ann-v2", 7)
3573 .validate(®istry, ClusterFeatureLevel(9), &voters)
3574 .unwrap_err();
3575 assert_eq!(
3576 error,
3577 FeatureActivationError::LevelRegression {
3578 current: ClusterFeatureLevel(9),
3579 attempted: ClusterFeatureLevel(7),
3580 }
3581 );
3582 let registry = registry_with("ai-hybrid", 9);
3585 let voters = vec![descriptor(1, &["ai-hybrid"])];
3586 activation("ai-hybrid", 9)
3587 .validate(®istry, ClusterFeatureLevel(9), &voters)
3588 .unwrap();
3589 }
3590
3591 #[test]
3592 fn activation_requires_at_least_one_voter() {
3593 let registry = registry_with("ann-v2", 7);
3594 let error = activation("ann-v2", 7)
3595 .validate(®istry, ClusterFeatureLevel::ZERO, &[])
3596 .unwrap_err();
3597 assert_eq!(error, FeatureActivationError::NoVoters);
3598 }
3599
3600 #[test]
3601 fn activation_record_round_trips_serde() {
3602 let record = activation("ann-v2", 7);
3603 let json = serde_json::to_vec(&record).unwrap();
3604 let back: FeatureActivation = serde_json::from_slice(&json).unwrap();
3605 assert_eq!(back, record);
3606 }
3607
3608 #[test]
3609 fn upgrade_plan_upgrades_followers_first_and_the_leader_last() {
3610 let target = VersionInfo::current();
3611 let nodes = vec![descriptor(1, &[]), descriptor(2, &[]), descriptor(3, &[])];
3612 let plan = plan_rolling_upgrade(&nodes, node_id(1), &target).unwrap();
3613 assert_eq!(plan.target, target);
3614 assert_eq!(
3615 plan.steps,
3616 vec![
3617 UpgradeStep::UpgradeFollower {
3618 node_id: node_id(2)
3619 },
3620 UpgradeStep::UpgradeFollower {
3621 node_id: node_id(3)
3622 },
3623 UpgradeStep::TransferLeadership { from: node_id(1) },
3624 UpgradeStep::UpgradeFormerLeader {
3625 node_id: node_id(1)
3626 },
3627 UpgradeStep::EnableNewFeatures,
3628 ]
3629 );
3630 }
3631
3632 #[test]
3633 fn upgrade_plan_for_a_single_node_skips_leadership_transfer() {
3634 let target = VersionInfo::current();
3635 let nodes = vec![descriptor(1, &[])];
3636 let plan = plan_rolling_upgrade(&nodes, node_id(1), &target).unwrap();
3637 assert_eq!(
3638 plan.steps,
3639 vec![
3640 UpgradeStep::UpgradeFormerLeader {
3641 node_id: node_id(1)
3642 },
3643 UpgradeStep::EnableNewFeatures,
3644 ]
3645 );
3646 }
3647
3648 #[test]
3649 fn upgrade_plan_verifies_compatibility_first() {
3650 let mut target = VersionInfo::current();
3651 target.protocol_min = target.protocol_max + 1;
3652 let nodes = vec![descriptor(1, &[]), descriptor(2, &[])];
3653 let error = plan_rolling_upgrade(&nodes, node_id(1), &target).unwrap_err();
3654 assert!(matches!(
3655 error,
3656 UpgradePlanError::IncompatibleNode {
3657 node,
3658 incompatibility: Incompatibility::ProtocolVersion { .. },
3659 } if node == node_id(1)
3660 ));
3661 }
3662
3663 #[test]
3664 fn upgrade_plan_rejects_malformed_membership() {
3665 let target = VersionInfo::current();
3666 assert_eq!(
3667 plan_rolling_upgrade(&[], node_id(1), &target).unwrap_err(),
3668 UpgradePlanError::EmptyMembership,
3669 );
3670 let nodes = vec![descriptor(1, &[])];
3671 assert_eq!(
3672 plan_rolling_upgrade(&nodes, node_id(9), &target).unwrap_err(),
3673 UpgradePlanError::LeaderNotInMembership { leader: node_id(9) },
3674 );
3675 let nodes = vec![descriptor(1, &[]), descriptor(1, &[])];
3676 assert_eq!(
3677 plan_rolling_upgrade(&nodes, node_id(1), &target).unwrap_err(),
3678 UpgradePlanError::DuplicateNode { node: node_id(1) },
3679 );
3680 }
3681
3682 #[test]
3683 fn rollback_is_a_binary_downgrade_until_the_first_feature_activates() {
3684 let before_activation = assess_rollback(&[]);
3685 assert_eq!(before_activation.path, RollbackPath::BinaryDowngrade);
3686 assert!(before_activation.activated_features.is_empty());
3687
3688 let after_activation = assess_rollback(&[activation("ann-v2", 7)]);
3689 assert_eq!(after_activation.path, RollbackPath::RestoreFromBackup);
3690 assert_eq!(
3691 after_activation.activated_features,
3692 vec!["ann-v2".to_owned()]
3693 );
3694 }
3695}
3696
3697#[cfg(test)]
3702mod stage3a_tests {
3703 use super::*;
3704 use crate::node::{BuildVersion, Locality, NodeCapacity};
3705 use mongreldb_consensus::network::InMemoryTransport;
3706 use mongreldb_log::commit_log::LogPosition;
3707 use std::path::Path;
3708 use std::time::{Duration, Instant};
3709
3710 const LEADER_TIMEOUT: Duration = Duration::from_secs(10);
3711 const META_GID: RaftGroupId = RaftGroupId::from_bytes([0xAA; 16]);
3712
3713 fn node_id(byte: u8) -> NodeId {
3714 NodeId::from_bytes([byte; 16])
3715 }
3716
3717 fn raft_id(byte: u8) -> RaftNodeId {
3718 raft_node_id(&node_id(byte))
3719 }
3720
3721 fn group_id(byte: u8) -> RaftGroupId {
3722 RaftGroupId::from_bytes([byte; 16])
3723 }
3724
3725 fn ts(micros: u64) -> HlcTimestamp {
3726 HlcTimestamp {
3727 physical_micros: micros,
3728 logical: 0,
3729 node_tiebreaker: 0,
3730 }
3731 }
3732
3733 fn cmd_id(byte: u8) -> [u8; 16] {
3734 [byte; 16]
3735 }
3736
3737 fn descriptor(byte: u8, features: &[&str]) -> NodeDescriptor {
3738 let mut version_info = VersionInfo::current();
3739 version_info.feature_set = features.iter().map(|feature| feature.to_string()).collect();
3740 NodeDescriptor {
3741 node_id: node_id(byte),
3742 rpc_address: format!("127.0.0.1:{}", 7100 + u16::from(byte)),
3743 locality: Locality::default(),
3744 capacity: NodeCapacity::default(),
3745 state: NodeState::Up,
3746 version: BuildVersion::current(),
3747 version_info,
3748 }
3749 }
3750
3751 fn registry_with(feature: &str, level: u64) -> FeatureRegistry {
3752 let mut registry = FeatureRegistry::current();
3753 registry.declare(feature, ClusterFeatureLevel(level));
3754 registry
3755 }
3756
3757 fn database(byte: u8, name: &str) -> DatabaseDescriptor {
3758 DatabaseDescriptor {
3759 database_id: DatabaseId::from_bytes([byte; 16]),
3760 name: name.to_owned(),
3761 created_at: ts(1_000),
3762 state: DatabaseState::Online,
3763 metadata_version: MetadataVersion::ZERO,
3764 }
3765 }
3766
3767 fn schema_record(table: u64, database_byte: u8, version: u64) -> TableSchemaRecord {
3768 TableSchemaRecord {
3769 table_id: TableId(table),
3770 database_id: DatabaseId::from_bytes([database_byte; 16]),
3771 schema_version: SchemaVersion(version),
3772 schema: serde_json::json!({"columns": [{"name": "pk", "type": "u64"}]}),
3773 metadata_version: MetadataVersion::ZERO,
3774 }
3775 }
3776
3777 fn tablet(byte: u8, table: u64, generation: u64) -> TabletDescriptor {
3778 TabletDescriptor {
3779 tablet_id: TabletId::from_bytes([byte; 16]),
3780 table_id: TableId(table),
3781 database_id: mongreldb_types::ids::DatabaseId::ZERO,
3782 raft_group_id: group_id(9),
3783 partition: PartitionBounds {
3784 low: Bound::Unbounded,
3785 high: Bound::Excluded(Key::from_bytes(b"m".to_vec())),
3786 },
3787 replicas: vec![ReplicaDescriptor {
3788 node_id: node_id(1),
3789 role: ReplicaRole::Voter,
3790 raft_node_id: raft_id(1),
3791 }],
3792 leader_hint: None,
3793 generation,
3794 state: TabletState::Active,
3795 }
3796 }
3797
3798 fn placement(byte: u8, members: &[(u8, ReplicaRole)]) -> ReplicaPlacement {
3799 ReplicaPlacement {
3800 raft_group_id: group_id(byte),
3801 replicas: members
3802 .iter()
3803 .map(|(node, role)| ReplicaDescriptor {
3804 node_id: node_id(*node),
3805 role: *role,
3806 raft_node_id: raft_id(*node),
3807 })
3808 .collect(),
3809 metadata_version: MetadataVersion::ZERO,
3810 }
3811 }
3812
3813 fn policy(replicas: u8) -> PlacementPolicy {
3814 PlacementPolicy {
3815 replicas,
3816 voter_constraints: vec![LocalityConstraint {
3817 key: "region".to_owned(),
3818 value: "us-central".to_owned(),
3819 required: true,
3820 }],
3821 leader_preferences: Vec::new(),
3822 prohibited_nodes: Vec::new(),
3823 }
3824 }
3825
3826 fn schema_job(job_id: u64, table: u64, database_byte: u8) -> SchemaJobRecord {
3827 SchemaJobRecord {
3828 job_id,
3829 database_id: DatabaseId::from_bytes([database_byte; 16]),
3830 table_id: TableId(table),
3831 kind: SchemaJobKind::IndexBuild,
3832 state: SchemaJobState::Pending,
3833 submitted_at: ts(1_000),
3834 updated_at: ts(1_000),
3835 error: None,
3836 metadata_version: MetadataVersion::ZERO,
3837 }
3838 }
3839
3840 fn activation(feature: &str, level: u64) -> FeatureActivation {
3841 FeatureActivation {
3842 feature: feature.to_owned(),
3843 level: ClusterFeatureLevel(level),
3844 activated_at: HlcTimestamp::ZERO,
3845 activated_by: node_id(1),
3846 }
3847 }
3848
3849 fn apply(
3850 state: &mut MetaState,
3851 registry: &FeatureRegistry,
3852 id: u8,
3853 command: MetaCommand,
3854 ) -> Result<(), MetaRejectionReason> {
3855 state.apply(&command, Some(cmd_id(id)), ts(1_000), registry)
3856 }
3857
3858 fn fast_group_config(config: &MetaGroupConfig) -> GroupConfig {
3859 let mut group = config.group_config();
3860 group.heartbeat_interval = Duration::from_millis(50);
3861 group.election_timeout_min = Duration::from_millis(150);
3862 group.election_timeout_max = Duration::from_millis(300);
3863 group.install_snapshot_timeout = Duration::from_millis(1_000);
3864 group
3865 }
3866
3867 fn meta_config(dir: &Path, node: u8, registry: FeatureRegistry) -> MetaGroupConfig {
3868 let mut config = MetaGroupConfig::new(dir.to_path_buf(), META_GID, node_id(node));
3869 config.registry = registry;
3870 config
3871 }
3872
3873 async fn wait_consensus_leader(among: &[&MetaGroup<InMemoryTransport>]) -> RaftNodeId {
3878 let allowed: BTreeSet<RaftNodeId> = among
3879 .iter()
3880 .map(|group| raft_node_id(&group.node_id()))
3881 .collect();
3882 let deadline = Instant::now() + LEADER_TIMEOUT;
3883 loop {
3884 let mut leaders = BTreeSet::new();
3885 let mut seen = 0_usize;
3886 for group in among {
3887 if let Some(leader) = group.group().metrics().current_leader {
3888 leaders.insert(leader);
3889 seen += 1;
3890 }
3891 }
3892 if seen == among.len() && leaders.len() == 1 {
3893 let leader = *leaders.iter().next().expect("one leader");
3894 if allowed.contains(&leader) {
3895 return leader;
3896 }
3897 }
3898 assert!(
3899 Instant::now() < deadline,
3900 "no consensus leader (saw {leaders:?})"
3901 );
3902 tokio::time::sleep(Duration::from_millis(5)).await;
3903 }
3904 }
3905
3906 async fn propose_to_settled_leader(
3907 groups: &BTreeMap<u8, MetaGroup<InMemoryTransport>>,
3908 members: &[u8],
3909 command_id: [u8; 16],
3910 command: MetaCommand,
3911 control: &ExecutionControl,
3912 ) -> MetaCommandReceipt {
3913 let deadline = Instant::now() + LEADER_TIMEOUT;
3914 loop {
3915 let member_refs = members
3916 .iter()
3917 .map(|member| &groups[member])
3918 .collect::<Vec<_>>();
3919 let leader = wait_consensus_leader(&member_refs).await;
3920 let leader_byte = members
3921 .iter()
3922 .copied()
3923 .find(|member| raft_id(*member) == leader)
3924 .expect("settled leader belongs to the live member set");
3925 match groups[&leader_byte]
3926 .propose(command_id, command.clone(), control)
3927 .await
3928 {
3929 Ok(receipt) => return receipt,
3930 Err(MetaError::Consensus(ConsensusError::NotLeader { .. })) => {
3931 assert!(
3932 Instant::now() < deadline,
3933 "meta leader kept changing during proposal"
3934 );
3935 tokio::task::yield_now().await;
3936 }
3937 Err(error) => panic!("meta proposal failed: {error}"),
3938 }
3939 }
3940 }
3941
3942 #[test]
3945 fn meta_command_record_round_trips_every_variant() {
3946 let commands = vec![
3947 MetaCommand::RegisterNode {
3948 descriptor: descriptor(1, &["ann-v2"]),
3949 },
3950 MetaCommand::UpdateNodeState {
3951 node_id: node_id(1),
3952 state: NodeState::Draining,
3953 expected_version: Some(MetadataVersion(3)),
3954 },
3955 MetaCommand::RemoveNode {
3956 node_id: node_id(1),
3957 },
3958 MetaCommand::CreateDatabase {
3959 descriptor: database(1, "app"),
3960 },
3961 MetaCommand::DropDatabase {
3962 database_id: DatabaseId::from_bytes([1; 16]),
3963 },
3964 MetaCommand::SetTableSchema {
3965 record: schema_record(1, 1, 1),
3966 },
3967 MetaCommand::SetTabletDescriptor {
3968 descriptor: tablet(1, 1, 1),
3969 },
3970 MetaCommand::RemoveTabletDescriptor {
3971 tablet_id: TabletId::from_bytes([1; 16]),
3972 generation: 1,
3973 },
3974 MetaCommand::SetReplicaPlacement {
3975 placement: placement(9, &[(1, ReplicaRole::Voter)]),
3976 },
3977 MetaCommand::SetPlacementPolicy {
3978 name: "default".to_owned(),
3979 policy: policy(3),
3980 },
3981 MetaCommand::SubmitSchemaJob {
3982 job: schema_job(7, 1, 1),
3983 },
3984 MetaCommand::UpdateSchemaJob {
3985 job_id: 7,
3986 state: SchemaJobState::Running,
3987 updated_at: ts(2_000),
3988 error: None,
3989 expected_version: None,
3990 },
3991 MetaCommand::SetClusterSetting {
3992 key: "jobs.max_concurrent".to_owned(),
3993 value: serde_json::json!(4),
3994 },
3995 MetaCommand::ActivateFeature {
3996 activation: activation("ann-v2", 7),
3997 },
3998 MetaCommand::SetTxnStatusPartition {
3999 partition: TxnStatusPartition {
4000 partition_id: 0,
4001 home_raft_group: group_id(9),
4002 },
4003 },
4004 MetaCommand::PublishSplit {
4005 command: split_publish_command(),
4006 },
4007 MetaCommand::PublishMerge {
4008 command: merge_publish_command(),
4009 },
4010 MetaCommand::AllocateRaftNodeIds { count: 3 },
4011 ];
4012 for command in commands {
4013 let record = MetaCommandRecord::new(command);
4014 let bytes = record.encode().unwrap();
4015 assert_eq!(MetaCommandRecord::decode(&bytes).unwrap(), record);
4016 }
4017 assert!(matches!(
4019 MetaCommandRecord::decode(b"not json"),
4020 Err(MetaDecodeError::Malformed(_))
4021 ));
4022 let future = MetaCommandRecord {
4023 format_version: META_COMMAND_FORMAT_VERSION + 1,
4024 command: MetaCommand::RemoveNode {
4025 node_id: node_id(1),
4026 },
4027 };
4028 assert_eq!(
4029 MetaCommandRecord::decode(&future.encode().unwrap()).unwrap_err(),
4030 MetaDecodeError::UnsupportedVersion {
4031 found: META_COMMAND_FORMAT_VERSION + 1,
4032 min: MIN_SUPPORTED_META_COMMAND_FORMAT_VERSION,
4033 max: META_COMMAND_FORMAT_VERSION,
4034 }
4035 );
4036 }
4037
4038 fn every_command_sequence() -> Vec<MetaCommand> {
4041 vec![
4042 MetaCommand::RegisterNode {
4043 descriptor: descriptor(1, &["ann-v2"]),
4044 },
4045 MetaCommand::UpdateNodeState {
4046 node_id: node_id(1),
4047 state: NodeState::Draining,
4048 expected_version: None,
4049 },
4050 MetaCommand::CreateDatabase {
4051 descriptor: database(1, "app"),
4052 },
4053 MetaCommand::SetTableSchema {
4054 record: schema_record(1, 1, 1),
4055 },
4056 MetaCommand::SetTabletDescriptor {
4057 descriptor: tablet(1, 1, 1),
4058 },
4059 MetaCommand::RemoveTabletDescriptor {
4060 tablet_id: TabletId::from_bytes([1; 16]),
4061 generation: 1,
4062 },
4063 MetaCommand::SetReplicaPlacement {
4064 placement: placement(9, &[(1, ReplicaRole::Voter)]),
4065 },
4066 MetaCommand::SetPlacementPolicy {
4067 name: "default".to_owned(),
4068 policy: policy(3),
4069 },
4070 MetaCommand::SubmitSchemaJob {
4071 job: schema_job(7, 1, 1),
4072 },
4073 MetaCommand::UpdateSchemaJob {
4074 job_id: 7,
4075 state: SchemaJobState::Running,
4076 updated_at: ts(2_000),
4077 error: None,
4078 expected_version: None,
4079 },
4080 MetaCommand::SetClusterSetting {
4081 key: "jobs.max_concurrent".to_owned(),
4082 value: serde_json::json!(4),
4083 },
4084 MetaCommand::ActivateFeature {
4085 activation: activation("ann-v2", 7),
4086 },
4087 MetaCommand::SetTxnStatusPartition {
4088 partition: TxnStatusPartition {
4089 partition_id: 0,
4090 home_raft_group: group_id(9),
4091 },
4092 },
4093 MetaCommand::RemoveNode {
4094 node_id: node_id(99),
4095 },
4096 MetaCommand::DropDatabase {
4097 database_id: DatabaseId::from_bytes([0xEE; 16]),
4098 },
4099 MetaCommand::AllocateRaftNodeIds { count: 3 },
4100 ]
4101 }
4102
4103 #[test]
4104 fn apply_round_trips_every_command() {
4105 let registry = registry_with("ann-v2", 7);
4106 let mut state = MetaState::default();
4107 for (index, command) in every_command_sequence().into_iter().enumerate() {
4108 let id = u8::try_from(index + 1).unwrap();
4109 apply(&mut state, ®istry, id, command).unwrap();
4110 }
4111 assert_eq!(state.metadata_version, MetadataVersion(16));
4112 assert!(state.rejections().is_empty());
4113 assert_eq!(state.next_raft_node_id(), FIRST_RAFT_NODE_ID + 3);
4115
4116 let node = state.node_record(node_id(1)).unwrap();
4117 assert_eq!(node.descriptor.state, NodeState::Draining);
4118 assert_eq!(node.metadata_version, MetadataVersion(2));
4119 assert_eq!(state.database_by_name("app").unwrap().name, "app");
4120 assert_eq!(
4121 state.table(TableId(1)).unwrap().schema_version,
4122 SchemaVersion(1)
4123 );
4124 assert!(state.tablets.is_empty());
4125 assert_eq!(state.placement(group_id(9)).unwrap().replicas.len(), 1);
4126 assert_eq!(state.placement_policy("default").unwrap().replicas, 3);
4127 assert_eq!(state.schema_job(7).unwrap().state, SchemaJobState::Running);
4128 assert_eq!(state.settings().max_concurrent_jobs, 4);
4129 assert_eq!(state.feature_level(), ClusterFeatureLevel(7));
4130 assert!(state.feature_active(®istry, "ann-v2"));
4131 assert_eq!(state.feature_activations()[0].activated_at, ts(1_000));
4133 assert_eq!(
4134 state.txn_status_partition(0).unwrap().home_raft_group,
4135 group_id(9)
4136 );
4137 }
4138
4139 #[test]
4142 fn apply_is_idempotent_for_record_replays() {
4143 let registry = registry_with("ann-v2", 7);
4144 let mut state = MetaState::default();
4145 apply(
4146 &mut state,
4147 ®istry,
4148 1,
4149 MetaCommand::RegisterNode {
4150 descriptor: descriptor(1, &["ann-v2"]),
4151 },
4152 )
4153 .unwrap();
4154 apply(
4155 &mut state,
4156 ®istry,
4157 2,
4158 MetaCommand::RegisterNode {
4159 descriptor: descriptor(1, &["ann-v2"]),
4160 },
4161 )
4162 .unwrap();
4163 assert_eq!(state.nodes.len(), 1);
4164 assert_eq!(
4167 state.node_record(node_id(1)).unwrap().metadata_version,
4168 MetadataVersion(1)
4169 );
4170 assert_eq!(state.metadata_version, MetadataVersion(2));
4171
4172 apply(
4173 &mut state,
4174 ®istry,
4175 3,
4176 MetaCommand::ActivateFeature {
4177 activation: activation("ann-v2", 7),
4178 },
4179 )
4180 .unwrap();
4181 apply(
4182 &mut state,
4183 ®istry,
4184 4,
4185 MetaCommand::ActivateFeature {
4186 activation: activation("ann-v2", 7),
4187 },
4188 )
4189 .unwrap();
4190 assert_eq!(state.feature_activations().len(), 1);
4191 assert_eq!(state.feature_level(), ClusterFeatureLevel(7));
4192 }
4193
4194 #[test]
4195 fn apply_is_deterministic_across_states() {
4196 let registry = registry_with("ann-v2", 7);
4197 let commands = every_command_sequence();
4198 let mut a = MetaState::default();
4199 let mut b = MetaState::default();
4200 for (index, command) in commands.iter().enumerate() {
4201 let id = u8::try_from(index + 1).unwrap();
4202 a.apply(command, Some(cmd_id(id)), ts(1_000), ®istry)
4203 .unwrap();
4204 b.apply(command, Some(cmd_id(id)), ts(1_000), ®istry)
4205 .unwrap();
4206 }
4207 assert_eq!(a, b);
4208 assert_eq!(
4210 serde_json::to_vec(&a).unwrap(),
4211 serde_json::to_vec(&b).unwrap()
4212 );
4213 }
4214
4215 fn key(bytes: &[u8]) -> Key {
4218 Key::from_bytes(bytes.to_vec())
4219 }
4220
4221 fn voter_on(node: u8, raft: u64) -> ReplicaDescriptor {
4222 ReplicaDescriptor {
4223 node_id: node_id(node),
4224 role: ReplicaRole::Voter,
4225 raft_node_id: raft,
4226 }
4227 }
4228
4229 fn split_source() -> TabletDescriptor {
4231 TabletDescriptor {
4232 tablet_id: TabletId::from_bytes([0x51; 16]),
4233 table_id: TableId(1),
4234 database_id: mongreldb_types::ids::DatabaseId::ZERO,
4235 raft_group_id: group_id(0x51),
4236 partition: PartitionBounds::new(Bound::Included(key(b"a")), Bound::Excluded(key(b"z")))
4237 .unwrap(),
4238 replicas: vec![voter_on(1, 101), voter_on(2, 102)],
4239 leader_hint: None,
4240 generation: 5,
4241 state: TabletState::Active,
4242 }
4243 }
4244
4245 fn split_plan() -> crate::split::SplitPlan {
4246 crate::split::TabletSplitPlanner::new("/unused")
4247 .plan(
4248 &split_source(),
4249 crate::split::SplitKeySelection::Explicit(key(b"m")),
4250 ts(150),
4251 [
4252 crate::split::ChildAllocation {
4253 tablet_id: TabletId::from_bytes([0x52; 16]),
4254 raft_group_id: group_id(0x52),
4255 replicas: vec![voter_on(1, 201), voter_on(2, 202)],
4256 },
4257 crate::split::ChildAllocation {
4258 tablet_id: TabletId::from_bytes([0x53; 16]),
4259 raft_group_id: group_id(0x53),
4260 replicas: vec![voter_on(1, 301), voter_on(2, 302)],
4261 },
4262 ],
4263 )
4264 .unwrap()
4265 }
4266
4267 fn split_publish_command() -> SplitPublishCommand {
4268 SplitPublishCommand::from_plan(&split_plan()).unwrap()
4269 }
4270
4271 fn seed_split_publish(
4274 state: &mut MetaState,
4275 registry: &FeatureRegistry,
4276 ) -> crate::split::SplitPlan {
4277 apply(
4278 state,
4279 registry,
4280 1,
4281 MetaCommand::CreateDatabase {
4282 descriptor: database(1, "app"),
4283 },
4284 )
4285 .unwrap();
4286 apply(
4287 state,
4288 registry,
4289 2,
4290 MetaCommand::SetTableSchema {
4291 record: schema_record(1, 1, 1),
4292 },
4293 )
4294 .unwrap();
4295 let plan = split_plan();
4296 apply(
4297 state,
4298 registry,
4299 3,
4300 MetaCommand::SetTabletDescriptor {
4301 descriptor: plan.source.clone(),
4302 },
4303 )
4304 .unwrap();
4305 let marked = plan
4306 .source
4307 .published_transition(TabletState::Splitting)
4308 .unwrap();
4309 apply(
4310 state,
4311 registry,
4312 4,
4313 MetaCommand::SetTabletDescriptor { descriptor: marked },
4314 )
4315 .unwrap();
4316 for (index, child) in plan.child_descriptors().into_iter().enumerate() {
4317 apply(
4318 state,
4319 registry,
4320 5 + u8::try_from(index).unwrap(),
4321 MetaCommand::SetTabletDescriptor { descriptor: child },
4322 )
4323 .unwrap();
4324 }
4325 plan
4326 }
4327
4328 fn merge_sources() -> [TabletDescriptor; 2] {
4331 let source =
4332 |byte: u8, low: Bound<Key>, high: Bound<Key>, generation: u64, raft_base: u64| {
4333 TabletDescriptor {
4334 tablet_id: TabletId::from_bytes([byte; 16]),
4335 table_id: TableId(1),
4336 database_id: mongreldb_types::ids::DatabaseId::ZERO,
4337 raft_group_id: group_id(byte),
4338 partition: PartitionBounds::new(low, high).unwrap(),
4339 replicas: vec![voter_on(1, raft_base), voter_on(2, raft_base + 1)],
4340 leader_hint: None,
4341 generation,
4342 state: TabletState::Active,
4343 }
4344 };
4345 [
4346 source(
4347 0x61,
4348 Bound::Included(key(b"a")),
4349 Bound::Excluded(key(b"m")),
4350 4,
4351 101,
4352 ),
4353 source(
4354 0x62,
4355 Bound::Included(key(b"m")),
4356 Bound::Excluded(key(b"z")),
4357 6,
4358 201,
4359 ),
4360 ]
4361 }
4362
4363 fn merge_plan() -> crate::merge::MergePlan {
4364 let [first, second] = merge_sources();
4365 crate::merge::MergePlanner::new("/unused")
4366 .plan(
4367 crate::merge::MergeInputs {
4368 first,
4369 second,
4370 first_schema: SchemaVersion(1),
4371 second_schema: SchemaVersion(1),
4372 active_schema_job: None,
4373 first_size_bytes: 1_000,
4374 second_size_bytes: 2_000,
4375 max_merged_size_bytes: 1_000_000,
4376 },
4377 ts(150),
4378 crate::split::ChildAllocation {
4379 tablet_id: TabletId::from_bytes([0x63; 16]),
4380 raft_group_id: group_id(0x63),
4381 replicas: vec![voter_on(1, 301), voter_on(2, 302)],
4382 },
4383 )
4384 .unwrap()
4385 }
4386
4387 fn merge_publish_command() -> MergePublishCommand {
4388 MergePublishCommand::from_plan(&merge_plan()).unwrap()
4389 }
4390
4391 fn seed_merge_publish(
4394 state: &mut MetaState,
4395 registry: &FeatureRegistry,
4396 ) -> crate::merge::MergePlan {
4397 apply(
4398 state,
4399 registry,
4400 1,
4401 MetaCommand::CreateDatabase {
4402 descriptor: database(1, "app"),
4403 },
4404 )
4405 .unwrap();
4406 apply(
4407 state,
4408 registry,
4409 2,
4410 MetaCommand::SetTableSchema {
4411 record: schema_record(1, 1, 1),
4412 },
4413 )
4414 .unwrap();
4415 let plan = merge_plan();
4416 for (index, source) in plan.sources.iter().enumerate() {
4417 let id = 3 + 2 * u8::try_from(index).unwrap();
4418 apply(
4419 state,
4420 registry,
4421 id,
4422 MetaCommand::SetTabletDescriptor {
4423 descriptor: source.clone(),
4424 },
4425 )
4426 .unwrap();
4427 let marked = source.published_transition(TabletState::Merging).unwrap();
4428 apply(
4429 state,
4430 registry,
4431 id + 1,
4432 MetaCommand::SetTabletDescriptor { descriptor: marked },
4433 )
4434 .unwrap();
4435 }
4436 apply(
4437 state,
4438 registry,
4439 7,
4440 MetaCommand::SetTabletDescriptor {
4441 descriptor: plan.replacement_descriptor(),
4442 },
4443 )
4444 .unwrap();
4445 plan
4446 }
4447
4448 #[test]
4449 fn publish_split_applies_atomically_and_replays_idempotently() {
4450 let registry = registry_with("ann-v2", 7);
4451 let mut state = MetaState::default();
4452 let plan = seed_split_publish(&mut state, ®istry);
4453 let command = SplitPublishCommand::from_plan(&plan).unwrap();
4454 apply(
4455 &mut state,
4456 ®istry,
4457 10,
4458 MetaCommand::PublishSplit {
4459 command: command.clone(),
4460 },
4461 )
4462 .unwrap();
4463 let source = state.tablet(plan.source.tablet_id).unwrap().clone();
4466 assert_eq!(source.state, TabletState::Retiring);
4467 assert_eq!(source.generation, 7);
4468 for child in &command.children {
4469 let stored = state.tablet(child.tablet_id).unwrap();
4470 assert_eq!(stored, child);
4471 assert_eq!(stored.state, TabletState::Active);
4472 assert_eq!(stored.generation, 7);
4473 assert!(stored.replicas.iter().all(|r| r.role == ReplicaRole::Voter));
4474 }
4475 assert!(state.rejections().is_empty());
4476 let version = state.metadata_version;
4480 apply(
4481 &mut state,
4482 ®istry,
4483 11,
4484 MetaCommand::PublishSplit { command },
4485 )
4486 .unwrap();
4487 assert!(state.rejections().is_empty());
4488 assert_eq!(state.tablet(plan.source.tablet_id).unwrap(), &source);
4489 assert_eq!(
4490 state
4491 .tablet_record(plan.source.tablet_id)
4492 .unwrap()
4493 .metadata_version,
4494 version
4495 );
4496 }
4497
4498 #[test]
4499 fn publish_split_rejection_matrix() {
4500 let registry = registry_with("ann-v2", 7);
4501
4502 let mut state = MetaState::default();
4505 apply(
4506 &mut state,
4507 ®istry,
4508 1,
4509 MetaCommand::CreateDatabase {
4510 descriptor: database(1, "app"),
4511 },
4512 )
4513 .unwrap();
4514 apply(
4515 &mut state,
4516 ®istry,
4517 2,
4518 MetaCommand::SetTableSchema {
4519 record: schema_record(1, 1, 1),
4520 },
4521 )
4522 .unwrap();
4523 apply(
4524 &mut state,
4525 ®istry,
4526 3,
4527 MetaCommand::SetTabletDescriptor {
4528 descriptor: split_source(),
4529 },
4530 )
4531 .unwrap();
4532 let error = apply(
4533 &mut state,
4534 ®istry,
4535 21,
4536 MetaCommand::PublishSplit {
4537 command: split_publish_command(),
4538 },
4539 )
4540 .unwrap_err();
4541 assert!(matches!(error, MetaRejectionReason::Conflict { .. }));
4542 assert_eq!(state.rejections().len(), 1);
4543 assert_eq!(state.rejections()[0].command_id, Some(cmd_id(21)));
4544
4545 let mut state = MetaState::default();
4547 let plan = seed_split_publish(&mut state, ®istry);
4548 apply(
4549 &mut state,
4550 ®istry,
4551 20,
4552 MetaCommand::RemoveTabletDescriptor {
4553 tablet_id: plan.child_descriptors()[1].tablet_id,
4554 generation: 6,
4555 },
4556 )
4557 .unwrap();
4558 let error = apply(
4559 &mut state,
4560 ®istry,
4561 21,
4562 MetaCommand::PublishSplit {
4563 command: split_publish_command(),
4564 },
4565 )
4566 .unwrap_err();
4567 assert!(matches!(error, MetaRejectionReason::NotFound { .. }));
4568
4569 let mut state = MetaState::default();
4571 let plan = seed_split_publish(&mut state, ®istry);
4572 let mut rogue = plan.child_descriptors()[0].clone();
4573 rogue.state = TabletState::Active;
4574 rogue.generation = 7;
4575 for replica in &mut rogue.replicas {
4576 replica.role = ReplicaRole::Voter;
4577 }
4578 apply(
4579 &mut state,
4580 ®istry,
4581 20,
4582 MetaCommand::SetTabletDescriptor { descriptor: rogue },
4583 )
4584 .unwrap();
4585 let error = apply(
4586 &mut state,
4587 ®istry,
4588 21,
4589 MetaCommand::PublishSplit {
4590 command: split_publish_command(),
4591 },
4592 )
4593 .unwrap_err();
4594 assert!(matches!(error, MetaRejectionReason::Conflict { .. }));
4595
4596 let mut state = MetaState::default();
4598 seed_split_publish(&mut state, ®istry);
4599 let mut command = split_publish_command();
4600 command.source.generation += 1;
4601 for child in &mut command.children {
4602 child.generation += 1;
4603 }
4604 let error = apply(
4605 &mut state,
4606 ®istry,
4607 21,
4608 MetaCommand::PublishSplit { command },
4609 )
4610 .unwrap_err();
4611 assert!(matches!(error, MetaRejectionReason::Conflict { .. }));
4612
4613 let mut state = MetaState::default();
4615 seed_split_publish(&mut state, ®istry);
4616 let mut command = split_publish_command();
4617 command.split_key = key(b"n");
4618 let error = apply(
4619 &mut state,
4620 ®istry,
4621 21,
4622 MetaCommand::PublishSplit { command },
4623 )
4624 .unwrap_err();
4625 assert!(matches!(error, MetaRejectionReason::Invalid { .. }));
4626
4627 let mut state = MetaState::default();
4629 seed_split_publish(&mut state, ®istry);
4630 let overlapping = TabletDescriptor {
4631 tablet_id: TabletId::from_bytes([0x70; 16]),
4632 table_id: TableId(1),
4633 database_id: mongreldb_types::ids::DatabaseId::ZERO,
4634 raft_group_id: group_id(0x70),
4635 partition: PartitionBounds::new(Bound::Included(key(b"a")), Bound::Excluded(key(b"b")))
4636 .unwrap(),
4637 replicas: vec![voter_on(1, 901)],
4638 leader_hint: None,
4639 generation: 1,
4640 state: TabletState::Active,
4641 };
4642 apply(
4643 &mut state,
4644 ®istry,
4645 20,
4646 MetaCommand::SetTabletDescriptor {
4647 descriptor: overlapping,
4648 },
4649 )
4650 .unwrap();
4651 let error = apply(
4652 &mut state,
4653 ®istry,
4654 21,
4655 MetaCommand::PublishSplit {
4656 command: split_publish_command(),
4657 },
4658 )
4659 .unwrap_err();
4660 assert!(matches!(error, MetaRejectionReason::Conflict { .. }));
4661
4662 let mut state = MetaState::default();
4664 let error = apply(
4665 &mut state,
4666 ®istry,
4667 21,
4668 MetaCommand::PublishSplit {
4669 command: split_publish_command(),
4670 },
4671 )
4672 .unwrap_err();
4673 assert!(matches!(error, MetaRejectionReason::NotFound { .. }));
4674 }
4675
4676 #[test]
4677 fn publish_merge_applies_atomically_with_lagging_generations_and_replays() {
4678 let registry = registry_with("ann-v2", 7);
4679 let mut state = MetaState::default();
4680 let plan = seed_merge_publish(&mut state, ®istry);
4681 let command = MergePublishCommand::from_plan(&plan).unwrap();
4682 apply(
4683 &mut state,
4684 ®istry,
4685 10,
4686 MetaCommand::PublishMerge {
4687 command: command.clone(),
4688 },
4689 )
4690 .unwrap();
4691 let replacement = state.tablet(command.replacement.tablet_id).unwrap();
4694 assert_eq!(replacement.state, TabletState::Active);
4695 assert_eq!(replacement.generation, 8);
4696 for source in &command.sources {
4697 let stored = state.tablet(source.tablet_id).unwrap();
4698 assert_eq!(stored.state, TabletState::Retiring);
4699 assert_eq!(stored.generation, 8);
4700 }
4701 assert!(state.rejections().is_empty());
4702 apply(
4704 &mut state,
4705 ®istry,
4706 11,
4707 MetaCommand::PublishMerge { command },
4708 )
4709 .unwrap();
4710 assert!(state.rejections().is_empty());
4711 }
4712
4713 #[test]
4714 fn publish_merge_rejection_matrix() {
4715 let registry = registry_with("ann-v2", 7);
4716
4717 let mut state = MetaState::default();
4720 let plan = seed_merge_publish(&mut state, ®istry);
4721 let mut active = plan.sources[0].clone();
4722 active.generation = 6;
4723 apply(
4724 &mut state,
4725 ®istry,
4726 20,
4727 MetaCommand::SetTabletDescriptor { descriptor: active },
4728 )
4729 .unwrap();
4730 let error = apply(
4731 &mut state,
4732 ®istry,
4733 21,
4734 MetaCommand::PublishMerge {
4735 command: merge_publish_command(),
4736 },
4737 )
4738 .unwrap_err();
4739 assert!(matches!(error, MetaRejectionReason::Conflict { .. }));
4740
4741 let mut state = MetaState::default();
4743 let plan = seed_merge_publish(&mut state, ®istry);
4744 apply(
4745 &mut state,
4746 ®istry,
4747 20,
4748 MetaCommand::RemoveTabletDescriptor {
4749 tablet_id: plan.replacement_descriptor().tablet_id,
4750 generation: 7,
4751 },
4752 )
4753 .unwrap();
4754 let error = apply(
4755 &mut state,
4756 ®istry,
4757 21,
4758 MetaCommand::PublishMerge {
4759 command: merge_publish_command(),
4760 },
4761 )
4762 .unwrap_err();
4763 assert!(matches!(error, MetaRejectionReason::NotFound { .. }));
4764
4765 let mut state = MetaState::default();
4767 let plan = seed_merge_publish(&mut state, ®istry);
4768 let mut rogue = plan.replacement_descriptor();
4769 rogue.state = TabletState::Active;
4770 rogue.generation = 8;
4771 for replica in &mut rogue.replicas {
4772 replica.role = ReplicaRole::Voter;
4773 }
4774 apply(
4775 &mut state,
4776 ®istry,
4777 20,
4778 MetaCommand::SetTabletDescriptor { descriptor: rogue },
4779 )
4780 .unwrap();
4781 let error = apply(
4782 &mut state,
4783 ®istry,
4784 21,
4785 MetaCommand::PublishMerge {
4786 command: merge_publish_command(),
4787 },
4788 )
4789 .unwrap_err();
4790 assert!(matches!(error, MetaRejectionReason::Conflict { .. }));
4791
4792 let mut state = MetaState::default();
4795 let plan = seed_merge_publish(&mut state, ®istry);
4796 let mut rogue = plan.replacement_descriptor();
4797 rogue.generation -= 1;
4798 apply(
4799 &mut state,
4800 ®istry,
4801 19,
4802 MetaCommand::RemoveTabletDescriptor {
4803 tablet_id: rogue.tablet_id,
4804 generation: 7,
4805 },
4806 )
4807 .unwrap();
4808 apply(
4809 &mut state,
4810 ®istry,
4811 20,
4812 MetaCommand::SetTabletDescriptor { descriptor: rogue },
4813 )
4814 .unwrap();
4815 let error = apply(
4816 &mut state,
4817 ®istry,
4818 21,
4819 MetaCommand::PublishMerge {
4820 command: merge_publish_command(),
4821 },
4822 )
4823 .unwrap_err();
4824 assert!(matches!(error, MetaRejectionReason::Conflict { .. }));
4825
4826 let mut state = MetaState::default();
4828 seed_merge_publish(&mut state, ®istry);
4829 let mut command = merge_publish_command();
4830 command.replacement.partition = command.sources[0].partition.clone();
4831 let error = apply(
4832 &mut state,
4833 ®istry,
4834 21,
4835 MetaCommand::PublishMerge { command },
4836 )
4837 .unwrap_err();
4838 assert!(matches!(error, MetaRejectionReason::Invalid { .. }));
4839
4840 let mut state = MetaState::default();
4842 seed_merge_publish(&mut state, ®istry);
4843 let overlapping = TabletDescriptor {
4844 tablet_id: TabletId::from_bytes([0x70; 16]),
4845 table_id: TableId(1),
4846 database_id: mongreldb_types::ids::DatabaseId::ZERO,
4847 raft_group_id: group_id(0x70),
4848 partition: PartitionBounds::new(Bound::Included(key(b"b")), Bound::Excluded(key(b"c")))
4849 .unwrap(),
4850 replicas: vec![voter_on(1, 901)],
4851 leader_hint: None,
4852 generation: 1,
4853 state: TabletState::Active,
4854 };
4855 apply(
4856 &mut state,
4857 ®istry,
4858 20,
4859 MetaCommand::SetTabletDescriptor {
4860 descriptor: overlapping,
4861 },
4862 )
4863 .unwrap();
4864 let error = apply(
4865 &mut state,
4866 ®istry,
4867 21,
4868 MetaCommand::PublishMerge {
4869 command: merge_publish_command(),
4870 },
4871 )
4872 .unwrap_err();
4873 assert!(matches!(error, MetaRejectionReason::Conflict { .. }));
4874 }
4875
4876 #[test]
4879 fn allocate_raft_node_ids_is_monotonic_unique_and_replay_safe() {
4880 let registry = registry_with("ann-v2", 7);
4881 let mut state = MetaState::default();
4882 apply(
4883 &mut state,
4884 ®istry,
4885 1,
4886 MetaCommand::AllocateRaftNodeIds { count: 3 },
4887 )
4888 .unwrap();
4889 assert_eq!(
4890 state.raft_id_allocation(&cmd_id(1)),
4891 Some(FIRST_RAFT_NODE_ID)
4892 );
4893 assert_eq!(state.next_raft_node_id(), FIRST_RAFT_NODE_ID + 3);
4894 apply(
4895 &mut state,
4896 ®istry,
4897 2,
4898 MetaCommand::AllocateRaftNodeIds { count: 2 },
4899 )
4900 .unwrap();
4901 assert_eq!(
4903 state.raft_id_allocation(&cmd_id(2)),
4904 Some(FIRST_RAFT_NODE_ID + 3)
4905 );
4906 assert_eq!(state.next_raft_node_id(), FIRST_RAFT_NODE_ID + 5);
4907 apply(
4909 &mut state,
4910 ®istry,
4911 1,
4912 MetaCommand::AllocateRaftNodeIds { count: 3 },
4913 )
4914 .unwrap();
4915 assert_eq!(state.next_raft_node_id(), FIRST_RAFT_NODE_ID + 5);
4916 assert_eq!(state.raft_id_allocations.len(), 2);
4917 let error = apply(
4919 &mut state,
4920 ®istry,
4921 3,
4922 MetaCommand::AllocateRaftNodeIds { count: 0 },
4923 )
4924 .unwrap_err();
4925 assert!(matches!(error, MetaRejectionReason::Invalid { .. }));
4926 let error = apply(
4927 &mut state,
4928 ®istry,
4929 4,
4930 MetaCommand::AllocateRaftNodeIds {
4931 count: MAX_RAFT_NODE_ID_ALLOCATION + 1,
4932 },
4933 )
4934 .unwrap_err();
4935 assert!(matches!(error, MetaRejectionReason::Invalid { .. }));
4936 assert_eq!(state.rejections().len(), 2);
4937 assert_eq!(state.next_raft_node_id(), FIRST_RAFT_NODE_ID + 5);
4938 }
4939
4940 #[test]
4941 fn allocator_skips_ids_in_use_and_survives_restarts() {
4942 let registry = registry_with("ann-v2", 7);
4943 let mut state = MetaState::default();
4944 apply(
4945 &mut state,
4946 ®istry,
4947 1,
4948 MetaCommand::CreateDatabase {
4949 descriptor: database(1, "app"),
4950 },
4951 )
4952 .unwrap();
4953 apply(
4954 &mut state,
4955 ®istry,
4956 2,
4957 MetaCommand::SetTableSchema {
4958 record: schema_record(1, 1, 1),
4959 },
4960 )
4961 .unwrap();
4962 for (id, node) in [(3, 1), (4, 2)] {
4965 apply(
4966 &mut state,
4967 ®istry,
4968 id,
4969 MetaCommand::RegisterNode {
4970 descriptor: descriptor(node, &[]),
4971 },
4972 )
4973 .unwrap();
4974 }
4975 let mut tablet = tablet(1, 1, 1);
4977 tablet.replicas = vec![voter_on(1, 1), voter_on(2, 3)];
4978 apply(
4979 &mut state,
4980 ®istry,
4981 5,
4982 MetaCommand::SetTabletDescriptor { descriptor: tablet },
4983 )
4984 .unwrap();
4985 apply(
4986 &mut state,
4987 ®istry,
4988 6,
4989 MetaCommand::SetReplicaPlacement {
4990 placement: ReplicaPlacement {
4991 raft_group_id: group_id(8),
4992 replicas: vec![voter_on(1, 5)],
4993 metadata_version: MetadataVersion::ZERO,
4994 },
4995 },
4996 )
4997 .unwrap();
4998 apply(
5001 &mut state,
5002 ®istry,
5003 7,
5004 MetaCommand::AllocateRaftNodeIds { count: 3 },
5005 )
5006 .unwrap();
5007 assert_eq!(state.raft_id_allocation(&cmd_id(7)), Some(6));
5008 assert_eq!(state.next_raft_node_id(), 9);
5009 apply(
5012 &mut state,
5013 ®istry,
5014 8,
5015 MetaCommand::RegisterNode {
5016 descriptor: descriptor(9, &[]),
5017 },
5018 )
5019 .unwrap();
5020 state.next_raft_node_id = raft_node_id(&node_id(9));
5021 apply(
5022 &mut state,
5023 ®istry,
5024 9,
5025 MetaCommand::AllocateRaftNodeIds { count: 2 },
5026 )
5027 .unwrap();
5028 assert_eq!(
5029 state.raft_id_allocation(&cmd_id(9)),
5030 Some(raft_node_id(&node_id(9)) + 1)
5031 );
5032
5033 let tmp = tempfile::tempdir().unwrap();
5036 let mut sink = MetaApplySink::open(tmp.path(), registry.clone()).unwrap();
5037 ApplySink::apply(
5038 &mut sink,
5039 &applied(
5040 1,
5041 meta_envelope(1, MetaCommand::AllocateRaftNodeIds { count: 4 }),
5042 ),
5043 )
5044 .unwrap();
5045 drop(sink);
5046 let mut reopened = MetaApplySink::open(tmp.path(), registry).unwrap();
5047 assert_eq!(reopened.state().next_raft_node_id(), FIRST_RAFT_NODE_ID + 4);
5048 ApplySink::apply(
5051 &mut reopened,
5052 &applied(
5053 2,
5054 meta_envelope(1, MetaCommand::AllocateRaftNodeIds { count: 4 }),
5055 ),
5056 )
5057 .unwrap();
5058 assert_eq!(reopened.state().next_raft_node_id(), FIRST_RAFT_NODE_ID + 4);
5059 ApplySink::apply(
5060 &mut reopened,
5061 &applied(
5062 3,
5063 meta_envelope(2, MetaCommand::AllocateRaftNodeIds { count: 1 }),
5064 ),
5065 )
5066 .unwrap();
5067 assert_eq!(
5068 reopened.state().raft_id_allocation(&cmd_id(2)),
5069 Some(FIRST_RAFT_NODE_ID + 4)
5070 );
5071 }
5072
5073 #[test]
5074 fn register_node_rejects_a_projection_collision_with_replica_raft_ids() {
5075 let registry = registry_with("ann-v2", 7);
5076 let mut state = MetaState::default();
5077 apply(
5078 &mut state,
5079 ®istry,
5080 1,
5081 MetaCommand::CreateDatabase {
5082 descriptor: database(1, "app"),
5083 },
5084 )
5085 .unwrap();
5086 apply(
5087 &mut state,
5088 ®istry,
5089 2,
5090 MetaCommand::SetTableSchema {
5091 record: schema_record(1, 1, 1),
5092 },
5093 )
5094 .unwrap();
5095 let mut tablet = tablet(1, 1, 1);
5096 tablet.replicas = vec![voter_on(1, 4242)];
5097 apply(
5098 &mut state,
5099 ®istry,
5100 3,
5101 MetaCommand::SetTabletDescriptor { descriptor: tablet },
5102 )
5103 .unwrap();
5104 let mut colliding = descriptor(7, &[]);
5109 let mut bytes = [0xAB; 16];
5110 bytes[..8].copy_from_slice(&4242_u64.to_le_bytes());
5111 colliding.node_id = NodeId::from_bytes(bytes);
5112 let error = apply(
5113 &mut state,
5114 ®istry,
5115 4,
5116 MetaCommand::RegisterNode {
5117 descriptor: colliding.clone(),
5118 },
5119 )
5120 .unwrap_err();
5121 assert!(matches!(error, MetaRejectionReason::Conflict { .. }));
5122 assert_eq!(state.rejections().len(), 1);
5123 apply(
5126 &mut state,
5127 ®istry,
5128 5,
5129 MetaCommand::RegisterNode {
5130 descriptor: descriptor(8, &[]),
5131 },
5132 )
5133 .unwrap();
5134 }
5135
5136 #[test]
5137 fn set_tablet_descriptor_rejects_structurally_invalid_descriptors() {
5138 let registry = registry_with("ann-v2", 7);
5139 let mut state = MetaState::default();
5140 apply(
5141 &mut state,
5142 ®istry,
5143 1,
5144 MetaCommand::CreateDatabase {
5145 descriptor: database(1, "app"),
5146 },
5147 )
5148 .unwrap();
5149 apply(
5150 &mut state,
5151 ®istry,
5152 2,
5153 MetaCommand::SetTableSchema {
5154 record: schema_record(1, 1, 1),
5155 },
5156 )
5157 .unwrap();
5158 let mut tablet = tablet(1, 1, 1);
5160 tablet.replicas = vec![voter_on(1, 7), voter_on(2, 7)];
5161 let error = apply(
5162 &mut state,
5163 ®istry,
5164 3,
5165 MetaCommand::SetTabletDescriptor { descriptor: tablet },
5166 )
5167 .unwrap_err();
5168 assert!(matches!(error, MetaRejectionReason::Invalid { .. }));
5169 assert_eq!(state.rejections().len(), 1);
5170 }
5171
5172 #[test]
5175 fn activation_refused_at_apply_until_every_voter_supports_it() {
5176 let registry = registry_with("ann-v2", 7);
5177 let mut state = MetaState::default();
5178 for (id, byte, features) in [
5179 (1_u8, 1_u8, vec!["ann-v2"]),
5180 (2, 2, vec![]),
5181 (3, 3, vec!["ann-v2"]),
5182 ] {
5183 apply(
5184 &mut state,
5185 ®istry,
5186 id,
5187 MetaCommand::RegisterNode {
5188 descriptor: descriptor(byte, &features),
5189 },
5190 )
5191 .unwrap();
5192 }
5193 let error = apply(
5194 &mut state,
5195 ®istry,
5196 4,
5197 MetaCommand::ActivateFeature {
5198 activation: activation("ann-v2", 7),
5199 },
5200 )
5201 .unwrap_err();
5202 assert_eq!(
5203 error,
5204 MetaRejectionReason::FeatureActivation(FeatureActivationError::UnsupportedByVoter {
5205 feature: "ann-v2".to_owned(),
5206 node: node_id(2),
5207 })
5208 );
5209 assert_eq!(state.feature_level(), ClusterFeatureLevel::ZERO);
5211 assert_eq!(state.rejections().len(), 1);
5212 assert_eq!(state.rejections()[0].command_id, Some(cmd_id(4)));
5213 assert_eq!(state.rejections()[0].reason, error);
5214
5215 apply(
5217 &mut state,
5218 ®istry,
5219 5,
5220 MetaCommand::RegisterNode {
5221 descriptor: descriptor(2, &["ann-v2"]),
5222 },
5223 )
5224 .unwrap();
5225 apply(
5226 &mut state,
5227 ®istry,
5228 6,
5229 MetaCommand::ActivateFeature {
5230 activation: activation("ann-v2", 7),
5231 },
5232 )
5233 .unwrap();
5234 assert_eq!(state.feature_level(), ClusterFeatureLevel(7));
5235 }
5236
5237 #[test]
5238 fn activation_apply_rechecks_registry_level_and_voters() {
5239 let mut registry = registry_with("ann-v2", 7);
5240 registry.declare("ai-hybrid", ClusterFeatureLevel(5));
5241 let mut state = MetaState::default();
5242 let error = apply(
5244 &mut state,
5245 ®istry,
5246 1,
5247 MetaCommand::ActivateFeature {
5248 activation: activation("ann-v2", 7),
5249 },
5250 )
5251 .unwrap_err();
5252 assert_eq!(
5253 error,
5254 MetaRejectionReason::FeatureActivation(FeatureActivationError::NoVoters)
5255 );
5256 apply(
5257 &mut state,
5258 ®istry,
5259 2,
5260 MetaCommand::RegisterNode {
5261 descriptor: descriptor(1, &["ann-v2", "ai-hybrid"]),
5262 },
5263 )
5264 .unwrap();
5265 let error = apply(
5267 &mut state,
5268 ®istry,
5269 3,
5270 MetaCommand::ActivateFeature {
5271 activation: activation("ann-v2", 6),
5272 },
5273 )
5274 .unwrap_err();
5275 assert!(matches!(
5276 error,
5277 MetaRejectionReason::FeatureActivation(
5278 FeatureActivationError::LevelBelowRequirement { .. }
5279 )
5280 ));
5281 let error = apply(
5283 &mut state,
5284 ®istry,
5285 4,
5286 MetaCommand::ActivateFeature {
5287 activation: activation("nope", 1),
5288 },
5289 )
5290 .unwrap_err();
5291 assert!(matches!(
5292 error,
5293 MetaRejectionReason::FeatureActivation(FeatureActivationError::UnknownFeature { .. })
5294 ));
5295 apply(
5297 &mut state,
5298 ®istry,
5299 5,
5300 MetaCommand::ActivateFeature {
5301 activation: activation("ann-v2", 7),
5302 },
5303 )
5304 .unwrap();
5305 let error = apply(
5306 &mut state,
5307 ®istry,
5308 6,
5309 MetaCommand::ActivateFeature {
5310 activation: activation("ai-hybrid", 5),
5311 },
5312 )
5313 .unwrap_err();
5314 assert!(matches!(
5315 error,
5316 MetaRejectionReason::FeatureActivation(FeatureActivationError::LevelRegression { .. })
5317 ));
5318 assert_eq!(state.rejections().len(), 4);
5319 }
5320
5321 #[test]
5324 fn settings_denylist_rejects_plaintext_secrets() {
5325 let registry = FeatureRegistry::current();
5326 let mut state = MetaState::default();
5327 for (id, key) in [
5328 (1_u8, "backup.private_key_pem"),
5329 (2, "ai.api_key"),
5330 (3, "admin.password"),
5331 (4, "tls.secret"),
5332 (5, "auth.token_endpoint"),
5333 (6, "service.credential"),
5334 (7, "secret.unknown"),
5336 ] {
5337 let error = apply(
5338 &mut state,
5339 ®istry,
5340 id,
5341 MetaCommand::SetClusterSetting {
5342 key: key.to_owned(),
5343 value: serde_json::json!("x"),
5344 },
5345 )
5346 .unwrap_err();
5347 assert_eq!(
5348 error,
5349 MetaRejectionReason::SecretSettingKey {
5350 key: key.to_owned()
5351 }
5352 );
5353 }
5354 assert!(state.rejections().len() == 7);
5355 assert_eq!(state.settings(), &ClusterSettings::default());
5357 }
5358
5359 #[test]
5360 fn settings_unknown_keys_and_bad_values_are_refused() {
5361 let registry = FeatureRegistry::current();
5362 let mut state = MetaState::default();
5363 let error = apply(
5364 &mut state,
5365 ®istry,
5366 1,
5367 MetaCommand::SetClusterSetting {
5368 key: "no.such.key".to_owned(),
5369 value: serde_json::json!(1),
5370 },
5371 )
5372 .unwrap_err();
5373 assert_eq!(
5374 error,
5375 MetaRejectionReason::UnknownSettingKey {
5376 key: "no.such.key".to_owned()
5377 }
5378 );
5379 for (id, key, value) in [
5380 (2_u8, "jobs.max_concurrent", serde_json::json!("four")),
5381 (3, "jobs.max_concurrent", serde_json::json!(0)),
5382 (4, "backup.enabled", serde_json::json!(1)),
5383 (
5384 5,
5385 "default_consistency",
5386 serde_json::json!("EventuallyConsistent"),
5387 ),
5388 ] {
5389 let error = apply(
5390 &mut state,
5391 ®istry,
5392 id,
5393 MetaCommand::SetClusterSetting {
5394 key: key.to_owned(),
5395 value,
5396 },
5397 )
5398 .unwrap_err();
5399 assert!(matches!(
5400 error,
5401 MetaRejectionReason::InvalidSettingValue { .. }
5402 ));
5403 }
5404 }
5405
5406 #[test]
5407 fn settings_apply_typed_values() {
5408 let registry = FeatureRegistry::current();
5409 let mut state = MetaState::default();
5410 for (id, key, value) in [
5411 (1_u8, "history_retention_epochs", serde_json::json!(12)),
5412 (2, "backup.enabled", serde_json::json!(true)),
5413 (3, "backup.interval_seconds", serde_json::json!(3_600)),
5414 (4, "backup.retention_count", serde_json::json!(3)),
5415 (
5416 5,
5417 "default_consistency",
5418 serde_json::json!({"BoundedStaleness": {"max_lag_ms": 250}}),
5419 ),
5420 (6, "ai.max_concurrent_requests", serde_json::json!(8)),
5421 (7, "ai.max_memory_bytes", serde_json::json!(1 << 20)),
5422 (8, "jobs.max_concurrent", serde_json::json!(4)),
5423 (
5424 9,
5425 "resource_groups.etl",
5426 serde_json::json!({"max_memory_bytes": 1024, "max_concurrent_queries": 2, "temp_disk_budget_bytes": 4096}),
5427 ),
5428 ] {
5429 apply(
5430 &mut state,
5431 ®istry,
5432 id,
5433 MetaCommand::SetClusterSetting {
5434 key: key.to_owned(),
5435 value,
5436 },
5437 )
5438 .unwrap();
5439 }
5440 let settings = state.settings();
5441 assert_eq!(settings.history_retention_epochs, 12);
5442 assert!(settings.backup.enabled);
5443 assert_eq!(settings.backup.interval_seconds, 3_600);
5444 assert_eq!(settings.backup.retention_count, 3);
5445 assert_eq!(
5446 settings.default_consistency,
5447 DefaultConsistency::BoundedStaleness { max_lag_ms: 250 }
5448 );
5449 assert_eq!(settings.ai.max_concurrent_requests, 8);
5450 assert_eq!(settings.ai.max_memory_bytes, 1 << 20);
5451 assert_eq!(settings.max_concurrent_jobs, 4);
5452 assert_eq!(settings.resource_groups["etl"].max_memory_bytes, 1_024);
5453 apply(
5455 &mut state,
5456 ®istry,
5457 10,
5458 MetaCommand::SetClusterSetting {
5459 key: "resource_groups.etl".to_owned(),
5460 value: serde_json::Value::Null,
5461 },
5462 )
5463 .unwrap();
5464 assert!(state.settings().resource_groups.is_empty());
5465 }
5466
5467 #[test]
5470 fn metadata_version_ticks_once_per_applied_command() {
5471 let registry = FeatureRegistry::current();
5472 let mut state = MetaState::default();
5473 assert_eq!(state.metadata_version, MetadataVersion::ZERO);
5474 apply(
5475 &mut state,
5476 ®istry,
5477 1,
5478 MetaCommand::RegisterNode {
5479 descriptor: descriptor(1, &[]),
5480 },
5481 )
5482 .unwrap();
5483 assert_eq!(state.metadata_version, MetadataVersion(1));
5484 apply(
5486 &mut state,
5487 ®istry,
5488 2,
5489 MetaCommand::UpdateNodeState {
5490 node_id: node_id(42),
5491 state: NodeState::Down,
5492 expected_version: None,
5493 },
5494 )
5495 .unwrap_err();
5496 assert_eq!(state.metadata_version, MetadataVersion(2));
5497 apply(
5498 &mut state,
5499 ®istry,
5500 3,
5501 MetaCommand::RemoveNode {
5502 node_id: node_id(1),
5503 },
5504 )
5505 .unwrap();
5506 assert_eq!(state.metadata_version, MetadataVersion(3));
5507 }
5508
5509 #[test]
5510 fn stale_and_conflicting_writes_are_refused() {
5511 let registry = FeatureRegistry::current();
5512 let mut state = MetaState::default();
5513 apply(
5514 &mut state,
5515 ®istry,
5516 1,
5517 MetaCommand::RegisterNode {
5518 descriptor: descriptor(1, &[]),
5519 },
5520 )
5521 .unwrap();
5522 apply(
5523 &mut state,
5524 ®istry,
5525 2,
5526 MetaCommand::CreateDatabase {
5527 descriptor: database(1, "app"),
5528 },
5529 )
5530 .unwrap();
5531 let node_version = state.node_record(node_id(1)).unwrap().metadata_version;
5533 let error = apply(
5534 &mut state,
5535 ®istry,
5536 3,
5537 MetaCommand::UpdateNodeState {
5538 node_id: node_id(1),
5539 state: NodeState::Down,
5540 expected_version: Some(MetadataVersion(node_version.get() + 9)),
5541 },
5542 )
5543 .unwrap_err();
5544 assert!(matches!(error, MetaRejectionReason::StaleWrite { .. }));
5545 apply(
5546 &mut state,
5547 ®istry,
5548 4,
5549 MetaCommand::UpdateNodeState {
5550 node_id: node_id(1),
5551 state: NodeState::Down,
5552 expected_version: Some(node_version),
5553 },
5554 )
5555 .unwrap();
5556
5557 apply(
5559 &mut state,
5560 ®istry,
5561 5,
5562 MetaCommand::SetTableSchema {
5563 record: schema_record(1, 1, 2),
5564 },
5565 )
5566 .unwrap();
5567 let error = apply(
5568 &mut state,
5569 ®istry,
5570 6,
5571 MetaCommand::SetTableSchema {
5572 record: schema_record(1, 1, 1),
5573 },
5574 )
5575 .unwrap_err();
5576 assert!(matches!(error, MetaRejectionReason::StaleWrite { .. }));
5577 let mut conflicting = schema_record(1, 1, 2);
5578 conflicting.schema = serde_json::json!({"columns": []});
5579 let error = apply(
5580 &mut state,
5581 ®istry,
5582 7,
5583 MetaCommand::SetTableSchema {
5584 record: conflicting,
5585 },
5586 )
5587 .unwrap_err();
5588 assert!(matches!(error, MetaRejectionReason::Conflict { .. }));
5589 apply(
5591 &mut state,
5592 ®istry,
5593 8,
5594 MetaCommand::SetTableSchema {
5595 record: schema_record(1, 1, 2),
5596 },
5597 )
5598 .unwrap();
5599
5600 apply(
5602 &mut state,
5603 ®istry,
5604 9,
5605 MetaCommand::SetTabletDescriptor {
5606 descriptor: tablet(1, 1, 5),
5607 },
5608 )
5609 .unwrap();
5610 let mut conflicted = tablet(1, 1, 5);
5611 conflicted.leader_hint = Some(node_id(1));
5612 let error = apply(
5613 &mut state,
5614 ®istry,
5615 10,
5616 MetaCommand::SetTabletDescriptor {
5617 descriptor: conflicted,
5618 },
5619 )
5620 .unwrap_err();
5621 assert!(matches!(error, MetaRejectionReason::Conflict { .. }));
5622 let error = apply(
5623 &mut state,
5624 ®istry,
5625 11,
5626 MetaCommand::SetTabletDescriptor {
5627 descriptor: tablet(1, 1, 4),
5628 },
5629 )
5630 .unwrap_err();
5631 assert!(matches!(error, MetaRejectionReason::StaleWrite { .. }));
5632 let error = apply(
5633 &mut state,
5634 ®istry,
5635 12,
5636 MetaCommand::RemoveTabletDescriptor {
5637 tablet_id: TabletId::from_bytes([1; 16]),
5638 generation: 4,
5639 },
5640 )
5641 .unwrap_err();
5642 assert!(matches!(error, MetaRejectionReason::StaleWrite { .. }));
5643 apply(
5644 &mut state,
5645 ®istry,
5646 13,
5647 MetaCommand::SetTabletDescriptor {
5648 descriptor: tablet(1, 1, 6),
5649 },
5650 )
5651 .unwrap();
5652
5653 let error = apply(
5655 &mut state,
5656 ®istry,
5657 14,
5658 MetaCommand::CreateDatabase {
5659 descriptor: database(2, "app"),
5660 },
5661 )
5662 .unwrap_err();
5663 assert!(matches!(error, MetaRejectionReason::Conflict { .. }));
5664 let error = apply(
5665 &mut state,
5666 ®istry,
5667 15,
5668 MetaCommand::CreateDatabase {
5669 descriptor: database(1, "other"),
5670 },
5671 )
5672 .unwrap_err();
5673 assert!(matches!(error, MetaRejectionReason::Conflict { .. }));
5674 apply(
5675 &mut state,
5676 ®istry,
5677 16,
5678 MetaCommand::CreateDatabase {
5679 descriptor: database(1, "app"),
5680 },
5681 )
5682 .unwrap();
5683 }
5684
5685 #[test]
5686 fn referential_integrity_is_enforced() {
5687 let registry = FeatureRegistry::current();
5688 let mut state = MetaState::default();
5689 apply(
5690 &mut state,
5691 ®istry,
5692 1,
5693 MetaCommand::RegisterNode {
5694 descriptor: descriptor(1, &[]),
5695 },
5696 )
5697 .unwrap();
5698 apply(
5699 &mut state,
5700 ®istry,
5701 2,
5702 MetaCommand::CreateDatabase {
5703 descriptor: database(1, "app"),
5704 },
5705 )
5706 .unwrap();
5707 apply(
5708 &mut state,
5709 ®istry,
5710 3,
5711 MetaCommand::SetTableSchema {
5712 record: schema_record(1, 1, 1),
5713 },
5714 )
5715 .unwrap();
5716 apply(
5717 &mut state,
5718 ®istry,
5719 4,
5720 MetaCommand::SetReplicaPlacement {
5721 placement: placement(9, &[(1, ReplicaRole::Voter)]),
5722 },
5723 )
5724 .unwrap();
5725 let error = apply(
5727 &mut state,
5728 ®istry,
5729 5,
5730 MetaCommand::RemoveNode {
5731 node_id: node_id(1),
5732 },
5733 )
5734 .unwrap_err();
5735 assert!(matches!(error, MetaRejectionReason::Conflict { .. }));
5736 let error = apply(
5738 &mut state,
5739 ®istry,
5740 6,
5741 MetaCommand::DropDatabase {
5742 database_id: DatabaseId::from_bytes([1; 16]),
5743 },
5744 )
5745 .unwrap_err();
5746 assert!(matches!(error, MetaRejectionReason::Conflict { .. }));
5747 let error = apply(
5749 &mut state,
5750 ®istry,
5751 7,
5752 MetaCommand::SetReplicaPlacement {
5753 placement: placement(8, &[(7, ReplicaRole::Voter)]),
5754 },
5755 )
5756 .unwrap_err();
5757 assert!(matches!(error, MetaRejectionReason::NotFound { .. }));
5758 let error = apply(
5760 &mut state,
5761 ®istry,
5762 8,
5763 MetaCommand::SetReplicaPlacement {
5764 placement: placement(8, &[(1, ReplicaRole::Voter), (1, ReplicaRole::Learner)]),
5765 },
5766 )
5767 .unwrap_err();
5768 assert!(matches!(error, MetaRejectionReason::Conflict { .. }));
5769 let error = apply(
5771 &mut state,
5772 ®istry,
5773 9,
5774 MetaCommand::SetTabletDescriptor {
5775 descriptor: tablet(2, 999, 1),
5776 },
5777 )
5778 .unwrap_err();
5779 assert!(matches!(error, MetaRejectionReason::NotFound { .. }));
5780 let error = apply(
5782 &mut state,
5783 ®istry,
5784 10,
5785 MetaCommand::SetTableSchema {
5786 record: schema_record(2, 42, 1),
5787 },
5788 )
5789 .unwrap_err();
5790 assert!(matches!(error, MetaRejectionReason::NotFound { .. }));
5791 apply(
5793 &mut state,
5794 ®istry,
5795 11,
5796 MetaCommand::SetReplicaPlacement {
5797 placement: placement(9, &[(1, ReplicaRole::Learner)]),
5798 },
5799 )
5800 .unwrap();
5801 let error = apply(
5802 &mut state,
5803 ®istry,
5804 12,
5805 MetaCommand::RemoveNode {
5806 node_id: node_id(1),
5807 },
5808 )
5809 .unwrap_err();
5810 assert!(
5811 matches!(error, MetaRejectionReason::Conflict { .. }),
5812 "learner replicas still reference the node"
5813 );
5814 }
5815
5816 #[test]
5817 fn schema_job_graph_is_enforced() {
5818 let registry = FeatureRegistry::current();
5819 let mut state = MetaState::default();
5820 apply(
5821 &mut state,
5822 ®istry,
5823 1,
5824 MetaCommand::RegisterNode {
5825 descriptor: descriptor(1, &[]),
5826 },
5827 )
5828 .unwrap();
5829 apply(
5830 &mut state,
5831 ®istry,
5832 2,
5833 MetaCommand::CreateDatabase {
5834 descriptor: database(1, "app"),
5835 },
5836 )
5837 .unwrap();
5838 apply(
5839 &mut state,
5840 ®istry,
5841 3,
5842 MetaCommand::SetTableSchema {
5843 record: schema_record(1, 1, 1),
5844 },
5845 )
5846 .unwrap();
5847 let mut running = schema_job(7, 1, 1);
5849 running.state = SchemaJobState::Running;
5850 let error = apply(
5851 &mut state,
5852 ®istry,
5853 4,
5854 MetaCommand::SubmitSchemaJob { job: running },
5855 )
5856 .unwrap_err();
5857 assert!(matches!(error, MetaRejectionReason::Invalid { .. }));
5858 apply(
5859 &mut state,
5860 ®istry,
5861 5,
5862 MetaCommand::SubmitSchemaJob {
5863 job: schema_job(7, 1, 1),
5864 },
5865 )
5866 .unwrap();
5867 let error = apply(
5869 &mut state,
5870 ®istry,
5871 6,
5872 MetaCommand::UpdateSchemaJob {
5873 job_id: 7,
5874 state: SchemaJobState::Succeeded,
5875 updated_at: ts(2_000),
5876 error: None,
5877 expected_version: None,
5878 },
5879 )
5880 .unwrap_err();
5881 assert!(matches!(error, MetaRejectionReason::Conflict { .. }));
5882 for (id, job_state) in [
5884 (7_u8, SchemaJobState::Running),
5885 (8, SchemaJobState::Succeeded),
5886 ] {
5887 apply(
5888 &mut state,
5889 ®istry,
5890 id,
5891 MetaCommand::UpdateSchemaJob {
5892 job_id: 7,
5893 state: job_state,
5894 updated_at: ts(2_000),
5895 error: None,
5896 expected_version: None,
5897 },
5898 )
5899 .unwrap();
5900 }
5901 let error = apply(
5903 &mut state,
5904 ®istry,
5905 9,
5906 MetaCommand::UpdateSchemaJob {
5907 job_id: 7,
5908 state: SchemaJobState::Running,
5909 updated_at: ts(3_000),
5910 error: None,
5911 expected_version: None,
5912 },
5913 )
5914 .unwrap_err();
5915 assert!(matches!(error, MetaRejectionReason::Conflict { .. }));
5916 let error = apply(
5918 &mut state,
5919 ®istry,
5920 10,
5921 MetaCommand::UpdateSchemaJob {
5922 job_id: 7,
5923 state: SchemaJobState::Failed,
5924 updated_at: ts(3_000),
5925 error: None,
5926 expected_version: Some(MetadataVersion(1)),
5927 },
5928 )
5929 .unwrap_err();
5930 assert!(matches!(error, MetaRejectionReason::StaleWrite { .. }));
5931 }
5932
5933 fn applied(byte: u8, command: ReplicatedCommand) -> AppliedCommand {
5936 AppliedCommand {
5937 position: LogPosition {
5938 term: 1,
5939 index: u64::from(byte),
5940 },
5941 command,
5942 }
5943 }
5944
5945 fn meta_envelope(id: u8, command: MetaCommand) -> ReplicatedCommand {
5946 let payload = MetaCommandRecord::new(command).encode().unwrap();
5947 ReplicatedCommand::new(
5948 CommandKind::Catalog,
5949 CommandEnvelope::new(COMMAND_TYPE_META_COMMAND, cmd_id(id), payload),
5950 ts(1_000),
5951 )
5952 }
5953
5954 #[test]
5955 fn sink_rejects_non_meta_payloads_and_transactions() {
5956 let tmp = tempfile::tempdir().unwrap();
5957 let mut sink = MetaApplySink::open(tmp.path(), FeatureRegistry::current()).unwrap();
5958 let foreign = ReplicatedCommand::new(
5960 CommandKind::Catalog,
5961 CommandEnvelope::new(999, cmd_id(1), b"payload".to_vec()),
5962 ts(1_000),
5963 );
5964 assert!(ApplySink::apply(&mut sink, &applied(1, foreign)).is_err());
5965 let transaction = ReplicatedCommand::new(
5967 CommandKind::Transaction,
5968 CommandEnvelope::new(1, cmd_id(2), b"rows".to_vec()),
5969 ts(1_000),
5970 );
5971 assert!(ApplySink::apply(&mut sink, &applied(2, transaction)).is_err());
5972 let maintenance = ReplicatedCommand::new(
5974 CommandKind::Maintenance,
5975 CommandEnvelope::new(3, cmd_id(3), b"directive".to_vec()),
5976 ts(1_000),
5977 );
5978 ApplySink::apply(&mut sink, &applied(3, maintenance)).unwrap();
5979 ApplySink::apply(&mut sink, &applied(4, ReplicatedCommand::Noop)).unwrap();
5980 assert_eq!(sink.metadata_version(), MetadataVersion::ZERO);
5981 }
5982
5983 #[test]
5984 fn sink_snapshot_install_preserves_state() {
5985 let tmp_a = tempfile::tempdir().unwrap();
5986 let tmp_b = tempfile::tempdir().unwrap();
5987 let registry = registry_with("ann-v2", 7);
5988 let mut sink = MetaApplySink::open(tmp_a.path(), registry.clone()).unwrap();
5989 for (id, command) in every_command_sequence().into_iter().enumerate() {
5990 let id = u8::try_from(id + 1).unwrap();
5991 ApplySink::apply(&mut sink, &applied(id, meta_envelope(id, command))).unwrap();
5992 }
5993 ApplySink::apply(
5995 &mut sink,
5996 &applied(
5997 42,
5998 meta_envelope(
5999 42,
6000 MetaCommand::SetClusterSetting {
6001 key: "ai.api_key".to_owned(),
6002 value: serde_json::json!("x"),
6003 },
6004 ),
6005 ),
6006 )
6007 .unwrap();
6008 let bytes = sink.snapshot().unwrap();
6009
6010 let mut restored = MetaApplySink::open(tmp_b.path(), registry).unwrap();
6011 restored.install(&bytes).unwrap();
6012 assert_eq!(restored.state(), sink.state());
6013 assert_eq!(restored.metadata_version(), sink.metadata_version());
6014 assert_eq!(restored.applied_position(), sink.applied_position());
6015 assert_eq!(restored.state().rejections().len(), 1);
6016
6017 assert!(restored.install(b"junk").is_err());
6019 let mut future = restored.snapshot().unwrap();
6020 let mut checkpoint: MetaStateCheckpoint = serde_json::from_slice(&future).unwrap();
6021 checkpoint.format_version = META_STATE_CHECKPOINT_FORMAT_VERSION + 1;
6022 future = serde_json::to_vec(&checkpoint).unwrap();
6023 assert!(restored.install(&future).is_err());
6024 let mut checkpoint: MetaStateCheckpoint =
6025 serde_json::from_slice(&restored.snapshot().unwrap()).unwrap();
6026 checkpoint.state.format_version = META_STATE_FORMAT_VERSION + 1;
6027 assert!(restored
6028 .install(&serde_json::to_vec(&checkpoint).unwrap())
6029 .is_err());
6030 assert_eq!(restored.state(), sink.state());
6032 }
6033
6034 #[test]
6035 fn sink_restart_recovers_from_its_checkpoint() {
6036 let tmp = tempfile::tempdir().unwrap();
6037 let registry = registry_with("ann-v2", 7);
6038 let mut sink = MetaApplySink::open(tmp.path(), registry.clone()).unwrap();
6039 for (id, command) in every_command_sequence().into_iter().enumerate() {
6040 let id = u8::try_from(id + 1).unwrap();
6041 ApplySink::apply(&mut sink, &applied(id, meta_envelope(id, command))).unwrap();
6042 }
6043 let before = sink.state().clone();
6044 let position = sink.applied_position();
6045 drop(sink);
6046
6047 let mut reopened = MetaApplySink::open(tmp.path(), registry).unwrap();
6050 assert_eq!(reopened.state(), &before);
6051 assert_eq!(reopened.metadata_version(), MetadataVersion(16));
6052 assert_eq!(reopened.applied_position(), position);
6053
6054 let replay = meta_envelope(
6057 15,
6058 MetaCommand::DropDatabase {
6059 database_id: DatabaseId::from_bytes([0xEE; 16]),
6060 },
6061 );
6062 ApplySink::apply(&mut reopened, &applied(15, replay)).unwrap();
6063 assert_eq!(reopened.state(), &before);
6064 let next = meta_envelope(
6066 17,
6067 MetaCommand::SetClusterSetting {
6068 key: "jobs.max_concurrent".to_owned(),
6069 value: serde_json::json!(8),
6070 },
6071 );
6072 ApplySink::apply(&mut reopened, &applied(17, next)).unwrap();
6073 assert_eq!(reopened.metadata_version(), MetadataVersion(17));
6074 assert_eq!(reopened.state().settings().max_concurrent_jobs, 8);
6075
6076 std::fs::write(
6078 tmp.path()
6079 .join("raft")
6080 .join("state")
6081 .join(META_STATE_CHECKPOINT_FILENAME),
6082 b"junk",
6083 )
6084 .unwrap();
6085 assert!(matches!(
6086 MetaApplySink::open(tmp.path(), FeatureRegistry::current()),
6087 Err(MetaError::CorruptCheckpoint(_))
6088 ));
6089 }
6090
6091 async fn single_node_group(
6094 dir: &Path,
6095 node: u8,
6096 registry: FeatureRegistry,
6097 transport: Arc<InMemoryTransport>,
6098 ) -> MetaGroup<InMemoryTransport> {
6099 let config = meta_config(dir, node, registry);
6100 let group_config = fast_group_config(&config);
6101 let meta = MetaGroup::create(config, group_config, transport)
6102 .await
6103 .unwrap();
6104 meta.bootstrap(&[(
6105 node_id(node),
6106 format!("127.0.0.1:{}", 7100 + u16::from(node)),
6107 )])
6108 .await
6109 .unwrap();
6110 meta.group().wait_leader(LEADER_TIMEOUT).await.unwrap();
6111 meta
6112 }
6113
6114 #[tokio::test]
6115 async fn single_node_meta_group_round_trips_every_command() {
6116 let tmp = tempfile::tempdir().unwrap();
6117 let transport = Arc::new(InMemoryTransport::new());
6118 let meta = single_node_group(
6119 &tmp.path().join("node-1"),
6120 1,
6121 registry_with("ann-v2", 7),
6122 transport,
6123 )
6124 .await;
6125 assert!(tmp
6127 .path()
6128 .join("node-1/groups")
6129 .join(META_GID.to_hex())
6130 .join("raft")
6131 .is_dir());
6132
6133 let control = ExecutionControl::default();
6134 let mut last_version = MetadataVersion::ZERO;
6135 for (index, command) in every_command_sequence().into_iter().enumerate() {
6136 let id = u8::try_from(index + 1).unwrap();
6137 let receipt = meta.propose(cmd_id(id), command, &control).await.unwrap();
6138 assert!(receipt.metadata_version > last_version);
6139 last_version = receipt.metadata_version;
6140 }
6141 assert_eq!(last_version, MetadataVersion(16));
6142 assert_eq!(meta.metadata_version(), MetadataVersion(16));
6143 let state = meta.state();
6144 assert_eq!(state.feature_level(), ClusterFeatureLevel(7));
6145 assert_eq!(state.settings().max_concurrent_jobs, 4);
6146 assert_eq!(state.schema_job(7).unwrap().state, SchemaJobState::Running);
6147 assert!(state.rejections().is_empty());
6148 meta.shutdown().await.unwrap();
6149 }
6150
6151 #[tokio::test]
6152 async fn idempotent_replay_through_the_group() {
6153 let tmp = tempfile::tempdir().unwrap();
6154 let transport = Arc::new(InMemoryTransport::new());
6155 let meta = single_node_group(
6156 &tmp.path().join("node-1"),
6157 1,
6158 FeatureRegistry::current(),
6159 transport,
6160 )
6161 .await;
6162 let control = ExecutionControl::default();
6163 let command = MetaCommand::RegisterNode {
6164 descriptor: descriptor(1, &[]),
6165 };
6166 let first = meta
6167 .propose(cmd_id(1), command.clone(), &control)
6168 .await
6169 .unwrap();
6170 assert_eq!(first.metadata_version, MetadataVersion(1));
6171 assert!(!first.receipt.response.duplicate);
6172 let retry = meta.propose(cmd_id(1), command, &control).await.unwrap();
6175 assert!(retry.receipt.response.duplicate);
6176 assert_eq!(retry.metadata_version, MetadataVersion(1));
6177 assert_eq!(meta.state().nodes.len(), 1);
6178 meta.shutdown().await.unwrap();
6179 }
6180
6181 #[tokio::test]
6182 async fn refused_commands_surface_typed_errors_through_the_group() {
6183 let tmp = tempfile::tempdir().unwrap();
6184 let transport = Arc::new(InMemoryTransport::new());
6185 let meta = single_node_group(
6186 &tmp.path().join("node-1"),
6187 1,
6188 registry_with("ann-v2", 7),
6189 transport,
6190 )
6191 .await;
6192 let control = ExecutionControl::default();
6193 meta.propose(
6194 cmd_id(1),
6195 MetaCommand::RegisterNode {
6196 descriptor: descriptor(1, &[]),
6197 },
6198 &control,
6199 )
6200 .await
6201 .unwrap();
6202 let error = meta
6204 .propose(
6205 cmd_id(2),
6206 MetaCommand::ActivateFeature {
6207 activation: activation("ann-v2", 7),
6208 },
6209 &control,
6210 )
6211 .await
6212 .unwrap_err();
6213 let MetaError::Rejected(reason) = error else {
6214 panic!("expected a typed rejection, got {error}");
6215 };
6216 assert_eq!(
6217 reason,
6218 MetaRejectionReason::FeatureActivation(FeatureActivationError::UnsupportedByVoter {
6219 feature: "ann-v2".to_owned(),
6220 node: node_id(1),
6221 })
6222 );
6223 let error = meta
6225 .propose(
6226 cmd_id(3),
6227 MetaCommand::SetClusterSetting {
6228 key: "backup.private_key_pem".to_owned(),
6229 value: serde_json::json!("pem"),
6230 },
6231 &control,
6232 )
6233 .await
6234 .unwrap_err();
6235 assert!(matches!(
6236 error,
6237 MetaError::Rejected(MetaRejectionReason::SecretSettingKey { .. })
6238 ));
6239 assert_eq!(meta.metadata_version(), MetadataVersion(3));
6241 assert_eq!(meta.state().rejections().len(), 2);
6242 meta.shutdown().await.unwrap();
6243 }
6244
6245 #[tokio::test]
6246 async fn add_and_remove_member_workflow() {
6247 let tmp = tempfile::tempdir().unwrap();
6248 let transport = Arc::new(InMemoryTransport::new());
6249 let meta1 = single_node_group(
6250 &tmp.path().join("node-1"),
6251 1,
6252 FeatureRegistry::current(),
6253 transport.clone(),
6254 )
6255 .await;
6256 let config2 = meta_config(&tmp.path().join("node-2"), 2, FeatureRegistry::current());
6258 let group_config2 = fast_group_config(&config2);
6259 let meta2 = MetaGroup::create(config2, group_config2, transport.clone())
6260 .await
6261 .unwrap();
6262 let control = ExecutionControl::default();
6263
6264 meta1
6267 .propose(
6268 cmd_id(1),
6269 MetaCommand::RegisterNode {
6270 descriptor: descriptor(1, &[]),
6271 },
6272 &control,
6273 )
6274 .await
6275 .unwrap();
6276
6277 let descriptor2 = descriptor(2, &[]);
6280 let receipt = meta1.add_member(&descriptor2, &control).await.unwrap();
6281 let (voters, _) = meta1.group().members();
6282 assert!(voters.contains(&raft_id(2)));
6283 meta2
6284 .group()
6285 .wait_applied_index(receipt.receipt.position.index, LEADER_TIMEOUT)
6286 .await
6287 .unwrap();
6288 assert_eq!(
6289 meta2.state().node(node_id(2)).unwrap().rpc_address,
6290 descriptor2.rpc_address
6291 );
6292
6293 meta1
6295 .propose(
6296 cmd_id(10),
6297 MetaCommand::SetReplicaPlacement {
6298 placement: placement(9, &[(2, ReplicaRole::Voter)]),
6299 },
6300 &control,
6301 )
6302 .await
6303 .unwrap();
6304 let error = meta1.remove_member(node_id(2), &control).await.unwrap_err();
6305 assert!(matches!(
6306 error,
6307 MetaError::Rejected(MetaRejectionReason::Conflict { .. })
6308 ));
6309 let (voters, _) = meta1.group().members();
6310 assert!(
6311 voters.contains(&raft_id(2)),
6312 "a refused removal leaves raft membership untouched"
6313 );
6314
6315 meta1
6317 .propose(
6318 cmd_id(11),
6319 MetaCommand::SetReplicaPlacement {
6320 placement: placement(9, &[(1, ReplicaRole::Voter)]),
6321 },
6322 &control,
6323 )
6324 .await
6325 .unwrap();
6326 meta1.remove_member(node_id(2), &control).await.unwrap();
6327 let (voters, _) = meta1.group().members();
6328 assert!(!voters.contains(&raft_id(2)));
6329 assert!(meta1.state().node(node_id(2)).is_none());
6330
6331 let error = meta1.remove_member(node_id(1), &control).await.unwrap_err();
6333 assert!(matches!(error, MetaError::InvalidRequest(_)));
6334
6335 meta2.shutdown().await.unwrap();
6336 meta1.shutdown().await.unwrap();
6337 }
6338
6339 #[tokio::test]
6340 async fn bootstrap_rejects_raft_id_projection_collisions() {
6341 let tmp = tempfile::tempdir().unwrap();
6342 let transport = Arc::new(InMemoryTransport::new());
6343 let config = meta_config(&tmp.path().join("node-1"), 1, FeatureRegistry::current());
6344 let group_config = fast_group_config(&config);
6345 let meta = MetaGroup::create(config, group_config, transport)
6346 .await
6347 .unwrap();
6348 let colliding = NodeId::from_bytes([1, 2, 3, 4, 5, 6, 7, 8, 9, 9, 9, 9, 9, 9, 9, 9]);
6351 let first = NodeId::from_bytes([1, 2, 3, 4, 5, 6, 7, 8, 8, 8, 8, 8, 8, 8, 8, 8]);
6352 let error = meta
6353 .bootstrap(&[
6354 (first, "127.0.0.1:7101".to_owned()),
6355 (colliding, "127.0.0.1:7102".to_owned()),
6356 ])
6357 .await
6358 .unwrap_err();
6359 assert!(matches!(error, MetaError::InvalidRequest(_)));
6360 meta.shutdown().await.unwrap();
6361 }
6362
6363 #[tokio::test]
6364 async fn create_rejects_a_mismatched_group_config() {
6365 let tmp = tempfile::tempdir().unwrap();
6366 let transport = Arc::new(InMemoryTransport::new());
6367 let config = meta_config(&tmp.path().join("node-1"), 1, FeatureRegistry::current());
6368 let mut group_config = fast_group_config(&config);
6369 group_config.dir = tmp.path().join("elsewhere");
6370 let result = MetaGroup::<InMemoryTransport>::create(config, group_config, transport).await;
6371 match result {
6372 Err(error) => assert!(matches!(error, MetaError::InvalidRequest(_))),
6373 Ok(meta) => {
6374 meta.shutdown().await.unwrap();
6375 panic!("expected create to reject a mismatched group config");
6376 }
6377 }
6378 }
6379
6380 #[tokio::test(flavor = "multi_thread", worker_threads = 4)]
6381 async fn three_node_meta_group_converges_after_leader_failover() {
6382 let tmp = tempfile::tempdir().unwrap();
6383 let transport = Arc::new(InMemoryTransport::new());
6384 let registry = || registry_with("ann-v2", 7);
6385 let mut groups: BTreeMap<u8, MetaGroup<InMemoryTransport>> = BTreeMap::new();
6386 for byte in [1_u8, 2, 3] {
6387 let config = meta_config(&tmp.path().join(format!("node-{byte}")), byte, registry());
6388 let group_config = fast_group_config(&config);
6389 groups.insert(
6390 byte,
6391 MetaGroup::create(config, group_config, transport.clone())
6392 .await
6393 .unwrap(),
6394 );
6395 }
6396 let members: Vec<(NodeId, String)> = [1_u8, 2, 3]
6397 .iter()
6398 .map(|byte| (node_id(*byte), format!("127.0.0.1:710{byte}")))
6399 .collect();
6400 groups[&1].bootstrap(&members).await.unwrap();
6401 let control = ExecutionControl::default();
6402 let mut proposals: Vec<MetaCommand> = [1_u8, 2, 3]
6403 .iter()
6404 .map(|byte| MetaCommand::RegisterNode {
6405 descriptor: descriptor(*byte, &["ann-v2"]),
6406 })
6407 .collect();
6408 proposals.extend([
6409 MetaCommand::CreateDatabase {
6410 descriptor: database(1, "app"),
6411 },
6412 MetaCommand::SetTableSchema {
6413 record: schema_record(1, 1, 1),
6414 },
6415 MetaCommand::SetReplicaPlacement {
6416 placement: placement(
6417 9,
6418 &[
6419 (1, ReplicaRole::Voter),
6420 (2, ReplicaRole::Voter),
6421 (3, ReplicaRole::Voter),
6422 ],
6423 ),
6424 },
6425 MetaCommand::SetPlacementPolicy {
6426 name: "default".to_owned(),
6427 policy: policy(3),
6428 },
6429 MetaCommand::ActivateFeature {
6430 activation: activation("ann-v2", 7),
6431 },
6432 MetaCommand::SetClusterSetting {
6433 key: "jobs.max_concurrent".to_owned(),
6434 value: serde_json::json!(4),
6435 },
6436 MetaCommand::SetTxnStatusPartition {
6437 partition: TxnStatusPartition {
6438 partition_id: 0,
6439 home_raft_group: group_id(9),
6440 },
6441 },
6442 ]);
6443 let mut last_index = 0_u64;
6444 for (seq, command) in proposals.into_iter().enumerate() {
6445 let id = u8::try_from(seq + 1).unwrap();
6446 let receipt =
6447 propose_to_settled_leader(&groups, &[1, 2, 3], cmd_id(id), command, &control).await;
6448 last_index = receipt.receipt.position.index;
6449 }
6450 for byte in [1_u8, 2, 3] {
6451 groups[&byte]
6452 .group()
6453 .wait_applied_index(last_index, LEADER_TIMEOUT)
6454 .await
6455 .unwrap();
6456 }
6457 assert_eq!(groups[&1].state(), groups[&2].state());
6458 assert_eq!(groups[&2].state(), groups[&3].state());
6459
6460 let leader_byte = {
6463 let leader = wait_consensus_leader(&[&groups[&1], &groups[&2], &groups[&3]]).await;
6464 [1_u8, 2, 3]
6465 .into_iter()
6466 .find(|byte| raft_id(*byte) == leader)
6467 .unwrap()
6468 };
6469 groups[&leader_byte].shutdown().await.unwrap();
6470 let survivors: Vec<u8> = [1_u8, 2, 3]
6471 .into_iter()
6472 .filter(|byte| *byte != leader_byte)
6473 .collect();
6474 let mut new_index = last_index;
6475 for (seq, command) in [
6476 MetaCommand::SubmitSchemaJob {
6477 job: schema_job(7, 1, 1),
6478 },
6479 MetaCommand::UpdateSchemaJob {
6480 job_id: 7,
6481 state: SchemaJobState::Running,
6482 updated_at: ts(9_000),
6483 error: None,
6484 expected_version: None,
6485 },
6486 ]
6487 .into_iter()
6488 .enumerate()
6489 {
6490 let id = u8::try_from(seq + 100).unwrap();
6491 let receipt =
6492 propose_to_settled_leader(&groups, &survivors, cmd_id(id), command, &control).await;
6493 new_index = receipt.receipt.position.index;
6494 }
6495 for byte in &survivors {
6496 groups[byte]
6497 .group()
6498 .wait_applied_index(new_index, LEADER_TIMEOUT)
6499 .await
6500 .unwrap();
6501 }
6502 assert_eq!(groups[&survivors[0]].state(), groups[&survivors[1]].state());
6503
6504 let config = meta_config(
6506 &tmp.path().join(format!("node-{leader_byte}")),
6507 leader_byte,
6508 registry(),
6509 );
6510 let group_config = fast_group_config(&config);
6511 let rejoined = MetaGroup::create(config, group_config, transport.clone())
6512 .await
6513 .unwrap();
6514 rejoined
6515 .group()
6516 .wait_applied_index(new_index, LEADER_TIMEOUT)
6517 .await
6518 .unwrap();
6519 assert_eq!(rejoined.state(), groups[&survivors[0]].state());
6520 assert_eq!(
6521 rejoined.metadata_version(),
6522 groups[&survivors[0]].metadata_version()
6523 );
6524 for group in groups.values() {
6525 group.shutdown().await.unwrap();
6526 }
6527 rejoined.shutdown().await.unwrap();
6528 }
6529
6530 #[tokio::test]
6531 async fn snapshot_install_preserves_group_state() {
6532 let tmp = tempfile::tempdir().unwrap();
6533 let transport = Arc::new(InMemoryTransport::new());
6534 let meta = single_node_group(
6535 &tmp.path().join("node-1"),
6536 1,
6537 registry_with("ann-v2", 7),
6538 transport.clone(),
6539 )
6540 .await;
6541 let control = ExecutionControl::default();
6542 for (index, command) in every_command_sequence().into_iter().enumerate() {
6543 let id = u8::try_from(index + 1).unwrap();
6544 meta.propose(cmd_id(id), command, &control).await.unwrap();
6545 }
6546 let snapshot = meta.group().snapshot().await.unwrap();
6547
6548 let config = meta_config(&tmp.path().join("node-2"), 2, registry_with("ann-v2", 7));
6551 let group_config = fast_group_config(&config);
6552 let fresh = MetaGroup::create(config, group_config, transport)
6553 .await
6554 .unwrap();
6555 fresh.group().install_snapshot(&snapshot).unwrap();
6556 assert_eq!(fresh.state(), meta.state());
6557 assert_eq!(fresh.metadata_version(), meta.metadata_version());
6558 assert_eq!(fresh.state().feature_level(), ClusterFeatureLevel(7));
6559 meta.shutdown().await.unwrap();
6560 fresh.shutdown().await.unwrap();
6561 }
6562}
6563
6564#[cfg(test)]
6569mod reconciliation_tests {
6570 use super::*;
6571 use crate::node::{BuildVersion, Locality, NodeCapacity};
6572 use mongreldb_log::commit_log::LogPosition;
6573
6574 fn node_id(byte: u8) -> NodeId {
6575 NodeId::from_bytes([byte; 16])
6576 }
6577
6578 fn group_id(byte: u8) -> RaftGroupId {
6579 RaftGroupId::from_bytes([byte; 16])
6580 }
6581
6582 fn tablet_id(byte: u8) -> TabletId {
6583 TabletId::from_bytes([byte; 16])
6584 }
6585
6586 fn ts(micros: u64) -> HlcTimestamp {
6587 HlcTimestamp {
6588 physical_micros: micros,
6589 logical: 0,
6590 node_tiebreaker: 0,
6591 }
6592 }
6593
6594 fn descriptor(byte: u8) -> NodeDescriptor {
6595 NodeDescriptor {
6596 node_id: node_id(byte),
6597 rpc_address: format!("127.0.0.1:{}", 7200 + u16::from(byte)),
6598 locality: Locality::default(),
6599 capacity: NodeCapacity::default(),
6600 state: NodeState::Up,
6601 version: BuildVersion::current(),
6602 version_info: VersionInfo::current(),
6603 }
6604 }
6605
6606 fn v1_tablet(byte: u8, state: v1::TabletState) -> v1::TabletDescriptor {
6607 v1::TabletDescriptor {
6608 tablet_id: tablet_id(byte),
6609 table_id: TableId(3),
6610 database_id: mongreldb_types::ids::DatabaseId::ZERO,
6611 raft_group_id: group_id(9),
6612 partition: v1::PartitionBounds {
6613 start: Some(b"a".to_vec()),
6614 end: Some(b"m".to_vec()),
6615 },
6616 replicas: vec![
6617 v1::ReplicaDescriptor {
6618 node_id: node_id(1),
6619 role: v1::ReplicaRole::Voter,
6620 },
6621 v1::ReplicaDescriptor {
6622 node_id: node_id(2),
6623 role: v1::ReplicaRole::Learner,
6624 },
6625 ],
6626 leader_hint: Some(node_id(1)),
6627 generation: 4,
6628 state,
6629 metadata_version: MetadataVersion(11),
6630 }
6631 }
6632
6633 fn v1_policy() -> v1::PlacementPolicy {
6634 v1::PlacementPolicy {
6635 replicas: 3,
6636 voter_constraints: vec![v1::LocalityConstraint {
6637 key: "region".to_owned(),
6638 value: "us-central".to_owned(),
6639 }],
6640 leader_preferences: vec![v1::LocalityConstraint {
6641 key: "zone".to_owned(),
6642 value: "a".to_owned(),
6643 }],
6644 prohibited_nodes: vec![node_id(9)],
6645 metadata_version: MetadataVersion(12),
6646 }
6647 }
6648
6649 fn v1_encode(command: v1::MetaCommand) -> Vec<u8> {
6651 serde_json::to_vec(&v1::MetaCommandRecord {
6652 format_version: 1,
6653 command,
6654 })
6655 .unwrap()
6656 }
6657
6658 #[test]
6659 fn v1_tablet_command_decodes_and_migrates_to_the_canonical_shapes() {
6660 let decoded = MetaCommandRecord::decode(&v1_encode(v1::MetaCommand::SetTabletDescriptor {
6661 descriptor: v1_tablet(1, v1::TabletState::Online),
6662 }))
6663 .unwrap();
6664 assert_eq!(decoded.format_version, META_COMMAND_FORMAT_VERSION);
6665 let MetaCommand::SetTabletDescriptor { descriptor } = decoded.command else {
6666 panic!("unexpected command variant: {:?}", decoded.command);
6667 };
6668 assert_eq!(
6670 descriptor.partition,
6671 PartitionBounds {
6672 low: Bound::Included(Key::from_bytes(b"a".to_vec())),
6673 high: Bound::Excluded(Key::from_bytes(b"m".to_vec())),
6674 }
6675 );
6676 assert_eq!(descriptor.state, TabletState::Active);
6678 assert_eq!(descriptor.leader_hint, Some(node_id(1)));
6679 assert_eq!(descriptor.generation, 4);
6680 assert_eq!(descriptor.replicas.len(), 2);
6681 assert_eq!(descriptor.replicas[0].role, ReplicaRole::Voter);
6682 assert_eq!(descriptor.replicas[1].role, ReplicaRole::Learner);
6683 assert_eq!(
6686 descriptor.replicas[0].raft_node_id,
6687 raft_node_id(&node_id(1))
6688 );
6689 assert_eq!(
6690 descriptor.replicas[1].raft_node_id,
6691 raft_node_id(&node_id(2))
6692 );
6693 }
6695
6696 #[test]
6697 fn v1_placement_and_policy_commands_decode_and_migrate() {
6698 let decoded = MetaCommandRecord::decode(&v1_encode(v1::MetaCommand::SetReplicaPlacement {
6699 placement: v1::ReplicaPlacement {
6700 raft_group_id: group_id(9),
6701 replicas: vec![v1::ReplicaDescriptor {
6702 node_id: node_id(3),
6703 role: v1::ReplicaRole::Voter,
6704 }],
6705 metadata_version: MetadataVersion(5),
6706 },
6707 }))
6708 .unwrap();
6709 let MetaCommand::SetReplicaPlacement { placement } = decoded.command else {
6710 panic!("unexpected command variant: {:?}", decoded.command);
6711 };
6712 assert_eq!(
6713 placement.replicas[0].raft_node_id,
6714 raft_node_id(&node_id(3))
6715 );
6716 assert_eq!(placement.metadata_version, MetadataVersion(5));
6717
6718 let decoded = MetaCommandRecord::decode(&v1_encode(v1::MetaCommand::SetPlacementPolicy {
6719 name: "default".to_owned(),
6720 policy: v1_policy(),
6721 }))
6722 .unwrap();
6723 let MetaCommand::SetPlacementPolicy { name, policy } = decoded.command else {
6724 panic!("unexpected command variant: {:?}", decoded.command);
6725 };
6726 assert_eq!(name, "default");
6727 assert_eq!(policy.replicas, 3);
6728 assert_eq!(
6730 policy.voter_constraints,
6731 vec![LocalityConstraint {
6732 key: "region".to_owned(),
6733 value: "us-central".to_owned(),
6734 required: true,
6735 }]
6736 );
6737 assert_eq!(
6738 policy.leader_preferences,
6739 vec![LocalityConstraint {
6740 key: "zone".to_owned(),
6741 value: "a".to_owned(),
6742 required: false,
6743 }]
6744 );
6745 assert_eq!(policy.prohibited_nodes, vec![node_id(9)]);
6746 }
6747
6748 #[test]
6749 fn v1_unaffected_command_variants_pass_through() {
6750 let decoded = MetaCommandRecord::decode(&v1_encode(v1::MetaCommand::RegisterNode {
6751 descriptor: descriptor(1),
6752 }))
6753 .unwrap();
6754 assert_eq!(
6755 decoded.command,
6756 MetaCommand::RegisterNode {
6757 descriptor: descriptor(1)
6758 }
6759 );
6760 let decoded =
6761 MetaCommandRecord::decode(&v1_encode(v1::MetaCommand::RemoveTabletDescriptor {
6762 tablet_id: tablet_id(1),
6763 generation: 9,
6764 }))
6765 .unwrap();
6766 assert_eq!(
6767 decoded.command,
6768 MetaCommand::RemoveTabletDescriptor {
6769 tablet_id: tablet_id(1),
6770 generation: 9,
6771 }
6772 );
6773 }
6774
6775 #[test]
6776 fn v1_checkpoint_loads_and_migrates_meta_state() {
6777 let tmp = tempfile::tempdir().unwrap();
6778 let state_dir = tmp.path().join("raft").join("state");
6779 std::fs::create_dir_all(&state_dir).unwrap();
6780 let mut state = v1::MetaState {
6781 metadata_version: MetadataVersion(20),
6782 ..v1::MetaState::default()
6783 };
6784 state
6785 .tablets
6786 .insert(tablet_id(1), v1_tablet(1, v1::TabletState::Offline));
6787 state.placements.insert(
6788 group_id(9),
6789 v1::ReplicaPlacement {
6790 raft_group_id: group_id(9),
6791 replicas: vec![v1::ReplicaDescriptor {
6792 node_id: node_id(1),
6793 role: v1::ReplicaRole::Voter,
6794 }],
6795 metadata_version: MetadataVersion(6),
6796 },
6797 );
6798 state
6799 .placement_policies
6800 .insert("default".to_owned(), v1_policy());
6801 let checkpoint = v1::MetaStateCheckpoint {
6802 format_version: 1,
6803 position: LogPosition { term: 2, index: 20 },
6804 command_id: Some([7; 16]),
6805 state,
6806 };
6807 std::fs::write(
6808 state_dir.join(META_STATE_CHECKPOINT_FILENAME),
6809 serde_json::to_vec(&checkpoint).unwrap(),
6810 )
6811 .unwrap();
6812
6813 let sink = MetaApplySink::open(tmp.path(), FeatureRegistry::current()).unwrap();
6814 assert_eq!(sink.applied_position(), LogPosition { term: 2, index: 20 });
6815 assert_eq!(sink.metadata_version(), MetadataVersion(20));
6816 let state = sink.state().clone();
6817 assert_eq!(state.format_version, META_STATE_FORMAT_VERSION);
6818 let record = state.tablet_record(tablet_id(1)).unwrap();
6820 assert_eq!(record.descriptor.state, TabletState::Retiring);
6821 assert_eq!(record.metadata_version, MetadataVersion(11));
6822 let placement = state.placement(group_id(9)).unwrap();
6823 assert_eq!(
6824 placement.replicas[0].raft_node_id,
6825 raft_node_id(&node_id(1))
6826 );
6827 assert_eq!(placement.metadata_version, MetadataVersion(6));
6828 let policy = state.placement_policy_record("default").unwrap();
6829 assert!(policy.policy.voter_constraints[0].required);
6830 assert!(!policy.policy.leader_preferences[0].required);
6831 assert_eq!(policy.metadata_version, MetadataVersion(12));
6832
6833 drop(sink);
6836 let mut reopened = MetaApplySink::open(tmp.path(), FeatureRegistry::current()).unwrap();
6837 assert_eq!(reopened.state(), &state);
6838 ApplySink::apply(
6839 &mut reopened,
6840 &AppliedCommand {
6841 position: LogPosition { term: 2, index: 21 },
6842 command: ReplicatedCommand::Noop,
6843 },
6844 )
6845 .unwrap();
6846 let on_disk = std::fs::read(state_dir.join(META_STATE_CHECKPOINT_FILENAME)).unwrap();
6847 let checkpoint: MetaStateCheckpoint = serde_json::from_slice(&on_disk).unwrap();
6848 assert_eq!(
6849 checkpoint.format_version,
6850 META_STATE_CHECKPOINT_FORMAT_VERSION
6851 );
6852 assert_eq!(checkpoint.state, *reopened.state());
6853 }
6854
6855 #[test]
6856 fn literal_v1_checkpoint_json_loads_through_serde_defaults() {
6857 let tablet_hex = tablet_id(2).to_hex();
6860 let group_hex = group_id(9).to_hex();
6861 let node_hex = node_id(1).to_hex();
6862 let json = serde_json::json!({
6863 "format_version": 1,
6864 "position": {"term": 1, "index": 9},
6865 "command_id": null,
6866 "state": {
6867 "format_version": 1,
6868 "metadata_version": 9,
6869 "tablets": {
6870 tablet_hex.as_str(): {
6871 "tablet_id": tablet_hex.as_str(),
6872 "table_id": 3,
6873 "raft_group_id": group_hex.as_str(),
6874 "partition": {"start": null, "end": [109]},
6875 "replicas": [{"node_id": node_hex.as_str(), "role": "Voter"}],
6876 "leader_hint": null,
6877 "generation": 4,
6878 "state": "Online",
6879 "metadata_version": 7
6880 }
6881 },
6882 "placement_policies": {
6883 "default": {
6884 "replicas": 3,
6885 "voter_constraints": [{"key": "region", "value": "us-central"}],
6886 "leader_preferences": [],
6887 "prohibited_nodes": [],
6888 "metadata_version": 8
6889 }
6890 }
6891 }
6892 });
6893 let tmp = tempfile::tempdir().unwrap();
6894 let state_dir = tmp.path().join("raft").join("state");
6895 std::fs::create_dir_all(&state_dir).unwrap();
6896 std::fs::write(
6897 state_dir.join(META_STATE_CHECKPOINT_FILENAME),
6898 serde_json::to_vec(&json).unwrap(),
6899 )
6900 .unwrap();
6901
6902 let sink = MetaApplySink::open(tmp.path(), FeatureRegistry::current()).unwrap();
6903 let record = sink.state().tablet_record(tablet_id(2)).unwrap();
6904 assert_eq!(
6905 record.descriptor.partition,
6906 PartitionBounds {
6907 low: Bound::Unbounded,
6908 high: Bound::Excluded(Key::from_bytes(b"m".to_vec())),
6909 }
6910 );
6911 assert_eq!(record.descriptor.state, TabletState::Active);
6912 assert_eq!(record.metadata_version, MetadataVersion(7));
6913 let policy = sink.state().placement_policy_record("default").unwrap();
6914 assert_eq!(policy.metadata_version, MetadataVersion(8));
6915 assert_eq!(sink.metadata_version(), MetadataVersion(9));
6916 }
6917
6918 #[test]
6919 fn unsupported_future_versions_fail_closed() {
6920 let future = serde_json::json!({
6921 "format_version": META_COMMAND_FORMAT_VERSION + 1,
6922 "command": {"RemoveNode": {"node_id": node_id(1)}},
6923 });
6924 assert_eq!(
6925 MetaCommandRecord::decode(&serde_json::to_vec(&future).unwrap()).unwrap_err(),
6926 MetaDecodeError::UnsupportedVersion {
6927 found: META_COMMAND_FORMAT_VERSION + 1,
6928 min: MIN_SUPPORTED_META_COMMAND_FORMAT_VERSION,
6929 max: META_COMMAND_FORMAT_VERSION,
6930 }
6931 );
6932
6933 let tmp = tempfile::tempdir().unwrap();
6934 let state_dir = tmp.path().join("raft").join("state");
6935 std::fs::create_dir_all(&state_dir).unwrap();
6936 let future = serde_json::json!({
6937 "format_version": META_STATE_CHECKPOINT_FORMAT_VERSION + 1,
6938 "position": {"term": 1, "index": 1},
6939 "command_id": null,
6940 "state": {"format_version": 1},
6941 });
6942 std::fs::write(
6943 state_dir.join(META_STATE_CHECKPOINT_FILENAME),
6944 serde_json::to_vec(&future).unwrap(),
6945 )
6946 .unwrap();
6947 assert!(matches!(
6948 MetaApplySink::open(tmp.path(), FeatureRegistry::current()),
6949 Err(MetaError::CorruptCheckpoint(_))
6950 ));
6951 }
6952
6953 #[test]
6954 fn v1_command_replays_through_the_sink_apply_path() {
6955 let tmp = tempfile::tempdir().unwrap();
6956 let mut sink = MetaApplySink::open(tmp.path(), FeatureRegistry::current()).unwrap();
6957 let register = MetaCommandRecord::new(MetaCommand::SetTableSchema {
6959 record: TableSchemaRecord {
6960 table_id: TableId(3),
6961 database_id: DatabaseId::from_bytes([4; 16]),
6962 schema_version: SchemaVersion(1),
6963 schema: serde_json::json!({"columns": []}),
6964 metadata_version: MetadataVersion::ZERO,
6965 },
6966 });
6967 let database = MetaCommandRecord::new(MetaCommand::CreateDatabase {
6968 descriptor: DatabaseDescriptor {
6969 database_id: DatabaseId::from_bytes([4; 16]),
6970 name: "app".to_owned(),
6971 created_at: ts(1_000),
6972 state: DatabaseState::Online,
6973 metadata_version: MetadataVersion::ZERO,
6974 },
6975 });
6976 let envelopes = [
6977 (1_u64, database.encode().unwrap()),
6978 (2, register.encode().unwrap()),
6979 (
6980 3,
6981 v1_encode(v1::MetaCommand::SetTabletDescriptor {
6982 descriptor: v1_tablet(1, v1::TabletState::Online),
6983 }),
6984 ),
6985 ];
6986 for (index, payload) in envelopes {
6987 let command = ReplicatedCommand::new(
6988 CommandKind::Catalog,
6989 CommandEnvelope::new(COMMAND_TYPE_META_COMMAND, [index as u8; 16], payload),
6990 ts(1_000),
6991 );
6992 ApplySink::apply(
6993 &mut sink,
6994 &AppliedCommand {
6995 position: LogPosition { term: 1, index },
6996 command,
6997 },
6998 )
6999 .unwrap();
7000 }
7001 let record = sink.state().tablet_record(tablet_id(1)).unwrap();
7003 assert_eq!(record.descriptor.state, TabletState::Active);
7004 assert_eq!(
7005 record.descriptor.replicas[0].raft_node_id,
7006 raft_node_id(&node_id(1))
7007 );
7008 assert_eq!(record.metadata_version, MetadataVersion(3));
7009 let mut descriptor = record.descriptor.clone();
7011 descriptor.generation = 5;
7012 let record_v2 = MetaCommandRecord::new(MetaCommand::SetTabletDescriptor {
7013 descriptor: descriptor.clone(),
7014 });
7015 let command = ReplicatedCommand::new(
7016 CommandKind::Catalog,
7017 CommandEnvelope::new(
7018 COMMAND_TYPE_META_COMMAND,
7019 [9; 16],
7020 record_v2.encode().unwrap(),
7021 ),
7022 ts(1_000),
7023 );
7024 ApplySink::apply(
7025 &mut sink,
7026 &AppliedCommand {
7027 position: LogPosition { term: 1, index: 4 },
7028 command,
7029 },
7030 )
7031 .unwrap();
7032 assert_eq!(sink.state().tablet(tablet_id(1)).unwrap(), &descriptor);
7033 assert!(sink.state().rejections().is_empty());
7034 }
7035
7036 #[test]
7037 fn reconciled_records_round_trip_serde() {
7038 let record = TabletRecord {
7039 descriptor: migrate_tablet(v1_tablet(1, v1::TabletState::Online)).descriptor,
7040 metadata_version: MetadataVersion(3),
7041 };
7042 let json = serde_json::to_vec(&record).unwrap();
7043 assert_eq!(
7044 serde_json::from_slice::<TabletRecord>(&json).unwrap(),
7045 record
7046 );
7047
7048 let policy = PlacementPolicyRecord {
7049 policy: migrate_policy(v1_policy()).policy,
7050 metadata_version: MetadataVersion(4),
7051 };
7052 let json = serde_json::to_vec(&policy).unwrap();
7053 assert_eq!(
7054 serde_json::from_slice::<PlacementPolicyRecord>(&json).unwrap(),
7055 policy
7056 );
7057
7058 let placement = ReplicaPlacement {
7059 raft_group_id: group_id(9),
7060 replicas: vec![ReplicaDescriptor {
7061 node_id: node_id(1),
7062 role: ReplicaRole::Voter,
7063 raft_node_id: raft_node_id(&node_id(1)),
7064 }],
7065 metadata_version: MetadataVersion(5),
7066 };
7067 let json = serde_json::to_vec(&placement).unwrap();
7068 assert_eq!(
7069 serde_json::from_slice::<ReplicaPlacement>(&json).unwrap(),
7070 placement
7071 );
7072 }
7073}