use crate::cluster::config::NodeClass;
use crate::app::node_info::ClusterView;
pub trait LeaderElection: Send + Sync {
fn elect(&self, view: &ClusterView) -> Option<String>;
fn name(&self) -> &str {
std::any::type_name::<Self>()
}
}
pub struct OldestNode {
pub required_class: Option<NodeClass>,
}
impl OldestNode {
pub fn any() -> Self {
Self {
required_class: None,
}
}
pub fn with_class(class: NodeClass) -> Self {
Self {
required_class: Some(class),
}
}
}
impl Default for OldestNode {
fn default() -> Self {
Self::any()
}
}
impl LeaderElection for OldestNode {
fn elect(&self, view: &ClusterView) -> Option<String> {
view.alive_nodes()
.filter(|node| match &self.required_class {
Some(required) => &node.class == required,
None => true,
})
.min_by_key(|node| (node.identity.incarnation, node.identity.name.clone()))
.map(|node| node.node_id())
}
fn name(&self) -> &str {
"OldestNode"
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::app::node_info::NodeInfo;
use crate::cluster::config::NodeIdentity;
use std::collections::HashMap;
fn make_node(name: &str, incarnation: u64, class: NodeClass) -> NodeInfo {
NodeInfo::new(
NodeIdentity {
name: name.into(),
host: "127.0.0.1".into(),
port: 7100,
incarnation,
},
class,
HashMap::new(),
)
}
#[test]
fn test_oldest_node_picks_lowest_incarnation() {
let mut view = ClusterView::new();
view.upsert_node(make_node("gamma", 300, NodeClass::Worker));
view.upsert_node(make_node("alpha", 100, NodeClass::Worker));
view.upsert_node(make_node("beta", 200, NodeClass::Worker));
let election = OldestNode::any();
let leader = election.elect(&view).unwrap();
assert!(leader.contains("alpha"), "expected alpha, got {leader}");
}
#[test]
fn test_oldest_node_skips_dead() {
let mut view = ClusterView::new();
let oldest = make_node("alpha", 100, NodeClass::Worker);
let oldest_id = oldest.node_id();
view.upsert_node(oldest);
view.upsert_node(make_node("beta", 200, NodeClass::Worker));
view.mark_failed(&oldest_id);
let election = OldestNode::any();
let leader = election.elect(&view).unwrap();
assert!(leader.contains("beta"), "expected beta, got {leader}");
}
#[test]
fn test_oldest_node_with_class_filter() {
let mut view = ClusterView::new();
view.upsert_node(make_node("alpha", 100, NodeClass::Worker));
view.upsert_node(make_node("beta", 200, NodeClass::Coordinator));
let election = OldestNode::with_class(NodeClass::Coordinator);
let leader = election.elect(&view).unwrap();
assert!(leader.contains("beta"), "expected beta, got {leader}");
}
#[test]
fn test_oldest_node_no_eligible_returns_none() {
let view = ClusterView::new();
let election = OldestNode::any();
assert!(election.elect(&view).is_none());
}
#[test]
fn test_oldest_node_all_dead_returns_none() {
let mut view = ClusterView::new();
let node = make_node("alpha", 100, NodeClass::Worker);
let node_id = node.node_id();
view.upsert_node(node);
view.mark_failed(&node_id);
let election = OldestNode::any();
assert!(election.elect(&view).is_none());
}
}