ursula-control 0.3.0

Control-plane state for Ursula dynamic node registration, group placement, and manual group migration.
Documentation
use std::collections::BTreeMap;
use std::collections::BTreeSet;

use serde::Deserialize;
use serde::Serialize;
use ursula_shard::RaftGroupId;

pub type NodeId = u64;

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum NodeState {
    Active,
    Draining,
    Disabled,
    Removed,
}

impl NodeState {
    pub fn is_migration_eligible(self) -> bool {
        matches!(self, Self::Active)
    }
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ClusterNode {
    pub node_id: NodeId,
    pub client_url: String,
    pub cluster_url: String,
    pub state: NodeState,
    pub registered_at_ms: u64,
    pub updated_at_ms: u64,
    #[serde(default)]
    pub labels: BTreeMap<String, String>,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct DataGroupPlacement {
    pub raft_group_id: RaftGroupId,
    pub voters: BTreeSet<NodeId>,
    pub learners: BTreeSet<NodeId>,
    pub draining: BTreeSet<NodeId>,
    pub epoch: u64,
    pub updated_at_ms: u64,
}

impl DataGroupPlacement {
    pub fn empty(raft_group_id: RaftGroupId) -> Self {
        Self {
            raft_group_id,
            voters: BTreeSet::new(),
            learners: BTreeSet::new(),
            draining: BTreeSet::new(),
            epoch: 0,
            updated_at_ms: 0,
        }
    }

    pub fn hosts(&self, node_id: NodeId) -> bool {
        self.voters.contains(&node_id) || self.learners.contains(&node_id)
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum LearnerStatus {
    Pending,
    Adding,
    CaughtUp,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
pub enum MigrationPhase {
    Validating,
    PreparingLocalEngines,
    AddingLearners,
    ChangingVoters,
    VerifyingMembership,
    CommittingPlacement,
    Finalizing,
    Succeeded,
    Failed,
}

impl MigrationPhase {
    pub fn is_running(self) -> bool {
        !matches!(self, Self::Succeeded | Self::Failed)
    }

    pub fn can_advance_to(self, next: Self) -> bool {
        if !self.is_running() || !next.is_running() {
            return false;
        }
        self < next
    }
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct GroupMigration {
    pub migration_id: u64,
    pub raft_group_id: RaftGroupId,
    pub from_voters: BTreeSet<NodeId>,
    pub target_voters: BTreeSet<NodeId>,
    pub added_nodes: BTreeSet<NodeId>,
    pub removed_voters: BTreeSet<NodeId>,
    pub retain_removed: bool,
    pub phase: MigrationPhase,
    pub per_node_learner_status: BTreeMap<NodeId, LearnerStatus>,
    pub last_error: Option<String>,
    pub retry_count: u32,
    pub created_at_ms: u64,
    pub updated_at_ms: u64,
}

impl GroupMigration {
    pub fn is_running(&self) -> bool {
        self.phase.is_running()
    }
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct MetaConfig {
    pub initial_meta_voters: BTreeSet<NodeId>,
    pub default_replication_factor: u32,
    pub autopilot_enabled: bool,
}

impl Default for MetaConfig {
    fn default() -> Self {
        Self {
            initial_meta_voters: BTreeSet::new(),
            default_replication_factor: 3,
            autopilot_enabled: false,
        }
    }
}