#![allow(dead_code)]
#[derive(Debug, Clone)]
pub struct LogEntry {
pub term: u64,
pub index: u64,
pub command: String,
}
impl LogEntry {
#[must_use]
pub fn new(term: u64, index: u64, command: impl Into<String>) -> Self {
Self {
term,
index,
command: command.into(),
}
}
#[must_use]
pub fn is_valid(&self) -> bool {
self.term > 0 && self.index > 0
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RaftRole {
Leader,
Follower,
Candidate,
}
impl RaftRole {
#[must_use]
pub fn can_accept_writes(&self) -> bool {
matches!(self, Self::Leader)
}
#[must_use]
pub fn name(&self) -> &str {
match self {
Self::Leader => "Leader",
Self::Follower => "Follower",
Self::Candidate => "Candidate",
}
}
}
impl std::fmt::Display for RaftRole {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.name())
}
}
#[derive(Debug)]
pub struct RaftState {
pub current_term: u64,
pub voted_for: Option<String>,
pub commit_index: u64,
pub last_applied: u64,
pub role: RaftRole,
}
impl RaftState {
#[must_use]
pub fn new() -> Self {
Self {
current_term: 0,
voted_for: None,
commit_index: 0,
last_applied: 0,
role: RaftRole::Follower,
}
}
pub fn advance_term(&mut self, new_term: u64) {
if new_term > self.current_term {
self.current_term = new_term;
self.voted_for = None;
}
}
pub fn become_candidate(&mut self) {
self.current_term += 1;
self.role = RaftRole::Candidate;
self.voted_for = None;
}
pub fn become_leader(&mut self) {
self.role = RaftRole::Leader;
}
pub fn become_follower(&mut self, term: u64) {
self.current_term = term;
self.role = RaftRole::Follower;
self.voted_for = None;
}
pub fn vote_for(&mut self, candidate_id: impl Into<String>) {
self.voted_for = Some(candidate_id.into());
}
pub fn update_commit_index(&mut self, index: u64) {
if index > self.commit_index {
self.commit_index = index;
}
}
pub fn apply_up_to(&mut self, index: u64) {
if index <= self.commit_index && index > self.last_applied {
self.last_applied = index;
}
}
}
impl Default for RaftState {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Default)]
pub struct RaftLog {
pub entries: Vec<LogEntry>,
}
impl RaftLog {
#[must_use]
pub fn new() -> Self {
Self {
entries: Vec::new(),
}
}
pub fn append(&mut self, entry: LogEntry) {
self.entries.push(entry);
}
#[must_use]
pub fn get(&self, index: u64) -> Option<&LogEntry> {
if index == 0 {
return None;
}
self.entries.get((index - 1) as usize)
}
#[must_use]
pub fn last_index(&self) -> u64 {
self.entries.len() as u64
}
#[must_use]
pub fn last_term(&self) -> u64 {
self.entries.last().map_or(0, |e| e.term)
}
#[must_use]
pub fn committed_entries(&self, commit_index: u64) -> Vec<&LogEntry> {
self.entries
.iter()
.filter(|e| e.index <= commit_index)
.collect()
}
pub fn truncate_after(&mut self, last_kept_index: u64) {
self.entries.retain(|e| e.index <= last_kept_index);
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_log_entry_is_valid() {
assert!(LogEntry::new(1, 1, "cmd").is_valid());
assert!(!LogEntry::new(0, 1, "cmd").is_valid()); assert!(!LogEntry::new(1, 0, "cmd").is_valid()); assert!(!LogEntry::new(0, 0, "cmd").is_valid());
}
#[test]
fn test_raft_role_can_accept_writes() {
assert!(RaftRole::Leader.can_accept_writes());
assert!(!RaftRole::Follower.can_accept_writes());
assert!(!RaftRole::Candidate.can_accept_writes());
}
#[test]
fn test_raft_role_display() {
assert_eq!(RaftRole::Leader.to_string(), "Leader");
assert_eq!(RaftRole::Follower.to_string(), "Follower");
assert_eq!(RaftRole::Candidate.to_string(), "Candidate");
}
#[test]
fn test_raft_state_initial() {
let state = RaftState::new();
assert_eq!(state.current_term, 0);
assert!(state.voted_for.is_none());
assert_eq!(state.commit_index, 0);
assert_eq!(state.last_applied, 0);
assert_eq!(state.role, RaftRole::Follower);
}
#[test]
fn test_raft_state_advance_term() {
let mut state = RaftState::new();
state.vote_for("node1");
state.advance_term(5);
assert_eq!(state.current_term, 5);
assert!(state.voted_for.is_none());
state.advance_term(3);
assert_eq!(state.current_term, 5);
}
#[test]
fn test_raft_state_become_candidate() {
let mut state = RaftState::new();
state.become_candidate();
assert_eq!(state.current_term, 1);
assert_eq!(state.role, RaftRole::Candidate);
}
#[test]
fn test_raft_state_become_leader() {
let mut state = RaftState::new();
state.become_candidate();
state.become_leader();
assert_eq!(state.role, RaftRole::Leader);
}
#[test]
fn test_raft_state_become_follower() {
let mut state = RaftState::new();
state.become_leader();
state.become_follower(7);
assert_eq!(state.role, RaftRole::Follower);
assert_eq!(state.current_term, 7);
assert!(state.voted_for.is_none());
}
#[test]
fn test_raft_state_update_commit_index() {
let mut state = RaftState::new();
state.update_commit_index(5);
assert_eq!(state.commit_index, 5);
state.update_commit_index(3);
assert_eq!(state.commit_index, 5);
}
#[test]
fn test_raft_state_apply_up_to() {
let mut state = RaftState::new();
state.update_commit_index(10);
state.apply_up_to(7);
assert_eq!(state.last_applied, 7);
state.apply_up_to(15);
assert_eq!(state.last_applied, 7);
}
#[test]
fn test_raft_log_append_and_get() {
let mut log = RaftLog::new();
assert!(log.is_empty());
assert_eq!(log.last_index(), 0);
assert_eq!(log.last_term(), 0);
log.append(LogEntry::new(1, 1, "set x=1"));
log.append(LogEntry::new(1, 2, "set y=2"));
log.append(LogEntry::new(2, 3, "set z=3"));
assert_eq!(log.last_index(), 3);
assert_eq!(log.last_term(), 2);
assert!(!log.is_empty());
}
#[test]
fn test_raft_log_get_valid_index() {
let mut log = RaftLog::new();
log.append(LogEntry::new(1, 1, "cmd1"));
log.append(LogEntry::new(2, 2, "cmd2"));
let e = log.get(1).expect("get should return a value");
assert_eq!(e.command, "cmd1");
assert_eq!(e.term, 1);
}
#[test]
fn test_raft_log_get_invalid_index() {
let log = RaftLog::new();
assert!(log.get(0).is_none());
assert!(log.get(1).is_none());
}
#[test]
fn test_raft_log_committed_entries() {
let mut log = RaftLog::new();
log.append(LogEntry::new(1, 1, "a"));
log.append(LogEntry::new(1, 2, "b"));
log.append(LogEntry::new(2, 3, "c"));
let committed = log.committed_entries(2);
assert_eq!(committed.len(), 2);
assert_eq!(committed[0].command, "a");
assert_eq!(committed[1].command, "b");
}
#[test]
fn test_raft_log_truncate_after() {
let mut log = RaftLog::new();
log.append(LogEntry::new(1, 1, "a"));
log.append(LogEntry::new(1, 2, "b"));
log.append(LogEntry::new(2, 3, "c"));
log.truncate_after(2);
assert_eq!(log.last_index(), 2);
assert!(log.get(3).is_none());
}
}