//! RPC messages for Raft consensus. use super::log::LogEntry; use serde::{Deserialize, Serialize}; /// All RPC message types in Raft. #[derive(Clone, Debug, Serialize, Deserialize)] pub enum RpcMessage { /// Request vote from candidate. RequestVote(RequestVote), /// Response to vote request. RequestVoteResponse(RequestVoteResponse), /// Append entries from leader. AppendEntries(AppendEntries), /// Response to append entries. AppendEntriesResponse(AppendEntriesResponse), /// Install snapshot from leader. InstallSnapshot(InstallSnapshot), /// Response to install snapshot. InstallSnapshotResponse(InstallSnapshotResponse), } impl RpcMessage { /// Returns the term of this message. pub fn term(&self) -> u64 { match self { RpcMessage::RequestVote(r) => r.term, RpcMessage::RequestVoteResponse(r) => r.term, RpcMessage::AppendEntries(r) => r.term, RpcMessage::AppendEntriesResponse(r) => r.term, RpcMessage::InstallSnapshot(r) => r.term, RpcMessage::InstallSnapshotResponse(r) => r.term, } } /// Serializes to bytes. pub fn to_bytes(&self) -> Vec { bincode::serialize(self).unwrap_or_default() } /// Deserializes from bytes. pub fn from_bytes(bytes: &[u8]) -> Option { bincode::deserialize(bytes).ok() } } /// RequestVote RPC (sent by candidates to gather votes). #[derive(Clone, Debug, Serialize, Deserialize)] pub struct RequestVote { /// Candidate's term. pub term: u64, /// Candidate requesting vote. pub candidate_id: u64, /// Index of candidate's last log entry. pub last_log_index: u64, /// Term of candidate's last log entry. pub last_log_term: u64, } impl RequestVote { /// Creates a new RequestVote message. pub fn new(term: u64, candidate_id: u64, last_log_index: u64, last_log_term: u64) -> Self { Self { term, candidate_id, last_log_index, last_log_term, } } } /// Response to RequestVote. #[derive(Clone, Debug, Serialize, Deserialize)] pub struct RequestVoteResponse { /// Current term (for candidate to update). pub term: u64, /// True if candidate received vote. pub vote_granted: bool, } impl RequestVoteResponse { /// Creates a positive response. pub fn grant(term: u64) -> Self { Self { term, vote_granted: true, } } /// Creates a negative response. pub fn deny(term: u64) -> Self { Self { term, vote_granted: false, } } } /// AppendEntries RPC (sent by leader for replication and heartbeat). #[derive(Clone, Debug, Serialize, Deserialize)] pub struct AppendEntries { /// Leader's term. pub term: u64, /// Leader ID (so follower can redirect clients). pub leader_id: u64, /// Index of log entry immediately preceding new ones. pub prev_log_index: u64, /// Term of prev_log_index entry. pub prev_log_term: u64, /// Log entries to store (empty for heartbeat). pub entries: Vec, /// Leader's commit index. pub leader_commit: u64, } impl AppendEntries { /// Creates a heartbeat (empty entries). pub fn heartbeat( term: u64, leader_id: u64, prev_log_index: u64, prev_log_term: u64, leader_commit: u64, ) -> Self { Self { term, leader_id, prev_log_index, prev_log_term, entries: Vec::new(), leader_commit, } } /// Creates an append entries request with entries. pub fn with_entries( term: u64, leader_id: u64, prev_log_index: u64, prev_log_term: u64, entries: Vec, leader_commit: u64, ) -> Self { Self { term, leader_id, prev_log_index, prev_log_term, entries, leader_commit, } } /// Returns true if this is a heartbeat (no entries). pub fn is_heartbeat(&self) -> bool { self.entries.is_empty() } } /// Response to AppendEntries. #[derive(Clone, Debug, Serialize, Deserialize)] pub struct AppendEntriesResponse { /// Current term (for leader to update). pub term: u64, /// True if follower contained entry matching prev_log_index and prev_log_term. pub success: bool, /// Index of last entry appended (for quick catch-up). pub match_index: u64, /// If false, the conflicting term (for optimization). pub conflict_term: Option, /// First index of conflicting term. pub conflict_index: Option, } impl AppendEntriesResponse { /// Creates a success response. pub fn success(term: u64, match_index: u64) -> Self { Self { term, success: true, match_index, conflict_term: None, conflict_index: None, } } /// Creates a failure response. pub fn failure(term: u64) -> Self { Self { term, success: false, match_index: 0, conflict_term: None, conflict_index: None, } } /// Creates a failure response with conflict info. pub fn conflict(term: u64, conflict_term: u64, conflict_index: u64) -> Self { Self { term, success: false, match_index: 0, conflict_term: Some(conflict_term), conflict_index: Some(conflict_index), } } } /// InstallSnapshot RPC (sent by leader when follower is too far behind). #[derive(Clone, Debug, Serialize, Deserialize)] pub struct InstallSnapshot { /// Leader's term. pub term: u64, /// Leader ID. pub leader_id: u64, /// Index of last entry included in snapshot. pub last_included_index: u64, /// Term of last entry included in snapshot. pub last_included_term: u64, /// Byte offset where chunk is positioned. pub offset: u64, /// Raw bytes of snapshot chunk. pub data: Vec, /// True if this is the last chunk. pub done: bool, } impl InstallSnapshot { /// Creates a new snapshot installation request. pub fn new( term: u64, leader_id: u64, last_included_index: u64, last_included_term: u64, offset: u64, data: Vec, done: bool, ) -> Self { Self { term, leader_id, last_included_index, last_included_term, offset, data, done, } } } /// Response to InstallSnapshot. #[derive(Clone, Debug, Serialize, Deserialize)] pub struct InstallSnapshotResponse { /// Current term (for leader to update). pub term: u64, } impl InstallSnapshotResponse { /// Creates a new response. pub fn new(term: u64) -> Self { Self { term } } } #[cfg(test)] mod tests { use super::*; use crate::replication::state::Command; #[test] fn test_request_vote() { let request = RequestVote::new(1, 1, 10, 1); assert_eq!(request.term, 1); assert_eq!(request.candidate_id, 1); let grant = RequestVoteResponse::grant(1); assert!(grant.vote_granted); let deny = RequestVoteResponse::deny(2); assert!(!deny.vote_granted); assert_eq!(deny.term, 2); } #[test] fn test_append_entries() { let heartbeat = AppendEntries::heartbeat(1, 1, 0, 0, 0); assert!(heartbeat.is_heartbeat()); let entries = vec![LogEntry::new(1, 1, Command::Noop)]; let append = AppendEntries::with_entries(1, 1, 0, 0, entries, 0); assert!(!append.is_heartbeat()); } #[test] fn test_rpc_message_serialization() { let request = RpcMessage::RequestVote(RequestVote::new(1, 1, 10, 1)); let bytes = request.to_bytes(); let decoded = RpcMessage::from_bytes(&bytes).unwrap(); assert_eq!(decoded.term(), 1); } #[test] fn test_append_entries_response() { let success = AppendEntriesResponse::success(1, 10); assert!(success.success); assert_eq!(success.match_index, 10); let failure = AppendEntriesResponse::failure(2); assert!(!failure.success); let conflict = AppendEntriesResponse::conflict(2, 1, 5); assert!(!conflict.success); assert_eq!(conflict.conflict_term, Some(1)); assert_eq!(conflict.conflict_index, Some(5)); } }