oximedia_distributed/
partition.rs1use std::collections::BTreeMap;
7use std::collections::HashMap;
8
9#[allow(dead_code)]
11#[derive(Debug, Clone, PartialEq, Eq)]
12pub struct ClusterNode {
13 pub node_id: String,
15 pub address: String,
17 pub weight: u32,
19}
20
21impl ClusterNode {
22 #[allow(dead_code)]
24 pub fn new(node_id: impl Into<String>, address: impl Into<String>, weight: u32) -> Self {
25 Self {
26 node_id: node_id.into(),
27 address: address.into(),
28 weight: weight.clamp(1, 100),
29 }
30 }
31}
32
33#[allow(dead_code)]
35#[derive(Debug, Clone)]
36pub struct Partition {
37 pub index: u32,
39 pub owner: String,
41 pub replicas: Vec<String>,
43}
44
45#[allow(dead_code)]
49pub struct ConsistentHashRing {
50 ring: BTreeMap<u64, String>,
52 nodes: HashMap<String, ClusterNode>,
54 vnodes_per_weight: u32,
56}
57
58impl ConsistentHashRing {
59 #[allow(dead_code)]
61 #[must_use]
62 pub fn new(vnodes_per_weight: u32) -> Self {
63 Self {
64 ring: BTreeMap::new(),
65 nodes: HashMap::new(),
66 vnodes_per_weight,
67 }
68 }
69
70 #[allow(dead_code)]
72 pub fn add_node(&mut self, node: ClusterNode) {
73 let vnodes = node.weight * self.vnodes_per_weight;
74 for i in 0..vnodes {
75 let key = format!("{}-{}", node.node_id, i);
76 let hash = fnv1a_hash(key.as_bytes());
77 self.ring.insert(hash, node.node_id.clone());
78 }
79 self.nodes.insert(node.node_id.clone(), node);
80 }
81
82 #[allow(dead_code)]
84 pub fn remove_node(&mut self, node_id: &str) {
85 if let Some(node) = self.nodes.remove(node_id) {
86 let vnodes = node.weight * self.vnodes_per_weight;
87 for i in 0..vnodes {
88 let key = format!("{node_id}-{i}");
89 let hash = fnv1a_hash(key.as_bytes());
90 self.ring.remove(&hash);
91 }
92 }
93 }
94
95 #[allow(dead_code)]
97 #[must_use]
98 pub fn get_node(&self, key: &[u8]) -> Option<&ClusterNode> {
99 if self.ring.is_empty() {
100 return None;
101 }
102 let hash = fnv1a_hash(key);
103 let node_id = self
105 .ring
106 .range(hash..)
107 .next()
108 .or_else(|| self.ring.iter().next())
109 .map(|(_, v)| v.as_str())?;
110 self.nodes.get(node_id)
111 }
112
113 #[allow(dead_code)]
115 #[must_use]
116 pub fn node_count(&self) -> usize {
117 self.nodes.len()
118 }
119
120 #[allow(dead_code)]
122 #[must_use]
123 pub fn vnode_count(&self) -> usize {
124 self.ring.len()
125 }
126}
127
128#[allow(dead_code)]
130fn fnv1a_hash(data: &[u8]) -> u64 {
131 const OFFSET_BASIS: u64 = 14695981039346656037;
132 const PRIME: u64 = 1099511628211;
133 let mut hash = OFFSET_BASIS;
134 for &byte in data {
135 hash ^= u64::from(byte);
136 hash = hash.wrapping_mul(PRIME);
137 }
138 hash
139}
140
141#[allow(dead_code)]
143pub struct PartitionTable {
144 pub partition_count: u32,
146 assignments: Vec<Partition>,
148}
149
150impl PartitionTable {
151 #[allow(dead_code)]
153 #[must_use]
154 pub fn new(partition_count: u32, nodes: &[ClusterNode]) -> Self {
155 let mut ring = ConsistentHashRing::new(10);
156 for node in nodes {
157 ring.add_node(node.clone());
158 }
159
160 let mut assignments = Vec::with_capacity(partition_count as usize);
161 for i in 0..partition_count {
162 let key = i.to_le_bytes();
163 let owner = ring
164 .get_node(&key)
165 .map(|n| n.node_id.clone())
166 .unwrap_or_default();
167 assignments.push(Partition {
168 index: i,
169 owner,
170 replicas: Vec::new(),
171 });
172 }
173
174 Self {
175 partition_count,
176 assignments,
177 }
178 }
179
180 #[allow(dead_code)]
182 #[must_use]
183 pub fn owner_of(&self, partition_index: u32) -> Option<&str> {
184 self.assignments
185 .get(partition_index as usize)
186 .map(|p| p.owner.as_str())
187 }
188
189 #[allow(dead_code)]
191 #[must_use]
192 pub fn partitions_for_node<'a>(&'a self, node_id: &str) -> Vec<&'a Partition> {
193 self.assignments
194 .iter()
195 .filter(|p| p.owner == node_id)
196 .collect()
197 }
198
199 #[allow(dead_code)]
201 pub fn rebalance(&mut self, nodes: &[ClusterNode]) {
202 let new = PartitionTable::new(self.partition_count, nodes);
203 self.assignments = new.assignments;
204 }
205}
206
207#[cfg(test)]
208mod tests {
209 use super::*;
210
211 fn make_nodes(n: usize) -> Vec<ClusterNode> {
212 (0..n)
213 .map(|i| ClusterNode::new(format!("node-{i}"), format!("127.0.0.1:{}", 7000 + i), 10))
214 .collect()
215 }
216
217 #[test]
218 fn test_cluster_node_weight_clamped() {
219 let node = ClusterNode::new("n0", "127.0.0.1:7000", 200);
220 assert_eq!(node.weight, 100);
221 let node2 = ClusterNode::new("n1", "127.0.0.1:7001", 0);
222 assert_eq!(node2.weight, 1);
223 }
224
225 #[test]
226 fn test_add_node_increases_vnode_count() {
227 let mut ring = ConsistentHashRing::new(10);
228 assert_eq!(ring.vnode_count(), 0);
229 ring.add_node(ClusterNode::new("n0", "127.0.0.1:7000", 1));
230 assert_eq!(ring.vnode_count(), 10);
231 }
232
233 #[test]
234 fn test_remove_node_decreases_vnode_count() {
235 let mut ring = ConsistentHashRing::new(10);
236 ring.add_node(ClusterNode::new("n0", "127.0.0.1:7000", 1));
237 ring.add_node(ClusterNode::new("n1", "127.0.0.1:7001", 1));
238 ring.remove_node("n0");
239 assert_eq!(ring.node_count(), 1);
240 assert_eq!(ring.vnode_count(), 10);
241 }
242
243 #[test]
244 fn test_get_node_empty_ring_returns_none() {
245 let ring = ConsistentHashRing::new(10);
246 assert!(ring.get_node(b"some_key").is_none());
247 }
248
249 #[test]
250 fn test_get_node_returns_some() {
251 let mut ring = ConsistentHashRing::new(10);
252 ring.add_node(ClusterNode::new("n0", "127.0.0.1:7000", 1));
253 assert!(ring.get_node(b"video/segment/0001").is_some());
254 }
255
256 #[test]
257 fn test_consistent_hashing_same_key_same_node() {
258 let mut ring = ConsistentHashRing::new(20);
259 for node in make_nodes(4) {
260 ring.add_node(node);
261 }
262 let node1 = ring.get_node(b"job-abc").map(|n| n.node_id.clone());
263 let node2 = ring.get_node(b"job-abc").map(|n| n.node_id.clone());
264 assert_eq!(node1, node2);
265 }
266
267 #[test]
268 fn test_fnv1a_hash_deterministic() {
269 let h1 = fnv1a_hash(b"hello");
270 let h2 = fnv1a_hash(b"hello");
271 assert_eq!(h1, h2);
272 }
273
274 #[test]
275 fn test_fnv1a_hash_different_inputs() {
276 assert_ne!(fnv1a_hash(b"foo"), fnv1a_hash(b"bar"));
277 }
278
279 #[test]
280 fn test_partition_table_all_partitions_assigned() {
281 let nodes = make_nodes(3);
282 let table = PartitionTable::new(64, &nodes);
283 for i in 0..64 {
284 assert!(!table.owner_of(i).unwrap_or("").is_empty());
285 }
286 }
287
288 #[test]
289 fn test_partition_table_owner_of_out_of_range() {
290 let nodes = make_nodes(2);
291 let table = PartitionTable::new(16, &nodes);
292 assert!(table.owner_of(100).is_none());
293 }
294
295 #[test]
296 fn test_partitions_for_node_coverage() {
297 let nodes = make_nodes(4);
298 let table = PartitionTable::new(64, &nodes);
299 let total: usize = nodes
300 .iter()
301 .map(|n| table.partitions_for_node(&n.node_id).len())
302 .sum();
303 assert_eq!(total, 64);
304 }
305
306 #[test]
307 fn test_rebalance_after_node_removal() {
308 let mut nodes = make_nodes(4);
309 let mut table = PartitionTable::new(32, &nodes);
310 nodes.remove(3);
311 table.rebalance(&nodes);
312 for i in 0..32 {
313 let owner = table.owner_of(i).unwrap_or("");
314 assert!(owner.starts_with("node-"));
315 assert_ne!(owner, "node-3");
316 }
317 }
318
319 #[test]
320 fn test_partition_table_empty_nodes() {
321 let table = PartitionTable::new(8, &[]);
322 for i in 0..8 {
324 assert_eq!(table.owner_of(i), Some(""));
325 }
326 }
327}