use std::collections::HashMap;
use std::sync::RwLock;
use std::time::{Duration, Instant};
#[derive(Debug, Clone)]
struct OldMasterTracker {
name: String,
failover_time: Instant,
demoted: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DemotionStrategy {
DemoteToSlave,
Isolate,
}
#[derive(Debug, Clone)]
pub struct SplitBrainAlert {
pub old_master: String,
pub new_master: String,
pub detected_at: Instant,
pub action: DemotionStrategy,
pub message: String,
}
pub struct SplitBrainDetector {
old_masters: RwLock<HashMap<String, OldMasterTracker>>,
check_interval: Duration,
alerts: RwLock<Vec<SplitBrainAlert>>,
}
impl SplitBrainDetector {
pub fn new(check_interval: Duration) -> Self {
Self {
old_masters: RwLock::new(HashMap::new()),
check_interval,
alerts: RwLock::new(Vec::new()),
}
}
pub fn register_old_master(&self, name: &str) {
let mut old_masters = self.old_masters.write().expect("old_masters lock poisoned");
old_masters.insert(
name.to_string(),
OldMasterTracker {
name: name.to_string(),
failover_time: Instant::now(),
demoted: false,
},
);
}
pub fn check_split_brain(
&self,
old_master: &str,
new_master: &str,
old_master_healthy: bool,
) -> SplitBrainStatus {
let old_masters = self.old_masters.read().expect("old_masters lock poisoned");
let tracker = old_masters.get(old_master);
if let Some(tracker) = tracker {
if old_master_healthy && !tracker.demoted && old_master != new_master {
return SplitBrainStatus::Detected {
old_master: old_master.to_string(),
new_master: new_master.to_string(),
};
}
}
SplitBrainStatus::NoSplitBrain
}
pub fn resolve_split_brain(
&self,
old_master: &str,
new_master: &str,
strategy: DemotionStrategy,
) -> SplitBrainAlert {
{
let mut old_masters = self.old_masters.write().expect("old_masters lock poisoned");
if let Some(tracker) = old_masters.get_mut(old_master) {
tracker.demoted = true;
}
}
let action_msg = match strategy {
DemotionStrategy::DemoteToSlave => "demoted to slave",
DemotionStrategy::Isolate => "isolated",
};
let message = format!(
"split-brain detected, old master '{}' {} (new master: '{}')",
old_master, action_msg, new_master
);
let alert = SplitBrainAlert {
old_master: old_master.to_string(),
new_master: new_master.to_string(),
detected_at: Instant::now(),
action: strategy,
message: message.clone(),
};
{
let mut alerts = self.alerts.write().expect("alerts lock poisoned");
alerts.push(alert.clone());
}
alert
}
pub fn periodic_check<F>(&self, new_master: &str, health_fn: F) -> Vec<SplitBrainStatus>
where
F: Fn(&str) -> bool,
{
let old_masters = self.old_masters.read().expect("old_masters lock poisoned");
let mut results = Vec::new();
for tracker in old_masters.values() {
if tracker.demoted {
continue;
}
let is_healthy = health_fn(&tracker.name);
if is_healthy && tracker.name != new_master {
results.push(SplitBrainStatus::Detected {
old_master: tracker.name.clone(),
new_master: new_master.to_string(),
});
}
}
results
}
pub fn mark_resolved(&self, old_master: &str) {
let mut old_masters = self.old_masters.write().expect("old_masters lock poisoned");
if let Some(tracker) = old_masters.get_mut(old_master) {
tracker.demoted = true;
}
}
pub fn alerts(&self) -> Vec<SplitBrainAlert> {
self.alerts.read().expect("alerts lock poisoned").clone()
}
pub fn check_interval(&self) -> Duration {
self.check_interval
}
pub fn is_demoted(&self, old_master: &str) -> bool {
let old_masters = self.old_masters.read().expect("old_masters lock poisoned");
old_masters
.get(old_master)
.map(|t| t.demoted)
.unwrap_or(false)
}
pub fn time_since_failover(&self, old_master: &str) -> Option<Duration> {
let old_masters = self.old_masters.read().expect("old_masters lock poisoned");
old_masters
.get(old_master)
.map(|t| t.failover_time.elapsed())
}
}
use super::manager::SplitBrainStatus;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_register_and_check_no_split_brain() {
let detector = SplitBrainDetector::new(Duration::from_secs(1));
detector.register_old_master("old_master");
let status = detector.check_split_brain("old_master", "new_master", false);
assert_eq!(status, SplitBrainStatus::NoSplitBrain);
}
#[test]
fn test_split_brain_detected_when_old_master_healthy() {
let detector = SplitBrainDetector::new(Duration::from_secs(1));
detector.register_old_master("old_master");
let status = detector.check_split_brain("old_master", "new_master", true);
assert_eq!(
status,
SplitBrainStatus::Detected {
old_master: "old_master".to_string(),
new_master: "new_master".to_string(),
}
);
}
#[test]
fn test_resolve_split_brain_demote() {
let detector = SplitBrainDetector::new(Duration::from_secs(1));
detector.register_old_master("old_master");
let alert = detector.resolve_split_brain(
"old_master",
"new_master",
DemotionStrategy::DemoteToSlave,
);
assert_eq!(alert.old_master, "old_master");
assert_eq!(alert.new_master, "new_master");
assert_eq!(alert.action, DemotionStrategy::DemoteToSlave);
assert!(alert.message.contains("demoted to slave"));
assert!(detector.is_demoted("old_master"));
}
#[test]
fn test_resolve_split_brain_isolate() {
let detector = SplitBrainDetector::new(Duration::from_secs(1));
detector.register_old_master("old_master");
let alert =
detector.resolve_split_brain("old_master", "new_master", DemotionStrategy::Isolate);
assert_eq!(alert.action, DemotionStrategy::Isolate);
assert!(alert.message.contains("isolated"));
assert!(detector.is_demoted("old_master"));
}
#[test]
fn test_no_split_brain_after_demotion() {
let detector = SplitBrainDetector::new(Duration::from_secs(1));
detector.register_old_master("old_master");
detector.resolve_split_brain("old_master", "new_master", DemotionStrategy::DemoteToSlave);
let status = detector.check_split_brain("old_master", "new_master", true);
assert_eq!(status, SplitBrainStatus::NoSplitBrain);
}
#[test]
fn test_periodic_check_detects_multiple() {
let detector = SplitBrainDetector::new(Duration::from_secs(1));
detector.register_old_master("old1");
detector.register_old_master("old2");
let results =
detector.periodic_check("new_master", |name| name == "old1" || name == "old2");
assert_eq!(results.len(), 2);
}
#[test]
fn test_periodic_check_skips_demoted() {
let detector = SplitBrainDetector::new(Duration::from_secs(1));
detector.register_old_master("old1");
detector.register_old_master("old2");
detector.resolve_split_brain("old1", "new_master", DemotionStrategy::DemoteToSlave);
let results = detector.periodic_check("new_master", |_| true);
assert_eq!(results.len(), 1);
match &results[0] {
SplitBrainStatus::Detected { old_master, .. } => {
assert_eq!(old_master, "old2");
}
_ => panic!("expected Detected"),
}
}
#[test]
fn test_alerts_recorded() {
let detector = SplitBrainDetector::new(Duration::from_secs(1));
detector.register_old_master("old1");
detector.register_old_master("old2");
detector.resolve_split_brain("old1", "new1", DemotionStrategy::DemoteToSlave);
detector.resolve_split_brain("old2", "new2", DemotionStrategy::Isolate);
let alerts = detector.alerts();
assert_eq!(alerts.len(), 2);
assert_eq!(alerts[0].old_master, "old1");
assert_eq!(alerts[1].old_master, "old2");
}
#[test]
fn test_mark_resolved() {
let detector = SplitBrainDetector::new(Duration::from_secs(1));
detector.register_old_master("old_master");
detector.mark_resolved("old_master");
assert!(detector.is_demoted("old_master"));
let status = detector.check_split_brain("old_master", "new_master", true);
assert_eq!(status, SplitBrainStatus::NoSplitBrain);
}
#[test]
fn test_check_interval() {
let detector = SplitBrainDetector::new(Duration::from_secs(5));
assert_eq!(detector.check_interval(), Duration::from_secs(5));
}
#[test]
fn test_no_split_brain_same_master() {
let detector = SplitBrainDetector::new(Duration::from_secs(1));
detector.register_old_master("master");
let status = detector.check_split_brain("master", "master", true);
assert_eq!(status, SplitBrainStatus::NoSplitBrain);
}
}