1use std::collections::{BTreeMap, BTreeSet};
9use std::path::Path;
10use std::sync::Arc;
11use std::time::{SystemTime, UNIX_EPOCH};
12
13use async_trait::async_trait;
14use openraft::Config;
15
16use crate::cmd::{Cmd, CmdResult};
17use crate::control::{ControlError, ControlPlane};
18use crate::raft::{MocraRaft, Node, NodeId};
19use crate::raft_http::{HttpNetwork, JoinRequest, raft_router};
20use crate::raft_log_store::RedbLogStore;
21use crate::raft_network::StubNetwork;
22use crate::raft_store::StateMachineStore;
23use crate::state_machine::StateMachine;
24
25fn now_ms() -> u64 {
26 SystemTime::now()
27 .duration_since(UNIX_EPOCH)
28 .map(|d| d.as_millis() as u64)
29 .unwrap_or(0)
30}
31
32#[derive(Debug, Clone)]
35pub struct ClusterStatus {
36 pub node_id: NodeId,
38 pub is_leader: bool,
40 pub current_leader: Option<NodeId>,
42 pub term: u64,
44 pub last_applied_index: Option<u64>,
46 pub member_count: usize,
48 pub voter_count: usize,
50}
51
52#[derive(Debug, Clone)]
55pub struct RaftTuning {
56 pub heartbeat_ms: u64,
58 pub election_timeout_min_ms: u64,
60 pub election_timeout_max_ms: u64,
62}
63
64impl Default for RaftTuning {
65 fn default() -> Self {
66 Self {
68 heartbeat_ms: 250,
69 election_timeout_min_ms: 600,
70 election_timeout_max_ms: 1200,
71 }
72 }
73}
74
75#[derive(Clone)]
77pub struct RaftControlPlane {
78 node_id: NodeId,
79 raft: MocraRaft,
80 sm: Arc<StateMachine>,
81 client: reqwest::Client,
83}
84
85impl RaftControlPlane {
86 pub async fn start_single_node(
92 node_id: NodeId,
93 dir: impl AsRef<Path>,
94 ) -> Result<Self, ControlError> {
95 let dir = dir.as_ref();
96 std::fs::create_dir_all(dir).ok();
97 let sm = Arc::new(StateMachine::open(dir.join("sm.redb"))?);
98
99 let config = Config {
100 heartbeat_interval: 250,
101 election_timeout_min: 299,
102 ..Default::default()
103 };
104 let config = Arc::new(
105 config
106 .validate()
107 .map_err(|e| ControlError::Config(e.to_string()))?,
108 );
109
110 let log_store = RedbLogStore::open(dir.join("log.redb"))
111 .map_err(|e| ControlError::Raft(e.to_string()))?;
112 let sm_store = StateMachineStore::new(sm.clone());
113 let raft = MocraRaft::new(node_id, config, StubNetwork, log_store, sm_store)
114 .await
115 .map_err(|e| ControlError::Raft(e.to_string()))?;
116
117 let mut members = BTreeMap::new();
120 members.insert(node_id, Node::default());
121 let _ = raft.initialize(members).await;
122
123 Ok(Self {
124 node_id,
125 raft,
126 sm,
127 client: reqwest::Client::new(),
128 })
129 }
130
131 pub async fn start_cluster_node(
138 node_id: NodeId,
139 dir: impl AsRef<Path>,
140 http_addr: impl Into<String>,
141 ) -> Result<Self, ControlError> {
142 Self::start_cluster_node_with(node_id, dir, http_addr, RaftTuning::default()).await
143 }
144
145 pub async fn start_cluster_node_with(
148 node_id: NodeId,
149 dir: impl AsRef<Path>,
150 http_addr: impl Into<String>,
151 tuning: RaftTuning,
152 ) -> Result<Self, ControlError> {
153 let http_addr = http_addr.into();
154 let dir = dir.as_ref();
155 std::fs::create_dir_all(dir).ok();
156 let sm = Arc::new(StateMachine::open(dir.join("sm.redb"))?);
157
158 let config = Arc::new(
159 Config {
160 heartbeat_interval: tuning.heartbeat_ms,
161 election_timeout_min: tuning.election_timeout_min_ms,
162 election_timeout_max: tuning.election_timeout_max_ms,
163 ..Default::default()
164 }
165 .validate()
166 .map_err(|e| ControlError::Config(e.to_string()))?,
167 );
168
169 let log_store = RedbLogStore::open(dir.join("log.redb"))
170 .map_err(|e| ControlError::Raft(e.to_string()))?;
171 let sm_store = StateMachineStore::new(sm.clone());
172 let raft = MocraRaft::new(node_id, config, HttpNetwork::new(), log_store, sm_store)
173 .await
174 .map_err(|e| ControlError::Raft(e.to_string()))?;
175
176 let router = raft_router(raft.clone());
178 let listener = tokio::net::TcpListener::bind(&http_addr)
179 .await
180 .map_err(|e| ControlError::Raft(format!("bind {http_addr}: {e}")))?;
181 tokio::spawn(async move {
182 let _ = axum::serve(listener, router).await;
183 });
184
185 Ok(Self {
186 node_id,
187 raft,
188 sm,
189 client: reqwest::Client::new(),
190 })
191 }
192
193 pub async fn init_cluster(
196 &self,
197 members: BTreeMap<NodeId, String>,
198 ) -> Result<(), ControlError> {
199 let m: BTreeMap<NodeId, Node> = members
200 .into_iter()
201 .map(|(id, addr)| (id, Node::new(addr)))
202 .collect();
203 self.raft
204 .initialize(m)
205 .await
206 .map_err(|e| ControlError::Raft(e.to_string()))?;
207 Ok(())
208 }
209
210 pub async fn add_learner(
212 &self,
213 node_id: NodeId,
214 addr: impl Into<String>,
215 ) -> Result<(), ControlError> {
216 self.raft
217 .add_learner(node_id, Node::new(addr.into()), true)
218 .await
219 .map_err(|e| ControlError::Raft(e.to_string()))?;
220 Ok(())
221 }
222
223 pub async fn change_membership(&self, voters: BTreeSet<NodeId>) -> Result<(), ControlError> {
226 self.raft
227 .change_membership(voters, false)
228 .await
229 .map_err(|e| ControlError::Raft(e.to_string()))?;
230 Ok(())
231 }
232
233 pub async fn join_cluster(
238 seed_addr: &str,
239 node_id: NodeId,
240 my_addr: &str,
241 ) -> Result<(), ControlError> {
242 let req = JoinRequest {
243 node_id,
244 addr: my_addr.to_string(),
245 };
246 let body = rmp_serde::to_vec(&req).map_err(|e| ControlError::Raft(e.to_string()))?;
247 let bytes = reqwest::Client::new()
248 .post(format!("http://{seed_addr}/cluster/join"))
249 .body(body)
250 .send()
251 .await
252 .map_err(|e| ControlError::Raft(e.to_string()))?
253 .bytes()
254 .await
255 .map_err(|e| ControlError::Raft(e.to_string()))?;
256 let res: Result<(), String> =
257 rmp_serde::from_slice(&bytes).map_err(|e| ControlError::Raft(e.to_string()))?;
258 res.map_err(ControlError::Raft)
259 }
260
261 pub fn raft(&self) -> &MocraRaft {
263 &self.raft
264 }
265
266 pub fn members(&self) -> Vec<(NodeId, String)> {
272 let mc = self.raft.metrics().borrow().membership_config.clone();
273 mc.membership()
274 .nodes()
275 .map(|(id, node)| (*id, node.addr.clone()))
276 .collect()
277 }
278
279 pub fn member_count(&self) -> usize {
281 let mc = self.raft.metrics().borrow().membership_config.clone();
282 mc.membership().nodes().count().max(1)
283 }
284
285 pub fn voter_count(&self) -> usize {
287 let mc = self.raft.metrics().borrow().membership_config.clone();
288 mc.membership().voter_ids().count()
289 }
290
291 pub fn current_leader(&self) -> Option<NodeId> {
293 self.raft.metrics().borrow().current_leader
294 }
295
296 pub fn status(&self) -> ClusterStatus {
299 let m = self.raft.metrics().borrow().clone();
300 let mc = m.membership_config.clone();
301 ClusterStatus {
302 node_id: self.node_id,
303 is_leader: m.current_leader == Some(self.node_id),
304 current_leader: m.current_leader,
305 term: m.current_term,
306 last_applied_index: m.last_applied.map(|l| l.index),
307 member_count: mc.membership().nodes().count().max(1),
308 voter_count: mc.membership().voter_ids().count(),
309 }
310 }
311
312 pub fn node_id(&self) -> NodeId {
314 self.node_id
315 }
316
317 pub fn owned_partitions(&self) -> Vec<u32> {
320 let members: Vec<NodeId> = self.members().into_iter().map(|(id, _)| id).collect();
321 crate::partition::partitions_owned_by(
322 self.node_id,
323 &members,
324 crate::partition::DEFAULT_PARTITIONS,
325 )
326 }
327
328 pub fn owns_key(&self, key: &str) -> bool {
334 let members: Vec<NodeId> = self.members().into_iter().map(|(id, _)| id).collect();
335 crate::partition::owns_key(
336 self.node_id,
337 key,
338 &members,
339 crate::partition::DEFAULT_PARTITIONS,
340 )
341 }
342
343 pub async fn acquire_partition(
351 &self,
352 partition: u32,
353 ttl_ms: u64,
354 ) -> Result<Option<u64>, ControlError> {
355 let key = format!("__part/{partition}");
356 self.acquire_lock(&key, &self.node_id.to_string(), ttl_ms)
357 .await
358 }
359
360 pub async fn release_partition(&self, partition: u32) -> Result<(), ControlError> {
362 let key = format!("__part/{partition}");
363 self.release_lock(&key, &self.node_id.to_string()).await
364 }
365
366 pub async fn shutdown(&self) -> Result<(), ControlError> {
372 self.raft
373 .shutdown()
374 .await
375 .map_err(|e| ControlError::Raft(e.to_string()))?;
376 Ok(())
377 }
378
379 pub async fn wait_leader(&self, timeout: std::time::Duration) -> Result<(), ControlError> {
382 self.raft
383 .wait(Some(timeout))
384 .state(openraft::ServerState::Leader, "wait leader")
385 .await
386 .map_err(|e| ControlError::Raft(e.to_string()))?;
387 Ok(())
388 }
389
390 async fn write(&self, cmd: Cmd) -> Result<CmdResult, ControlError> {
402 use openraft::error::{ClientWriteError, RaftError};
403
404 const MAX_ATTEMPTS: u32 = 6;
405 for attempt in 0..MAX_ATTEMPTS {
406 match self.raft.client_write(cmd.clone()).await {
407 Ok(resp) => return Ok(resp.data),
408 Err(RaftError::APIError(ClientWriteError::ForwardToLeader(f))) => {
409 match &f.leader_node {
410 Some(node) => {
413 return forward_write_to_leader(&self.client, &node.addr, &cmd).await;
414 }
415 None => {
418 tokio::time::sleep(std::time::Duration::from_millis(
419 150 * (attempt as u64 + 1),
420 ))
421 .await;
422 }
423 }
424 }
425 Err(e) => return Err(ControlError::Raft(e.to_string())),
426 }
427 }
428 Err(ControlError::Raft(
429 "write failed: no leader elected after retries".to_string(),
430 ))
431 }
432}
433
434async fn forward_write_to_leader(
437 client: &reqwest::Client,
438 leader_addr: &str,
439 cmd: &Cmd,
440) -> Result<CmdResult, ControlError> {
441 let body = rmp_serde::to_vec(cmd).map_err(|e| ControlError::Raft(e.to_string()))?;
442 let bytes = client
443 .post(format!("http://{leader_addr}/cluster/write"))
444 .body(body)
445 .send()
446 .await
447 .map_err(|e| ControlError::Raft(e.to_string()))?
448 .bytes()
449 .await
450 .map_err(|e| ControlError::Raft(e.to_string()))?;
451 let res: Result<CmdResult, String> =
452 rmp_serde::from_slice(&bytes).map_err(|e| ControlError::Raft(e.to_string()))?;
453 res.map_err(ControlError::Raft)
454}
455
456#[async_trait]
457impl ControlPlane for RaftControlPlane {
458 async fn set(&self, key: &[u8], value: &[u8]) -> Result<(), ControlError> {
459 self.write(Cmd::Set {
460 key: key.to_vec(),
461 value: value.to_vec(),
462 })
463 .await?;
464 Ok(())
465 }
466
467 async fn get(&self, key: &[u8]) -> Result<Option<Vec<u8>>, ControlError> {
468 Ok(self.sm.get(key)?)
470 }
471
472 async fn delete(&self, key: &[u8]) -> Result<(), ControlError> {
473 self.write(Cmd::Delete { key: key.to_vec() }).await?;
474 Ok(())
475 }
476
477 async fn cas(
478 &self,
479 key: &[u8],
480 expect: Option<&[u8]>,
481 value: &[u8],
482 ) -> Result<bool, ControlError> {
483 match self
484 .write(Cmd::Cas {
485 key: key.to_vec(),
486 expect: expect.map(|e| e.to_vec()),
487 value: value.to_vec(),
488 })
489 .await?
490 {
491 CmdResult::Bool(b) => Ok(b),
492 _ => Ok(false),
493 }
494 }
495
496 async fn acquire_lock(
497 &self,
498 key: &str,
499 holder: &str,
500 ttl_ms: u64,
501 ) -> Result<Option<u64>, ControlError> {
502 match self
503 .write(Cmd::AcquireLock {
504 key: key.to_string(),
505 holder: holder.to_string(),
506 now_ms: now_ms(),
507 ttl_ms,
508 })
509 .await?
510 {
511 CmdResult::Fencing(t) => Ok(t),
512 _ => Ok(None),
513 }
514 }
515
516 async fn renew_lock(&self, key: &str, holder: &str, ttl_ms: u64) -> Result<bool, ControlError> {
517 match self
518 .write(Cmd::RenewLock {
519 key: key.to_string(),
520 holder: holder.to_string(),
521 now_ms: now_ms(),
522 ttl_ms,
523 })
524 .await?
525 {
526 CmdResult::Bool(b) => Ok(b),
527 _ => Ok(false),
528 }
529 }
530
531 async fn release_lock(&self, key: &str, holder: &str) -> Result<(), ControlError> {
532 self.write(Cmd::ReleaseLock {
533 key: key.to_string(),
534 holder: holder.to_string(),
535 })
536 .await?;
537 Ok(())
538 }
539}
540
541#[cfg(test)]
542mod tests {
543 use super::*;
544 use std::time::Duration;
545
546 #[tokio::test]
547 async fn single_node_raft_applies_commands() {
548 let dir = tempfile::tempdir().unwrap();
549 let cp = RaftControlPlane::start_single_node(1, dir.path())
550 .await
551 .unwrap();
552
553 cp.raft()
555 .wait(Some(Duration::from_secs(10)))
556 .state(openraft::ServerState::Leader, "become leader")
557 .await
558 .unwrap();
559
560 cp.set(b"k", b"v").await.unwrap();
562 assert_eq!(cp.get(b"k").await.unwrap(), Some(b"v".to_vec()));
563
564 assert!(cp.cas(b"k", Some(b"v"), b"v2").await.unwrap());
566 assert_eq!(cp.get(b"k").await.unwrap(), Some(b"v2".to_vec()));
567
568 assert_eq!(cp.acquire_lock("lock", "a", 5000).await.unwrap(), Some(1));
570 assert_eq!(cp.acquire_lock("lock", "b", 5000).await.unwrap(), None);
571
572 let st = cp.status();
575 assert!(st.is_leader);
576 assert_eq!(st.node_id, 1);
577 assert_eq!(st.current_leader, Some(1));
578 assert_eq!(st.member_count, 1);
579 assert_eq!(st.voter_count, 1);
580 assert!(st.last_applied_index.unwrap_or(0) > 0);
581 }
582
583 #[tokio::test]
584 async fn single_node_recovers_state_after_restart() {
585 let dir = tempfile::tempdir().unwrap();
588
589 {
591 let cp = RaftControlPlane::start_single_node(1, dir.path())
592 .await
593 .unwrap();
594 cp.wait_leader(Duration::from_secs(10)).await.unwrap();
595 cp.set(b"persist", b"value").await.unwrap();
596 assert!(cp.cas(b"persist", Some(b"value"), b"v2").await.unwrap());
597 assert_eq!(
598 cp.acquire_lock("L", "owner", 60_000).await.unwrap(),
599 Some(1)
600 );
601 cp.shutdown().await.unwrap();
604 }
605 tokio::time::sleep(Duration::from_millis(200)).await;
607
608 {
611 let cp = RaftControlPlane::start_single_node(1, dir.path())
612 .await
613 .unwrap();
614 cp.wait_leader(Duration::from_secs(10)).await.unwrap();
615 assert_eq!(cp.get(b"persist").await.unwrap(), Some(b"v2".to_vec()));
617 assert_eq!(cp.acquire_lock("L", "other", 60_000).await.unwrap(), None);
620 cp.set(b"after", b"restart").await.unwrap();
623 assert_eq!(cp.get(b"after").await.unwrap(), Some(b"restart".to_vec()));
624 }
625 }
626
627 #[tokio::test]
628 async fn three_node_cluster_replicates_over_http() {
629 let d1 = tempfile::tempdir().unwrap();
630 let d2 = tempfile::tempdir().unwrap();
631 let d3 = tempfile::tempdir().unwrap();
632 let (a1, a2, a3) = ("127.0.0.1:27801", "127.0.0.1:27802", "127.0.0.1:27803");
633
634 let cp1 = RaftControlPlane::start_cluster_node(1, d1.path(), a1)
635 .await
636 .unwrap();
637 let cp2 = RaftControlPlane::start_cluster_node(2, d2.path(), a2)
638 .await
639 .unwrap();
640 let cp3 = RaftControlPlane::start_cluster_node(3, d3.path(), a3)
641 .await
642 .unwrap();
643
644 tokio::time::sleep(Duration::from_millis(300)).await;
646
647 let mut init = BTreeMap::new();
649 init.insert(1u64, a1.to_string());
650 cp1.init_cluster(init).await.unwrap();
651 cp1.raft()
652 .wait(Some(Duration::from_secs(10)))
653 .state(openraft::ServerState::Leader, "become leader")
654 .await
655 .unwrap();
656
657 cp1.add_learner(2, a2).await.unwrap();
660 cp1.add_learner(3, a3).await.unwrap();
661 let voters: BTreeSet<NodeId> = [1, 2, 3].into_iter().collect();
662 cp1.change_membership(voters).await.unwrap();
663
664 assert_eq!(cp1.member_count(), 3);
666 assert_eq!(cp1.voter_count(), 3);
667 assert_eq!(cp1.current_leader(), Some(1));
668 let mut addrs: Vec<String> = cp1.members().into_iter().map(|(_, a)| a).collect();
669 addrs.sort();
670 assert_eq!(addrs, vec![a1.to_string(), a2.to_string(), a3.to_string()]);
671
672 cp1.set(b"hello", b"world").await.unwrap();
674
675 for cp in [&cp2, &cp3] {
677 let mut ok = false;
678 for _ in 0..50 {
679 if cp.get(b"hello").await.unwrap() == Some(b"world".to_vec()) {
680 ok = true;
681 break;
682 }
683 tokio::time::sleep(Duration::from_millis(100)).await;
684 }
685 assert!(ok, "follower did not replicate the write");
686 }
687
688 assert_eq!(cp1.acquire_lock("L", "a", 5000).await.unwrap(), Some(1));
692 assert_eq!(cp1.acquire_lock("L", "b", 5000).await.unwrap(), None);
693
694 for cp in [&cp2, &cp3] {
697 for _ in 0..50 {
698 if cp.member_count() == 3 {
699 break;
700 }
701 tokio::time::sleep(Duration::from_millis(100)).await;
702 }
703 assert_eq!(cp.member_count(), 3);
704 }
705
706 let p1: BTreeSet<u32> = cp1.owned_partitions().into_iter().collect();
709 let p2: BTreeSet<u32> = cp2.owned_partitions().into_iter().collect();
710 let p3: BTreeSet<u32> = cp3.owned_partitions().into_iter().collect();
711 assert!(p1.is_disjoint(&p2) && p1.is_disjoint(&p3) && p2.is_disjoint(&p3));
712 assert_eq!(
713 p1.len() + p2.len() + p3.len(),
714 crate::partition::DEFAULT_PARTITIONS as usize
715 );
716 let owners = [
718 cp1.owns_key("account-42"),
719 cp2.owns_key("account-42"),
720 cp3.owns_key("account-42"),
721 ];
722 assert_eq!(owners.iter().filter(|&&b| b).count(), 1);
723
724 let t1 = cp1.acquire_partition(7, 5000).await.unwrap();
728 assert!(t1.is_some());
729 assert_eq!(cp2.acquire_partition(7, 5000).await.unwrap(), None);
730 cp1.release_partition(7).await.unwrap();
731 let t2 = cp2.acquire_partition(7, 5000).await.unwrap();
732 assert!(t2 > t1, "fencing token must increase: {t2:?} !> {t1:?}");
733 }
734
735 #[tokio::test]
736 async fn cluster_handles_voter_removal() {
737 let d1 = tempfile::tempdir().unwrap();
740 let d2 = tempfile::tempdir().unwrap();
741 let d3 = tempfile::tempdir().unwrap();
742 let (a1, a2, a3) = ("127.0.0.1:27871", "127.0.0.1:27872", "127.0.0.1:27873");
743
744 let cp1 = RaftControlPlane::start_cluster_node(1, d1.path(), a1)
745 .await
746 .unwrap();
747 let cp2 = RaftControlPlane::start_cluster_node(2, d2.path(), a2)
748 .await
749 .unwrap();
750 let cp3 = RaftControlPlane::start_cluster_node(3, d3.path(), a3)
751 .await
752 .unwrap();
753 let _ = &cp3;
754 tokio::time::sleep(Duration::from_millis(300)).await;
755
756 let mut init = BTreeMap::new();
757 init.insert(1u64, a1.to_string());
758 cp1.init_cluster(init).await.unwrap();
759 cp1.wait_leader(Duration::from_secs(10)).await.unwrap();
760 cp1.add_learner(2, a2).await.unwrap();
761 cp1.add_learner(3, a3).await.unwrap();
762 cp1.change_membership([1, 2, 3].into_iter().collect())
763 .await
764 .unwrap();
765 assert_eq!(cp1.member_count(), 3);
766 cp1.set(b"before", b"3nodes").await.unwrap();
767
768 cp1.change_membership([1u64, 2].into_iter().collect())
770 .await
771 .unwrap();
772 assert_eq!(cp1.member_count(), 2);
773 assert_eq!(cp1.voter_count(), 2);
774
775 cp1.set(b"after", b"2nodes").await.unwrap();
777 let mut ok = false;
778 for _ in 0..50 {
779 if cp2.get(b"after").await.unwrap() == Some(b"2nodes".to_vec()) {
780 ok = true;
781 break;
782 }
783 tokio::time::sleep(Duration::from_millis(100)).await;
784 }
785 assert!(ok, "shrunk cluster failed to replicate a new write");
786 assert_eq!(cp2.get(b"before").await.unwrap(), Some(b"3nodes".to_vec()));
787 }
788
789 #[tokio::test]
790 async fn new_node_catches_up_via_snapshot_install() {
791 let d1 = tempfile::tempdir().unwrap();
796 let d2 = tempfile::tempdir().unwrap();
797 let (a1, a2) = ("127.0.0.1:27861", "127.0.0.1:27862");
798
799 let cp1 = RaftControlPlane::start_cluster_node(1, d1.path(), a1)
800 .await
801 .unwrap();
802 let cp2 = RaftControlPlane::start_cluster_node(2, d2.path(), a2)
803 .await
804 .unwrap();
805 tokio::time::sleep(Duration::from_millis(300)).await;
806
807 let mut init = BTreeMap::new();
808 init.insert(1u64, a1.to_string());
809 cp1.init_cluster(init).await.unwrap();
810 cp1.wait_leader(Duration::from_secs(10)).await.unwrap();
811
812 for i in 0..30u32 {
814 cp1.set(format!("k{i}").as_bytes(), b"v").await.unwrap();
815 }
816
817 cp1.raft().trigger().snapshot().await.unwrap();
819 let mut snap_idx = None;
820 for _ in 0..50 {
821 tokio::time::sleep(Duration::from_millis(100)).await;
822 if let Some(s) = cp1.raft().metrics().borrow().snapshot.clone() {
823 snap_idx = Some(s.index);
824 break;
825 }
826 }
827 let snap_idx = snap_idx.expect("snapshot should have been built");
828
829 cp1.raft().trigger().purge_log(snap_idx).await.unwrap();
832
833 cp1.add_learner(2, a2).await.unwrap();
836
837 for i in [0u32, 15, 29] {
839 let mut ok = false;
840 for _ in 0..50 {
841 if cp2.get(format!("k{i}").as_bytes()).await.unwrap() == Some(b"v".to_vec()) {
842 ok = true;
843 break;
844 }
845 tokio::time::sleep(Duration::from_millis(100)).await;
846 }
847 assert!(ok, "node 2 did not receive k{i} via snapshot install");
848 }
849 }
850
851 #[tokio::test]
852 async fn cluster_rebalances_partitions_as_nodes_join() {
853 let d1 = tempfile::tempdir().unwrap();
858 let d2 = tempfile::tempdir().unwrap();
859 let d3 = tempfile::tempdir().unwrap();
860 let (a1, a2, a3) = ("127.0.0.1:27851", "127.0.0.1:27852", "127.0.0.1:27853");
861
862 let cp1 = RaftControlPlane::start_cluster_node(1, d1.path(), a1)
863 .await
864 .unwrap();
865 let cp2 = RaftControlPlane::start_cluster_node(2, d2.path(), a2)
866 .await
867 .unwrap();
868 let cp3 = RaftControlPlane::start_cluster_node(3, d3.path(), a3)
869 .await
870 .unwrap();
871 let _ = (&cp2, &cp3); tokio::time::sleep(Duration::from_millis(300)).await;
873
874 let mut init = BTreeMap::new();
875 init.insert(1u64, a1.to_string());
876 cp1.init_cluster(init).await.unwrap();
877 cp1.wait_leader(Duration::from_secs(10)).await.unwrap();
878
879 assert_eq!(cp1.member_count(), 1);
881 let set1: BTreeSet<u32> = cp1.owned_partitions().into_iter().collect();
882 assert_eq!(set1.len(), crate::partition::DEFAULT_PARTITIONS as usize);
883
884 cp1.add_learner(2, a2).await.unwrap();
886 assert_eq!(cp1.member_count(), 2);
887 let set2: BTreeSet<u32> = cp1.owned_partitions().into_iter().collect();
888 assert!(
889 set2.is_subset(&set1),
890 "node 1's partitions must only shrink"
891 );
892 assert!(
893 set2.len() < set1.len(),
894 "node 2 must take a share from node 1"
895 );
896
897 cp1.add_learner(3, a3).await.unwrap();
901 assert_eq!(cp1.member_count(), 3);
902 let set3: BTreeSet<u32> = cp1.owned_partitions().into_iter().collect();
903 assert!(
904 set3.is_subset(&set2),
905 "node 1 must never regain a partition when node 3 joins"
906 );
907 assert!(
908 set3.len() < set1.len(),
909 "node 1 sheds partitions overall as cluster grows"
910 );
911 }
912
913 #[tokio::test]
914 async fn cluster_survives_leader_failure() {
915 let d1 = tempfile::tempdir().unwrap();
918 let d2 = tempfile::tempdir().unwrap();
919 let d3 = tempfile::tempdir().unwrap();
920 let (a1, a2, a3) = ("127.0.0.1:27821", "127.0.0.1:27822", "127.0.0.1:27823");
921
922 let cp1 = RaftControlPlane::start_cluster_node(1, d1.path(), a1)
923 .await
924 .unwrap();
925 let cp2 = RaftControlPlane::start_cluster_node(2, d2.path(), a2)
926 .await
927 .unwrap();
928 let cp3 = RaftControlPlane::start_cluster_node(3, d3.path(), a3)
929 .await
930 .unwrap();
931 tokio::time::sleep(Duration::from_millis(300)).await;
932
933 let mut init = BTreeMap::new();
934 init.insert(1u64, a1.to_string());
935 cp1.init_cluster(init).await.unwrap();
936 cp1.wait_leader(Duration::from_secs(10)).await.unwrap();
937 cp1.add_learner(2, a2).await.unwrap();
938 cp1.add_learner(3, a3).await.unwrap();
939 let voters: BTreeSet<NodeId> = [1, 2, 3].into_iter().collect();
940 cp1.change_membership(voters).await.unwrap();
941
942 cp1.set(b"before", b"crash").await.unwrap();
944
945 cp1.shutdown().await.unwrap();
947
948 let mut new_leader = None;
951 for _ in 0..100 {
952 tokio::time::sleep(Duration::from_millis(100)).await;
953 let l2 = cp2.current_leader();
954 let l3 = cp3.current_leader();
955 if let Some(l) = l2 {
957 if l != 1 && Some(l) == l3 {
958 new_leader = Some(l);
959 break;
960 }
961 }
962 }
963 let leader = new_leader.expect("cluster failed to elect a new leader after leader crash");
964 assert!(leader == 2 || leader == 3, "unexpected new leader {leader}");
965
966 let mut wrote = false;
969 for _ in 0..100 {
970 if cp2.set(b"after", b"failover").await.is_ok() {
971 wrote = true;
972 break;
973 }
974 tokio::time::sleep(Duration::from_millis(100)).await;
975 }
976 assert!(
977 wrote,
978 "surviving majority could not commit a write after failover"
979 );
980
981 let mut replicated = false;
984 for _ in 0..50 {
985 if cp3.get(b"after").await.unwrap() == Some(b"failover".to_vec()) {
986 replicated = true;
987 break;
988 }
989 tokio::time::sleep(Duration::from_millis(100)).await;
990 }
991 assert!(
992 replicated,
993 "post-failover write did not replicate to the other survivor"
994 );
995 assert_eq!(cp3.get(b"before").await.unwrap(), Some(b"crash".to_vec()));
996 }
997}