Skip to main content

mocra_cluster/
raft_node.rs

1//! Single-node Raft control plane: assembles the openraft node; commands are committed through
2//! Raft and then applied to the redb state machine.
3//!
4//! Multi-node support (join / membership changes / network RPC) is a follow-up item; this gets the
5//! single-node consensus path working first:
6//! `client_write(Cmd)` → Raft replicates and commits → `StateMachineStore::apply` → redb.
7
8use 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/// A cluster status snapshot, returned by [`RaftControlPlane::status`] for operational monitoring
33/// / health checks.
34#[derive(Debug, Clone)]
35pub struct ClusterStatus {
36    /// This node's id.
37    pub node_id: NodeId,
38    /// Whether this node is currently the leader.
39    pub is_leader: bool,
40    /// The currently known leader (`None` during a leader election).
41    pub current_leader: Option<NodeId>,
42    /// The current Raft term.
43    pub term: u64,
44    /// The highest log position applied to the state machine (`None` = nothing yet).
45    pub last_applied_index: Option<u64>,
46    /// Total number of members (voters + learners).
47    pub member_count: usize,
48    /// The number of members in the voting core.
49    pub voter_count: usize,
50}
51
52/// Raft timing knobs: the defaults suit a LAN; on high-latency / wide-area networks, scale them up
53/// to avoid falsely concluding that the leader is unreachable.
54#[derive(Debug, Clone)]
55pub struct RaftTuning {
56    /// Leader heartbeat interval (ms).
57    pub heartbeat_ms: u64,
58    /// Lower bound of the election timeout (ms). Should be far larger than the heartbeat interval.
59    pub election_timeout_min_ms: u64,
60    /// Upper bound of the election timeout (ms).
61    pub election_timeout_max_ms: u64,
62}
63
64impl Default for RaftTuning {
65    fn default() -> Self {
66        // LAN defaults: 250ms heartbeat, 600~1200ms election.
67        Self {
68            heartbeat_ms: 250,
69            election_timeout_min_ms: 600,
70            election_timeout_max_ms: 1200,
71        }
72    }
73}
74
75/// An openraft-based control plane.
76#[derive(Clone)]
77pub struct RaftControlPlane {
78    node_id: NodeId,
79    raft: MocraRaft,
80    sm: Arc<StateMachine>,
81    /// A shared HTTP client (reusing the connection pool) for the write-forwarding hot path.
82    client: reqwest::Client,
83}
84
85impl RaftControlPlane {
86    /// Starts a single-node Raft control plane.
87    ///
88    /// Two redb files live under `dir`: `sm.redb` (the state machine) and `log.redb` (the Raft
89    /// log) — both the state machine and the log are persisted, so the whole control plane is
90    /// self-contained and recoverable after a crash.
91    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        // Initialize the single-node cluster (this node is the only voter). Ignored if it was
118        // already initialized.
119        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    /// Starts a **cluster** node: the HTTP network plus its own HTTP server (exposing the Raft RPC
132    /// and `/cluster/join`).
133    ///
134    /// - `dir`: the directory for the redb state machine + log.
135    /// - `http_addr`: the address this node binds and advertises (e.g. `127.0.0.1:7001`), i.e. the
136    ///   address that identifies it within the cluster.
137    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    /// Same as [`start_cluster_node`](Self::start_cluster_node), but with customizable Raft timing
146    /// ([`RaftTuning`]) for high-latency / wide-area clusters.
147    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        // Start the HTTP server exposing the Raft RPC + join.
177        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    /// Initializes a new cluster (the given `{id: addr}` becomes the initial voting set; normally
194    /// called exactly once, on the first core node).
195    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    /// Adds a node as a learner (plan A: a worker starts out as a learner).
211    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    /// Changes the voting membership (plan A: a small voting core of 3~5; learners/workers are
224    /// unaffected).
225    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    /// Sends a join request from this node to a seed node (adding itself to the cluster as a
234    /// learner).
235    /// "Register against any node to join": `seed_addr` is the address of any known node in the
236    /// cluster (it should be the current leader).
237    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    /// The underlying openraft handle (membership changes / status queries).
262    pub fn raft(&self) -> &MocraRaft {
263        &self.raft
264    }
265
266    /// All members currently configured in the cluster (voters + learners) and their addresses.
267    ///
268    /// Based on the Raft membership configuration (strongly consistent) rather than live liveness
269    /// probing — good enough for approximate distributed semantics such as "spread rate limits
270    /// across the member count" and "partition ownership".
271    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    /// Total number of cluster members (voters + learners). At least 1.
280    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    /// The number of members in the voting core (plan A's small voting set, typically 3~5).
286    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    /// The currently known leader (returns `None` when unknown / during a leader election).
292    pub fn current_leader(&self) -> Option<NodeId> {
293        self.raft.metrics().borrow().current_leader
294    }
295
296    /// A cluster status snapshot (observability / operational monitoring: leader, term, applied
297    /// position, member count).
298    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    /// This node's id.
313    pub fn node_id(&self) -> NodeId {
314        self.node_id
315    }
316
317    /// The partitions this node is responsible for under the current membership view (rendezvous
318    /// assignment, default partition count).
319    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    /// Whether a given account / session key is handled by this node (rendezvous assignment,
329    /// default partition count).
330    ///
331    /// No negotiation needed: membership comes from Raft's strongly consistent view and the hash
332    /// is deterministic, so every node reaches the same conclusion.
333    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    /// Claims a partition's **ownership lease** and returns a monotonic fencing token.
344    ///
345    /// For situations that need a strong guarantee at the moment membership changes: while two
346    /// nodes' views briefly disagree, only the node holding the newest token can safely handle the
347    /// partition; downstream writes from a stale owner can be rejected on their smaller token.
348    /// Steady-state ownership is decided by [`owns_key`](Self::owns_key); this method layers a
349    /// Raft-backed strong guarantee on top of it.
350    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    /// Releases the partition ownership lease (only if it is still held by this node).
361    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    /// Gracefully shuts down this node's Raft runtime: stops background tasks and releases storage
367    /// (including the redb file lock).
368    ///
369    /// Call it before crash recovery / restart to make sure the redb database handle is released,
370    /// so reopening the same directory no longer reports "Database already open".
371    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    /// Waits for this node to become the leader (near-instant on a single node; a multi-node
380    /// leader election takes one election-timeout window).
381    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    /// Replicates and commits a command through Raft, returning the state machine's apply result.
391    ///
392    /// - This node is the leader → commit directly.
393    /// - This node is a follower and the leader is known → forward the [`Cmd`] over HTTP to the
394    ///   leader's `/cluster/write` ("register against any node and it works").
395    /// - **A leader election is in progress, with no leader yet** → the command is definitely not
396    ///   committed, so back off briefly and **retry the local client_write** (this is the only
397    ///   case that is retried, precisely because it is known to be uncommitted, so the
398    ///   non-idempotent CAS / AcquireLock commands cannot be applied twice; a failed forwarded
399    ///   HTTP call is not retried — the leader may already have applied it, leaving the result
400    ///   uncertain).
401    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                        // Leader known: forward once (an HTTP failure returns immediately with no
411                        // retry — the result is uncertain).
412                        Some(node) => {
413                            return forward_write_to_leader(&self.client, &node.addr, &cmd).await;
414                        }
415                        // No leader during the election: the command is uncommitted, so back off
416                        // and retry (safe).
417                        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
434/// Forwards a command to the leader's `/cluster/write` endpoint and parses the result (reusing the
435/// shared client).
436async 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        // Local read (for a linearizable read, call ensure_linearizable first — a follow-up item).
469        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        // A single node becomes the leader quickly.
554        cp.raft()
555            .wait(Some(Duration::from_secs(10)))
556            .state(openraft::ServerState::Leader, "become leader")
557            .await
558            .unwrap();
559
560        // set / get through Raft.
561        cp.set(b"k", b"v").await.unwrap();
562        assert_eq!(cp.get(b"k").await.unwrap(), Some(b"v".to_vec()));
563
564        // cas through Raft.
565        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        // Distributed lock + fencing through Raft.
569        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        // Status snapshot: a single node is its own leader, has 1 member, and the applied position
573        // advances with each write.
574        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        // Crash recovery: the control plane (state machine + log) is fully persisted in redb, so
586        // reopening the same directory should restore the committed state.
587        let dir = tempfile::tempdir().unwrap();
588
589        // First lifetime: write and commit (once client_write returns, the data is persisted).
590        {
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            // Shut down gracefully to release the redb handle; committed data is already on disk.
602            // cp is dropped when the block ends.
603            cp.shutdown().await.unwrap();
604        }
605        // Let the background tasks exit fully and the file lock be released.
606        tokio::time::sleep(Duration::from_millis(200)).await;
607
608        // Second lifetime: reopen the same directory → recover from redb (no external log replay
609        // needed).
610        {
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            // KV / CAS results are restored.
616            assert_eq!(cp.get(b"persist").await.unwrap(), Some(b"v2".to_vec()));
617            // Lock state is restored: the same key is still held by owner, so nobody else can
618            // take it.
619            assert_eq!(cp.acquire_lock("L", "other", 60_000).await.unwrap(), None);
620            // Writes can continue after recovery (the log is appendable and the fencing counter
621            // continues monotonically).
622            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        // Give the HTTP server a moment to start up.
645        tokio::time::sleep(Duration::from_millis(300)).await;
646
647        // Node 1 initializes a single-node cluster → becomes the leader.
648        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        // Add nodes 2 and 3 as learners (add_learner blocks until they catch up), then promote
658        // them to voters (plan A's core).
659        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        // Membership API: all three nodes are registered and all are voters; the leader is known.
665        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        // Write on the leader → replicated to a majority through Raft.
673        cp1.set(b"hello", b"world").await.unwrap();
674
675        // A local read on a follower should be eventually consistent (poll until it applies).
676        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        // Distributed lock + fencing through Raft (committed on the leader).
689        // (Locks live in the LOCKS table; KV replication is already verified and locks travel the
690        // same Raft log, so the followers are not queried separately.)
691        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        // Wait for the followers to see the full membership view too (membership is replicated
695        // after being committed through Raft).
696        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        // Partition ownership: the partitions each of the three nodes is responsible for do not
707        // overlap, and their union covers everything (consistent without negotiation).
708        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        // Any given account is claimed by exactly one node (all three views agree).
717        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        // Fencing ownership lease: cp1 claims partition 7 → gets a token; cp2 is rejected for the
725        // same partition; once cp1 releases it, cp2 can take it, and the token increases
726        // monotonically (split-brain protection).
727        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        // Scale-down / decommission: remove one of the 3 voting nodes → the cluster keeps
738        // committing under the new majority (the quorum is recomputed).
739        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        // Remove node 3 (shrinking to {1,2}); the new majority = 2/2.
769        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        // Commits still work after the scale-down and replicate to the remaining members.
776        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        // Log compaction + snapshot recovery: the leader writes a number of entries → triggers a
792        // snapshot → purges the snapshotted log → by the time the new node joins the log has been
793        // purged, so it can only catch up via **install_snapshot** (not by replaying the log).
794        // Exercises the whole RaftSnapshotBuilder → install_snapshot RPC → redb restore chain.
795        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        // Write a number of entries (committed quickly while still single-node).
813        for i in 0..30u32 {
814            cp1.set(format!("k{i}").as_bytes(), b"v").await.unwrap();
815        }
816
817        // Trigger a snapshot and wait for it to finish building (metrics.snapshot appears).
818        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        // Purge the snapshotted log (after this a new node cannot catch up by replay and must go
830        // through snapshot installation).
831        cp1.raft().trigger().purge_log(snap_idx).await.unwrap();
832
833        // A new node joins: add_learner blocks until it catches up — and it can only catch up via
834        // install_snapshot.
835        cp1.add_learner(2, a2).await.unwrap();
836
837        // Node 2 should now hold every KV from the snapshot.
838        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        // Dynamic self-organizing network: nodes join one by one and partition ownership
854        // rebalances as membership grows.
855        // The key HRW property holds in a **live cluster** — an existing node's ownership set only
856        // shrinks, and a newly added node takes a share.
857        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); // Must stay alive so that add_learner can replicate.
872        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        // Single node: owns every partition.
880        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        // Node 2 joins: ownership splits and node 1 gives up a share to node 2 (a proper subset).
885        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        // Node 3 joins: the key HRW invariant — node 1's ownership can only shrink and it
898        // **never regains** a partition (set3 ⊆ set2); node 3 takes a share from both node 1
899        // and node 2.
900        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        // 3 voting nodes; after the leader is killed, the surviving majority should elect a new
916        // leader and keep committing (writes are forwarded to the new leader).
917        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        // Write before the crash, committed through Raft.
943        cp1.set(b"before", b"crash").await.unwrap();
944
945        // Kill the leader (node 1).
946        cp1.shutdown().await.unwrap();
947
948        // The two surviving nodes should elect a new leader (2 or 3) within a few
949        // election-timeout windows.
950        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            // The new leader must be one of the surviving nodes, and both survivors must agree.
956            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        // Write via a surviving node (a non-leader forwards to the new leader); retry through the
967        // election churn until it succeeds.
968        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        // The other survivor eventually reads the new value (replicated through Raft), and the
982        // pre-crash data is still there.
983        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}