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),
}
}
#[inline]
#[must_use]
pub fn config_with_learners(
term: Term,
index: Index,
voters: &[NodeId],
learners: &[NodeId],
) -> Self {
Self {
term,
index,
kind: EntryKind::Config,
command: encode_config(voters, learners),
}
}
#[must_use]
pub fn members(&self) -> Option<Vec<NodeId>> {
match self.kind {
EntryKind::Normal => None,
EntryKind::Config => Some(decode_config(&self.command).0),
}
}
#[must_use]
pub fn learners(&self) -> Option<Vec<NodeId>> {
match self.kind {
EntryKind::Normal => None,
EntryKind::Config => Some(decode_config(&self.command).1),
}
}
}
pub(crate) const CONFIG_SENTINEL: NodeId = u64::MAX;
#[must_use]
pub(crate) fn encode_config(voters: &[NodeId], learners: &[NodeId]) -> Vec<u8> {
if learners.is_empty() {
return encode_members(voters);
}
let mut buf = encode_members(voters);
buf.extend_from_slice(&CONFIG_SENTINEL.to_le_bytes());
buf.extend_from_slice(&encode_members(learners));
buf
}
#[must_use]
pub(crate) fn decode_config(bytes: &[u8]) -> (Vec<NodeId>, Vec<NodeId>) {
let all = decode_members(bytes);
match all.iter().position(|&id| id == CONFIG_SENTINEL) {
Some(pos) => (all[..pos].to_vec(), all[pos + 1..].to_vec()),
None => (all, Vec::new()),
}
}
#[must_use]
pub(crate) fn encode_config_vec(voters: &[NodeId], learners: &[NodeId]) -> Vec<NodeId> {
if learners.is_empty() {
return voters.to_vec();
}
let mut v = voters.to_vec();
v.push(CONFIG_SENTINEL);
v.extend_from_slice(learners);
v
}
#[must_use]
pub(crate) fn decode_config_vec(config: &[NodeId]) -> (Vec<NodeId>, Vec<NodeId>) {
match config.iter().position(|&id| id == CONFIG_SENTINEL) {
Some(pos) => (config[..pos].to_vec(), config[pos + 1..].to_vec()),
None => (config.to_vec(), Vec::new()),
}
}
#[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_config_codec_round_trips_voters_and_learners() {
for (voters, learners) in [
(vec![1, 2, 3], vec![]),
(vec![1, 2, 3], vec![4]),
(vec![1], vec![2, 3, 4]),
(vec![], vec![9]),
] {
let (v, l) = decode_config(&encode_config(&voters, &learners));
assert_eq!(v, voters);
assert_eq!(l, learners);
let (vv, ll) = decode_config_vec(&encode_config_vec(&voters, &learners));
assert_eq!(vv, voters);
assert_eq!(ll, learners);
}
}
#[test]
fn test_learner_free_config_is_byte_compatible_with_members() {
let voters = [1, 2, 3, 7];
assert_eq!(encode_config(&voters, &[]), encode_members(&voters));
assert_eq!(
LogEntry::config(1, 1, &voters).command,
encode_members(&voters)
);
assert_eq!(LogEntry::config(1, 1, &voters).learners(), Some(vec![]));
}
#[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());
}
}