1use std::collections::HashMap;
7
8use serde::{Deserialize, Serialize};
9
10use crate::ipc::GlobalPid;
11use crate::process::Pid;
12
13pub struct ConsistentHashRing {
23 ring: Vec<(u64, String)>,
25 virtual_nodes: usize,
27}
28
29impl ConsistentHashRing {
30 pub fn new(virtual_nodes: usize) -> Self {
33 Self {
34 ring: Vec::new(),
35 virtual_nodes,
36 }
37 }
38
39 pub fn add_node(&mut self, node_id: &str) {
41 for i in 0..self.virtual_nodes {
42 let key = format!("{node_id}:{i}");
43 let hash = Self::hash(&key);
44 self.ring.push((hash, node_id.to_string()));
45 }
46 self.ring.sort_by_key(|(h, _)| *h);
47 }
48
49 pub fn remove_node(&mut self, node_id: &str) {
51 self.ring.retain(|(_, id)| id != node_id);
52 }
53
54 pub fn assign(&self, pid: u64) -> Option<&str> {
56 if self.ring.is_empty() {
57 return None;
58 }
59 let hash = Self::hash(&pid.to_string());
60 let pos = self.ring.partition_point(|(h, _)| *h < hash);
62 let idx = if pos >= self.ring.len() { 0 } else { pos };
63 Some(&self.ring[idx].1)
64 }
65
66 pub fn node_count(&self) -> usize {
68 let mut nodes: std::collections::HashSet<&str> = std::collections::HashSet::new();
69 for (_, id) in &self.ring {
70 nodes.insert(id);
71 }
72 nodes.len()
73 }
74
75 fn hash(key: &str) -> u64 {
76 use std::hash::{Hash, Hasher};
77 let mut hasher = std::collections::hash_map::DefaultHasher::new();
78 key.hash(&mut hasher);
79 hasher.finish()
80 }
81}
82
83#[non_exhaustive]
87#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
88pub enum ConsensusRole {
89 Follower,
90 Candidate,
91 Leader,
92}
93
94#[derive(Debug, Clone, Serialize, Deserialize)]
96pub struct ConsensusEntry {
97 pub term: u64,
98 pub index: u64,
99 pub operation: ConsensusOp,
100}
101
102#[non_exhaustive]
104#[derive(Debug, Clone, Serialize, Deserialize)]
105pub enum ConsensusOp {
106 RegisterService { name: String, node_id: String },
108 DeregisterService { name: String, node_id: String },
110 RegisterProcess {
112 global_pid: String,
113 agent_type: String,
114 },
115 DeregisterProcess { global_pid: String },
117}
118
119pub struct MetadataConsensus {
126 role: ConsensusRole,
128 term: u64,
130 commit_index: u64,
132 log: Vec<ConsensusEntry>,
134}
135
136impl MetadataConsensus {
137 pub fn new() -> Self {
139 Self {
140 role: ConsensusRole::Follower,
141 term: 0,
142 commit_index: 0,
143 log: Vec::new(),
144 }
145 }
146
147 pub fn role(&self) -> ConsensusRole {
149 self.role
150 }
151
152 pub fn term(&self) -> u64 {
154 self.term
155 }
156
157 pub fn commit_index(&self) -> u64 {
159 self.commit_index
160 }
161
162 pub fn set_role(&mut self, role: ConsensusRole) {
164 self.role = role;
165 }
166
167 pub fn advance_term(&mut self, new_term: u64) {
169 if new_term > self.term {
170 self.term = new_term;
171 self.role = ConsensusRole::Follower;
172 }
173 }
174
175 pub fn append(&mut self, operation: ConsensusOp) -> u64 {
178 let index = self.log.len() as u64 + 1;
179 self.log.push(ConsensusEntry {
180 term: self.term,
181 index,
182 operation,
183 });
184 index
185 }
186
187 pub fn commit(&mut self, up_to: u64) -> Vec<&ConsensusEntry> {
190 let old = self.commit_index;
191 self.commit_index = up_to.min(self.log.len() as u64);
192 self.log
193 .iter()
194 .filter(|e| e.index > old && e.index <= self.commit_index)
195 .collect()
196 }
197
198 pub fn entries(&self) -> &[ConsensusEntry] {
200 &self.log
201 }
202
203 pub fn log_len(&self) -> usize {
205 self.log.len()
206 }
207}
208
209impl Default for MetadataConsensus {
210 fn default() -> Self {
211 Self::new()
212 }
213}
214
215pub struct CrdtGossipState {
223 entries: HashMap<String, (serde_json::Value, u64)>,
225 clock: u64,
227}
228
229impl CrdtGossipState {
230 pub fn new() -> Self {
231 Self {
232 entries: HashMap::new(),
233 clock: 0,
234 }
235 }
236
237 pub fn set(&mut self, key: String, value: serde_json::Value) {
239 self.clock += 1;
240 self.entries.insert(key, (value, self.clock));
241 }
242
243 pub fn get(&self, key: &str) -> Option<&serde_json::Value> {
245 self.entries.get(key).map(|(v, _)| v)
246 }
247
248 pub fn merge(&mut self, remote_entries: &[(String, serde_json::Value, u64)]) {
250 for (key, value, remote_clock) in remote_entries {
251 match self.entries.get(key) {
252 Some((_, local_clock)) if local_clock >= remote_clock => {
253 }
255 _ => {
256 self.entries
257 .insert(key.clone(), (value.clone(), *remote_clock));
258 if *remote_clock > self.clock {
259 self.clock = *remote_clock;
260 }
261 }
262 }
263 }
264 }
265
266 pub fn delta_since(&self, since_clock: u64) -> Vec<(String, serde_json::Value, u64)> {
268 self.entries
269 .iter()
270 .filter(|(_, (_, clock))| *clock > since_clock)
271 .map(|(k, (v, c))| (k.clone(), v.clone(), *c))
272 .collect()
273 }
274
275 pub fn len(&self) -> usize {
277 self.entries.len()
278 }
279
280 pub fn is_empty(&self) -> bool {
282 self.entries.is_empty()
283 }
284
285 pub fn clock(&self) -> u64 {
287 self.clock
288 }
289}
290
291impl Default for CrdtGossipState {
292 fn default() -> Self {
293 Self::new()
294 }
295}
296
297#[derive(Debug, Clone, Serialize, Deserialize)]
299pub struct ProcessAdvertisement {
300 pub global_pid: GlobalPid,
302 pub agent_type: String,
304 pub capabilities: Vec<String>,
306 pub services: Vec<String>,
308 pub status: ProcessStatus,
310 pub last_updated: u64,
312 pub resource_summary: Option<ResourceSummary>,
314}
315
316#[non_exhaustive]
318#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
319pub enum ProcessStatus {
320 Running,
321 Suspended,
322 Stopping,
323 Unreachable,
324}
325
326#[derive(Debug, Clone, Serialize, Deserialize)]
328pub struct ResourceSummary {
329 pub memory_bytes: u64,
331 pub cpu_time_us: u64,
333 pub inbox_depth: u32,
335}
336
337pub struct DistributedProcessTable {
339 processes: HashMap<String, ProcessAdvertisement>,
341}
342
343impl DistributedProcessTable {
344 pub fn new() -> Self {
345 Self {
346 processes: HashMap::new(),
347 }
348 }
349
350 pub fn merge(&mut self, advert: ProcessAdvertisement) {
352 let key = advert.global_pid.to_string();
353 match self.processes.get(&key) {
354 Some(existing) if existing.last_updated >= advert.last_updated => {
355 }
357 _ => {
358 self.processes.insert(key, advert);
359 }
360 }
361 }
362
363 pub fn locate(&self, pid: Pid, node_id: &str) -> Option<&ProcessAdvertisement> {
365 let key = format!("{node_id}:{pid}");
366 self.processes.get(&key)
367 }
368
369 pub fn find_by_type(&self, agent_type: &str) -> Vec<&ProcessAdvertisement> {
371 self.processes
372 .values()
373 .filter(|p| p.agent_type == agent_type && p.status == ProcessStatus::Running)
374 .collect()
375 }
376
377 pub fn find_by_capability(&self, capability: &str) -> Vec<&ProcessAdvertisement> {
379 self.processes
380 .values()
381 .filter(|p| {
382 p.capabilities.contains(&capability.to_string())
383 && p.status == ProcessStatus::Running
384 })
385 .collect()
386 }
387
388 pub fn least_loaded_node(&self) -> Option<String> {
390 let mut node_loads: HashMap<String, u32> = HashMap::new();
391 for advert in self.processes.values() {
392 if advert.status == ProcessStatus::Running {
393 *node_loads
394 .entry(advert.global_pid.node_id.clone())
395 .or_default() += 1;
396 }
397 }
398 node_loads
399 .into_iter()
400 .min_by_key(|(_, count)| *count)
401 .map(|(node, _)| node)
402 }
403
404 pub fn remove_node(&mut self, node_id: &str) {
406 self.processes
407 .retain(|_, p| p.global_pid.node_id != node_id);
408 }
409
410 pub fn mark_node_unreachable(&mut self, node_id: &str) {
412 for advert in self.processes.values_mut() {
413 if advert.global_pid.node_id == node_id {
414 advert.status = ProcessStatus::Unreachable;
415 }
416 }
417 }
418
419 pub fn all_advertisements(&self) -> Vec<&ProcessAdvertisement> {
421 self.processes.values().collect()
422 }
423
424 pub fn len(&self) -> usize {
426 self.processes.len()
427 }
428
429 pub fn is_empty(&self) -> bool {
430 self.processes.is_empty()
431 }
432
433 pub fn assign_pid(&self, pid: u64, ring: &ConsistentHashRing) -> Option<String> {
435 ring.assign(pid).map(|s| s.to_string())
436 }
437}
438
439impl Default for DistributedProcessTable {
440 fn default() -> Self {
441 Self::new()
442 }
443}
444
445#[cfg(test)]
446mod tests {
447 use super::*;
448
449 fn make_advert(node: &str, pid: Pid, agent_type: &str, ts: u64) -> ProcessAdvertisement {
450 ProcessAdvertisement {
451 global_pid: GlobalPid::local(pid, node),
452 agent_type: agent_type.to_string(),
453 capabilities: vec!["code".to_string()],
454 services: vec![],
455 status: ProcessStatus::Running,
456 last_updated: ts,
457 resource_summary: None,
458 }
459 }
460
461 #[test]
462 fn process_advertisement_serde_roundtrip() {
463 let advert = make_advert("node-a", 1, "coder", 100);
464 let json = serde_json::to_string(&advert).unwrap();
465 let restored: ProcessAdvertisement = serde_json::from_str(&json).unwrap();
466 assert_eq!(restored.global_pid.node_id, "node-a");
467 assert_eq!(restored.global_pid.pid, 1);
468 assert_eq!(restored.agent_type, "coder");
469 assert_eq!(restored.last_updated, 100);
470 }
471
472 #[test]
473 fn merge_lww_newer_wins() {
474 let mut table = DistributedProcessTable::new();
475
476 let old = make_advert("node-a", 1, "coder", 100);
477 let new = ProcessAdvertisement {
478 agent_type: "reviewer".to_string(),
479 last_updated: 200,
480 ..make_advert("node-a", 1, "coder", 200)
481 };
482
483 table.merge(old);
484 assert_eq!(table.len(), 1);
485
486 table.merge(new);
488 let found = table.locate(1, "node-a").unwrap();
489 assert_eq!(found.agent_type, "reviewer");
490 assert_eq!(found.last_updated, 200);
491 }
492
493 #[test]
494 fn merge_lww_older_ignored() {
495 let mut table = DistributedProcessTable::new();
496
497 let newer = make_advert("node-a", 1, "reviewer", 200);
498 let older = make_advert("node-a", 1, "coder", 100);
499
500 table.merge(newer);
501 table.merge(older); let found = table.locate(1, "node-a").unwrap();
504 assert_eq!(found.agent_type, "reviewer");
505 }
506
507 #[test]
508 fn find_by_type_returns_matching() {
509 let mut table = DistributedProcessTable::new();
510 table.merge(make_advert("node-a", 1, "coder", 100));
511 table.merge(make_advert("node-b", 2, "reviewer", 100));
512 table.merge(make_advert("node-c", 3, "coder", 100));
513
514 let coders = table.find_by_type("coder");
515 assert_eq!(coders.len(), 2);
516 for c in &coders {
517 assert_eq!(c.agent_type, "coder");
518 }
519 }
520
521 #[test]
522 fn find_by_capability() {
523 let mut table = DistributedProcessTable::new();
524
525 let mut advert = make_advert("node-a", 1, "coder", 100);
526 advert.capabilities = vec!["code".to_string(), "review".to_string()];
527 table.merge(advert);
528
529 let mut advert2 = make_advert("node-b", 2, "tester", 100);
530 advert2.capabilities = vec!["test".to_string()];
531 table.merge(advert2);
532
533 let reviewers = table.find_by_capability("review");
534 assert_eq!(reviewers.len(), 1);
535 assert_eq!(reviewers[0].global_pid.node_id, "node-a");
536 }
537
538 #[test]
539 fn least_loaded_node() {
540 let mut table = DistributedProcessTable::new();
541 table.merge(make_advert("node-a", 1, "coder", 100));
543 table.merge(make_advert("node-a", 2, "coder", 100));
544 table.merge(make_advert("node-a", 3, "coder", 100));
545 table.merge(make_advert("node-b", 1, "coder", 100));
546
547 let node = table.least_loaded_node().unwrap();
548 assert_eq!(node, "node-b");
549 }
550
551 #[test]
552 fn remove_node() {
553 let mut table = DistributedProcessTable::new();
554 table.merge(make_advert("node-a", 1, "coder", 100));
555 table.merge(make_advert("node-a", 2, "coder", 100));
556 table.merge(make_advert("node-b", 1, "coder", 100));
557
558 table.remove_node("node-a");
559 assert_eq!(table.len(), 1);
560 assert!(table.locate(1, "node-a").is_none());
561 assert!(table.locate(1, "node-b").is_some());
562 }
563
564 #[test]
565 fn mark_node_unreachable() {
566 let mut table = DistributedProcessTable::new();
567 table.merge(make_advert("node-a", 1, "coder", 100));
568 table.merge(make_advert("node-a", 2, "coder", 100));
569 table.merge(make_advert("node-b", 1, "coder", 100));
570
571 table.mark_node_unreachable("node-a");
572
573 let a1 = table.locate(1, "node-a").unwrap();
574 assert_eq!(a1.status, ProcessStatus::Unreachable);
575 let a2 = table.locate(2, "node-a").unwrap();
576 assert_eq!(a2.status, ProcessStatus::Unreachable);
577
578 let b1 = table.locate(1, "node-b").unwrap();
580 assert_eq!(b1.status, ProcessStatus::Running);
581 }
582
583 #[test]
586 fn consistent_hash_ring_add_remove() {
587 let mut ring = ConsistentHashRing::new(64);
588 assert_eq!(ring.node_count(), 0);
589
590 ring.add_node("node-a");
591 assert_eq!(ring.node_count(), 1);
592
593 ring.add_node("node-b");
594 assert_eq!(ring.node_count(), 2);
595
596 ring.remove_node("node-a");
597 assert_eq!(ring.node_count(), 1);
598
599 assert_eq!(ring.assign(42), Some("node-b"));
601 }
602
603 #[test]
604 fn consistent_hash_ring_assignment_stable() {
605 let mut ring = ConsistentHashRing::new(64);
606 ring.add_node("node-a");
607 ring.add_node("node-b");
608 ring.add_node("node-c");
609
610 let first = ring.assign(12345).unwrap().to_string();
612 for _ in 0..100 {
613 assert_eq!(ring.assign(12345).unwrap(), first);
614 }
615 }
616
617 #[test]
618 fn consistent_hash_ring_redistribution() {
619 let mut ring = ConsistentHashRing::new(64);
620 ring.add_node("node-a");
621 ring.add_node("node-b");
622 ring.add_node("node-c");
623
624 let before: Vec<String> = (0..1000)
626 .map(|pid| ring.assign(pid).unwrap().to_string())
627 .collect();
628
629 ring.remove_node("node-b");
631
632 let after: Vec<String> = (0..1000)
633 .map(|pid| ring.assign(pid).unwrap().to_string())
634 .collect();
635
636 let mut moved = 0;
638 for i in 0..1000 {
639 if before[i] != after[i] {
640 moved += 1;
641 assert_eq!(before[i], "node-b");
643 }
644 }
645 assert!(moved > 0, "at least some PIDs should be redistributed");
647 assert!(moved < 1000, "not all PIDs should move");
648 }
649
650 #[test]
651 fn consistent_hash_ring_empty_returns_none() {
652 let ring = ConsistentHashRing::new(64);
653 assert!(ring.assign(1).is_none());
654 }
655
656 #[test]
657 fn pid_to_node_assignment() {
658 let mut ring = ConsistentHashRing::new(64);
659 ring.add_node("node-a");
660 ring.add_node("node-b");
661
662 let table = DistributedProcessTable::new();
663 let node = table.assign_pid(42, &ring);
664 assert!(node.is_some());
665 let n = node.unwrap();
666 assert!(n == "node-a" || n == "node-b");
667 }
668
669 #[test]
672 fn consensus_append_commit_cycle() {
673 let mut mc = MetadataConsensus::new();
674 assert_eq!(mc.role(), ConsensusRole::Follower);
675 assert_eq!(mc.term(), 0);
676 assert_eq!(mc.commit_index(), 0);
677
678 mc.set_role(ConsensusRole::Leader);
679 assert_eq!(mc.role(), ConsensusRole::Leader);
680
681 let idx1 = mc.append(ConsensusOp::RegisterService {
682 name: "cache".into(),
683 node_id: "node-a".into(),
684 });
685 assert_eq!(idx1, 1);
686
687 let idx2 = mc.append(ConsensusOp::RegisterProcess {
688 global_pid: "node-a:1".into(),
689 agent_type: "coder".into(),
690 });
691 assert_eq!(idx2, 2);
692
693 assert_eq!(mc.log_len(), 2);
694
695 let committed = mc.commit(2);
697 assert_eq!(committed.len(), 2);
698 assert_eq!(mc.commit_index(), 2);
699
700 let committed2 = mc.commit(2);
702 assert_eq!(committed2.len(), 0);
703 }
704
705 #[test]
706 fn consensus_advance_term() {
707 let mut mc = MetadataConsensus::new();
708 mc.set_role(ConsensusRole::Leader);
709 mc.advance_term(5);
710 assert_eq!(mc.term(), 5);
711 assert_eq!(mc.role(), ConsensusRole::Follower);
713
714 mc.advance_term(3);
716 assert_eq!(mc.term(), 5);
717 }
718
719 #[test]
720 fn consensus_commit_clamps_to_log_len() {
721 let mut mc = MetadataConsensus::new();
722 mc.append(ConsensusOp::DeregisterProcess {
723 global_pid: "node-a:1".into(),
724 });
725 mc.commit(999);
727 assert_eq!(mc.commit_index(), 1);
728 }
729
730 #[test]
733 fn crdt_set_get() {
734 let mut state = CrdtGossipState::new();
735 assert!(state.is_empty());
736
737 state.set("key1".into(), serde_json::json!("value1"));
738 assert_eq!(state.len(), 1);
739 assert_eq!(state.get("key1"), Some(&serde_json::json!("value1")));
740 assert!(state.get("missing").is_none());
741 }
742
743 #[test]
744 fn crdt_merge_lww() {
745 let mut local = CrdtGossipState::new();
746 local.set("key1".into(), serde_json::json!("local_v1"));
747 let remote = vec![("key1".into(), serde_json::json!("remote_v2"), 5u64)];
751 local.merge(&remote);
752 assert_eq!(local.get("key1"), Some(&serde_json::json!("remote_v2")));
753 assert_eq!(local.clock(), 5);
754
755 let old_remote = vec![("key1".into(), serde_json::json!("old_value"), 2u64)];
757 local.merge(&old_remote);
758 assert_eq!(local.get("key1"), Some(&serde_json::json!("remote_v2")));
759 }
760
761 #[test]
762 fn crdt_delta_since() {
763 let mut state = CrdtGossipState::new();
764 state.set("a".into(), serde_json::json!(1));
765 state.set("b".into(), serde_json::json!(2));
766 state.set("c".into(), serde_json::json!(3));
767
768 let delta = state.delta_since(1);
770 assert_eq!(delta.len(), 2);
771
772 let all = state.delta_since(0);
774 assert_eq!(all.len(), 3);
775
776 let none = state.delta_since(state.clock());
778 assert_eq!(none.len(), 0);
779 }
780
781 #[test]
782 fn crdt_concurrent_updates_converge() {
783 let mut node_a = CrdtGossipState::new();
786 let mut node_b = CrdtGossipState::new();
787
788 node_a.set("key1".into(), serde_json::json!("A"));
790 node_b.set("warmup".into(), serde_json::json!("x"));
792 node_b.set("key1".into(), serde_json::json!("B"));
793 let delta_a = node_a.delta_since(0);
797 let delta_b = node_b.delta_since(0);
798
799 node_a.merge(&delta_b);
800 node_b.merge(&delta_a);
801
802 assert_eq!(node_a.get("key1"), Some(&serde_json::json!("B")));
804 assert_eq!(node_b.get("key1"), Some(&serde_json::json!("B")));
805 }
806}