use crate::error::{Error, Result};
use crate::types::{HardState, Index, LogEntry, Term};
pub trait RaftLog {
fn last_index(&self) -> Index;
fn last_term(&self) -> Term;
fn term_at(&self, index: Index) -> Option<Term>;
fn entry(&self, index: Index) -> Option<LogEntry>;
fn append(&mut self, entries: &[LogEntry]) -> Result<()>;
fn truncate(&mut self, from: Index) -> Result<()>;
fn hard_state(&self) -> HardState;
fn set_hard_state(&mut self, state: HardState) -> Result<()>;
fn sync(&mut self) -> Result<()>;
}
#[derive(Clone, Debug, Default)]
pub struct MemoryLog {
entries: Vec<LogEntry>,
hard: HardState,
}
impl MemoryLog {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[inline]
#[must_use]
pub fn len(&self) -> usize {
self.entries.len()
}
#[inline]
#[must_use]
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
}
impl RaftLog for MemoryLog {
#[inline]
fn last_index(&self) -> Index {
self.entries.len() as Index
}
#[inline]
fn last_term(&self) -> Term {
self.entries.last().map_or(0, |e| e.term)
}
fn term_at(&self, index: Index) -> Option<Term> {
if index == 0 {
return Some(0);
}
self.entries.get((index - 1) as usize).map(|e| e.term)
}
fn entry(&self, index: Index) -> Option<LogEntry> {
if index == 0 {
return None;
}
self.entries.get((index - 1) as usize).cloned()
}
fn append(&mut self, entries: &[LogEntry]) -> Result<()> {
if entries.is_empty() {
return Ok(());
}
let expected = self.last_index() + 1;
if entries[0].index != expected {
return Err(Error::storage(
"append entries",
format!(
"non-contiguous append: expected index {expected}, got {}",
entries[0].index
),
));
}
for pair in entries.windows(2) {
if pair[1].index != pair[0].index + 1 {
return Err(Error::storage(
"append entries",
"entries within the batch are not contiguous",
));
}
}
self.entries.extend_from_slice(entries);
Ok(())
}
fn truncate(&mut self, from: Index) -> Result<()> {
if from == 0 {
return Err(Error::storage(
"truncate log",
"cannot truncate the sentinel at index 0",
));
}
let keep = (from - 1) as usize;
if keep < self.entries.len() {
self.entries.truncate(keep);
}
Ok(())
}
#[inline]
fn hard_state(&self) -> HardState {
self.hard
}
#[inline]
fn set_hard_state(&mut self, state: HardState) -> Result<()> {
self.hard = state;
Ok(())
}
#[inline]
fn sync(&mut self) -> Result<()> {
Ok(())
}
}
#[cfg(test)]
mod tests {
#![allow(clippy::unwrap_used, clippy::expect_used)]
use super::*;
fn entry(term: Term, index: Index) -> LogEntry {
LogEntry::new(term, index, vec![index as u8])
}
#[test]
fn test_empty_log_reports_zero() {
let log = MemoryLog::new();
assert_eq!(log.last_index(), 0);
assert_eq!(log.last_term(), 0);
assert!(log.is_empty());
assert_eq!(log.entry(0), None);
assert_eq!(log.entry(1), None);
}
#[test]
fn test_term_at_sentinel_is_zero() {
assert_eq!(MemoryLog::new().term_at(0), Some(0));
}
#[test]
fn test_append_and_read_back() {
let mut log = MemoryLog::new();
log.append(&[entry(1, 1), entry(1, 2)]).unwrap();
log.append(&[entry(2, 3)]).unwrap();
assert_eq!(log.last_index(), 3);
assert_eq!(log.last_term(), 2);
assert_eq!(log.term_at(2), Some(1));
assert_eq!(log.term_at(3), Some(2));
assert_eq!(log.term_at(4), None);
assert_eq!(log.entry(3).unwrap().term, 2);
}
#[test]
fn test_append_empty_is_noop() {
let mut log = MemoryLog::new();
log.append(&[]).unwrap();
assert_eq!(log.last_index(), 0);
}
#[test]
fn test_append_rejects_gap() {
let mut log = MemoryLog::new();
let err = log.append(&[entry(1, 2)]).unwrap_err();
assert!(matches!(err, Error::Storage { .. }));
}
#[test]
fn test_append_rejects_internally_noncontiguous_batch() {
let mut log = MemoryLog::new();
let err = log.append(&[entry(1, 1), entry(1, 3)]).unwrap_err();
assert!(matches!(err, Error::Storage { .. }));
}
#[test]
fn test_truncate_removes_tail() {
let mut log = MemoryLog::new();
log.append(&[entry(1, 1), entry(1, 2), entry(1, 3)])
.unwrap();
log.truncate(2).unwrap();
assert_eq!(log.last_index(), 1);
assert_eq!(log.entry(2), None);
}
#[test]
fn test_truncate_past_end_is_noop() {
let mut log = MemoryLog::new();
log.append(&[entry(1, 1)]).unwrap();
log.truncate(5).unwrap();
assert_eq!(log.last_index(), 1);
}
#[test]
fn test_truncate_zero_is_rejected() {
let mut log = MemoryLog::new();
assert!(log.truncate(0).is_err());
}
#[test]
fn test_hard_state_round_trips() {
let mut log = MemoryLog::new();
let hs = HardState {
term: 4,
voted_for: Some(2),
};
log.set_hard_state(hs).unwrap();
assert_eq!(log.hard_state(), hs);
}
#[test]
fn test_sync_is_ok() {
assert!(MemoryLog::new().sync().is_ok());
}
}