Skip to main content

krabka_voters/
lib.rs

1//! KIP-853 voter set value types. A voter is (id, directory-id, endpoints,
2//! kraft.version range).
3//!
4//! This is a pure value-type leaf crate, with no IO, no async, and no crypto,
5//! so it compiles for `wasm32-unknown-unknown`. `krabka-metadata` re-exports it
6//! as its `voters` module. The deterministic consensus core,
7//! `krabka-kraft-core`, embeds a [`VoterSet`] in its quorum state.
8
9#![doc(html_root_url = "https://docs.rs/krabka-voters/0.8.0")]
10
11use std::collections::{BTreeMap, BTreeSet};
12
13/// A broker or controller node id: the canonical [`krabka_ids::NodeId`]
14/// newtype, which is a `u64` internally and Kafka's `int32` on the wire.
15///
16/// This crate re-exports the type, so the consensus stack keeps the name
17/// `krabka_voters::NodeId`.
18pub use krabka_ids::NodeId;
19use serde::{Deserialize, Serialize};
20use uuid::Uuid;
21
22/// A single listener endpoint advertised by a voter.
23#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
24pub struct VoterEndpoint {
25    pub name: String,
26    pub host: String,
27    pub port: u16,
28}
29
30/// Supported kraft.version range for a voter. The range is inclusive.
31#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
32pub struct KRaftVersionRange {
33    pub min: u16,
34    pub max: u16,
35}
36
37impl Default for KRaftVersionRange {
38    fn default() -> Self {
39        Self { min: 0, max: 1 }
40    }
41}
42
43/// One voter's full identity.
44#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
45pub struct Voter {
46    pub id: NodeId,
47    pub directory_id: Uuid,
48    pub endpoints: Vec<VoterEndpoint>,
49    pub kraft_version: KRaftVersionRange,
50}
51
52/// The authoritative voter set, ordered by node id.
53#[derive(Debug, Clone, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
54pub struct VoterSet {
55    voters: BTreeMap<NodeId, Voter>,
56}
57
58impl VoterSet {
59    #[must_use]
60    pub fn from_voters(voters: impl IntoIterator<Item = Voter>) -> Self {
61        Self {
62            voters: voters.into_iter().map(|v| (v.id, v)).collect(),
63        }
64    }
65
66    #[must_use]
67    pub fn contains(&self, id: NodeId) -> bool {
68        self.voters.contains_key(&id)
69    }
70
71    #[must_use]
72    pub fn get(&self, id: NodeId) -> Option<&Voter> {
73        self.voters.get(&id)
74    }
75
76    #[must_use]
77    pub fn ids(&self) -> BTreeSet<NodeId> {
78        self.voters.keys().copied().collect()
79    }
80
81    #[must_use]
82    pub fn len(&self) -> usize {
83        self.voters.len()
84    }
85
86    #[must_use]
87    pub fn is_empty(&self) -> bool {
88        self.voters.is_empty()
89    }
90
91    pub fn iter(&self) -> impl Iterator<Item = &Voter> {
92        self.voters.values()
93    }
94
95    /// Return a copy with `voter` added or replaced.
96    #[must_use]
97    pub fn with_voter(&self, voter: Voter) -> Self {
98        let mut next = self.clone();
99        next.voters.insert(voter.id, voter);
100        next
101    }
102
103    /// Return a copy with `id` removed.
104    #[must_use]
105    pub fn without_voter(&self, id: NodeId) -> Self {
106        let mut next = self.clone();
107        next.voters.remove(&id);
108        next
109    }
110}
111
112#[cfg(test)]
113mod tests {
114    use assert2::check;
115
116    use super::*;
117
118    fn sample(id: NodeId) -> Voter {
119        Voter {
120            id,
121            directory_id: Uuid::from_u128(u128::from(id.0)),
122            endpoints: vec![VoterEndpoint {
123                name: "CONTROLLER".into(),
124                host: "127.0.0.1".into(),
125                port: 9093,
126            }],
127            kraft_version: KRaftVersionRange::default(),
128        }
129    }
130
131    #[test]
132    fn add_remove_are_immutable_copies() {
133        let base = VoterSet::from_voters([sample(NodeId(1))]);
134        let added = base.with_voter(sample(NodeId(2)));
135        let removed = added.without_voter(NodeId(1));
136        assert2::assert!(base.ids().into_iter().collect::<Vec<_>>() == vec![NodeId(1)]);
137        assert2::assert!(added.ids().into_iter().collect::<Vec<_>>() == vec![NodeId(1), NodeId(2)]);
138        assert2::assert!(removed.ids().into_iter().collect::<Vec<_>>() == vec![NodeId(2)]);
139    }
140
141    #[test]
142    fn ids_are_sorted() {
143        let set = VoterSet::from_voters([sample(NodeId(3)), sample(NodeId(1)), sample(NodeId(2))]);
144        assert2::assert!(
145            set.ids().into_iter().collect::<Vec<_>>() == vec![NodeId(1), NodeId(2), NodeId(3)]
146        );
147    }
148
149    #[test]
150    fn accessors_reflect_contents() {
151        let set = VoterSet::from_voters([sample(NodeId(1)), sample(NodeId(2))]);
152        check!(
153            (
154                set.len(),
155                set.is_empty(),
156                set.get(NodeId(1)),
157                set.get(NodeId(99)),
158                set.iter().count(),
159                // Both a present and an absent id: a `contains` stuck at either
160                // answer is still right about half the membership questions.
161                set.contains(NodeId(1)),
162                set.contains(NodeId(99)),
163            ) == (2, false, Some(&sample(NodeId(1))), None, 2, true, false)
164        );
165
166        let empty = VoterSet::default();
167        check!((empty.len(), empty.is_empty(), empty.iter().count()) == (0, true, 0));
168    }
169}