use std::collections::{BTreeMap, BTreeSet};
use std::path::Path;
use std::sync::Arc;
use std::time::{SystemTime, UNIX_EPOCH};
use async_trait::async_trait;
use openraft::Config;
use crate::cmd::{Cmd, CmdResult};
use crate::control::{ControlError, ControlPlane};
use crate::raft::{MocraRaft, Node, NodeId};
use crate::raft_http::{HttpNetwork, JoinRequest, raft_router};
use crate::raft_log_store::RedbLogStore;
use crate::raft_network::StubNetwork;
use crate::raft_store::StateMachineStore;
use crate::state_machine::StateMachine;
fn now_ms() -> u64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_millis() as u64)
.unwrap_or(0)
}
#[derive(Debug, Clone)]
pub struct ClusterStatus {
pub node_id: NodeId,
pub is_leader: bool,
pub current_leader: Option<NodeId>,
pub term: u64,
pub last_applied_index: Option<u64>,
pub member_count: usize,
pub voter_count: usize,
}
#[derive(Debug, Clone)]
pub struct RaftTuning {
pub heartbeat_ms: u64,
pub election_timeout_min_ms: u64,
pub election_timeout_max_ms: u64,
}
impl Default for RaftTuning {
fn default() -> Self {
Self {
heartbeat_ms: 250,
election_timeout_min_ms: 600,
election_timeout_max_ms: 1200,
}
}
}
#[derive(Clone)]
pub struct RaftControlPlane {
node_id: NodeId,
raft: MocraRaft,
sm: Arc<StateMachine>,
client: reqwest::Client,
}
impl RaftControlPlane {
pub async fn start_single_node(
node_id: NodeId,
dir: impl AsRef<Path>,
) -> Result<Self, ControlError> {
let dir = dir.as_ref();
std::fs::create_dir_all(dir).ok();
let sm = Arc::new(StateMachine::open(dir.join("sm.redb"))?);
let config = Config {
heartbeat_interval: 250,
election_timeout_min: 299,
..Default::default()
};
let config = Arc::new(
config
.validate()
.map_err(|e| ControlError::Config(e.to_string()))?,
);
let log_store = RedbLogStore::open(dir.join("log.redb"))
.map_err(|e| ControlError::Raft(e.to_string()))?;
let sm_store = StateMachineStore::new(sm.clone());
let raft = MocraRaft::new(node_id, config, StubNetwork, log_store, sm_store)
.await
.map_err(|e| ControlError::Raft(e.to_string()))?;
let mut members = BTreeMap::new();
members.insert(node_id, Node::default());
let _ = raft.initialize(members).await;
Ok(Self {
node_id,
raft,
sm,
client: reqwest::Client::new(),
})
}
pub async fn start_cluster_node(
node_id: NodeId,
dir: impl AsRef<Path>,
http_addr: impl Into<String>,
) -> Result<Self, ControlError> {
Self::start_cluster_node_with(node_id, dir, http_addr, RaftTuning::default()).await
}
pub async fn start_cluster_node_with(
node_id: NodeId,
dir: impl AsRef<Path>,
http_addr: impl Into<String>,
tuning: RaftTuning,
) -> Result<Self, ControlError> {
let http_addr = http_addr.into();
let dir = dir.as_ref();
std::fs::create_dir_all(dir).ok();
let sm = Arc::new(StateMachine::open(dir.join("sm.redb"))?);
let config = Arc::new(
Config {
heartbeat_interval: tuning.heartbeat_ms,
election_timeout_min: tuning.election_timeout_min_ms,
election_timeout_max: tuning.election_timeout_max_ms,
..Default::default()
}
.validate()
.map_err(|e| ControlError::Config(e.to_string()))?,
);
let log_store = RedbLogStore::open(dir.join("log.redb"))
.map_err(|e| ControlError::Raft(e.to_string()))?;
let sm_store = StateMachineStore::new(sm.clone());
let raft = MocraRaft::new(node_id, config, HttpNetwork::new(), log_store, sm_store)
.await
.map_err(|e| ControlError::Raft(e.to_string()))?;
let router = raft_router(raft.clone());
let listener = tokio::net::TcpListener::bind(&http_addr)
.await
.map_err(|e| ControlError::Raft(format!("bind {http_addr}: {e}")))?;
tokio::spawn(async move {
let _ = axum::serve(listener, router).await;
});
Ok(Self {
node_id,
raft,
sm,
client: reqwest::Client::new(),
})
}
pub async fn init_cluster(
&self,
members: BTreeMap<NodeId, String>,
) -> Result<(), ControlError> {
let m: BTreeMap<NodeId, Node> = members
.into_iter()
.map(|(id, addr)| (id, Node::new(addr)))
.collect();
self.raft
.initialize(m)
.await
.map_err(|e| ControlError::Raft(e.to_string()))?;
Ok(())
}
pub async fn add_learner(
&self,
node_id: NodeId,
addr: impl Into<String>,
) -> Result<(), ControlError> {
self.raft
.add_learner(node_id, Node::new(addr.into()), true)
.await
.map_err(|e| ControlError::Raft(e.to_string()))?;
Ok(())
}
pub async fn change_membership(&self, voters: BTreeSet<NodeId>) -> Result<(), ControlError> {
self.raft
.change_membership(voters, false)
.await
.map_err(|e| ControlError::Raft(e.to_string()))?;
Ok(())
}
pub async fn join_cluster(
seed_addr: &str,
node_id: NodeId,
my_addr: &str,
) -> Result<(), ControlError> {
let req = JoinRequest {
node_id,
addr: my_addr.to_string(),
};
let body = rmp_serde::to_vec(&req).map_err(|e| ControlError::Raft(e.to_string()))?;
let bytes = reqwest::Client::new()
.post(format!("http://{seed_addr}/cluster/join"))
.body(body)
.send()
.await
.map_err(|e| ControlError::Raft(e.to_string()))?
.bytes()
.await
.map_err(|e| ControlError::Raft(e.to_string()))?;
let res: Result<(), String> =
rmp_serde::from_slice(&bytes).map_err(|e| ControlError::Raft(e.to_string()))?;
res.map_err(ControlError::Raft)
}
pub fn raft(&self) -> &MocraRaft {
&self.raft
}
pub fn members(&self) -> Vec<(NodeId, String)> {
let mc = self.raft.metrics().borrow().membership_config.clone();
mc.membership()
.nodes()
.map(|(id, node)| (*id, node.addr.clone()))
.collect()
}
pub fn member_count(&self) -> usize {
let mc = self.raft.metrics().borrow().membership_config.clone();
mc.membership().nodes().count().max(1)
}
pub fn voter_count(&self) -> usize {
let mc = self.raft.metrics().borrow().membership_config.clone();
mc.membership().voter_ids().count()
}
pub fn current_leader(&self) -> Option<NodeId> {
self.raft.metrics().borrow().current_leader
}
pub fn status(&self) -> ClusterStatus {
let m = self.raft.metrics().borrow().clone();
let mc = m.membership_config.clone();
ClusterStatus {
node_id: self.node_id,
is_leader: m.current_leader == Some(self.node_id),
current_leader: m.current_leader,
term: m.current_term,
last_applied_index: m.last_applied.map(|l| l.index),
member_count: mc.membership().nodes().count().max(1),
voter_count: mc.membership().voter_ids().count(),
}
}
pub fn node_id(&self) -> NodeId {
self.node_id
}
pub fn owned_partitions(&self) -> Vec<u32> {
let members: Vec<NodeId> = self.members().into_iter().map(|(id, _)| id).collect();
crate::partition::partitions_owned_by(
self.node_id,
&members,
crate::partition::DEFAULT_PARTITIONS,
)
}
pub fn owns_key(&self, key: &str) -> bool {
let members: Vec<NodeId> = self.members().into_iter().map(|(id, _)| id).collect();
crate::partition::owns_key(
self.node_id,
key,
&members,
crate::partition::DEFAULT_PARTITIONS,
)
}
pub async fn acquire_partition(
&self,
partition: u32,
ttl_ms: u64,
) -> Result<Option<u64>, ControlError> {
let key = format!("__part/{partition}");
self.acquire_lock(&key, &self.node_id.to_string(), ttl_ms)
.await
}
pub async fn release_partition(&self, partition: u32) -> Result<(), ControlError> {
let key = format!("__part/{partition}");
self.release_lock(&key, &self.node_id.to_string()).await
}
pub async fn shutdown(&self) -> Result<(), ControlError> {
self.raft
.shutdown()
.await
.map_err(|e| ControlError::Raft(e.to_string()))?;
Ok(())
}
pub async fn wait_leader(&self, timeout: std::time::Duration) -> Result<(), ControlError> {
self.raft
.wait(Some(timeout))
.state(openraft::ServerState::Leader, "wait leader")
.await
.map_err(|e| ControlError::Raft(e.to_string()))?;
Ok(())
}
async fn write(&self, cmd: Cmd) -> Result<CmdResult, ControlError> {
use openraft::error::{ClientWriteError, RaftError};
const MAX_ATTEMPTS: u32 = 6;
for attempt in 0..MAX_ATTEMPTS {
match self.raft.client_write(cmd.clone()).await {
Ok(resp) => return Ok(resp.data),
Err(RaftError::APIError(ClientWriteError::ForwardToLeader(f))) => {
match &f.leader_node {
Some(node) => {
return forward_write_to_leader(&self.client, &node.addr, &cmd).await;
}
None => {
tokio::time::sleep(std::time::Duration::from_millis(
150 * (attempt as u64 + 1),
))
.await;
}
}
}
Err(e) => return Err(ControlError::Raft(e.to_string())),
}
}
Err(ControlError::Raft(
"write failed: no leader elected after retries".to_string(),
))
}
}
async fn forward_write_to_leader(
client: &reqwest::Client,
leader_addr: &str,
cmd: &Cmd,
) -> Result<CmdResult, ControlError> {
let body = rmp_serde::to_vec(cmd).map_err(|e| ControlError::Raft(e.to_string()))?;
let bytes = client
.post(format!("http://{leader_addr}/cluster/write"))
.body(body)
.send()
.await
.map_err(|e| ControlError::Raft(e.to_string()))?
.bytes()
.await
.map_err(|e| ControlError::Raft(e.to_string()))?;
let res: Result<CmdResult, String> =
rmp_serde::from_slice(&bytes).map_err(|e| ControlError::Raft(e.to_string()))?;
res.map_err(ControlError::Raft)
}
#[async_trait]
impl ControlPlane for RaftControlPlane {
async fn set(&self, key: &[u8], value: &[u8]) -> Result<(), ControlError> {
self.write(Cmd::Set {
key: key.to_vec(),
value: value.to_vec(),
})
.await?;
Ok(())
}
async fn get(&self, key: &[u8]) -> Result<Option<Vec<u8>>, ControlError> {
Ok(self.sm.get(key)?)
}
async fn delete(&self, key: &[u8]) -> Result<(), ControlError> {
self.write(Cmd::Delete { key: key.to_vec() }).await?;
Ok(())
}
async fn cas(
&self,
key: &[u8],
expect: Option<&[u8]>,
value: &[u8],
) -> Result<bool, ControlError> {
match self
.write(Cmd::Cas {
key: key.to_vec(),
expect: expect.map(|e| e.to_vec()),
value: value.to_vec(),
})
.await?
{
CmdResult::Bool(b) => Ok(b),
_ => Ok(false),
}
}
async fn acquire_lock(
&self,
key: &str,
holder: &str,
ttl_ms: u64,
) -> Result<Option<u64>, ControlError> {
match self
.write(Cmd::AcquireLock {
key: key.to_string(),
holder: holder.to_string(),
now_ms: now_ms(),
ttl_ms,
})
.await?
{
CmdResult::Fencing(t) => Ok(t),
_ => Ok(None),
}
}
async fn renew_lock(&self, key: &str, holder: &str, ttl_ms: u64) -> Result<bool, ControlError> {
match self
.write(Cmd::RenewLock {
key: key.to_string(),
holder: holder.to_string(),
now_ms: now_ms(),
ttl_ms,
})
.await?
{
CmdResult::Bool(b) => Ok(b),
_ => Ok(false),
}
}
async fn release_lock(&self, key: &str, holder: &str) -> Result<(), ControlError> {
self.write(Cmd::ReleaseLock {
key: key.to_string(),
holder: holder.to_string(),
})
.await?;
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::time::Duration;
#[tokio::test]
async fn single_node_raft_applies_commands() {
let dir = tempfile::tempdir().unwrap();
let cp = RaftControlPlane::start_single_node(1, dir.path())
.await
.unwrap();
cp.raft()
.wait(Some(Duration::from_secs(10)))
.state(openraft::ServerState::Leader, "become leader")
.await
.unwrap();
cp.set(b"k", b"v").await.unwrap();
assert_eq!(cp.get(b"k").await.unwrap(), Some(b"v".to_vec()));
assert!(cp.cas(b"k", Some(b"v"), b"v2").await.unwrap());
assert_eq!(cp.get(b"k").await.unwrap(), Some(b"v2".to_vec()));
assert_eq!(cp.acquire_lock("lock", "a", 5000).await.unwrap(), Some(1));
assert_eq!(cp.acquire_lock("lock", "b", 5000).await.unwrap(), None);
let st = cp.status();
assert!(st.is_leader);
assert_eq!(st.node_id, 1);
assert_eq!(st.current_leader, Some(1));
assert_eq!(st.member_count, 1);
assert_eq!(st.voter_count, 1);
assert!(st.last_applied_index.unwrap_or(0) > 0);
}
#[tokio::test]
async fn single_node_recovers_state_after_restart() {
let dir = tempfile::tempdir().unwrap();
{
let cp = RaftControlPlane::start_single_node(1, dir.path())
.await
.unwrap();
cp.wait_leader(Duration::from_secs(10)).await.unwrap();
cp.set(b"persist", b"value").await.unwrap();
assert!(cp.cas(b"persist", Some(b"value"), b"v2").await.unwrap());
assert_eq!(
cp.acquire_lock("L", "owner", 60_000).await.unwrap(),
Some(1)
);
cp.shutdown().await.unwrap();
}
tokio::time::sleep(Duration::from_millis(200)).await;
{
let cp = RaftControlPlane::start_single_node(1, dir.path())
.await
.unwrap();
cp.wait_leader(Duration::from_secs(10)).await.unwrap();
assert_eq!(cp.get(b"persist").await.unwrap(), Some(b"v2".to_vec()));
assert_eq!(cp.acquire_lock("L", "other", 60_000).await.unwrap(), None);
cp.set(b"after", b"restart").await.unwrap();
assert_eq!(cp.get(b"after").await.unwrap(), Some(b"restart".to_vec()));
}
}
#[tokio::test]
async fn three_node_cluster_replicates_over_http() {
let d1 = tempfile::tempdir().unwrap();
let d2 = tempfile::tempdir().unwrap();
let d3 = tempfile::tempdir().unwrap();
let (a1, a2, a3) = ("127.0.0.1:27801", "127.0.0.1:27802", "127.0.0.1:27803");
let cp1 = RaftControlPlane::start_cluster_node(1, d1.path(), a1)
.await
.unwrap();
let cp2 = RaftControlPlane::start_cluster_node(2, d2.path(), a2)
.await
.unwrap();
let cp3 = RaftControlPlane::start_cluster_node(3, d3.path(), a3)
.await
.unwrap();
tokio::time::sleep(Duration::from_millis(300)).await;
let mut init = BTreeMap::new();
init.insert(1u64, a1.to_string());
cp1.init_cluster(init).await.unwrap();
cp1.raft()
.wait(Some(Duration::from_secs(10)))
.state(openraft::ServerState::Leader, "become leader")
.await
.unwrap();
cp1.add_learner(2, a2).await.unwrap();
cp1.add_learner(3, a3).await.unwrap();
let voters: BTreeSet<NodeId> = [1, 2, 3].into_iter().collect();
cp1.change_membership(voters).await.unwrap();
assert_eq!(cp1.member_count(), 3);
assert_eq!(cp1.voter_count(), 3);
assert_eq!(cp1.current_leader(), Some(1));
let mut addrs: Vec<String> = cp1.members().into_iter().map(|(_, a)| a).collect();
addrs.sort();
assert_eq!(addrs, vec![a1.to_string(), a2.to_string(), a3.to_string()]);
cp1.set(b"hello", b"world").await.unwrap();
for cp in [&cp2, &cp3] {
let mut ok = false;
for _ in 0..50 {
if cp.get(b"hello").await.unwrap() == Some(b"world".to_vec()) {
ok = true;
break;
}
tokio::time::sleep(Duration::from_millis(100)).await;
}
assert!(ok, "follower did not replicate the write");
}
assert_eq!(cp1.acquire_lock("L", "a", 5000).await.unwrap(), Some(1));
assert_eq!(cp1.acquire_lock("L", "b", 5000).await.unwrap(), None);
for cp in [&cp2, &cp3] {
for _ in 0..50 {
if cp.member_count() == 3 {
break;
}
tokio::time::sleep(Duration::from_millis(100)).await;
}
assert_eq!(cp.member_count(), 3);
}
let p1: BTreeSet<u32> = cp1.owned_partitions().into_iter().collect();
let p2: BTreeSet<u32> = cp2.owned_partitions().into_iter().collect();
let p3: BTreeSet<u32> = cp3.owned_partitions().into_iter().collect();
assert!(p1.is_disjoint(&p2) && p1.is_disjoint(&p3) && p2.is_disjoint(&p3));
assert_eq!(
p1.len() + p2.len() + p3.len(),
crate::partition::DEFAULT_PARTITIONS as usize
);
let owners = [
cp1.owns_key("account-42"),
cp2.owns_key("account-42"),
cp3.owns_key("account-42"),
];
assert_eq!(owners.iter().filter(|&&b| b).count(), 1);
let t1 = cp1.acquire_partition(7, 5000).await.unwrap();
assert!(t1.is_some());
assert_eq!(cp2.acquire_partition(7, 5000).await.unwrap(), None);
cp1.release_partition(7).await.unwrap();
let t2 = cp2.acquire_partition(7, 5000).await.unwrap();
assert!(t2 > t1, "fencing token must increase: {t2:?} !> {t1:?}");
}
#[tokio::test]
async fn cluster_handles_voter_removal() {
let d1 = tempfile::tempdir().unwrap();
let d2 = tempfile::tempdir().unwrap();
let d3 = tempfile::tempdir().unwrap();
let (a1, a2, a3) = ("127.0.0.1:27871", "127.0.0.1:27872", "127.0.0.1:27873");
let cp1 = RaftControlPlane::start_cluster_node(1, d1.path(), a1)
.await
.unwrap();
let cp2 = RaftControlPlane::start_cluster_node(2, d2.path(), a2)
.await
.unwrap();
let cp3 = RaftControlPlane::start_cluster_node(3, d3.path(), a3)
.await
.unwrap();
let _ = &cp3;
tokio::time::sleep(Duration::from_millis(300)).await;
let mut init = BTreeMap::new();
init.insert(1u64, a1.to_string());
cp1.init_cluster(init).await.unwrap();
cp1.wait_leader(Duration::from_secs(10)).await.unwrap();
cp1.add_learner(2, a2).await.unwrap();
cp1.add_learner(3, a3).await.unwrap();
cp1.change_membership([1, 2, 3].into_iter().collect())
.await
.unwrap();
assert_eq!(cp1.member_count(), 3);
cp1.set(b"before", b"3nodes").await.unwrap();
cp1.change_membership([1u64, 2].into_iter().collect())
.await
.unwrap();
assert_eq!(cp1.member_count(), 2);
assert_eq!(cp1.voter_count(), 2);
cp1.set(b"after", b"2nodes").await.unwrap();
let mut ok = false;
for _ in 0..50 {
if cp2.get(b"after").await.unwrap() == Some(b"2nodes".to_vec()) {
ok = true;
break;
}
tokio::time::sleep(Duration::from_millis(100)).await;
}
assert!(ok, "shrunk cluster failed to replicate a new write");
assert_eq!(cp2.get(b"before").await.unwrap(), Some(b"3nodes".to_vec()));
}
#[tokio::test]
async fn new_node_catches_up_via_snapshot_install() {
let d1 = tempfile::tempdir().unwrap();
let d2 = tempfile::tempdir().unwrap();
let (a1, a2) = ("127.0.0.1:27861", "127.0.0.1:27862");
let cp1 = RaftControlPlane::start_cluster_node(1, d1.path(), a1)
.await
.unwrap();
let cp2 = RaftControlPlane::start_cluster_node(2, d2.path(), a2)
.await
.unwrap();
tokio::time::sleep(Duration::from_millis(300)).await;
let mut init = BTreeMap::new();
init.insert(1u64, a1.to_string());
cp1.init_cluster(init).await.unwrap();
cp1.wait_leader(Duration::from_secs(10)).await.unwrap();
for i in 0..30u32 {
cp1.set(format!("k{i}").as_bytes(), b"v").await.unwrap();
}
cp1.raft().trigger().snapshot().await.unwrap();
let mut snap_idx = None;
for _ in 0..50 {
tokio::time::sleep(Duration::from_millis(100)).await;
if let Some(s) = cp1.raft().metrics().borrow().snapshot.clone() {
snap_idx = Some(s.index);
break;
}
}
let snap_idx = snap_idx.expect("snapshot should have been built");
cp1.raft().trigger().purge_log(snap_idx).await.unwrap();
cp1.add_learner(2, a2).await.unwrap();
for i in [0u32, 15, 29] {
let mut ok = false;
for _ in 0..50 {
if cp2.get(format!("k{i}").as_bytes()).await.unwrap() == Some(b"v".to_vec()) {
ok = true;
break;
}
tokio::time::sleep(Duration::from_millis(100)).await;
}
assert!(ok, "node 2 did not receive k{i} via snapshot install");
}
}
#[tokio::test]
async fn cluster_rebalances_partitions_as_nodes_join() {
let d1 = tempfile::tempdir().unwrap();
let d2 = tempfile::tempdir().unwrap();
let d3 = tempfile::tempdir().unwrap();
let (a1, a2, a3) = ("127.0.0.1:27851", "127.0.0.1:27852", "127.0.0.1:27853");
let cp1 = RaftControlPlane::start_cluster_node(1, d1.path(), a1)
.await
.unwrap();
let cp2 = RaftControlPlane::start_cluster_node(2, d2.path(), a2)
.await
.unwrap();
let cp3 = RaftControlPlane::start_cluster_node(3, d3.path(), a3)
.await
.unwrap();
let _ = (&cp2, &cp3); tokio::time::sleep(Duration::from_millis(300)).await;
let mut init = BTreeMap::new();
init.insert(1u64, a1.to_string());
cp1.init_cluster(init).await.unwrap();
cp1.wait_leader(Duration::from_secs(10)).await.unwrap();
assert_eq!(cp1.member_count(), 1);
let set1: BTreeSet<u32> = cp1.owned_partitions().into_iter().collect();
assert_eq!(set1.len(), crate::partition::DEFAULT_PARTITIONS as usize);
cp1.add_learner(2, a2).await.unwrap();
assert_eq!(cp1.member_count(), 2);
let set2: BTreeSet<u32> = cp1.owned_partitions().into_iter().collect();
assert!(
set2.is_subset(&set1),
"node 1's partitions must only shrink"
);
assert!(
set2.len() < set1.len(),
"node 2 must take a share from node 1"
);
cp1.add_learner(3, a3).await.unwrap();
assert_eq!(cp1.member_count(), 3);
let set3: BTreeSet<u32> = cp1.owned_partitions().into_iter().collect();
assert!(
set3.is_subset(&set2),
"node 1 must never regain a partition when node 3 joins"
);
assert!(
set3.len() < set1.len(),
"node 1 sheds partitions overall as cluster grows"
);
}
#[tokio::test]
async fn cluster_survives_leader_failure() {
let d1 = tempfile::tempdir().unwrap();
let d2 = tempfile::tempdir().unwrap();
let d3 = tempfile::tempdir().unwrap();
let (a1, a2, a3) = ("127.0.0.1:27821", "127.0.0.1:27822", "127.0.0.1:27823");
let cp1 = RaftControlPlane::start_cluster_node(1, d1.path(), a1)
.await
.unwrap();
let cp2 = RaftControlPlane::start_cluster_node(2, d2.path(), a2)
.await
.unwrap();
let cp3 = RaftControlPlane::start_cluster_node(3, d3.path(), a3)
.await
.unwrap();
tokio::time::sleep(Duration::from_millis(300)).await;
let mut init = BTreeMap::new();
init.insert(1u64, a1.to_string());
cp1.init_cluster(init).await.unwrap();
cp1.wait_leader(Duration::from_secs(10)).await.unwrap();
cp1.add_learner(2, a2).await.unwrap();
cp1.add_learner(3, a3).await.unwrap();
let voters: BTreeSet<NodeId> = [1, 2, 3].into_iter().collect();
cp1.change_membership(voters).await.unwrap();
cp1.set(b"before", b"crash").await.unwrap();
cp1.shutdown().await.unwrap();
let mut new_leader = None;
for _ in 0..100 {
tokio::time::sleep(Duration::from_millis(100)).await;
let l2 = cp2.current_leader();
let l3 = cp3.current_leader();
if let Some(l) = l2 {
if l != 1 && Some(l) == l3 {
new_leader = Some(l);
break;
}
}
}
let leader = new_leader.expect("cluster failed to elect a new leader after leader crash");
assert!(leader == 2 || leader == 3, "unexpected new leader {leader}");
let mut wrote = false;
for _ in 0..100 {
if cp2.set(b"after", b"failover").await.is_ok() {
wrote = true;
break;
}
tokio::time::sleep(Duration::from_millis(100)).await;
}
assert!(
wrote,
"surviving majority could not commit a write after failover"
);
let mut replicated = false;
for _ in 0..50 {
if cp3.get(b"after").await.unwrap() == Some(b"failover".to_vec()) {
replicated = true;
break;
}
tokio::time::sleep(Duration::from_millis(100)).await;
}
assert!(
replicated,
"post-failover write did not replicate to the other survivor"
);
assert_eq!(cp3.get(b"before").await.unwrap(), Some(b"crash".to_vec()));
}
}