nodedb_cluster/
conf_change.rs1pub const CONF_CHANGE_PREFIX: u8 = 0xC1;
20
21#[derive(
23 Debug,
24 Clone,
25 Copy,
26 PartialEq,
27 Eq,
28 serde::Serialize,
29 serde::Deserialize,
30 zerompk::ToMessagePack,
31 zerompk::FromMessagePack,
32)]
33#[repr(u8)]
34#[msgpack(c_enum)]
35pub enum ConfChangeType {
36 AddNode = 0,
38 RemoveNode = 1,
40 AddLearner = 2,
42 PromoteLearner = 3,
44 RemoveLearner = 4,
48}
49
50#[derive(
52 Debug,
53 Clone,
54 serde::Serialize,
55 serde::Deserialize,
56 zerompk::ToMessagePack,
57 zerompk::FromMessagePack,
58)]
59pub struct ConfChange {
60 pub change_type: ConfChangeType,
61 pub node_id: u64,
63}
64
65impl ConfChange {
66 pub fn to_entry_data(&self) -> crate::error::Result<Vec<u8>> {
68 let mut data = vec![CONF_CHANGE_PREFIX];
69 let payload =
70 zerompk::to_msgpack_vec(self).map_err(|e| crate::error::ClusterError::Codec {
71 detail: format!("conf_change encode: {e}"),
72 })?;
73 data.extend_from_slice(&payload);
74 Ok(data)
75 }
76
77 pub fn from_entry_data(data: &[u8]) -> Option<Self> {
81 if data.first() != Some(&CONF_CHANGE_PREFIX) {
82 return None;
83 }
84 zerompk::from_msgpack(&data[1..]).ok()
85 }
86
87 pub fn is_conf_change(data: &[u8]) -> bool {
89 data.first() == Some(&CONF_CHANGE_PREFIX)
90 }
91}
92
93#[cfg(test)]
94mod tests {
95 use super::*;
96
97 #[test]
98 fn roundtrip_add_node() {
99 let cc = ConfChange {
100 change_type: ConfChangeType::AddNode,
101 node_id: 42,
102 };
103 let data = cc.to_entry_data().unwrap();
104 assert_eq!(data[0], CONF_CHANGE_PREFIX);
105
106 let decoded = ConfChange::from_entry_data(&data).unwrap();
107 assert_eq!(decoded.change_type, ConfChangeType::AddNode);
108 assert_eq!(decoded.node_id, 42);
109 }
110
111 #[test]
112 fn roundtrip_remove_node() {
113 let cc = ConfChange {
114 change_type: ConfChangeType::RemoveNode,
115 node_id: 7,
116 };
117 let data = cc.to_entry_data().unwrap();
118 let decoded = ConfChange::from_entry_data(&data).unwrap();
119 assert_eq!(decoded.change_type, ConfChangeType::RemoveNode);
120 assert_eq!(decoded.node_id, 7);
121 }
122
123 #[test]
124 fn regular_data_not_conf_change() {
125 assert!(!ConfChange::is_conf_change(b"hello"));
126 assert!(!ConfChange::is_conf_change(&[]));
127 assert!(ConfChange::from_entry_data(b"hello").is_none());
128 }
129
130 #[test]
131 fn prefix_is_msgpack_never_used_byte() {
132 assert_eq!(CONF_CHANGE_PREFIX, 0xC1);
136 }
137
138 #[test]
139 fn prefix_does_not_collide_with_msgpack_metadata_entry() {
140 let cc = ConfChange {
144 change_type: ConfChangeType::AddNode,
145 node_id: 1,
146 };
147 let msgpack_struct = zerompk::to_msgpack_vec(&cc).unwrap();
148 assert_ne!(msgpack_struct.first(), Some(&CONF_CHANGE_PREFIX));
149 }
150
151 #[test]
152 fn all_change_types() {
153 for ct in [
154 ConfChangeType::AddNode,
155 ConfChangeType::RemoveNode,
156 ConfChangeType::AddLearner,
157 ConfChangeType::PromoteLearner,
158 ConfChangeType::RemoveLearner,
159 ] {
160 let cc = ConfChange {
161 change_type: ct,
162 node_id: 1,
163 };
164 let data = cc.to_entry_data().unwrap();
165 let decoded = ConfChange::from_entry_data(&data).unwrap();
166 assert_eq!(decoded.change_type, ct);
167 }
168 }
169}