pub type NodeId = u64;
pub type Term = u64;
pub type Index = u64;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum Role {
Follower,
Candidate,
Leader,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "framing", derive(pack_io::Serialize, pack_io::Deserialize))]
pub enum EntryKind {
#[default]
Normal,
Config,
}
#[derive(Clone, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "framing", derive(pack_io::Serialize, pack_io::Deserialize))]
pub struct LogEntry {
pub term: Term,
pub index: Index,
pub kind: EntryKind,
pub command: Vec<u8>,
}
impl LogEntry {
#[inline]
#[must_use]
pub fn new(term: Term, index: Index, command: Vec<u8>) -> Self {
Self {
term,
index,
kind: EntryKind::Normal,
command,
}
}
#[inline]
#[must_use]
pub fn config(term: Term, index: Index, members: &[NodeId]) -> Self {
Self {
term,
index,
kind: EntryKind::Config,
command: encode_members(members),
}
}
#[must_use]
pub fn members(&self) -> Option<Vec<NodeId>> {
match self.kind {
EntryKind::Normal => None,
EntryKind::Config => Some(decode_members(&self.command)),
}
}
}
#[must_use]
pub(crate) fn encode_members(members: &[NodeId]) -> Vec<u8> {
let mut buf = Vec::with_capacity(members.len() * 8);
for &id in members {
buf.extend_from_slice(&id.to_le_bytes());
}
buf
}
#[must_use]
pub(crate) fn decode_members(bytes: &[u8]) -> Vec<NodeId> {
bytes
.chunks_exact(8)
.map(|c| {
let mut id = [0u8; 8];
id.copy_from_slice(c);
NodeId::from_le_bytes(id)
})
.collect()
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct HardState {
pub term: Term,
pub voted_for: Option<NodeId>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "framing", derive(pack_io::Serialize, pack_io::Deserialize))]
pub struct Snapshot {
pub index: Index,
pub term: Term,
pub config: Vec<NodeId>,
pub data: Vec<u8>,
}
impl Snapshot {
#[inline]
#[must_use]
pub fn new(index: Index, term: Term, data: Vec<u8>) -> Self {
Self {
index,
term,
config: Vec::new(),
data,
}
}
#[inline]
#[must_use]
pub fn with_config(index: Index, term: Term, config: Vec<NodeId>, data: Vec<u8>) -> Self {
Self {
index,
term,
config,
data,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_log_entry_new_sets_all_fields() {
let e = LogEntry::new(3, 9, vec![1, 2, 3]);
assert_eq!(e.term, 3);
assert_eq!(e.index, 9);
assert_eq!(e.command, vec![1, 2, 3]);
}
#[test]
fn test_hard_state_default_is_term_zero_no_vote() {
let hs = HardState::default();
assert_eq!(
hs,
HardState {
term: 0,
voted_for: None
}
);
}
#[test]
fn test_role_is_copy_and_comparable() {
let r = Role::Leader;
let copy = r;
assert_eq!(r, copy);
assert_ne!(Role::Follower, Role::Candidate);
}
#[test]
fn test_normal_entry_has_no_members() {
let e = LogEntry::new(1, 1, b"cmd".to_vec());
assert_eq!(e.kind, EntryKind::Normal);
assert_eq!(e.members(), None);
}
#[test]
fn test_config_entry_round_trips_members() {
let e = LogEntry::config(3, 9, &[1, 2, 3, 99]);
assert_eq!(e.kind, EntryKind::Config);
assert_eq!(e.members(), Some(vec![1, 2, 3, 99]));
}
#[test]
fn test_empty_config_entry() {
assert_eq!(LogEntry::config(1, 1, &[]).members(), Some(vec![]));
}
#[test]
fn test_member_codec_round_trips() {
for members in [vec![], vec![0], vec![1, 2, 3], vec![u64::MAX, 0, 7]] {
assert_eq!(decode_members(&encode_members(&members)), members);
}
}
#[test]
fn test_decode_members_ignores_trailing_partial_chunk() {
let mut bytes = encode_members(&[5, 6]);
bytes.push(0xFF); assert_eq!(decode_members(&bytes), vec![5, 6]);
}
#[test]
fn test_snapshot_with_config_carries_membership() {
let snap = Snapshot::with_config(5, 2, vec![1, 2, 3], vec![0xAB]);
assert_eq!(snap.config, vec![1, 2, 3]);
assert!(Snapshot::new(5, 2, vec![]).config.is_empty());
}
}