1use std::collections::HashMap;
4
5use nodedb_types::id::{DatabaseId, VShardId};
6
7use crate::error::{ClusterError, Result};
8
9pub const VSHARD_COUNT: u32 = VShardId::COUNT;
15
16#[derive(
27 Debug,
28 Clone,
29 serde::Serialize,
30 serde::Deserialize,
31 zerompk::ToMessagePack,
32 zerompk::FromMessagePack,
33)]
34pub struct RoutingTable {
35 vshard_to_group: Vec<u64>,
37 group_members: HashMap<u64, GroupInfo>,
39}
40
41#[derive(
42 Debug,
43 Clone,
44 Default,
45 serde::Serialize,
46 serde::Deserialize,
47 zerompk::ToMessagePack,
48 zerompk::FromMessagePack,
49)]
50pub struct GroupInfo {
51 pub leader: u64,
53 pub members: Vec<u64>,
55 #[serde(default)]
62 pub learners: Vec<u64>,
63 #[serde(default)]
68 pub placement: Option<Vec<u64>>,
69}
70
71impl RoutingTable {
72 pub fn uniform(num_groups: u64, nodes: &[u64], replication_factor: usize) -> Self {
83 assert!(!nodes.is_empty(), "need at least one node");
84 assert!(num_groups > 0, "need at least 1 data group");
85 assert!(replication_factor > 0, "need at least RF=1");
86
87 let mut vshard_to_group = Vec::with_capacity(VSHARD_COUNT as usize);
89 for i in 0..VSHARD_COUNT {
90 vshard_to_group.push(1 + (i as u64) % num_groups);
91 }
92
93 let mut group_members = HashMap::new();
94 for idx in 0..num_groups {
96 let group_id = idx + 1;
97 let rf = replication_factor.min(nodes.len());
98 let start = (idx as usize * rf) % nodes.len();
99 let members: Vec<u64> = (0..rf).map(|i| nodes[(start + i) % nodes.len()]).collect();
100 let leader = members[0];
101 group_members.insert(
102 group_id,
103 GroupInfo {
104 leader,
105 members,
106 learners: Vec::new(),
107 placement: None,
108 },
109 );
110 }
111 let rf = replication_factor.min(nodes.len());
113 let meta_members: Vec<u64> = (0..rf).map(|i| nodes[i % nodes.len()]).collect();
114 let meta_leader = meta_members[0];
115 group_members.insert(
116 0,
117 GroupInfo {
118 leader: meta_leader,
119 members: meta_members,
120 learners: Vec::new(),
121 placement: None,
122 },
123 );
124
125 Self {
126 vshard_to_group,
127 group_members,
128 }
129 }
130
131 pub fn group_for_vshard(&self, vshard_id: u32) -> Result<u64> {
133 self.vshard_to_group
134 .get(vshard_id as usize)
135 .copied()
136 .ok_or(ClusterError::VShardNotMapped { vshard_id })
137 }
138
139 pub fn leader_for_vshard(&self, vshard_id: u32) -> Result<u64> {
141 let group_id = self.group_for_vshard(vshard_id)?;
142 let info = self
143 .group_members
144 .get(&group_id)
145 .ok_or(ClusterError::GroupNotFound { group_id })?;
146 Ok(info.leader)
147 }
148
149 pub fn group_info(&self, group_id: u64) -> Option<&GroupInfo> {
151 self.group_members.get(&group_id)
152 }
153
154 pub fn set_leader(&mut self, group_id: u64, leader: u64) {
156 if let Some(info) = self.group_members.get_mut(&group_id) {
157 info.leader = leader;
158 }
159 }
160
161 pub fn reassign_vshard(&mut self, vshard_id: u32, new_group_id: u64) {
164 if (vshard_id as usize) < self.vshard_to_group.len() {
165 self.vshard_to_group[vshard_id as usize] = new_group_id;
166 }
167 }
168
169 pub fn vshards_for_group(&self, group_id: u64) -> Vec<u32> {
171 self.vshard_to_group
172 .iter()
173 .enumerate()
174 .filter(|(_, gid)| **gid == group_id)
175 .map(|(i, _)| i as u32)
176 .collect()
177 }
178
179 pub fn num_groups(&self) -> usize {
181 self.group_members.len()
182 }
183
184 pub fn group_ids(&self) -> Vec<u64> {
186 self.group_members.keys().copied().collect()
187 }
188
189 pub fn set_group_members(&mut self, group_id: u64, members: Vec<u64>) {
191 if let Some(info) = self.group_members.get_mut(&group_id) {
192 info.members = members;
193 }
194 }
195
196 pub fn set_placement(&mut self, group_id: u64, placement: Vec<u64>) {
200 if let Some(info) = self.group_members.get_mut(&group_id) {
201 info.placement = Some(placement);
202 }
203 }
204
205 pub fn effective_placement(&self, group_id: u64) -> Vec<u64> {
208 match self.group_members.get(&group_id) {
209 Some(g) => g.placement.clone().unwrap_or_else(|| g.members.clone()),
210 None => Vec::new(),
211 }
212 }
213
214 pub fn remove_group_member(&mut self, group_id: u64, node_id: u64) -> bool {
223 let Some(info) = self.group_members.get_mut(&group_id) else {
224 return false;
225 };
226 let before_members = info.members.len();
227 let before_learners = info.learners.len();
228 info.members.retain(|&id| id != node_id);
229 info.learners.retain(|&id| id != node_id);
230 if info.leader == node_id {
231 info.leader = 0;
232 }
233 info.members.len() != before_members || info.learners.len() != before_learners
234 }
235
236 pub fn set_group_learners(&mut self, group_id: u64, learners: Vec<u64>) {
238 if let Some(info) = self.group_members.get_mut(&group_id) {
239 info.learners = learners;
240 }
241 }
242
243 pub fn add_group_learner(&mut self, group_id: u64, peer: u64) {
246 if let Some(info) = self.group_members.get_mut(&group_id)
247 && !info.members.contains(&peer)
248 && !info.learners.contains(&peer)
249 {
250 info.learners.push(peer);
251 }
252 }
253
254 pub fn remove_group_learner(&mut self, group_id: u64, peer: u64) -> bool {
258 if let Some(info) = self.group_members.get_mut(&group_id) {
259 let before = info.learners.len();
260 info.learners.retain(|&id| id != peer);
261 info.learners.len() != before
262 } else {
263 false
264 }
265 }
266
267 pub fn promote_group_learner(&mut self, group_id: u64, peer: u64) -> bool {
270 if let Some(info) = self.group_members.get_mut(&group_id)
271 && let Some(pos) = info.learners.iter().position(|&id| id == peer)
272 {
273 info.learners.remove(pos);
274 if !info.members.contains(&peer) {
275 info.members.push(peer);
276 }
277 return true;
278 }
279 false
280 }
281
282 pub fn vshard_to_group(&self) -> &[u64] {
284 &self.vshard_to_group
285 }
286
287 pub fn group_members(&self) -> &HashMap<u64, GroupInfo> {
289 &self.group_members
290 }
291
292 pub fn from_parts(vshard_to_group: Vec<u64>, group_members: HashMap<u64, GroupInfo>) -> Self {
294 Self {
295 vshard_to_group,
296 group_members,
297 }
298 }
299}
300
301pub fn vshard_for_collection(database_id: DatabaseId, collection: &str) -> u32 {
311 VShardId::from_collection_in_database(database_id, collection).as_u32()
312}
313
314pub fn fnv1a_hash(key: &str) -> u64 {
320 let mut hash: u64 = 0xcbf29ce484222325;
321 for byte in key.as_bytes() {
322 hash ^= *byte as u64;
323 hash = hash.wrapping_mul(0x100000001b3);
324 }
325 hash
326}
327
328pub fn partition_hash(placement_hash_id: crate::catalog::PlacementHashId, key: &str) -> u64 {
336 crate::catalog::placement_hash(placement_hash_id, key.as_bytes())
337}
338
339#[cfg(test)]
340mod tests {
341 use super::*;
342
343 #[test]
344 fn uniform_distribution() {
345 let rt = RoutingTable::uniform(16, &[1, 2, 3], 3);
348 assert_eq!(rt.num_groups(), 17);
350
351 for gid in 1..=16u64 {
353 let shards = rt.vshards_for_group(gid);
354 assert_eq!(shards.len(), 64);
355 }
356
357 assert_eq!(rt.vshards_for_group(0).len(), 0);
359 }
360
361 #[test]
362 fn leader_lookup() {
363 let rt = RoutingTable::uniform(4, &[10, 20, 30], 3);
364 let leader = rt.leader_for_vshard(0).unwrap();
365 assert!(leader > 0);
367 }
368
369 #[test]
370 fn reassign_vshard() {
371 let mut rt = RoutingTable::uniform(4, &[1, 2, 3], 3);
372 let old_group = rt.group_for_vshard(0).unwrap();
373 let new_group = if old_group < 4 { old_group + 1 } else { 1 };
375 rt.reassign_vshard(0, new_group);
376 assert_eq!(rt.group_for_vshard(0).unwrap(), new_group);
377 }
378
379 #[test]
380 fn set_leader() {
381 let mut rt = RoutingTable::uniform(2, &[1, 2, 3], 3);
382 rt.set_leader(1, 99);
384 assert_eq!(rt.leader_for_vshard(0).unwrap(), 99);
385 }
386
387 #[test]
388 fn remove_group_member_strips_voter_and_clears_leader() {
389 let mut rt = RoutingTable::uniform(2, &[1, 2, 3], 3);
390 rt.set_leader(1, 2);
392 assert!(rt.remove_group_member(1, 2));
393 let info = rt.group_info(1).unwrap();
394 assert!(!info.members.contains(&2));
395 assert_eq!(info.leader, 0, "leader hint should be cleared");
396 }
397
398 #[test]
399 fn remove_group_member_strips_learner_only() {
400 let mut rt = RoutingTable::uniform(2, &[1, 2, 3], 3);
401 rt.add_group_learner(1, 9);
402 assert!(rt.remove_group_member(1, 9));
403 let info = rt.group_info(1).unwrap();
404 assert!(!info.learners.contains(&9));
405 }
406
407 #[test]
408 fn remove_group_member_unknown_group_returns_false() {
409 let mut rt = RoutingTable::uniform(1, &[1, 2], 2);
410 assert!(!rt.remove_group_member(99, 1));
411 }
412
413 #[test]
414 fn remove_group_learner_removes_from_learners_only() {
415 let mut rt = RoutingTable::uniform(2, &[1, 2, 3], 3);
416 rt.add_group_learner(1, 9);
417 let members_before = rt.group_info(1).unwrap().members.clone();
418
419 assert!(rt.remove_group_learner(1, 9));
420
421 let info = rt.group_info(1).unwrap();
422 assert!(!info.learners.contains(&9), "learner must be removed");
423 assert_eq!(info.members, members_before, "voters must not be affected");
424 }
425
426 #[test]
427 fn remove_group_learner_noop_for_voter_and_absent() {
428 let mut rt = RoutingTable::uniform(2, &[1, 2, 3], 3);
429 let members_before = rt.group_info(1).unwrap().members.clone();
430
431 assert!(!rt.remove_group_learner(1, members_before[0]));
433 assert_eq!(rt.group_info(1).unwrap().members, members_before);
434
435 assert!(!rt.remove_group_learner(1, 999));
437 }
438
439 #[test]
440 fn remove_group_learner_unknown_group_returns_false() {
441 let mut rt = RoutingTable::uniform(1, &[1, 2], 2);
442 assert!(!rt.remove_group_learner(99, 1));
443 }
444
445 #[test]
446 fn vshard_not_mapped() {
447 let rt = RoutingTable::uniform(2, &[1, 2], 2);
448 assert!(rt.group_for_vshard(1023).is_ok());
450 }
451
452 #[test]
453 fn partition_hash_fnv1a_vs_xxhash3_differ() {
454 use crate::catalog::PlacementHashId;
455 let key = "some-partition-key";
456 let fnv = partition_hash(PlacementHashId::Fnv1a, key);
457 let xx3 = partition_hash(PlacementHashId::XxHash3, key);
458 assert_ne!(fnv, xx3, "FNV-1a and XxHash3 must produce distinct values");
459 }
460
461 #[test]
462 fn vshard_for_collection_matches_types_layer() {
463 for db_raw in [0u64, 1, 2, 1024, 999_999] {
470 let db = DatabaseId::new(db_raw);
471 for name in ["users", "orders", "events", "a", "this_is_a_long_name"] {
472 assert_eq!(
473 vshard_for_collection(db, name),
474 VShardId::from_collection_in_database(db, name).as_u32(),
475 "drift detected: db={db_raw} collection={name}"
476 );
477 }
478 }
479 }
480
481 #[test]
482 fn vshard_for_collection_diverges_across_databases() {
483 let v_default = vshard_for_collection(DatabaseId::DEFAULT, "users");
488 let v_other = vshard_for_collection(DatabaseId::new(1024), "users");
489 assert_ne!(
490 v_default, v_other,
491 "same collection name across databases must route independently"
492 );
493 }
494
495 #[test]
496 fn set_placement_and_effective_placement() {
497 let mut rt = RoutingTable::uniform(2, &[1, 2, 3], 3);
498
499 let members = rt.group_info(1).unwrap().members.clone();
501 assert_eq!(rt.effective_placement(1), members);
502 assert!(rt.group_info(1).unwrap().placement.is_none());
503
504 rt.set_placement(1, vec![10, 20]);
506 assert_eq!(rt.effective_placement(1), vec![10, 20]);
507 assert_eq!(rt.group_info(1).unwrap().placement, Some(vec![10, 20]));
508
509 assert_eq!(rt.effective_placement(999), Vec::<u64>::new());
511
512 rt.set_placement(999, vec![1]);
514 assert_eq!(rt.effective_placement(999), Vec::<u64>::new());
515 }
516
517 #[test]
518 fn partition_hash_deterministic() {
519 use crate::catalog::PlacementHashId;
520 let key = "some-partition-key";
521 assert_eq!(
522 partition_hash(PlacementHashId::Fnv1a, key),
523 partition_hash(PlacementHashId::Fnv1a, key)
524 );
525 assert_eq!(
526 partition_hash(PlacementHashId::XxHash3, key),
527 partition_hash(PlacementHashId::XxHash3, key)
528 );
529 }
530}