use crate::multi_raft::MultiRaft;
use crate::routing::RoutingTable;
use crate::rpc_codec::RaftRpc;
use crate::topology::{ClusterTopology, NodeInfo, NodeState};
use crate::transport::{NexarTransport, RaftRpcHandler};
use nodedb_raft::message::LogEntry;
use std::sync::{Arc, RwLock};
use std::time::{Duration, Instant};
use super::super::loop_core::{CommitApplier, RaftLoop};
use super::membership::{JoinDecision, decide_join};
struct NoopApplier;
impl CommitApplier for NoopApplier {
fn apply_committed(&self, _group_id: u64, entries: &[LogEntry]) -> u64 {
entries.last().map(|e| e.index).unwrap_or(0)
}
}
fn make_transport(node_id: u64) -> Arc<NexarTransport> {
Arc::new(
NexarTransport::new(
node_id,
"127.0.0.1:0".parse().unwrap(),
crate::transport::credentials::TransportCredentials::Insecure,
)
.unwrap(),
)
}
#[tokio::test]
async fn rpc_handler_routes_append_entries() {
let dir = tempfile::tempdir().unwrap();
let transport = make_transport(1);
let rt = RoutingTable::uniform(1, &[1], 1);
let mut mr = MultiRaft::new(1, rt, dir.path().to_path_buf());
mr.add_group(0, vec![]).unwrap();
for node in mr.groups_mut().values_mut() {
node.election_deadline_override(Instant::now() - Duration::from_millis(1));
}
let topo = Arc::new(RwLock::new(ClusterTopology::new()));
let raft_loop = RaftLoop::new(mr, transport, topo, NoopApplier);
raft_loop.do_tick();
tokio::time::sleep(Duration::from_millis(20)).await;
let req = RaftRpc::AppendEntriesRequest(nodedb_raft::AppendEntriesRequest {
term: 99,
leader_id: 2,
prev_log_index: 0,
prev_log_term: 0,
entries: vec![],
leader_commit: 0,
group_id: 0,
});
let resp = raft_loop.handle_rpc(req).await.unwrap();
match resp {
RaftRpc::AppendEntriesResponse(r) => {
assert!(r.success);
assert_eq!(r.term, 99);
}
other => panic!("expected AppendEntriesResponse, got {other:?}"),
}
}
#[tokio::test]
async fn rpc_handler_routes_request_vote() {
let dir = tempfile::tempdir().unwrap();
let transport = make_transport(1);
let rt = RoutingTable::uniform(1, &[1, 2, 3], 3);
let mut mr = MultiRaft::new(1, rt, dir.path().to_path_buf());
mr.add_group(0, vec![2, 3]).unwrap();
let topo = Arc::new(RwLock::new(ClusterTopology::new()));
let raft_loop = RaftLoop::new(mr, transport, topo, NoopApplier);
let req = RaftRpc::RequestVoteRequest(nodedb_raft::RequestVoteRequest {
term: 1,
candidate_id: 2,
last_log_index: 0,
last_log_term: 0,
group_id: 0,
});
let resp = raft_loop.handle_rpc(req).await.unwrap();
match resp {
RaftRpc::RequestVoteResponse(r) => {
assert!(r.vote_granted);
assert_eq!(r.term, 1);
}
other => panic!("expected RequestVoteResponse, got {other:?}"),
}
}
#[tokio::test]
async fn rpc_handler_accepts_join_on_bootstrap_seed() {
let dir = tempfile::tempdir().unwrap();
let transport = make_transport(1);
let rt = RoutingTable::uniform(2, &[1], 1);
let mut mr = MultiRaft::new(1, rt, dir.path().to_path_buf());
mr.add_group(0, vec![]).unwrap();
mr.add_group(1, vec![]).unwrap();
mr.add_group(2, vec![]).unwrap();
for node in mr.groups_mut().values_mut() {
node.election_deadline_override(Instant::now() - Duration::from_millis(1));
}
let mut topology = ClusterTopology::new();
topology.add_node(NodeInfo::new(
1,
"127.0.0.1:9400".parse().unwrap(),
NodeState::Active,
));
let topo = Arc::new(RwLock::new(topology));
let raft_loop = RaftLoop::new(mr, transport, topo.clone(), NoopApplier);
raft_loop.do_tick();
tokio::time::sleep(Duration::from_millis(20)).await;
let req = RaftRpc::JoinRequest(crate::rpc_codec::JoinRequest {
node_id: 2,
listen_addr: "127.0.0.1:9401".into(),
wire_version: crate::topology::CLUSTER_WIRE_FORMAT_VERSION,
spiffe_id: None,
spki_pin: None,
});
let resp = raft_loop.handle_rpc(req).await.unwrap();
match resp {
RaftRpc::JoinResponse(r) => {
assert!(
r.success,
"join should succeed on bootstrap seed: {}",
r.error
);
assert_eq!(r.nodes.len(), 2);
assert_eq!(r.groups.len(), 3);
assert_eq!(r.vshard_to_group.len(), 1024);
for g in &r.groups {
assert!(
g.learners.contains(&2),
"expected node 2 as learner in group {}, got learners={:?} members={:?}",
g.group_id,
g.learners,
g.members
);
}
}
other => panic!("expected JoinResponse, got {other:?}"),
}
let topo_guard = topo.read().unwrap();
assert_eq!(topo_guard.node_count(), 2);
assert!(topo_guard.contains(2));
}
#[test]
fn decide_join_self_leader_admits() {
assert_eq!(
decide_join(7, 7, Some("10.0.0.7:9400".into())),
JoinDecision::Admit
);
}
#[test]
fn decide_join_no_leader_yet_admits() {
assert_eq!(decide_join(0, 7, None), JoinDecision::Admit);
}
#[test]
fn decide_join_other_leader_redirects() {
assert_eq!(
decide_join(1, 7, Some("10.0.0.1:9400".into())),
JoinDecision::Redirect {
leader_addr: "10.0.0.1:9400".into()
}
);
}
#[test]
fn decide_join_other_leader_unknown_addr_still_redirects() {
assert_eq!(
decide_join(1, 7, None),
JoinDecision::Redirect {
leader_addr: String::new()
}
);
}