Skip to main content

mongreldb_cluster/
meta.rs

1//! Cluster meta (spec sections 11-12, Stages 2-3).
2//!
3//! Stage 2H (spec section 11.8, ADR-0010) landed the rolling-upgrade control
4//! surface: the cluster feature level and feature registry, the
5//! [`FeatureActivation`] record, rolling-upgrade planning
6//! ([`plan_rolling_upgrade`]), and rollback assessment ([`assess_rollback`]).
7//!
8//! Stage 3A (spec section 12.1) lands the meta control plane itself: a
9//! dedicated Raft group owning the cluster's control-plane state — never user
10//! row data. [`MetaState`] is the deterministic, serde-versioned state
11//! machine (membership and node descriptors, databases, table schemas,
12//! tablet descriptors, replica placement and policies, schema/index jobs,
13//! transaction status partitions, cluster settings, and the feature flags
14//! above); [`MetaCommand`] is the versioned command enum riding
15//! [`ReplicatedCommand::Catalog`] envelopes; [`MetaApplySink`] binds the
16//! state to `mongreldb-consensus`'s apply path; and [`MetaGroup`] is the
17//! bootstrap/membership/propose helper the node runtime drives.
18//!
19//! # Reconciliation notes
20//!
21//! - The descriptor family ([`TabletDescriptor`], [`ReplicaDescriptor`],
22//!   [`ReplicaRole`], [`PartitionBounds`], [`TabletState`]) and the placement
23//!   contract ([`PlacementPolicy`], [`LocalityConstraint`]) are the canonical
24//!   `crate::tablet` / `crate::placement` types, re-exported here so
25//!   `crate::meta::*` paths keep resolving. Meta records wrap them with the
26//!   meta state's [`MetadataVersion`] ([`TabletRecord`], [`ReplicaPlacement`],
27//!   [`PlacementPolicyRecord`]); a descriptor's own `generation` remains the
28//!   last-writer-wins guard.
29//! - [`SchemaJobKind`]/[`SchemaJobState`] minimally mirror the core job
30//!   registry's concepts (`mongreldb-core` `jobs.rs`), which the cluster
31//!   crate deliberately does not depend on. Distributed DDL (spec section
32//!   12.11) reconciles the two registries.
33//! - [`DefaultConsistency`] is a payload-free mirror of
34//!   `mongreldb_consensus::read::ReadConsistency` suitable as a cluster-wide
35//!   default (request-scoped token/timestamp variants carry no payload here).
36//!
37//! # Format v1 migration (spec sections 4.10, 17)
38//!
39//! The first meta control-plane build (format v1) replicated meta-local
40//! minimal mirrors of the `crate::tablet` / `crate::placement` types. The
41//! type reconciliation adopted the canonical types as format v2. v1 command
42//! records and v1 state checkpoints remain decodable: every decode probes the
43//! `format_version` field and routes v1 payloads through the [`v1`]
44//! compatibility module, migrating them to the canonical shapes before apply:
45//!
46//! - partition bounds `{start, end}` map onto `{low, high}` with the v1
47//!   semantics (start inclusive, end exclusive);
48//! - tablet states map `Online -> Active`, `Offline -> Retiring` (`Creating`
49//!   is unchanged);
50//! - v1 replicas carried no per-group raft id; they gain
51//!   `raft_node_id = raft_node_id(node_id)`, the projection the v1 group
52//!   actually used;
53//! - v1 voter constraints were hard requirements (`required = true`) and v1
54//!   leader preferences soft ones (`required = false`);
55//! - records keep their v1 `metadata_version`.
56//!
57//! New writes are stamped v2; v1 is accepted forever (the minimum supported
58//! version constants stay at 1) so checkpoints at
59//! `raft/state/meta-state.json` and log entries written by a v1 binary load
60//! and replay after upgrade.
61
62use 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/// Cluster-wide feature level (spec section 17: separate from binary
87/// version; ADR-0010 decision 3).
88///
89/// The level never lowers: it rises only when a [`FeatureActivation`] is
90/// applied at quorum, and rolling it back requires the restore-based path
91/// documented in [`RollbackPath::RestoreFromBackup`].
92#[derive(
93    Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize,
94)]
95pub struct ClusterFeatureLevel(pub u64);
96
97impl ClusterFeatureLevel {
98    /// The level of a cluster that has activated no features.
99    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/// Registry of gated features: feature name to the minimum
109/// [`ClusterFeatureLevel`] at which the feature may be activated.
110///
111/// Declarations are append-only and levels are never reused for a different
112/// feature (spec section 4.10). Stage 2H ships the activation mechanism
113/// before the first gated feature — ADR-0010 requires feature work to land
114/// dark at least one release before activation — so
115/// [`FeatureRegistry::current`] is empty; later waves declare features there.
116#[derive(Clone, Debug, Default, PartialEq, Eq)]
117pub struct FeatureRegistry {
118    required_level: BTreeMap<String, u64>,
119}
120
121impl FeatureRegistry {
122    /// The feature registry of the running binary.
123    pub fn current() -> Self {
124        Self::default()
125    }
126
127    /// Declare a gated feature and the minimum level that activates it.
128    pub fn declare(&mut self, feature: impl Into<String>, level: ClusterFeatureLevel) {
129        self.required_level.insert(feature.into(), level.0);
130    }
131
132    /// The minimum level at which `feature` may be activated, if the feature
133    /// is registered.
134    pub fn required_level(&self, feature: &str) -> Option<ClusterFeatureLevel> {
135        self.required_level
136            .get(feature)
137            .copied()
138            .map(ClusterFeatureLevel)
139    }
140
141    /// Whether `feature` is active at `level` (spec section 11.8).
142    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    /// The registered feature names; a node's advertised
148    /// [`VersionInfo::feature_set`] is drawn from this set.
149    pub fn feature_names(&self) -> BTreeSet<String> {
150        self.required_level.keys().cloned().collect()
151    }
152}
153
154/// Record of one cluster feature activation (spec section 11.8).
155///
156/// Feature activation is a replicated catalog command (ADR-0010 decision 4):
157/// the catalog-command variant that carries this record through the command
158/// envelope lands with the meta-group integration, and the apply path there
159/// re-runs [`FeatureActivation::validate`] at quorum. Defined here so the
160/// record shape and the activation rule exist before that integration.
161#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
162#[serde(deny_unknown_fields)]
163pub struct FeatureActivation {
164    /// Registered name of the feature being activated.
165    pub feature: String,
166    /// Cluster feature level this activation raises the cluster to.
167    pub level: ClusterFeatureLevel,
168    /// Commit timestamp of the activation (assigned by the commit sequencer
169    /// once the command is replicated).
170    pub activated_at: HlcTimestamp,
171    /// Node that proposed the activation.
172    pub activated_by: NodeId,
173}
174
175/// Why a [`FeatureActivation`] may not be applied. Activation failures fail
176/// closed (ADR-0010).
177///
178/// Serde derives exist so the meta group's rejection journal
179/// ([`MetaRejection`]) can record the typed reason inside replicated state.
180#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, thiserror::Error)]
181pub enum FeatureActivationError {
182    /// The feature is not declared in this binary's registry.
183    #[error("feature `{feature}` is not declared in the feature registry")]
184    UnknownFeature {
185        /// The feature that was to be activated.
186        feature: String,
187    },
188    /// The activation level is below the feature's registered minimum.
189    #[error(
190        "feature `{feature}` requires cluster feature level {required}; \
191         activation attempted at {attempted}"
192    )]
193    LevelBelowRequirement {
194        /// The feature that was to be activated.
195        feature: String,
196        /// Registered minimum level for the feature.
197        required: ClusterFeatureLevel,
198        /// Level the activation attempted.
199        attempted: ClusterFeatureLevel,
200    },
201    /// The activation would lower the cluster feature level; the level never
202    /// regresses (ADR-0010: no in-place un-activate).
203    #[error(
204        "cluster feature level never lowers: current level {current}, \
205         activation attempted at {attempted}"
206    )]
207    LevelRegression {
208        /// Current cluster feature level.
209        current: ClusterFeatureLevel,
210        /// Level the activation attempted.
211        attempted: ClusterFeatureLevel,
212    },
213    /// A voter's advertisement does not include the feature (spec section
214    /// 11.8 step 5: enable new features only after every voter supports
215    /// them).
216    #[error("feature `{feature}` cannot activate: voter {node} does not support it")]
217    UnsupportedByVoter {
218        /// The feature that was to be activated.
219        feature: String,
220        /// The first voter whose [`VersionInfo::feature_set`] lacks it.
221        node: NodeId,
222    },
223    /// Activation with no voters is meaningless; fail closed.
224    #[error("feature activation requires at least one voter")]
225    NoVoters,
226}
227
228impl FeatureActivation {
229    /// Validate the activation against the registry, the current cluster
230    /// level, and every voter's advertised [`VersionInfo`].
231    ///
232    /// `voters` must be exactly the current voter set of the group that will
233    /// apply the command. The rule (spec section 11.8 step 5): a feature may
234    /// activate only when every voter supports it, at a level that satisfies
235    /// the registry minimum and never lowers the cluster level.
236    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/// One ordered step of a rolling upgrade (spec section 11.8, ADR-0010
276/// decision 6).
277#[derive(Clone, Copy, Debug, PartialEq, Eq)]
278pub enum UpgradeStep {
279    /// Upgrade one follower to the target binary, one at a time, waiting for
280    /// it to rejoin and catch up before the next step.
281    UpgradeFollower {
282        /// The follower to upgrade.
283        node_id: NodeId,
284    },
285    /// Move leadership off the current leader so its upgrade interrupts no
286    /// writes.
287    TransferLeadership {
288        /// The leader to move leadership away from.
289        from: NodeId,
290    },
291    /// Upgrade the former leader; it is always the last node upgraded.
292    UpgradeFormerLeader {
293        /// The former leader to upgrade.
294        node_id: NodeId,
295    },
296    /// Final, explicit gate: propose [`FeatureActivation`]s for the new
297    /// binary's features, only after every voter runs the target binary.
298    /// Never implicit — activation is an operator decision applied at quorum
299    /// (ADR-0010 decision 3).
300    EnableNewFeatures,
301}
302
303/// A validated rolling-upgrade plan: the target advertisement plus the
304/// ordered steps to reach it.
305#[derive(Clone, Debug, PartialEq, Eq)]
306pub struct UpgradePlan {
307    /// Version advertisement every node is upgraded to.
308    pub target: VersionInfo,
309    /// Ordered upgrade steps; see [`UpgradeStep`].
310    pub steps: Vec<UpgradeStep>,
311}
312
313/// Why a rolling upgrade cannot be planned. Planning failures fail closed
314/// (ADR-0010).
315#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
316pub enum UpgradePlanError {
317    /// No nodes were supplied.
318    #[error("cannot plan a rolling upgrade for an empty membership")]
319    EmptyMembership,
320    /// The named leader is absent from the supplied membership.
321    #[error("current leader {leader} is not present in the supplied membership")]
322    LeaderNotInMembership {
323        /// The leader that was looked up.
324        leader: NodeId,
325    },
326    /// The same node appeared twice.
327    #[error("node {node} appears more than once in the supplied membership")]
328    DuplicateNode {
329        /// The duplicated node.
330        node: NodeId,
331    },
332    /// A node's advertisement cannot interoperate with the target binary
333    /// (spec section 11.8 step 1: verify compatibility).
334    #[error("node {node} is not compatible with the upgrade target: {incompatibility}")]
335    IncompatibleNode {
336        /// The incompatible node.
337        node: NodeId,
338        /// The first non-overlapping advertised range.
339        incompatibility: Incompatibility,
340    },
341}
342
343/// Plan a rolling upgrade of `nodes` to the `target` binary (spec section
344/// 11.8).
345///
346/// Every node's advertised [`VersionInfo`] is verified against `target`
347/// first (step 1); any mismatch fails closed with
348/// [`UpgradePlanError::IncompatibleNode`]. The resulting plan upgrades
349/// followers one at a time in membership order (step 2), transfers
350/// leadership off `current_leader` (step 3 — omitted for a single-node
351/// membership, where there is no peer to receive it), upgrades the former
352/// leader last (step 4), and ends with the explicit enable-new-features gate
353/// (step 5), which the operator executes via [`FeatureActivation`].
354pub 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/// The supported rollback path for an upgrade in flight (spec section 17;
407/// ADR-0010 reversal strategy).
408#[derive(Clone, Copy, Debug, PartialEq, Eq)]
409pub enum RollbackPath {
410    /// Binary downgrade node by node, former leader last. Supported only
411    /// before any feature activation: no required N-only command has been
412    /// emitted and snapshots are still written in a format the previous
413    /// reader accepts, so every byte of durable state remains
414    /// previous-binary readable.
415    BinaryDowngrade,
416    /// Restore-based rollback: binary downgrade alone is insufficient once a
417    /// feature has activated. Restore from a backup/snapshot taken before
418    /// activation, then replay the committed log up to a pre-activation
419    /// fence (spec section 17: on-disk downgrade is not implied).
420    RestoreFromBackup,
421}
422
423/// Assessment of how an upgrade in flight may be abandoned.
424#[derive(Clone, Debug, PartialEq, Eq)]
425pub struct RollbackAssessment {
426    /// The supported rollback path.
427    pub path: RollbackPath,
428    /// Features whose activation closed the binary-downgrade window; empty
429    /// when [`RollbackPath::BinaryDowngrade`] is still available.
430    pub activated_features: Vec<String>,
431}
432
433/// Assess the supported rollback path given the features activated so far.
434///
435/// Before any feature activation a node downgrade is safe
436/// ([`RollbackPath::BinaryDowngrade`]); the first activation ends the
437/// rollback window and leaves only the restore-based path (spec section 17).
438pub 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
454// ---------------------------------------------------------------------------
455// Stage 3A — meta control plane (spec section 12.1)
456// ---------------------------------------------------------------------------
457//
458// The meta group is a dedicated Raft group owning control-plane state only;
459// user row data never enters it (spec section 12.1). Commands ride
460// `ReplicatedCommand::Catalog` envelopes stamped with
461// [`COMMAND_TYPE_META_COMMAND`]; the apply path is deterministic and total:
462// a refused command never faults the raft state machine — it is recorded in
463// the bounded rejection journal ([`MetaState::rejections`]) and surfaced to
464// the proposer by [`MetaGroup::propose`].
465//
466// # Serde versioning (spec sections 4.10, 17)
467//
468// [`MetaState`] and [`MetaCommandRecord`] carry explicit `format_version`
469// fields checked on decode (fail closed outside the supported range). Unlike
470// the crate's static metadata files, these types deliberately omit
471// `deny_unknown_fields`: they travel in log entries and snapshots, where
472// spec section 17 requires an N-1 node to ignore optional N fields during
473// the rolling-upgrade window. Additive evolution lands as new optional
474// fields with serde defaults; new required command variants ship dark behind
475// feature activation (ADR-0010).
476
477/// Format version of [`MetaCommandRecord`] payloads this build writes.
478///
479/// v2 reconciles the tablet/placement payload types onto the canonical
480/// `crate::tablet` / `crate::placement` shapes; v1 payloads (meta-local
481/// mirrors) remain decodable and migrate on read (see the module docs).
482pub const META_COMMAND_FORMAT_VERSION: u32 = 2;
483/// Oldest [`MetaCommandRecord`] format version this build accepts.
484pub const MIN_SUPPORTED_META_COMMAND_FORMAT_VERSION: u32 = 1;
485/// Format version of [`MetaState`] snapshots this build writes (see
486/// [`META_COMMAND_FORMAT_VERSION`] for the v1 reconciliation).
487pub const META_STATE_FORMAT_VERSION: u32 = 2;
488/// Oldest [`MetaState`] snapshot format version this build accepts.
489pub const MIN_SUPPORTED_META_STATE_FORMAT_VERSION: u32 = 1;
490/// `CommandEnvelope::command_type` of meta control-plane commands.
491///
492/// Envelope discriminants are never reused (spec section 4.10): `1` is the
493/// transaction command (`mongreldb-core` `commit_log`), `2` the engine
494/// catalog command, and `3` the maintenance command (`mongreldb-core`
495/// `replicated_apply`); `4` is the meta control-plane command.
496pub const COMMAND_TYPE_META_COMMAND: u32 = 4;
497/// Bound on [`MetaState::rejections`] (mirrors the engine catalog's
498/// `COMMAND_HISTORY_LIMIT`).
499/// Bound on the meta rejection journal (review **N6**): proposers read the
500/// journal immediately after local apply via `client_write`, so age-out before
501/// the proposer observes a rejection is not a production hazard. Growing the
502/// journal unboundedly would be worse.
503pub const META_REJECTION_LIMIT: usize = 256;
504/// First per-group raft node id the meta-owned allocator hands out (spec
505/// section 12.1: the meta control plane owns the node-id ↔ raft-id mapping
506/// for tablet groups). Id 0 is never allocated; the meta group itself uses
507/// the `raft_node_id` projection of the member node ids, which the
508/// allocator skips over (and [`MetaCommand::RegisterNode`] refuses a node
509/// whose projection collides with an id already assigned to a replica).
510pub const FIRST_RAFT_NODE_ID: u64 = 1;
511/// Largest single [`MetaCommand::AllocateRaftNodeIds`] request.
512pub const MAX_RAFT_NODE_ID_ALLOCATION: u32 = 4096;
513/// Bound on [`MetaState::raft_id_allocations`] (the idempotent-replay
514/// record of recent allocations).
515pub const RAFT_ID_ALLOCATION_RECORD_LIMIT: usize = 1024;
516/// Bound on the collision-skip scan of one allocation: the allocator
517/// advances past at most this many already-used ids before refusing
518/// (fail closed; in practice ids collide negligibly often).
519pub const RAFT_ID_ALLOCATION_SCAN_LIMIT: u64 = 1 << 20;
520/// Substrings (matched case-insensitively) that bar a key from the cluster
521/// settings: secrets are never stored as plaintext cluster settings (spec
522/// section 16.2). TLS private keys, backup credentials, and encryption-key
523/// material live in static node configuration or the engine's key hierarchy,
524/// never in replicated meta state. The match is deliberately conservative —
525/// a legitimate key containing one of these substrings must be renamed.
526pub const SECRET_SETTING_KEY_DENYLIST: &[&str] = &[
527    "secret",
528    "password",
529    "passwd",
530    "private_key",
531    "api_key",
532    "token",
533    "credential",
534];
535
536/// The fixed cluster-setting keys ([`MetaCommand::SetClusterSetting`]).
537/// `resource_groups.<name>` is the one dynamic key shape; everything else is
538/// listed here.
539pub 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
550/// Default for [`ClusterSettings::max_concurrent_jobs`] (mirrors the core job
551/// registry's `DEFAULT_MAX_CONCURRENT_JOBS`).
552pub const DEFAULT_MAX_CONCURRENT_JOBS: u32 = 2;
553/// Default for [`BackupPolicy::interval_seconds`] (daily).
554pub const DEFAULT_BACKUP_INTERVAL_SECONDS: u64 = 86_400;
555/// Default for [`BackupPolicy::retention_count`].
556pub const DEFAULT_BACKUP_RETENTION_COUNT: u32 = 7;
557/// Default for [`AiLimits::max_concurrent_requests`].
558pub const DEFAULT_AI_MAX_CONCURRENT_REQUESTS: u32 = 16;
559/// Default for [`AiLimits::max_memory_bytes`] (512 MiB).
560pub const DEFAULT_AI_MAX_MEMORY_BYTES: u64 = 512 * 1024 * 1024;
561
562fn zero_metadata_version() -> MetadataVersion {
563    MetadataVersion::ZERO
564}
565
566/// Lowercase hex of a byte string (command-id map keys; JSON map keys must
567/// be strings).
568fn 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/// Errors of the meta control-plane surface (group factory, membership
579/// workflow, proposals).
580#[derive(Debug, thiserror::Error)]
581pub enum MetaError {
582    /// Consensus group failure (including the routed
583    /// [`ConsensusError::NotLeader`] leader hint, spec section 11.7).
584    #[error(transparent)]
585    Consensus(#[from] ConsensusError),
586    /// Encoding a [`MetaCommandRecord`] failed.
587    #[error("meta command encoding failed: {0}")]
588    Encode(String),
589    /// The command committed but the apply path refused it; the typed reason
590    /// is also journaled in [`MetaState::rejections`].
591    #[error("meta command refused at apply: {0}")]
592    Rejected(MetaRejectionReason),
593    /// The caller's request was malformed for this group (raft-id projection
594    /// collision, mismatched group config, invalid workflow order).
595    #[error("invalid meta group request: {0}")]
596    InvalidRequest(String),
597    /// Meta group I/O failure.
598    #[error("meta group I/O error: {0}")]
599    Io(#[from] std::io::Error),
600    /// The caller-supplied operating-system CSPRNG failed.
601    #[error("operating-system CSPRNG failed: {0}")]
602    Rng(String),
603    /// The sink's durable checkpoint failed verification (fails closed, spec
604    /// section 4.10).
605    #[error("corrupt meta state checkpoint: {0}")]
606    CorruptCheckpoint(String),
607}
608
609/// Why a [`MetaCommandRecord`] payload could not be decoded. Decode failures
610/// fail closed (spec section 4.10).
611#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
612pub enum MetaDecodeError {
613    /// The payload is not a well-formed record.
614    #[error("meta command decode failed: {0}")]
615    Malformed(String),
616    /// The record's format version is outside the supported range.
617    #[error("unsupported meta command format version {found} (supported {min}..={max})")]
618    UnsupportedVersion {
619        /// Version found in the payload.
620        found: u32,
621        /// Oldest version this build accepts.
622        min: u32,
623        /// Newest version this build accepts.
624        max: u32,
625    },
626}
627
628// ---------------------------------------------------------------------------
629// Ownership records (spec section 12.1)
630// ---------------------------------------------------------------------------
631
632/// Lifecycle state of a logical database.
633#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
634pub enum DatabaseState {
635    /// Serving traffic.
636    Online,
637    /// Being dropped; drained of tablets before removal.
638    Dropping,
639}
640
641/// One logical database owned by the meta group (spec section 12.1).
642#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
643pub struct DatabaseDescriptor {
644    /// The database's durable identifier.
645    pub database_id: DatabaseId,
646    /// Unique database name.
647    pub name: String,
648    /// Creation timestamp stamped by the proposer.
649    pub created_at: HlcTimestamp,
650    /// Lifecycle state.
651    pub state: DatabaseState,
652    /// Meta state's [`MetadataVersion`] of the last modification.
653    #[serde(default = "zero_metadata_version")]
654    pub metadata_version: MetadataVersion,
655}
656
657/// Replicated schema of one table: the opaque schema document (JSON) plus
658/// its monotonic version. Last-writer-wins by `schema_version`.
659#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
660pub struct TableSchemaRecord {
661    /// The table's durable identifier.
662    pub table_id: TableId,
663    /// Database owning the table.
664    pub database_id: DatabaseId,
665    /// Monotonic schema version (never reused, never lowered).
666    pub schema_version: SchemaVersion,
667    /// The schema document (opaque to the meta group; the engine interprets
668    /// it at DDL apply time).
669    pub schema: serde_json::Value,
670    /// Meta state's [`MetadataVersion`] of the last modification.
671    #[serde(default = "zero_metadata_version")]
672    pub metadata_version: MetadataVersion,
673}
674
675// ---------------------------------------------------------------------------
676// Canonical descriptor types (reconciled with crate::tablet / crate::placement)
677// ---------------------------------------------------------------------------
678
679/// The canonical locality constraint (`crate::placement`), re-exported.
680pub use crate::placement::LocalityConstraint;
681/// The canonical placement policy (`crate::placement`), re-exported.
682pub use crate::placement::PlacementPolicy;
683/// The canonical partition bounds (`crate::tablet`), re-exported.
684pub use crate::tablet::PartitionBounds;
685/// The canonical replica descriptor (`crate::tablet`), re-exported.
686pub use crate::tablet::ReplicaDescriptor;
687/// The canonical replica role (`crate::tablet`), re-exported.
688pub use crate::tablet::ReplicaRole;
689/// The canonical tablet descriptor (`crate::tablet`), re-exported.
690pub use crate::tablet::TabletDescriptor;
691/// The canonical tablet lifecycle state (`crate::tablet`), re-exported.
692pub use crate::tablet::TabletState;
693
694/// Replica set of one raft group (spec section 12.1 "replica placement"): the
695/// canonical `crate::tablet` replica descriptors wrapped with the meta state's
696/// modification version.
697#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
698pub struct ReplicaPlacement {
699    /// The group whose replicas are placed.
700    pub raft_group_id: RaftGroupId,
701    /// Placed replicas and their roles.
702    pub replicas: Vec<ReplicaDescriptor>,
703    /// Meta state's [`MetadataVersion`] of the last modification.
704    #[serde(default = "zero_metadata_version")]
705    pub metadata_version: MetadataVersion,
706}
707
708/// Replicated tablet record: the canonical `crate::tablet` descriptor wrapped
709/// with the meta state's modification version (observability and the
710/// optimistic-concurrency token of other records; the descriptor's own
711/// `generation` remains the last-writer-wins guard).
712#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
713pub struct TabletRecord {
714    /// The tablet descriptor.
715    pub descriptor: TabletDescriptor,
716    /// Meta state's [`MetadataVersion`] of the last modification.
717    #[serde(default = "zero_metadata_version")]
718    pub metadata_version: MetadataVersion,
719}
720
721/// Replicated placement-policy record: the canonical `crate::placement`
722/// policy wrapped with the meta state's modification version.
723#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
724pub struct PlacementPolicyRecord {
725    /// The placement policy.
726    pub policy: PlacementPolicy,
727    /// Meta state's [`MetadataVersion`] of the last modification.
728    #[serde(default = "zero_metadata_version")]
729    pub metadata_version: MetadataVersion,
730}
731
732/// Schema/index job kinds owned by the meta group. Minimal mirror of the
733/// core registry's DDL-relevant `JobKind`s (see module reconciliation notes;
734/// spec section 12.11 wires these to distributed DDL).
735#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
736pub enum SchemaJobKind {
737    /// Online secondary-index build.
738    IndexBuild,
739    /// Backfill of a newly added or altered column.
740    ColumnBackfill,
741    /// Validation of existing rows against a new schema constraint.
742    SchemaValidation,
743}
744
745/// Schema job lifecycle. Minimal mirror of the core registry's `JobState`
746/// (see module reconciliation notes); the transition graph is identical.
747#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
748pub enum SchemaJobState {
749    /// Submitted, waiting for admission.
750    Pending,
751    /// Admitted; a worker is actively driving it.
752    Running,
753    /// Parked; resumes from the last durable checkpoint.
754    Paused,
755    /// Cancellation requested; rollback has not finished.
756    Cancelling,
757    /// Terminal: every phase completed and published.
758    Succeeded,
759    /// Terminal: failed or cancelled.
760    Failed,
761    /// A phase failed or a cancel was observed; rollback is in progress.
762    RollingBack,
763}
764
765impl SchemaJobState {
766    /// Terminal states have no outgoing edges.
767    pub fn is_terminal(self) -> bool {
768        matches!(self, Self::Succeeded | Self::Failed)
769    }
770
771    /// Whether the `self -> next` edge exists in the job graph (mirrors the
772    /// core registry's `JobState::can_transition`).
773    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/// One replicated schema/index job record (spec section 12.1 "schema/index
795/// jobs").
796#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
797pub struct SchemaJobRecord {
798    /// Job identifier (allocated by the proposer; never reused).
799    pub job_id: u64,
800    /// Database owning the job's target.
801    pub database_id: DatabaseId,
802    /// Table the job operates on.
803    pub table_id: TableId,
804    /// Job kind.
805    pub kind: SchemaJobKind,
806    /// Lifecycle state; submissions start [`SchemaJobState::Pending`].
807    pub state: SchemaJobState,
808    /// Submission timestamp stamped by the proposer.
809    pub submitted_at: HlcTimestamp,
810    /// Timestamp of the last state update.
811    pub updated_at: HlcTimestamp,
812    /// Failure detail for terminal/rolling-back states.
813    pub error: Option<String>,
814    /// Meta state's [`MetadataVersion`] of the last modification.
815    #[serde(default = "zero_metadata_version")]
816    pub metadata_version: MetadataVersion,
817}
818
819/// One transaction status partition and its home group (spec sections 12.1,
820/// 12.8): the coordinator record of a distributed transaction lives on the
821/// partition's home raft group.
822#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
823pub struct TxnStatusPartition {
824    /// Partition identifier (derived from the transaction id, spec section
825    /// 12.8).
826    pub partition_id: u32,
827    /// Raft group owning the partition's transaction status records.
828    pub home_raft_group: RaftGroupId,
829}
830
831// ---------------------------------------------------------------------------
832// Dynamic cluster settings (spec section 16.2)
833// ---------------------------------------------------------------------------
834
835/// Per-resource-group limits stored as cluster settings. A zero field means
836/// "no group-specific cap" (the node's static limits still apply).
837#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
838#[serde(default)]
839pub struct ResourceGroupSetting {
840    /// Memory cap in bytes.
841    pub max_memory_bytes: u64,
842    /// Concurrent query cap.
843    pub max_concurrent_queries: u32,
844    /// Temporary-disk (spill) budget in bytes.
845    pub temp_disk_budget_bytes: u64,
846}
847
848/// Cluster-wide backup policy setting (spec section 16.2). Backup
849/// destinations and their credentials are static node configuration — never
850/// cluster settings (see [`SECRET_SETTING_KEY_DENYLIST`]).
851#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
852#[serde(default)]
853pub struct BackupPolicy {
854    /// Whether scheduled backups run.
855    pub enabled: bool,
856    /// Interval between scheduled backups.
857    pub interval_seconds: u64,
858    /// How many completed backups are retained.
859    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/// Cluster-wide AI limits (spec section 16.2; AI limits are a security
873/// boundary — the engine fails closed at or below these).
874#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
875#[serde(default)]
876pub struct AiLimits {
877    /// Concurrent AI request cap.
878    pub max_concurrent_requests: u32,
879    /// Total AI memory budget in bytes.
880    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/// Cluster-wide default read consistency (spec sections 11.4, 16.2).
893/// Payload-free mirror of `mongreldb_consensus::read::ReadConsistency` (the
894/// request-scoped token/timestamp variants carry no payload here).
895#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
896pub enum DefaultConsistency {
897    /// Leader read-index + wait applied (the default; strongest).
898    #[default]
899    Linearizable,
900    /// Wait until the replica applied the session's last write.
901    ReadYourWrites,
902    /// Serve at a requested snapshot timestamp.
903    Snapshot,
904    /// Serve if the applied watermark lags by at most `max_lag_ms`.
905    BoundedStaleness {
906        /// Maximum tolerated lag in milliseconds.
907        max_lag_ms: u64,
908    },
909    /// Serve the local applied watermark immediately.
910    Eventual,
911}
912
913/// The replicated dynamic cluster settings (spec section 16.2). Placement
914/// policies are first-class records ([`MetaState::placement_policies`], set
915/// by [`MetaCommand::SetPlacementPolicy`]) rather than scalar settings, and
916/// feature activation rides [`MetaCommand::ActivateFeature`]; the remaining
917/// section 16.2 categories live here. Secrets are never stored as plaintext
918/// settings (enforced by [`SECRET_SETTING_KEY_DENYLIST`]).
919#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
920#[serde(default)]
921pub struct ClusterSettings {
922    /// Resource-group limits by group name.
923    pub resource_groups: BTreeMap<String, ResourceGroupSetting>,
924    /// MVCC history retention in epochs (0 = disabled; mirrors the core's
925    /// `history_retention_epochs`).
926    pub history_retention_epochs: u64,
927    /// Cluster-wide backup policy.
928    pub backup: BackupPolicy,
929    /// Default read consistency for sessions that do not request one.
930    pub default_consistency: DefaultConsistency,
931    /// Cluster-wide AI limits.
932    pub ai: AiLimits,
933    /// Bound on concurrently active jobs.
934    pub max_concurrent_jobs: u32,
935}
936
937impl ClusterSettings {
938    /// Applies one key/value setting; see [`KNOWN_SETTING_KEYS`] and
939    /// `resource_groups.<name>` (a `null` value removes the group). The
940    /// denylist runs first, so a denied key is refused even when unknown.
941    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// ---------------------------------------------------------------------------
1046// Apply outcomes
1047// ---------------------------------------------------------------------------
1048
1049/// Why a [`MetaCommand`] was refused at apply. Refusals are deterministic
1050/// (every replica reaches the same conclusion from the same state) and never
1051/// fault the raft state machine: they are journaled in
1052/// [`MetaState::rejections`] and reported to the proposer by
1053/// [`MetaGroup::propose`].
1054#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, thiserror::Error)]
1055pub enum MetaRejectionReason {
1056    /// Feature activation failed validation (spec section 11.8 step 5;
1057    /// re-validated at apply, ADR-0010 decision 4).
1058    #[error(transparent)]
1059    FeatureActivation(#[from] FeatureActivationError),
1060    /// The setting key is on the secrets denylist (spec section 16.2).
1061    #[error(
1062        "cluster setting key `{key}` is denied: secrets are never stored as \
1063         plaintext cluster settings"
1064    )]
1065    SecretSettingKey {
1066        /// The denied key.
1067        key: String,
1068    },
1069    /// The setting key maps to no typed setting.
1070    #[error("unknown cluster setting key `{key}`")]
1071    UnknownSettingKey {
1072        /// The unknown key.
1073        key: String,
1074    },
1075    /// The setting value failed typed parsing or validation.
1076    #[error("invalid value for cluster setting `{key}`: {reason}")]
1077    InvalidSettingValue {
1078        /// The setting key.
1079        key: String,
1080        /// Why the value failed.
1081        reason: String,
1082    },
1083    /// A last-writer-wins guard rejected an out-of-date write.
1084    #[error("stale write to {resource}: current version {current}, attempted {attempted}")]
1085    StaleWrite {
1086        /// What was being written.
1087        resource: String,
1088        /// Version currently stored.
1089        current: MetadataVersion,
1090        /// Version the command carried.
1091        attempted: MetadataVersion,
1092    },
1093    /// The command conflicts with existing state.
1094    #[error("conflicting {resource}: {reason}")]
1095    Conflict {
1096        /// What conflicted.
1097        resource: String,
1098        /// Why it conflicted.
1099        reason: String,
1100    },
1101    /// A referenced record does not exist.
1102    #[error("{resource} not found")]
1103    NotFound {
1104        /// What was looked up.
1105        resource: String,
1106    },
1107    /// The command itself is malformed.
1108    #[error("invalid meta command: {reason}")]
1109    Invalid {
1110        /// Why the command is invalid.
1111        reason: String,
1112    },
1113    /// A meta-plane binding's proposal transport failed (leader loss,
1114    /// timeout, shutdown) before the command's outcome was known. Never
1115    /// journaled from apply — apply-time refusals are deterministic; this
1116    /// surfaces only through the
1117    /// [`crate::split::TabletMetaPlane`]/[`crate::merge::MergeMetaPlane`]
1118    /// seams the node runtime drives. Every split/merge descriptor write is
1119    /// idempotent, so retrying the failed step is safe.
1120    #[error("meta proposal failed: {reason}")]
1121    ProposalFailed {
1122        /// What failed.
1123        reason: String,
1124    },
1125    /// The node receiving the proposal is not the meta leader (spec section
1126    /// 11.7): the caller re-resolves the leader and retries there. Carried
1127    /// structured (never stringified) so gateways and split/merge drivers can
1128    /// pattern-match it; split/merge descriptor writes are idempotent, so the
1129    /// retry is safe.
1130    #[error("not the meta leader (current leader: {leader:?})")]
1131    NotLeader {
1132        /// The node's current belief about the meta leader's raft id, if any.
1133        leader: Option<u64>,
1134    },
1135}
1136
1137/// One journaled refusal: the refused command's id and the typed reason.
1138#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
1139pub struct MetaRejection {
1140    /// Leader-assigned id of the refused command (`None` when the command
1141    /// carried no id; always `Some` through [`MetaGroup::propose`]).
1142    pub command_id: Option<[u8; 16]>,
1143    /// Why the command was refused.
1144    pub reason: MetaRejectionReason,
1145}
1146
1147// ---------------------------------------------------------------------------
1148// MetaCommand
1149// ---------------------------------------------------------------------------
1150
1151/// One replicated meta control-plane command (spec section 12.1).
1152///
1153/// Every variant is deterministic and idempotent at apply: records are
1154/// versioned ([`MetadataVersion`], `schema_version`, or `generation` per
1155/// record) and last-writer-wins guards reject stale or conflicting writes
1156/// with a typed [`MetaRejectionReason`] instead of faulting the state
1157/// machine.
1158#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1159pub enum MetaCommand {
1160    /// Registers (or re-registers) a cluster node's descriptor. Identical
1161    /// re-registration is a no-op.
1162    RegisterNode {
1163        /// The node's advertised descriptor.
1164        descriptor: NodeDescriptor,
1165    },
1166    /// Updates a node's lifecycle state; `Decommissioned` is terminal.
1167    UpdateNodeState {
1168        /// The node to update.
1169        node_id: NodeId,
1170        /// Requested lifecycle state.
1171        state: NodeState,
1172        /// Optimistic-concurrency guard: when `Some`, the write applies only
1173        /// if the record's current [`MetadataVersion`] equals it.
1174        expected_version: Option<MetadataVersion>,
1175    },
1176    /// Removes a node from membership. Refused while any tablet or placement
1177    /// still references the node; absent nodes are a no-op.
1178    RemoveNode {
1179        /// The node to remove.
1180        node_id: NodeId,
1181    },
1182    /// Creates a logical database. Name and id must both be free.
1183    CreateDatabase {
1184        /// The new database's descriptor.
1185        descriptor: DatabaseDescriptor,
1186    },
1187    /// Drops a database. Refused while tables reference it; absent databases
1188    /// are a no-op.
1189    DropDatabase {
1190        /// The database to drop.
1191        database_id: DatabaseId,
1192    },
1193    /// Publishes a table schema (last-writer-wins by `schema_version`).
1194    SetTableSchema {
1195        /// The schema record to publish.
1196        record: TableSchemaRecord,
1197    },
1198    /// Publishes a tablet descriptor (last-writer-wins by `generation`).
1199    ///
1200    /// Review **m7**: LWW is the *intended* concurrency model for
1201    /// `SetTabletDescriptor` / `SetTableSchema`. Only `UpdateNodeState` and
1202    /// `UpdateSchemaJob` carry `expected_version` CAS tokens. Two admins
1203    /// doing read-modify-write on one tablet descriptor can lose updates
1204    /// silently when the later write carries a higher generation — tooling
1205    /// must re-read before rewrite, or use a CAS-guarded command when added.
1206    SetTabletDescriptor {
1207        /// The tablet descriptor to publish.
1208        descriptor: TabletDescriptor,
1209    },
1210    /// Removes a tablet descriptor at or above its stored `generation`.
1211    RemoveTabletDescriptor {
1212        /// The tablet to remove.
1213        tablet_id: TabletId,
1214        /// Removal generation; below the stored generation the command is
1215        /// stale.
1216        generation: u64,
1217    },
1218    /// Publishes the replica placement of one raft group.
1219    SetReplicaPlacement {
1220        /// The placement to publish.
1221        placement: ReplicaPlacement,
1222    },
1223    /// Publishes (or replaces) a named placement policy (spec section 12.7).
1224    SetPlacementPolicy {
1225        /// Policy name.
1226        name: String,
1227        /// The policy.
1228        policy: PlacementPolicy,
1229    },
1230    /// Submits a schema/index job (starts [`SchemaJobState::Pending`]).
1231    SubmitSchemaJob {
1232        /// The job record.
1233        job: SchemaJobRecord,
1234    },
1235    /// Transitions a schema job along the legal state graph.
1236    UpdateSchemaJob {
1237        /// The job to transition.
1238        job_id: u64,
1239        /// Requested state.
1240        state: SchemaJobState,
1241        /// Timestamp of the update.
1242        updated_at: HlcTimestamp,
1243        /// Failure detail to record (cleared with `None`).
1244        error: Option<String>,
1245        /// Optimistic-concurrency guard; see
1246        /// [`MetaCommand::UpdateNodeState::expected_version`].
1247        expected_version: Option<MetadataVersion>,
1248    },
1249    /// Sets one dynamic cluster setting (spec section 16.2; see
1250    /// [`KNOWN_SETTING_KEYS`] and [`SECRET_SETTING_KEY_DENYLIST`]).
1251    SetClusterSetting {
1252        /// Setting key.
1253        key: String,
1254        /// Setting value (typed per key).
1255        value: serde_json::Value,
1256    },
1257    /// Activates a cluster feature (spec section 11.8 step 5): refused at
1258    /// apply unless every non-decommissioned registered node supports the
1259    /// feature and the level satisfies the registry.
1260    ActivateFeature {
1261        /// The activation record.
1262        activation: FeatureActivation,
1263    },
1264    /// Publishes one transaction status partition's home group.
1265    SetTxnStatusPartition {
1266        /// The partition record.
1267        partition: TxnStatusPartition,
1268    },
1269    /// Publishes the atomic routing change of one tablet split (spec section
1270    /// 12.5 step 8): the children become `Active` and the source `Retiring`
1271    /// in ONE command. Refused unless the stored source is the command's
1272    /// `Splitting` precursor and the stored children its `Creating`
1273    /// precursors at exactly one generation below the publication
1274    /// generation, the child bounds partition the source at the split key,
1275    /// and no other routable tablet of the table overlaps a child. An exact
1276    /// re-application (the stored descriptors already carry the command's
1277    /// content) is a no-op, so a split resumed after a crash in the
1278    /// publication barrier may re-publish.
1279    PublishSplit {
1280        /// The publication (see [`crate::split::SplitPublishCommand`]).
1281        command: SplitPublishCommand,
1282    },
1283    /// Publishes the atomic routing change of one tablet merge (spec section
1284    /// 12.6): the hidden replacement becomes `Active` and both sources
1285    /// `Retiring` in ONE command. Refused unless the stored sources are the
1286    /// command's `Merging` precursors and the stored replacement its
1287    /// `Creating` precursor, with the command-wide generation one above the
1288    /// highest stored generation (a lagging source jumps to it), the
1289    /// replacement bounds covering exactly the source union, and no other
1290    /// routable tablet of the table overlapping the replacement. Exact
1291    /// re-application is a no-op (see [`Self::PublishSplit`]).
1292    PublishMerge {
1293        /// The publication (see [`crate::merge::MergePublishCommand`]).
1294        command: MergePublishCommand,
1295    },
1296    /// Allocates `count` fresh per-group raft node ids from the meta-owned
1297    /// allocator (spec section 12.1: the meta control plane owns the
1298    /// node-id ↔ raft-id mapping; tablet replica raft ids come from this
1299    /// allocator, never the ad-hoc node-id projection the meta group itself
1300    /// uses). Ids are drawn from a monotonic counter that skips ids already
1301    /// in use (registered-node projections, tablet and placement replicas).
1302    /// The allocation is recorded under the command id, so a replayed
1303    /// command never double-allocates; the proposer reads the base back
1304    /// through [`MetaGroup::allocate_raft_node_ids`].
1305    AllocateRaftNodeIds {
1306        /// Number of ids to allocate
1307        /// (`1..=MAX_RAFT_NODE_ID_ALLOCATION`).
1308        count: u32,
1309    },
1310}
1311
1312/// The versioned envelope payload carrying one [`MetaCommand`] (spec section
1313/// 4.10). Serialized as JSON into a [`CommandEnvelope`] stamped with
1314/// [`COMMAND_TYPE_META_COMMAND`].
1315#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1316pub struct MetaCommandRecord {
1317    /// Format version; see [`META_COMMAND_FORMAT_VERSION`].
1318    pub format_version: u32,
1319    /// The command.
1320    pub command: MetaCommand,
1321}
1322
1323impl MetaCommandRecord {
1324    /// Wraps `command` at the current format version.
1325    pub fn new(command: MetaCommand) -> Self {
1326        Self {
1327            format_version: META_COMMAND_FORMAT_VERSION,
1328            command,
1329        }
1330    }
1331
1332    /// Serializes the record (JSON; human-readable so 128-bit ids take their
1333    /// canonical hex form).
1334    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    /// Parses and verifies a record produced by [`MetaCommandRecord::encode`];
1339    /// v1 records migrate to the canonical shapes (see the module docs).
1340    /// Unknown versions and malformed payloads fail closed.
1341    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// ---------------------------------------------------------------------------
1372// MetaState
1373// ---------------------------------------------------------------------------
1374
1375/// A registered node: the advertised descriptor plus the meta state's
1376/// modification version (the optimistic-concurrency token for
1377/// [`MetaCommand::UpdateNodeState`]).
1378#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
1379pub struct NodeRecord {
1380    /// The node's advertised descriptor.
1381    pub descriptor: NodeDescriptor,
1382    /// Meta state's [`MetadataVersion`] of the last modification.
1383    #[serde(default = "zero_metadata_version")]
1384    pub metadata_version: MetadataVersion,
1385}
1386
1387/// The replicated control-plane state of the meta group (spec section 12.1).
1388///
1389/// Deterministic: every replica applies the same commands in the same order
1390/// and reaches byte-identical state. Versioned: `format_version` gates the
1391/// snapshot format and `metadata_version` ticks once per applied command
1392/// (accepted or refused — a refusal appends to the rejection journal, which
1393/// is itself state), giving readers a monotonic watermark.
1394#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1395#[serde(default)]
1396pub struct MetaState {
1397    /// Snapshot format version; see [`META_STATE_FORMAT_VERSION`].
1398    pub format_version: u32,
1399    /// Monotonic per-applied-command version.
1400    pub metadata_version: MetadataVersion,
1401    /// Cluster membership: registered node descriptors and locality.
1402    pub nodes: BTreeMap<NodeId, NodeRecord>,
1403    /// Logical databases.
1404    pub databases: BTreeMap<DatabaseId, DatabaseDescriptor>,
1405    /// Replicated table schemas.
1406    pub tables: BTreeMap<TableId, TableSchemaRecord>,
1407    /// Tablet records (canonical descriptor + modification version).
1408    pub tablets: BTreeMap<TabletId, TabletRecord>,
1409    /// Replica placements by raft group.
1410    pub placements: BTreeMap<RaftGroupId, ReplicaPlacement>,
1411    /// Named placement policy records (spec section 12.7).
1412    pub placement_policies: BTreeMap<String, PlacementPolicyRecord>,
1413    /// Schema/index jobs.
1414    pub schema_jobs: BTreeMap<u64, SchemaJobRecord>,
1415    /// Transaction status partitions (spec section 12.8).
1416    pub txn_status_partitions: BTreeMap<u32, TxnStatusPartition>,
1417    /// Dynamic cluster settings (spec section 16.2).
1418    pub settings: ClusterSettings,
1419    /// Cluster feature level (never lowers; ADR-0010).
1420    pub feature_level: ClusterFeatureLevel,
1421    /// Applied feature activations, oldest first.
1422    pub feature_activations: Vec<FeatureActivation>,
1423    /// Bounded journal of refused commands, oldest first
1424    /// ([`META_REJECTION_LIMIT`]).
1425    pub rejections: VecDeque<MetaRejection>,
1426    /// Next per-group raft node id the meta-owned allocator hands out
1427    /// ([`MetaCommand::AllocateRaftNodeIds`]); monotonic and never reused.
1428    /// Starts at [`FIRST_RAFT_NODE_ID`].
1429    pub next_raft_node_id: u64,
1430    /// Recent raft-node-id allocations by command id (hex-encoded, base of
1431    /// each allocated range), the idempotent-replay record of
1432    /// [`MetaCommand::AllocateRaftNodeIds`]; bounded by
1433    /// [`RAFT_ID_ALLOCATION_RECORD_LIMIT`]. (Hex keys: JSON map keys must be
1434    /// strings.)
1435    pub raft_id_allocations: BTreeMap<String, u64>,
1436    /// Eviction order of [`Self::raft_id_allocations`], oldest first.
1437    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    /// Applies one committed command. `metadata_version` ticks first (every
1466    /// applied command moves the watermark); a refusal journals the reason
1467    /// and leaves the records untouched.
1468    ///
1469    /// `commit_ts` is the leader-assigned commit timestamp of the command's
1470    /// log entry: an [`FeatureActivation::activated_at`] left as
1471    /// [`HlcTimestamp::ZERO`] by the proposer is stamped with it here, so
1472    /// every replica records the identical timestamp.
1473    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                        // The meta group addresses its members by the
1514                        // `raft_node_id` projection of their node ids; tablet
1515                        // groups must never reuse a projection (the runtime
1516                        // attaches one raft node per id to its transport
1517                        // registry). Fail closed when the projection is
1518                        // already assigned to a tablet/placement replica.
1519                        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                // Re-validate at quorum (ADR-0010 decision 4): the voter set
1925                // is every registered, non-decommissioned node's advertised
1926                // descriptor — the meta group owns cluster membership, so its
1927                // registry is the authoritative voter view (spec section 12.1
1928                // "cluster membership").
1929                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    /// Validates and applies the atomic split publication (spec section 12.5
1971    /// step 8): one command flips the children to `Active` and the source to
1972    /// `Retiring` at one shared generation. See [`MetaCommand::PublishSplit`]
1973    /// for the acceptance contract.
1974    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        // Idempotent replay: a split resumed after a crash in the
1991        // publication barrier re-publishes the identical command.
1992        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        // The stored source must be the command's `Splitting` precursor at
2007        // exactly `publish - 1` (it was marked at `g + 1`; the publication
2008        // assigns `g + 2`).
2009        self.require_stored_precursor(
2010            &command.source,
2011            TabletState::Splitting,
2012            precursor_generation,
2013            false,
2014        )?;
2015        // Each stored child must be the command's `Creating` precursor (same
2016        // content with learner replicas) at exactly `publish - 1`.
2017        for child in &command.children {
2018            self.require_stored_precursor(
2019                child,
2020                TabletState::Creating,
2021                precursor_generation,
2022                true,
2023            )?;
2024        }
2025        // No other routable tablet of the table may overlap a child (the
2026        // participants are excluded; `Creating`/`Retiring` tablets are not
2027        // serving, so they cannot double-serve a key).
2028        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    /// Validates and applies the atomic merge publication (spec section
2057    /// 12.6): one command flips the hidden replacement to `Active` and both
2058    /// sources to `Retiring` at one command-wide generation. See
2059    /// [`MetaCommand::PublishMerge`] for the acceptance contract.
2060    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        // The stored replacement must be the command's `Creating` precursor
2091        // at exactly `publish - 1` (it was created at `max(g1, g2) + 1`);
2092        // that anchors the command-wide generation: both sources were marked
2093        // at their own `g + 1`, at or below `publish - 1`.
2094        self.require_stored_precursor(
2095            &command.replacement,
2096            TabletState::Creating,
2097            precursor_generation,
2098            true,
2099        )?;
2100        // Each stored source must be the command's `Merging` precursor below
2101        // the publication generation (a source whose generation lags jumps
2102        // to the command-wide generation at publish).
2103        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    /// The stored descriptor of `published.tablet_id`, verified as the
2153    /// publication's precursor: in `precursor_state` at `precursor_generation`
2154    /// with otherwise identical content (a published child's learner replicas
2155    /// promote to voters, so those compare with roles normalized).
2156    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    /// Fails when any routable tablet of `published.table_id` outside
2197    /// `participants` overlaps `published`'s bounds (two serving tablets
2198    /// covering one key would double-serve reads).
2199    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    /// Applies one raft-node-id allocation (see
2224    /// [`MetaCommand::AllocateRaftNodeIds`]): draws `count` ids from the
2225    /// monotonic counter, skipping ids already in use, and records the base
2226    /// under the command id for idempotent replay.
2227    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                // Idempotent replay of an already-applied allocation.
2243                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    /// One registered node's descriptor.
2303    pub fn node(&self, node_id: NodeId) -> Option<&NodeDescriptor> {
2304        self.nodes.get(&node_id).map(|record| &record.descriptor)
2305    }
2306
2307    /// One registered node's record (descriptor + modification version).
2308    pub fn node_record(&self, node_id: NodeId) -> Option<&NodeRecord> {
2309        self.nodes.get(&node_id)
2310    }
2311
2312    /// Every registered node descriptor, in node-id order.
2313    pub fn node_descriptors(&self) -> Vec<NodeDescriptor> {
2314        self.nodes
2315            .values()
2316            .map(|record| record.descriptor.clone())
2317            .collect()
2318    }
2319
2320    /// One database descriptor by id.
2321    pub fn database(&self, database_id: DatabaseId) -> Option<&DatabaseDescriptor> {
2322        self.databases.get(&database_id)
2323    }
2324
2325    /// One database descriptor by name.
2326    pub fn database_by_name(&self, name: &str) -> Option<&DatabaseDescriptor> {
2327        self.databases
2328            .values()
2329            .find(|database| database.name == name)
2330    }
2331
2332    /// One table's schema record.
2333    pub fn table(&self, table_id: TableId) -> Option<&TableSchemaRecord> {
2334        self.tables.get(&table_id)
2335    }
2336
2337    /// One tablet descriptor (the canonical `crate::tablet` shape).
2338    pub fn tablet(&self, tablet_id: TabletId) -> Option<&TabletDescriptor> {
2339        self.tablets
2340            .get(&tablet_id)
2341            .map(|record| &record.descriptor)
2342    }
2343
2344    /// One tablet record (descriptor + modification version).
2345    pub fn tablet_record(&self, tablet_id: TabletId) -> Option<&TabletRecord> {
2346        self.tablets.get(&tablet_id)
2347    }
2348
2349    /// One raft group's replica placement.
2350    pub fn placement(&self, raft_group_id: RaftGroupId) -> Option<&ReplicaPlacement> {
2351        self.placements.get(&raft_group_id)
2352    }
2353
2354    /// One named placement policy (the canonical `crate::placement` shape).
2355    pub fn placement_policy(&self, name: &str) -> Option<&PlacementPolicy> {
2356        self.placement_policies
2357            .get(name)
2358            .map(|record| &record.policy)
2359    }
2360
2361    /// One named placement policy record (policy + modification version).
2362    pub fn placement_policy_record(&self, name: &str) -> Option<&PlacementPolicyRecord> {
2363        self.placement_policies.get(name)
2364    }
2365
2366    /// One schema job record.
2367    pub fn schema_job(&self, job_id: u64) -> Option<&SchemaJobRecord> {
2368        self.schema_jobs.get(&job_id)
2369    }
2370
2371    /// One transaction status partition.
2372    pub fn txn_status_partition(&self, partition_id: u32) -> Option<&TxnStatusPartition> {
2373        self.txn_status_partitions.get(&partition_id)
2374    }
2375
2376    /// The dynamic cluster settings.
2377    pub fn settings(&self) -> &ClusterSettings {
2378        &self.settings
2379    }
2380
2381    /// The cluster feature level.
2382    pub fn feature_level(&self) -> ClusterFeatureLevel {
2383        self.feature_level
2384    }
2385
2386    /// Whether `feature` is active at the state's current feature level.
2387    pub fn feature_active(&self, registry: &FeatureRegistry, feature: &str) -> bool {
2388        registry.feature_supported(self.feature_level, feature)
2389    }
2390
2391    /// The applied feature activations, oldest first.
2392    pub fn feature_activations(&self) -> &[FeatureActivation] {
2393        &self.feature_activations
2394    }
2395
2396    /// The bounded refusal journal, oldest first.
2397    pub fn rejections(&self) -> &VecDeque<MetaRejection> {
2398        &self.rejections
2399    }
2400
2401    /// The next id the meta-owned raft-node-id allocator hands out.
2402    pub fn next_raft_node_id(&self) -> u64 {
2403        self.next_raft_node_id
2404    }
2405
2406    /// The base of the range allocated by the command with id `command_id`,
2407    /// when the allocation record is still retained
2408    /// ([`RAFT_ID_ALLOCATION_RECORD_LIMIT`]).
2409    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
2416// ---------------------------------------------------------------------------
2417// Format v1 compatibility (spec sections 4.10, 17; module docs)
2418// ---------------------------------------------------------------------------
2419//
2420// The serde shapes the first meta control-plane build (format v1) persisted
2421// and replicated, kept verbatim so v1 command records and v1 state
2422// checkpoints decode forever. Every field is read by the migration functions
2423// below, which map the v1 meta-local mirrors onto the canonical
2424// `crate::tablet` / `crate::placement` types (see the module docs for the
2425// mapping decisions).
2426mod 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    /// The v1 command enum: identical variant set; only the tablet/placement
2499    /// payloads differ from the current (v2) shapes.
2500    #[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
2622/// v1 replicas carried no per-group raft id; the projection of the node id is
2623/// the id the v1 raft group actually used for the replica.
2624fn 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
2632/// v1 bounds were `Option` endpoints (start inclusive, end exclusive); the
2633/// canonical bounds carry the same semantics as `Bound` endpoints.
2634fn 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 "being removed; drained before the descriptor is deleted" is the
2652        // canonical "retired from routing, retained until no pins remain".
2653        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
2686/// v1 voter constraints were hard requirements ("every voter must satisfy");
2687/// v1 leader preferences were soft ("preferences, in priority order").
2688fn 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        // v1 pre-dates the meta-owned raft-id allocator; the counter starts
2806        // fresh (ad-hoc replica raft ids of v1 deployments stay as recorded
2807        // in their descriptors and are skipped by the collision scan).
2808        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/// Decode-time probe of the `format_version` field every versioned meta
2824/// payload (command records, state checkpoints) carries as its first field.
2825#[derive(Deserialize)]
2826struct FormatVersionProbe {
2827    format_version: u32,
2828}
2829
2830/// Parses and verifies a checkpoint produced by [`MetaStateCheckpoint`]
2831/// serialization; v1 checkpoints migrate (see the module docs). Unknown
2832/// versions and malformed payloads fail closed.
2833fn 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
2857// ---------------------------------------------------------------------------
2858// MetaApplySink
2859// ---------------------------------------------------------------------------
2860
2861/// Name of the sink's durable checkpoint file inside `<group dir>/raft/state/`.
2862pub const META_STATE_CHECKPOINT_FILENAME: &str = "meta-state.json";
2863/// Format version of the checkpoint file this build writes (v2 carries the
2864/// reconciled canonical tablet/placement types; see the module docs).
2865pub const META_STATE_CHECKPOINT_FORMAT_VERSION: u32 = 2;
2866/// Oldest checkpoint format version this build accepts.
2867pub const MIN_SUPPORTED_META_STATE_CHECKPOINT_FORMAT_VERSION: u32 = 1;
2868
2869/// The sink's durable checkpoint: the applied state plus the log watermark
2870/// it reflects. The same record is the sink's snapshot payload, so snapshot
2871/// install and restart recovery restore byte-identical state.
2872#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
2873pub struct MetaStateCheckpoint {
2874    /// Checkpoint format version; see [`META_STATE_CHECKPOINT_FORMAT_VERSION`].
2875    pub format_version: u32,
2876    /// Log position the state reflects (the replay watermark).
2877    pub position: LogPosition,
2878    /// Last applied command id (informational; the state machine owns the
2879    /// idempotent-replay set).
2880    pub command_id: Option<[u8; 16]>,
2881    /// The applied meta state.
2882    pub state: MetaState,
2883}
2884
2885/// The [`ApplySink`] binding the meta group's committed commands to
2886/// [`MetaState`] (spec section 12.1).
2887///
2888/// # Persistence
2889///
2890/// The sink checkpoints [`MetaStateCheckpoint`] to
2891/// `<group dir>/raft/state/meta-state.json` after every dispatched entry
2892/// (atomic temp-write + rename + directory fsync, the crate's metadata
2893/// idiom). This is required, not decorative: the consensus state machine
2894/// persists its apply checkpoint per batch, so after a restart openraft
2895/// replays only entries *after* that checkpoint — an in-memory-only sink
2896/// would come back empty while the log says it applied everything (the
2897/// engine sink's applied database root is its equivalent durable state).
2898///
2899/// The dispatch order is sink-first, checkpoint-second (see the consensus
2900/// state machine docs): a crash in that window can redeliver an entry the
2901/// sink already applied and persisted. The checkpoint's `position` is the
2902/// durable watermark — redelivered entries at or below it are skipped, so
2903/// `metadata_version` and the records never double-apply.
2904///
2905/// Apply is deterministic and total. Refused commands are journaled in
2906/// state, never returned as state-machine errors; genuine faults fail
2907/// closed: an undecodable payload, an envelope that is not a
2908/// [`COMMAND_TYPE_META_COMMAND`] catalog command, or — per spec section
2909/// 12.1's "no user row data" — a transaction command misrouted to the meta
2910/// group.
2911pub struct MetaApplySink {
2912    state: MetaState,
2913    position: LogPosition,
2914    command_id: Option<[u8; 16]>,
2915    registry: FeatureRegistry,
2916    /// `<group dir>/raft/state`.
2917    state_dir: PathBuf,
2918}
2919
2920impl MetaApplySink {
2921    /// Opens (creating if needed) the sink under `group_dir`, loading the
2922    /// persisted checkpoint when present. A present but undecodable or
2923    /// unsupported-version checkpoint fails closed (spec section 4.10).
2924    ///
2925    /// `registry` is the binary's feature registry (identical on every
2926    /// replica of one deployment); [`FeatureRegistry::current`] in
2927    /// production.
2928    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    /// The current replicated state.
2966    pub fn state(&self) -> &MetaState {
2967        &self.state
2968    }
2969
2970    /// The monotonic per-applied-command version.
2971    pub fn metadata_version(&self) -> MetadataVersion {
2972        self.state.metadata_version
2973    }
2974
2975    /// The log position the state reflects (the crash-window replay
2976    /// watermark).
2977    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        // Crash-window replay: the sink persisted this entry (or a later
3001        // one) already; skip it so versions and records never double-apply.
3002        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                // A refusal is journaled state, not a state-machine error.
3020                let _ = self.state.apply(
3021                    &record.command,
3022                    command.command_id(),
3023                    commit_ts,
3024                    &self.registry,
3025                );
3026            }
3027            // Maintenance commands are node-runtime directives and Noop
3028            // advances the commit index; neither touches meta state.
3029            ReplicatedCommand::Maintenance(_) | ReplicatedCommand::Noop => {}
3030            // The meta group owns control-plane state only (spec section
3031            // 12.1): a transaction command here is misrouted — fail closed.
3032            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// ---------------------------------------------------------------------------
3080// MetaGroup: bootstrap and membership workflow (spec sections 12.1, 12.7)
3081// ---------------------------------------------------------------------------
3082
3083/// Static layout and identity of one meta-group member.
3084///
3085/// # Directory layout (mirrors the engine sink's single-group layout)
3086///
3087/// ```text
3088/// node-data/
3089///   groups/
3090///     <meta-group-id>/
3091///       raft/        log segments, vote, state machine checkpoint + snapshots
3092///       raft/state/meta-state.json   the sink's durable MetaState checkpoint
3093/// ```
3094///
3095/// The applied meta state is durable in the sink's checkpoint file (see
3096/// [`MetaApplySink`]) and travels inside raft snapshots for catch-up.
3097#[derive(Debug, Clone)]
3098pub struct MetaGroupConfig {
3099    /// The node's local data root (`node-data`).
3100    pub node_data: PathBuf,
3101    /// The dedicated meta group's durable identifier (minted at cluster
3102    /// bootstrap; never a tablet group).
3103    pub meta_group_id: RaftGroupId,
3104    /// This node's durable id.
3105    pub node_id: NodeId,
3106    /// The binary's feature registry gating [`MetaCommand::ActivateFeature`]
3107    /// at apply (identical on every replica of one deployment).
3108    pub registry: FeatureRegistry,
3109    /// Durable log storage configuration.
3110    pub storage: StorageConfig,
3111    /// Bound on the apply idempotency set (S2B-004).
3112    pub idempotency_retention: usize,
3113}
3114
3115impl MetaGroupConfig {
3116    /// Required identities; registry, storage, and retention default to
3117    /// production values.
3118    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    /// `<node-data>/groups/<meta-group-id>` — the group directory handed to
3131    /// [`ConsensusGroup`].
3132    pub fn group_dir(&self) -> PathBuf {
3133        self.node_data
3134            .join("groups")
3135            .join(self.meta_group_id.to_hex())
3136    }
3137
3138    /// The group's text identifier (`meta-<hex>`).
3139    pub fn cluster_name(&self) -> String {
3140        format!("meta-{}", self.meta_group_id.to_hex())
3141    }
3142
3143    /// The default [`GroupConfig`] for this member (production timings;
3144    /// callers may tune it before passing it to [`MetaGroup::create`]).
3145    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/// Proof that a meta command was committed and applied (and not refused).
3158#[derive(Debug, Clone)]
3159pub struct MetaCommandReceipt {
3160    /// The consensus commit receipt (position, commit timestamp, command id,
3161    /// idempotent-replay flag).
3162    pub receipt: GroupCommitReceipt,
3163    /// The meta state's watermark after applying the command.
3164    pub metadata_version: MetadataVersion,
3165}
3166
3167/// One member of the dedicated meta control-plane group (spec section 12.1):
3168/// a [`ConsensusGroup`] whose apply sink is a [`MetaApplySink`], plus the
3169/// bootstrap/membership/propose workflow the node runtime drives.
3170pub struct MetaGroup<T: RaftTransport> {
3171    group: ConsensusGroup<T>,
3172    sink: Arc<Mutex<MetaApplySink>>,
3173    config: MetaGroupConfig,
3174}
3175
3176/// Builds one openraft node value for the membership calls without naming
3177/// the openraft type: the cluster crate deliberately has no openraft
3178/// dependency (ADR-0004 confines it to `mongreldb-consensus`), so the
3179/// concrete node type is inferred from the consuming [`ConsensusGroup`] call
3180/// and constructed through its serde shape (`{"addr": ..}`), which
3181/// `mongreldb-consensus` enables via openraft's `serde` feature.
3182fn 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
3190/// Mints a command id for the workflow's internal proposals.
3191pub(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    /// Opens the group's durable state and starts the raft task with a
3199    /// [`MetaApplySink`] installed. `group_config` must match
3200    /// [`MetaGroupConfig::group_config`] (tuned timings are fine; identity
3201    /// and directory are not — mismatches fail closed).
3202    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    /// The underlying consensus group (snapshots, membership, transfer,
3239    /// read barriers, shutdown).
3240    pub fn group(&self) -> &ConsensusGroup<T> {
3241        &self.group
3242    }
3243
3244    /// This node's durable id.
3245    pub fn node_id(&self) -> NodeId {
3246        self.config.node_id
3247    }
3248
3249    /// The meta group's durable id.
3250    pub fn meta_group_id(&self) -> RaftGroupId {
3251        self.config.meta_group_id
3252    }
3253
3254    /// Bootstraps a pristine meta group with the given voter set of
3255    /// `(node_id, rpc_address)` pairs (call on one pristine member; check
3256    /// [`MetaGroup::is_initialized`] on reopen). The 64-bit raft-id
3257    /// projection of distinct node ids must not collide (ADR: collisions are
3258    /// rejected at cluster bootstrap by this layer — the consensus adapter
3259    /// treats raft ids as opaque).
3260    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    /// Whether this node already holds an initialized membership.
3282    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    /// Adds one member to the meta group (spec section 12.7's movement
3290    /// protocol, meta-group form): add learner and wait until it is
3291    /// line-rate, promote it to voter through joint consensus, then register
3292    /// its descriptor in replicated meta state. Registration comes last so
3293    /// the feature-activation voter view (registered, non-decommissioned
3294    /// descriptors) reflects only nodes that actually vote.
3295    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    /// Removes one member: the replicated [`MetaCommand::RemoveNode`] first
3323    /// (validating the node hosts no remaining replicas), then the
3324    /// joint-consensus removal. Validate-then-act keeps the two membership
3325    /// views from diverging on a refusal, and a retry after a mid-workflow
3326    /// failure is idempotent (`RemoveNode` of an absent node is a no-op).
3327    /// Leadership must be transferred off the node first (spec section
3328    /// 11.6); removing the current leader fails closed.
3329    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    /// Proposes one meta command (quorum durability; spec section 11.3) and
3353    /// waits for commit + apply. `command_id` is the caller's idempotency
3354    /// token: a retry with the same id and payload replays the original
3355    /// apply without re-dispatching (S2B-004).
3356    ///
3357    /// The command rides a [`COMMAND_TYPE_META_COMMAND`] catalog envelope.
3358    /// When the apply path refused the command, the typed
3359    /// [`MetaRejectionReason`] is returned (and journaled in state); the
3360    /// raft entry itself committed normally.
3361    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        // client_write returns after local apply, so the local sink's view
3374        // already includes this command (or its refusal).
3375        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    /// A point-in-time clone of the replicated meta state at this node's
3399    /// applied watermark. For a linearizable view, run
3400    /// [`ConsensusGroup::read_index`] (via [`MetaGroup::group`]) first.
3401    pub fn state(&self) -> MetaState {
3402        self.sink
3403            .lock()
3404            .expect("meta sink lock poisoned")
3405            .state()
3406            .clone()
3407    }
3408
3409    /// Allocates `count` fresh per-group raft node ids from the meta-owned
3410    /// allocator (spec section 12.1), returning them in ascending order.
3411    /// Tablet replica raft ids come from this allocator — never the ad-hoc
3412    /// node-id projection the meta group itself uses (see
3413    /// [`MetaCommand::AllocateRaftNodeIds`]). The allocation is idempotent
3414    /// under its command id and durable in replicated state, so ids are
3415    /// never reused across failovers and restarts.
3416    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    /// The local applied watermark (monotonic per applied command).
3438    pub fn metadata_version(&self) -> MetadataVersion {
3439        self.sink
3440            .lock()
3441            .expect("meta sink lock poisoned")
3442            .metadata_version()
3443    }
3444
3445    /// Graceful shutdown of the underlying group.
3446    pub async fn shutdown(&self) -> Result<(), MetaError> {
3447        self.group.shutdown().await.map_err(MetaError::Consensus)
3448    }
3449
3450    /// Process-free crash simulation: stops the raft task without the
3451    /// graceful storage close (see [`ConsensusGroup::crash`]); everything
3452    /// fsynced survives, which is exactly the split/merge crash-resume
3453    /// contract.
3454    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        // Unknown features are never supported (fail closed).
3504        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(&registry, 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        // Once the last voter advertises support, activation validates.
3526        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(&registry, 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(&registry, 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(&registry, 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(&registry, 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        // A second feature registered at the cluster's current level may
3583        // still activate: the level does not lower.
3584        let registry = registry_with("ai-hybrid", 9);
3585        let voters = vec![descriptor(1, &["ai-hybrid"])];
3586        activation("ai-hybrid", 9)
3587            .validate(&registry, 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(&registry, 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// ---------------------------------------------------------------------------
3698// Stage 3A tests
3699// ---------------------------------------------------------------------------
3700
3701#[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    /// Waits until every group in `among` agrees on one leader **that is one
3874    /// of `among`** — a stopped leader's id lingers in the survivors' metrics
3875    /// until the next election, so a bare consensus check can otherwise
3876    /// return a node outside the live set.
3877    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    // -- serde + record plumbing -------------------------------------------
3943
3944    #[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        // Malformed payloads and unsupported versions fail closed.
4018        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    // -- single-node apply round-trips (every command) ----------------------
4039
4040    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, &registry, id, command).unwrap();
4110        }
4111        assert_eq!(state.metadata_version, MetadataVersion(16));
4112        assert!(state.rejections().is_empty());
4113        // The allocator command appended to the sequence handed out three ids.
4114        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(&registry, "ann-v2"));
4131        // A ZERO activated_at is stamped with the entry's commit timestamp.
4132        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    // -- idempotent, deterministic apply ------------------------------------
4140
4141    #[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            &registry,
4148            1,
4149            MetaCommand::RegisterNode {
4150                descriptor: descriptor(1, &["ann-v2"]),
4151            },
4152        )
4153        .unwrap();
4154        apply(
4155            &mut state,
4156            &registry,
4157            2,
4158            MetaCommand::RegisterNode {
4159                descriptor: descriptor(1, &["ann-v2"]),
4160            },
4161        )
4162        .unwrap();
4163        assert_eq!(state.nodes.len(), 1);
4164        // The replay was a record-level no-op: the record keeps the version
4165        // of the first write while the state watermark ticks per command.
4166        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            &registry,
4175            3,
4176            MetaCommand::ActivateFeature {
4177                activation: activation("ann-v2", 7),
4178            },
4179        )
4180        .unwrap();
4181        apply(
4182            &mut state,
4183            &registry,
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), &registry)
4203                .unwrap();
4204            b.apply(command, Some(cmd_id(id)), ts(1_000), &registry)
4205                .unwrap();
4206        }
4207        assert_eq!(a, b);
4208        // Snapshots of identical states are byte-identical.
4209        assert_eq!(
4210            serde_json::to_vec(&a).unwrap(),
4211            serde_json::to_vec(&b).unwrap()
4212        );
4213    }
4214
4215    // -- split/merge publish adoption (spec sections 12.5-12.6) --------------
4216
4217    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    /// The pre-split source: table 1, [a, z), `Active` at generation 5.
4230    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    /// Seeds a state with the database/table, the `Splitting` source, and
4272    /// the `Creating` children of [`split_plan`], returning the plan.
4273    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    /// The merge sources: table 1, adjacent [a, m) at generation 4 and
4329    /// [m, z) at generation 6, placed on the same two nodes.
4330    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    /// Seeds a state with the database/table, the `Merging` sources, and the
4392    /// `Creating` replacement of [`merge_plan`], returning the plan.
4393    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, &registry);
4453        let command = SplitPublishCommand::from_plan(&plan).unwrap();
4454        apply(
4455            &mut state,
4456            &registry,
4457            10,
4458            MetaCommand::PublishSplit {
4459                command: command.clone(),
4460            },
4461        )
4462        .unwrap();
4463        // One atomic publication: children Active, source Retiring, all at
4464        // the publication generation (g + 2 = 7), learners promoted.
4465        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        // Idempotent replay (a resumed split re-publishes after a crash in
4477        // the barrier): a different command id carrying the identical
4478        // publication is a no-op.
4479        let version = state.metadata_version;
4480        apply(
4481            &mut state,
4482            &registry,
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        // The source was never marked Splitting (only the database, table,
4503        // and Active source are seeded).
4504        let mut state = MetaState::default();
4505        apply(
4506            &mut state,
4507            &registry,
4508            1,
4509            MetaCommand::CreateDatabase {
4510                descriptor: database(1, "app"),
4511            },
4512        )
4513        .unwrap();
4514        apply(
4515            &mut state,
4516            &registry,
4517            2,
4518            MetaCommand::SetTableSchema {
4519                record: schema_record(1, 1, 1),
4520            },
4521        )
4522        .unwrap();
4523        apply(
4524            &mut state,
4525            &registry,
4526            3,
4527            MetaCommand::SetTabletDescriptor {
4528                descriptor: split_source(),
4529            },
4530        )
4531        .unwrap();
4532        let error = apply(
4533            &mut state,
4534            &registry,
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        // A child descriptor is missing.
4546        let mut state = MetaState::default();
4547        let plan = seed_split_publish(&mut state, &registry);
4548        apply(
4549            &mut state,
4550            &registry,
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            &registry,
4561            21,
4562            MetaCommand::PublishSplit {
4563                command: split_publish_command(),
4564            },
4565        )
4566        .unwrap_err();
4567        assert!(matches!(error, MetaRejectionReason::NotFound { .. }));
4568
4569        // A child is not in Creating.
4570        let mut state = MetaState::default();
4571        let plan = seed_split_publish(&mut state, &registry);
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            &registry,
4581            20,
4582            MetaCommand::SetTabletDescriptor { descriptor: rogue },
4583        )
4584        .unwrap();
4585        let error = apply(
4586            &mut state,
4587            &registry,
4588            21,
4589            MetaCommand::PublishSplit {
4590                command: split_publish_command(),
4591            },
4592        )
4593        .unwrap_err();
4594        assert!(matches!(error, MetaRejectionReason::Conflict { .. }));
4595
4596        // The publication generation does not follow the stored precursors.
4597        let mut state = MetaState::default();
4598        seed_split_publish(&mut state, &registry);
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            &registry,
4607            21,
4608            MetaCommand::PublishSplit { command },
4609        )
4610        .unwrap_err();
4611        assert!(matches!(error, MetaRejectionReason::Conflict { .. }));
4612
4613        // The child bounds do not partition the source at the split key.
4614        let mut state = MetaState::default();
4615        seed_split_publish(&mut state, &registry);
4616        let mut command = split_publish_command();
4617        command.split_key = key(b"n");
4618        let error = apply(
4619            &mut state,
4620            &registry,
4621            21,
4622            MetaCommand::PublishSplit { command },
4623        )
4624        .unwrap_err();
4625        assert!(matches!(error, MetaRejectionReason::Invalid { .. }));
4626
4627        // Another routable tablet of the table overlaps a child.
4628        let mut state = MetaState::default();
4629        seed_split_publish(&mut state, &registry);
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            &registry,
4645            20,
4646            MetaCommand::SetTabletDescriptor {
4647                descriptor: overlapping,
4648            },
4649        )
4650        .unwrap();
4651        let error = apply(
4652            &mut state,
4653            &registry,
4654            21,
4655            MetaCommand::PublishSplit {
4656                command: split_publish_command(),
4657            },
4658        )
4659        .unwrap_err();
4660        assert!(matches!(error, MetaRejectionReason::Conflict { .. }));
4661
4662        // The table does not exist.
4663        let mut state = MetaState::default();
4664        let error = apply(
4665            &mut state,
4666            &registry,
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, &registry);
4681        let command = MergePublishCommand::from_plan(&plan).unwrap();
4682        apply(
4683            &mut state,
4684            &registry,
4685            10,
4686            MetaCommand::PublishMerge {
4687                command: command.clone(),
4688            },
4689        )
4690        .unwrap();
4691        // The command-wide generation is max(g1, g2) + 2 = 8: the source
4692        // marked at generation 5 jumps to 8 with the rest.
4693        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        // Idempotent replay.
4703        apply(
4704            &mut state,
4705            &registry,
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        // A source was never marked Merging (it is stored Active, one
4718        // generation above the mark it never took).
4719        let mut state = MetaState::default();
4720        let plan = seed_merge_publish(&mut state, &registry);
4721        let mut active = plan.sources[0].clone();
4722        active.generation = 6;
4723        apply(
4724            &mut state,
4725            &registry,
4726            20,
4727            MetaCommand::SetTabletDescriptor { descriptor: active },
4728        )
4729        .unwrap();
4730        let error = apply(
4731            &mut state,
4732            &registry,
4733            21,
4734            MetaCommand::PublishMerge {
4735                command: merge_publish_command(),
4736            },
4737        )
4738        .unwrap_err();
4739        assert!(matches!(error, MetaRejectionReason::Conflict { .. }));
4740
4741        // The replacement descriptor is missing.
4742        let mut state = MetaState::default();
4743        let plan = seed_merge_publish(&mut state, &registry);
4744        apply(
4745            &mut state,
4746            &registry,
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            &registry,
4757            21,
4758            MetaCommand::PublishMerge {
4759                command: merge_publish_command(),
4760            },
4761        )
4762        .unwrap_err();
4763        assert!(matches!(error, MetaRejectionReason::NotFound { .. }));
4764
4765        // The replacement is not in Creating.
4766        let mut state = MetaState::default();
4767        let plan = seed_merge_publish(&mut state, &registry);
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            &registry,
4777            20,
4778            MetaCommand::SetTabletDescriptor { descriptor: rogue },
4779        )
4780        .unwrap();
4781        let error = apply(
4782            &mut state,
4783            &registry,
4784            21,
4785            MetaCommand::PublishMerge {
4786                command: merge_publish_command(),
4787            },
4788        )
4789        .unwrap_err();
4790        assert!(matches!(error, MetaRejectionReason::Conflict { .. }));
4791
4792        // The replacement's stored generation is off the generation math
4793        // (remove the seeded descriptor, re-create it one generation low).
4794        let mut state = MetaState::default();
4795        let plan = seed_merge_publish(&mut state, &registry);
4796        let mut rogue = plan.replacement_descriptor();
4797        rogue.generation -= 1;
4798        apply(
4799            &mut state,
4800            &registry,
4801            19,
4802            MetaCommand::RemoveTabletDescriptor {
4803                tablet_id: rogue.tablet_id,
4804                generation: 7,
4805            },
4806        )
4807        .unwrap();
4808        apply(
4809            &mut state,
4810            &registry,
4811            20,
4812            MetaCommand::SetTabletDescriptor { descriptor: rogue },
4813        )
4814        .unwrap();
4815        let error = apply(
4816            &mut state,
4817            &registry,
4818            21,
4819            MetaCommand::PublishMerge {
4820                command: merge_publish_command(),
4821            },
4822        )
4823        .unwrap_err();
4824        assert!(matches!(error, MetaRejectionReason::Conflict { .. }));
4825
4826        // The replacement bounds are not the union of the sources.
4827        let mut state = MetaState::default();
4828        seed_merge_publish(&mut state, &registry);
4829        let mut command = merge_publish_command();
4830        command.replacement.partition = command.sources[0].partition.clone();
4831        let error = apply(
4832            &mut state,
4833            &registry,
4834            21,
4835            MetaCommand::PublishMerge { command },
4836        )
4837        .unwrap_err();
4838        assert!(matches!(error, MetaRejectionReason::Invalid { .. }));
4839
4840        // Another routable tablet of the table overlaps the replacement.
4841        let mut state = MetaState::default();
4842        seed_merge_publish(&mut state, &registry);
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            &registry,
4858            20,
4859            MetaCommand::SetTabletDescriptor {
4860                descriptor: overlapping,
4861            },
4862        )
4863        .unwrap();
4864        let error = apply(
4865            &mut state,
4866            &registry,
4867            21,
4868            MetaCommand::PublishMerge {
4869                command: merge_publish_command(),
4870            },
4871        )
4872        .unwrap_err();
4873        assert!(matches!(error, MetaRejectionReason::Conflict { .. }));
4874    }
4875
4876    // -- meta-owned raft-node-id allocation -----------------------------------
4877
4878    #[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            &registry,
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            &registry,
4897            2,
4898            MetaCommand::AllocateRaftNodeIds { count: 2 },
4899        )
4900        .unwrap();
4901        // The ranges never overlap and the counter only advances.
4902        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        // Replaying the first command id is a no-op: no double allocation.
4908        apply(
4909            &mut state,
4910            &registry,
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        // Bounds are enforced and journaled.
4918        let error = apply(
4919            &mut state,
4920            &registry,
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            &registry,
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            &registry,
4947            1,
4948            MetaCommand::CreateDatabase {
4949                descriptor: database(1, "app"),
4950            },
4951        )
4952        .unwrap();
4953        apply(
4954            &mut state,
4955            &registry,
4956            2,
4957            MetaCommand::SetTableSchema {
4958                record: schema_record(1, 1, 1),
4959            },
4960        )
4961        .unwrap();
4962        // Register the replica nodes first (placements validate node
4963        // references); their projections are far above the ids below.
4964        for (id, node) in [(3, 1), (4, 2)] {
4965            apply(
4966                &mut state,
4967                &registry,
4968                id,
4969                MetaCommand::RegisterNode {
4970                    descriptor: descriptor(node, &[]),
4971                },
4972            )
4973            .unwrap();
4974        }
4975        // Tablet replicas hold raft ids 1 and 3; a placement holds 5.
4976        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            &registry,
4981            5,
4982            MetaCommand::SetTabletDescriptor { descriptor: tablet },
4983        )
4984        .unwrap();
4985        apply(
4986            &mut state,
4987            &registry,
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        // The allocation [1, 4) collides at 1 and 3, [4, 7) at 5: the first
4999        // free window of three is [6, 9).
5000        apply(
5001            &mut state,
5002            &registry,
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        // A registered node's projection is skipped too: force the counter
5010        // onto it and watch the allocation step over it.
5011        apply(
5012            &mut state,
5013            &registry,
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            &registry,
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        // Restart durability: the counter and the replay records live in the
5034        // durable checkpoint.
5035        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        // The replay record survived: the same command id does not allocate
5049        // again, and a fresh command continues above the first range.
5050        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            &registry,
5080            1,
5081            MetaCommand::CreateDatabase {
5082                descriptor: database(1, "app"),
5083            },
5084        )
5085        .unwrap();
5086        apply(
5087            &mut state,
5088            &registry,
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            &registry,
5100            3,
5101            MetaCommand::SetTabletDescriptor { descriptor: tablet },
5102        )
5103        .unwrap();
5104        // A node whose raft-id projection is already a tablet replica's id
5105        // is refused: tablet groups address replicas by id, and the meta
5106        // group addresses its members by projection — a collision would
5107        // attach two raft nodes under one id on a node hosting both.
5108        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            &registry,
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        // A non-colliding registration still succeeds, and an identical
5124        // re-registration stays a no-op.
5125        apply(
5126            &mut state,
5127            &registry,
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            &registry,
5143            1,
5144            MetaCommand::CreateDatabase {
5145                descriptor: database(1, "app"),
5146            },
5147        )
5148        .unwrap();
5149        apply(
5150            &mut state,
5151            &registry,
5152            2,
5153            MetaCommand::SetTableSchema {
5154                record: schema_record(1, 1, 1),
5155            },
5156        )
5157        .unwrap();
5158        // Duplicate raft ids within one group are refused at apply.
5159        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            &registry,
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    // -- feature activation gating at apply ----------------------------------
5173
5174    #[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                &registry,
5186                id,
5187                MetaCommand::RegisterNode {
5188                    descriptor: descriptor(byte, &features),
5189                },
5190            )
5191            .unwrap();
5192        }
5193        let error = apply(
5194            &mut state,
5195            &registry,
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        // The refusal is journaled with the command id; the level is unmoved.
5210        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        // Node 2 re-registers with support; activation now applies.
5216        apply(
5217            &mut state,
5218            &registry,
5219            5,
5220            MetaCommand::RegisterNode {
5221                descriptor: descriptor(2, &["ann-v2"]),
5222            },
5223        )
5224        .unwrap();
5225        apply(
5226            &mut state,
5227            &registry,
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        // No registered nodes at all: fail closed.
5243        let error = apply(
5244            &mut state,
5245            &registry,
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            &registry,
5259            2,
5260            MetaCommand::RegisterNode {
5261                descriptor: descriptor(1, &["ann-v2", "ai-hybrid"]),
5262            },
5263        )
5264        .unwrap();
5265        // Below the registry minimum.
5266        let error = apply(
5267            &mut state,
5268            &registry,
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        // Undeclared feature.
5282        let error = apply(
5283            &mut state,
5284            &registry,
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        // Activate, then attempt to lower the level.
5296        apply(
5297            &mut state,
5298            &registry,
5299            5,
5300            MetaCommand::ActivateFeature {
5301                activation: activation("ann-v2", 7),
5302            },
5303        )
5304        .unwrap();
5305        let error = apply(
5306            &mut state,
5307            &registry,
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    // -- cluster settings ----------------------------------------------------
5322
5323    #[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            // The denylist runs before the known-key check (fail closed).
5335            (7, "secret.unknown"),
5336        ] {
5337            let error = apply(
5338                &mut state,
5339                &registry,
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        // Nothing was written.
5356        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            &registry,
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                &registry,
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                &registry,
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        // A null value removes the group.
5454        apply(
5455            &mut state,
5456            &registry,
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    // -- versioning, LWW guards, integrity -----------------------------------
5468
5469    #[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            &registry,
5477            1,
5478            MetaCommand::RegisterNode {
5479                descriptor: descriptor(1, &[]),
5480            },
5481        )
5482        .unwrap();
5483        assert_eq!(state.metadata_version, MetadataVersion(1));
5484        // A refused command still ticks: the refusal itself is journaled state.
5485        apply(
5486            &mut state,
5487            &registry,
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            &registry,
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            &registry,
5516            1,
5517            MetaCommand::RegisterNode {
5518                descriptor: descriptor(1, &[]),
5519            },
5520        )
5521        .unwrap();
5522        apply(
5523            &mut state,
5524            &registry,
5525            2,
5526            MetaCommand::CreateDatabase {
5527                descriptor: database(1, "app"),
5528            },
5529        )
5530        .unwrap();
5531        // Optimistic-concurrency guard on node state.
5532        let node_version = state.node_record(node_id(1)).unwrap().metadata_version;
5533        let error = apply(
5534            &mut state,
5535            &registry,
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            &registry,
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        // Schema versions move forward only.
5558        apply(
5559            &mut state,
5560            &registry,
5561            5,
5562            MetaCommand::SetTableSchema {
5563                record: schema_record(1, 1, 2),
5564            },
5565        )
5566        .unwrap();
5567        let error = apply(
5568            &mut state,
5569            &registry,
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            &registry,
5582            7,
5583            MetaCommand::SetTableSchema {
5584                record: conflicting,
5585            },
5586        )
5587        .unwrap_err();
5588        assert!(matches!(error, MetaRejectionReason::Conflict { .. }));
5589        // Identical replay is a no-op.
5590        apply(
5591            &mut state,
5592            &registry,
5593            8,
5594            MetaCommand::SetTableSchema {
5595                record: schema_record(1, 1, 2),
5596            },
5597        )
5598        .unwrap();
5599
5600        // Tablet generations move forward only.
5601        apply(
5602            &mut state,
5603            &registry,
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            &registry,
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            &registry,
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            &registry,
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            &registry,
5646            13,
5647            MetaCommand::SetTabletDescriptor {
5648                descriptor: tablet(1, 1, 6),
5649            },
5650        )
5651        .unwrap();
5652
5653        // Database id/name uniqueness.
5654        let error = apply(
5655            &mut state,
5656            &registry,
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            &registry,
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            &registry,
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            &registry,
5692            1,
5693            MetaCommand::RegisterNode {
5694                descriptor: descriptor(1, &[]),
5695            },
5696        )
5697        .unwrap();
5698        apply(
5699            &mut state,
5700            &registry,
5701            2,
5702            MetaCommand::CreateDatabase {
5703                descriptor: database(1, "app"),
5704            },
5705        )
5706        .unwrap();
5707        apply(
5708            &mut state,
5709            &registry,
5710            3,
5711            MetaCommand::SetTableSchema {
5712                record: schema_record(1, 1, 1),
5713            },
5714        )
5715        .unwrap();
5716        apply(
5717            &mut state,
5718            &registry,
5719            4,
5720            MetaCommand::SetReplicaPlacement {
5721                placement: placement(9, &[(1, ReplicaRole::Voter)]),
5722            },
5723        )
5724        .unwrap();
5725        // Node removal is refused while a placement references it.
5726        let error = apply(
5727            &mut state,
5728            &registry,
5729            5,
5730            MetaCommand::RemoveNode {
5731                node_id: node_id(1),
5732            },
5733        )
5734        .unwrap_err();
5735        assert!(matches!(error, MetaRejectionReason::Conflict { .. }));
5736        // Database drop is refused while tables reference it.
5737        let error = apply(
5738            &mut state,
5739            &registry,
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        // A placement may not reference an unregistered node.
5748        let error = apply(
5749            &mut state,
5750            &registry,
5751            7,
5752            MetaCommand::SetReplicaPlacement {
5753                placement: placement(8, &[(7, ReplicaRole::Voter)]),
5754            },
5755        )
5756        .unwrap_err();
5757        assert!(matches!(error, MetaRejectionReason::NotFound { .. }));
5758        // Duplicate replica nodes are refused.
5759        let error = apply(
5760            &mut state,
5761            &registry,
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        // A tablet's table must exist.
5770        let error = apply(
5771            &mut state,
5772            &registry,
5773            9,
5774            MetaCommand::SetTabletDescriptor {
5775                descriptor: tablet(2, 999, 1),
5776            },
5777        )
5778        .unwrap_err();
5779        assert!(matches!(error, MetaRejectionReason::NotFound { .. }));
5780        // A schema's database must exist.
5781        let error = apply(
5782            &mut state,
5783            &registry,
5784            10,
5785            MetaCommand::SetTableSchema {
5786                record: schema_record(2, 42, 1),
5787            },
5788        )
5789        .unwrap_err();
5790        assert!(matches!(error, MetaRejectionReason::NotFound { .. }));
5791        // Once the placement moves off, removal succeeds.
5792        apply(
5793            &mut state,
5794            &registry,
5795            11,
5796            MetaCommand::SetReplicaPlacement {
5797                placement: placement(9, &[(1, ReplicaRole::Learner)]),
5798            },
5799        )
5800        .unwrap();
5801        let error = apply(
5802            &mut state,
5803            &registry,
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            &registry,
5823            1,
5824            MetaCommand::RegisterNode {
5825                descriptor: descriptor(1, &[]),
5826            },
5827        )
5828        .unwrap();
5829        apply(
5830            &mut state,
5831            &registry,
5832            2,
5833            MetaCommand::CreateDatabase {
5834                descriptor: database(1, "app"),
5835            },
5836        )
5837        .unwrap();
5838        apply(
5839            &mut state,
5840            &registry,
5841            3,
5842            MetaCommand::SetTableSchema {
5843                record: schema_record(1, 1, 1),
5844            },
5845        )
5846        .unwrap();
5847        // Submissions must start Pending.
5848        let mut running = schema_job(7, 1, 1);
5849        running.state = SchemaJobState::Running;
5850        let error = apply(
5851            &mut state,
5852            &registry,
5853            4,
5854            MetaCommand::SubmitSchemaJob { job: running },
5855        )
5856        .unwrap_err();
5857        assert!(matches!(error, MetaRejectionReason::Invalid { .. }));
5858        apply(
5859            &mut state,
5860            &registry,
5861            5,
5862            MetaCommand::SubmitSchemaJob {
5863                job: schema_job(7, 1, 1),
5864            },
5865        )
5866        .unwrap();
5867        // Pending -> Succeeded is not an edge.
5868        let error = apply(
5869            &mut state,
5870            &registry,
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        // Legal walk: Pending -> Running -> Succeeded.
5883        for (id, job_state) in [
5884            (7_u8, SchemaJobState::Running),
5885            (8, SchemaJobState::Succeeded),
5886        ] {
5887            apply(
5888                &mut state,
5889                &registry,
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        // Terminal states have no outgoing edges.
5902        let error = apply(
5903            &mut state,
5904            &registry,
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        // Stale optimistic-concurrency guard.
5917        let error = apply(
5918            &mut state,
5919            &registry,
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    // -- sink: envelope discipline + snapshots -------------------------------
5934
5935    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        // A catalog envelope with a foreign command type fails closed.
5959        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        // A transaction command is misrouted to the meta group: fail closed.
5966        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        // Maintenance and Noop are documented no-ops.
5973        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        // Plus one journaled refusal, so the journal round-trips too.
5994        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        // Corrupt payloads and unsupported versions fail closed.
6018        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        // The failed installs left the state untouched.
6031        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        // Reopen: the state is recovered from the durable checkpoint without
6048        // replaying the log.
6049        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        // Crash-window replay: redelivering an entry at or below the
6055        // checkpoint watermark is skipped without double-applying.
6056        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        // A new entry above the watermark applies and checkpoints.
6065        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        // A present-but-corrupt checkpoint fails closed.
6077        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    // -- group integration ----------------------------------------------------
6092
6093    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        // Directory layout: node-data/groups/<meta-group-id>/raft.
6126        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        // A client retry with the same command id and payload commits a new
6173        // entry but is recognized as a replay at apply (S2B-004).
6174        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        // The single voter lacks the feature: refused at apply, typed error.
6203        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        // Denied settings key, same path.
6224        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        // Both refusals are journaled; the watermark still moved.
6240        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        // Node 2 runs a pristine (never initialized) meta group member.
6257        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        // The bootstrap member registers its descriptor (initial-membership
6265        // registration is part of the bootstrap workflow).
6266        meta1
6267            .propose(
6268                cmd_id(1),
6269                MetaCommand::RegisterNode {
6270                    descriptor: descriptor(1, &[]),
6271                },
6272                &control,
6273            )
6274            .await
6275            .unwrap();
6276
6277        // add_member: learner, catch-up, promote, then the descriptor lands
6278        // in replicated state.
6279        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        // A placement referencing node 2 blocks its removal.
6294        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        // Move the placement off, then remove: meta state first, raft second.
6316        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        // Removing the current leader fails closed.
6332        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        // Distinct node ids sharing the first eight bytes project to the
6349        // same raft id: rejected at bootstrap by this layer.
6350        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        // Fail over: stop the leader; the survivors elect a new one and keep
6461        // accepting commands.
6462        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        // The failed node rejoins from its durable state and converges.
6505        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        // A fresh member installs the image: identical state, watermark, and
6549        // feature level without replaying the log.
6550        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// ---------------------------------------------------------------------------
6565// Type reconciliation tests: v1 payloads decode and migrate (module docs)
6566// ---------------------------------------------------------------------------
6567
6568#[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    /// Encodes a v1 command record exactly as the v1 build did.
6650    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        // start/end map onto low/high with v1 semantics (inclusive/exclusive).
6669        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        // Online maps onto Active; roles and the leader hint carry over.
6677        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        // v1 replicas carried no raft id: they gain the node-id projection
6684        // the v1 group actually used.
6685        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        // The command-level descriptor carries no meta modification version.
6694    }
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        // Voter constraints were hard requirements; leader preferences soft.
6729        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        // Offline maps onto Retiring; the record version is preserved.
6819        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        // The migrated sink checkpoints v2: after an apply forces a persist,
6834        // a reopen reads its own format and the on-disk bytes are v2.
6835        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        // The exact byte shape a v1 build wrote (sparse state fields ride the
6858        // serde defaults): guards the v1 compatibility module against drift.
6859        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        // Register the table the tablet references, then the v1 tablet write.
6958        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        // The v1 payload applied as the migrated canonical descriptor.
7002        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        // And a v2 write at a higher generation wins over the migrated record.
7010        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}