#![allow(dead_code)]
use std::sync::atomic::{AtomicU64, Ordering};
use std::time::Instant;
#[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()
}
}
#[derive(Debug, Default)]
pub struct RaftMetrics {
propose_commit_samples: AtomicU64,
propose_commit_sum_us: AtomicU64,
propose_commit_max_us: AtomicU64,
heartbeat_samples: AtomicU64,
heartbeat_sum_us: AtomicU64,
heartbeat_max_us: AtomicU64,
}
#[derive(Debug, Clone)]
pub struct RaftMetricsSnapshot {
pub propose_commit_samples: u64,
pub propose_commit_avg_ms: f64,
pub propose_commit_max_ms: f64,
pub heartbeat_samples: u64,
pub heartbeat_rtt_avg_ms: f64,
pub heartbeat_rtt_max_ms: f64,
}
impl RaftMetrics {
#[must_use]
pub fn new() -> Self {
Self::default()
}
pub fn record_propose_commit(&self, propose_start: Instant) {
let elapsed_us = propose_start.elapsed().as_micros() as u64;
self.propose_commit_samples.fetch_add(1, Ordering::Relaxed);
self.propose_commit_sum_us
.fetch_add(elapsed_us, Ordering::Relaxed);
let mut current = self.propose_commit_max_us.load(Ordering::Relaxed);
while elapsed_us > current {
match self.propose_commit_max_us.compare_exchange_weak(
current,
elapsed_us,
Ordering::Relaxed,
Ordering::Relaxed,
) {
Ok(_) => break,
Err(c) => current = c,
}
}
}
pub fn record_heartbeat_rtt(&self, send_start: Instant) {
let elapsed_us = send_start.elapsed().as_micros() as u64;
self.heartbeat_samples.fetch_add(1, Ordering::Relaxed);
self.heartbeat_sum_us
.fetch_add(elapsed_us, Ordering::Relaxed);
let mut current = self.heartbeat_max_us.load(Ordering::Relaxed);
while elapsed_us > current {
match self.heartbeat_max_us.compare_exchange_weak(
current,
elapsed_us,
Ordering::Relaxed,
Ordering::Relaxed,
) {
Ok(_) => break,
Err(c) => current = c,
}
}
}
pub fn record_propose_commit_us(&self, latency_us: u64) {
self.propose_commit_samples.fetch_add(1, Ordering::Relaxed);
self.propose_commit_sum_us
.fetch_add(latency_us, Ordering::Relaxed);
let mut current = self.propose_commit_max_us.load(Ordering::Relaxed);
while latency_us > current {
match self.propose_commit_max_us.compare_exchange_weak(
current,
latency_us,
Ordering::Relaxed,
Ordering::Relaxed,
) {
Ok(_) => break,
Err(c) => current = c,
}
}
}
pub fn record_heartbeat_rtt_us(&self, rtt_us: u64) {
self.heartbeat_samples.fetch_add(1, Ordering::Relaxed);
self.heartbeat_sum_us.fetch_add(rtt_us, Ordering::Relaxed);
let mut current = self.heartbeat_max_us.load(Ordering::Relaxed);
while rtt_us > current {
match self.heartbeat_max_us.compare_exchange_weak(
current,
rtt_us,
Ordering::Relaxed,
Ordering::Relaxed,
) {
Ok(_) => break,
Err(c) => current = c,
}
}
}
#[must_use]
pub fn report(&self) -> RaftMetricsSnapshot {
let pc_samples = self.propose_commit_samples.load(Ordering::Relaxed);
let pc_sum = self.propose_commit_sum_us.load(Ordering::Relaxed);
let pc_max = self.propose_commit_max_us.load(Ordering::Relaxed);
let hb_samples = self.heartbeat_samples.load(Ordering::Relaxed);
let hb_sum = self.heartbeat_sum_us.load(Ordering::Relaxed);
let hb_max = self.heartbeat_max_us.load(Ordering::Relaxed);
let pc_avg_ms = if pc_samples > 0 {
pc_sum as f64 / pc_samples as f64 / 1000.0
} else {
0.0
};
let hb_avg_ms = if hb_samples > 0 {
hb_sum as f64 / hb_samples as f64 / 1000.0
} else {
0.0
};
RaftMetricsSnapshot {
propose_commit_samples: pc_samples,
propose_commit_avg_ms: pc_avg_ms,
propose_commit_max_ms: pc_max as f64 / 1000.0,
heartbeat_samples: hb_samples,
heartbeat_rtt_avg_ms: hb_avg_ms,
heartbeat_rtt_max_ms: hb_max as f64 / 1000.0,
}
}
pub fn reset(&self) {
self.propose_commit_samples.store(0, Ordering::Relaxed);
self.propose_commit_sum_us.store(0, Ordering::Relaxed);
self.propose_commit_max_us.store(0, Ordering::Relaxed);
self.heartbeat_samples.store(0, Ordering::Relaxed);
self.heartbeat_sum_us.store(0, Ordering::Relaxed);
self.heartbeat_max_us.store(0, Ordering::Relaxed);
}
}
#[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());
}
#[test]
fn test_raft_metrics_captures_latency() {
let metrics = RaftMetrics::new();
metrics.record_propose_commit_us(2_000); metrics.record_propose_commit_us(4_000); metrics.record_propose_commit_us(6_000);
metrics.record_heartbeat_rtt_us(500); metrics.record_heartbeat_rtt_us(1_500);
let snap = metrics.report();
assert_eq!(snap.propose_commit_samples, 3);
assert!((snap.propose_commit_avg_ms - 4.0).abs() < 0.01);
assert!((snap.propose_commit_max_ms - 6.0).abs() < 0.01);
assert_eq!(snap.heartbeat_samples, 2);
assert!((snap.heartbeat_rtt_avg_ms - 1.0).abs() < 0.01);
assert!((snap.heartbeat_rtt_max_ms - 1.5).abs() < 0.01);
}
#[test]
fn test_raft_metrics_empty_report() {
let metrics = RaftMetrics::new();
let snap = metrics.report();
assert_eq!(snap.propose_commit_samples, 0);
assert_eq!(snap.heartbeat_samples, 0);
assert_eq!(snap.propose_commit_avg_ms, 0.0);
assert_eq!(snap.heartbeat_rtt_avg_ms, 0.0);
}
#[test]
fn test_raft_metrics_reset() {
let metrics = RaftMetrics::new();
metrics.record_propose_commit_us(1_000);
metrics.record_heartbeat_rtt_us(500);
metrics.reset();
let snap = metrics.report();
assert_eq!(snap.propose_commit_samples, 0);
assert_eq!(snap.heartbeat_samples, 0);
}
#[test]
fn test_raft_metrics_instant_recording() {
let metrics = RaftMetrics::new();
let t = Instant::now();
std::thread::sleep(std::time::Duration::from_micros(100));
metrics.record_propose_commit(t);
let snap = metrics.report();
assert_eq!(snap.propose_commit_samples, 1);
assert!(snap.propose_commit_max_ms >= 0.0);
}
#[test]
fn test_raft_metrics_max_tracks_correctly() {
let metrics = RaftMetrics::new();
metrics.record_propose_commit_us(100);
metrics.record_propose_commit_us(9_000); metrics.record_propose_commit_us(500);
let snap = metrics.report();
assert!((snap.propose_commit_max_ms - 9.0).abs() < 0.01);
}
}