1use std::collections::HashMap;
33
34const FNV_OFFSET_BASIS: u64 = 14_695_981_039_346_656_037;
39const FNV_PRIME: u64 = 1_099_511_628_211;
40
41fn fnv1a(data: &[u8]) -> u64 {
43 let mut hash = FNV_OFFSET_BASIS;
44 for &byte in data {
45 hash ^= u64::from(byte);
46 hash = hash.wrapping_mul(FNV_PRIME);
47 }
48 hash
49}
50
51fn fnv1a_str(s: &str) -> u64 {
53 fnv1a(s.as_bytes())
54}
55
56#[derive(Debug, Clone, PartialEq, Eq)]
62pub struct DigestEntry {
63 pub key: String,
65 pub value_hash: u64,
67 pub version: u64,
69 pub updated_at_secs: u64,
71}
72
73impl DigestEntry {
74 pub fn new(key: String, value_hash: u64, version: u64, updated_at_secs: u64) -> Self {
76 Self {
77 key,
78 value_hash,
79 version,
80 updated_at_secs,
81 }
82 }
83
84 fn root_contribution(&self) -> u64 {
89 fnv1a_str(&self.key) ^ self.value_hash ^ self.version
90 }
91}
92
93#[derive(Debug, Clone, PartialEq, Eq)]
102pub struct MerkleDigest {
103 pub entries: Vec<DigestEntry>,
105 pub root_hash: u64,
107}
108
109impl MerkleDigest {
110 pub fn from_sorted(entries: Vec<DigestEntry>) -> Self {
114 let root_hash = entries
115 .iter()
116 .fold(0u64, |acc, e| acc ^ e.root_contribution());
117 Self { entries, root_hash }
118 }
119
120 pub fn matches(&self, other: &MerkleDigest) -> bool {
123 self.root_hash == other.root_hash
124 }
125
126 pub fn diff_keys(&self, other: &MerkleDigest) -> Vec<String> {
132 let other_map: HashMap<&str, &DigestEntry> =
134 other.entries.iter().map(|e| (e.key.as_str(), e)).collect();
135 let self_map: HashMap<&str, &DigestEntry> =
136 self.entries.iter().map(|e| (e.key.as_str(), e)).collect();
137
138 let mut diff: Vec<String> = Vec::new();
139
140 for e in &self.entries {
142 match other_map.get(e.key.as_str()) {
143 None => diff.push(e.key.clone()),
144 Some(o) => {
145 if o.value_hash != e.value_hash || o.version != e.version {
146 diff.push(e.key.clone());
147 }
148 }
149 }
150 }
151
152 for e in &other.entries {
154 if !self_map.contains_key(e.key.as_str()) {
155 diff.push(e.key.clone());
156 }
157 }
158
159 diff.sort();
160 diff.dedup();
161 diff
162 }
163}
164
165#[derive(Debug, Clone, Default, PartialEq, Eq)]
171pub struct ReconcileResult {
172 pub sent_keys: Vec<String>,
174 pub requested_keys: Vec<String>,
176 pub conflict_keys: Vec<String>,
178}
179
180#[derive(Debug, Clone)]
186pub struct AntiEntropyConfig {
187 pub sync_interval_secs: u64,
189 pub max_diff_keys: usize,
192}
193
194impl Default for AntiEntropyConfig {
195 fn default() -> Self {
196 Self {
197 sync_interval_secs: 30,
198 max_diff_keys: 100,
199 }
200 }
201}
202
203pub struct GossipAntiEntropy {
214 local_state: HashMap<String, DigestEntry>,
216 config: AntiEntropyConfig,
218}
219
220impl GossipAntiEntropy {
221 pub fn new(config: AntiEntropyConfig) -> Self {
223 Self {
224 local_state: HashMap::new(),
225 config,
226 }
227 }
228
229 pub fn upsert(&mut self, key: String, value_hash: u64, version: u64, now_secs: u64) {
234 let should_insert = match self.local_state.get(&key) {
235 None => true,
236 Some(existing) => version > existing.version,
237 };
238
239 if should_insert {
240 self.local_state.insert(
241 key.clone(),
242 DigestEntry::new(key, value_hash, version, now_secs),
243 );
244 }
245 }
246
247 pub fn build_digest(&self) -> MerkleDigest {
252 let mut entries: Vec<DigestEntry> = self.local_state.values().cloned().collect();
253 entries.sort_by(|a, b| a.key.cmp(&b.key));
254 MerkleDigest::from_sorted(entries)
255 }
256
257 pub fn reconcile(&self, local: &MerkleDigest, remote: &MerkleDigest) -> ReconcileResult {
267 if local.matches(remote) {
268 return ReconcileResult::default();
269 }
270
271 let diff = local.diff_keys(remote);
272
273 let local_map: HashMap<&str, &DigestEntry> =
275 local.entries.iter().map(|e| (e.key.as_str(), e)).collect();
276 let remote_map: HashMap<&str, &DigestEntry> =
277 remote.entries.iter().map(|e| (e.key.as_str(), e)).collect();
278
279 let mut result = ReconcileResult::default();
280 let mut total = 0usize;
281
282 for key in &diff {
283 if total >= self.config.max_diff_keys {
284 break;
285 }
286
287 let local_entry = local_map.get(key.as_str()).copied();
288 let remote_entry = remote_map.get(key.as_str()).copied();
289
290 match (local_entry, remote_entry) {
291 (Some(_), None) => {
293 result.sent_keys.push(key.clone());
294 total += 1;
295 }
296 (None, Some(_)) => {
298 result.requested_keys.push(key.clone());
299 total += 1;
300 }
301 (Some(l), Some(r)) => {
303 if l.version > r.version {
304 result.sent_keys.push(key.clone());
306 total += 1;
307 } else if r.version > l.version {
308 result.requested_keys.push(key.clone());
310 total += 1;
311 } else {
312 result.conflict_keys.push(key.clone());
314 total += 1;
315 }
316 }
317 (None, None) => {}
321 }
322 }
323
324 result
325 }
326
327 pub fn apply_remote_entry(&mut self, entry: DigestEntry) {
329 self.upsert(
330 entry.key.clone(),
331 entry.value_hash,
332 entry.version,
333 entry.updated_at_secs,
334 );
335 }
336
337 pub fn stats(&self) -> (usize, u64) {
341 let digest = self.build_digest();
342 (self.local_state.len(), digest.root_hash)
343 }
344}
345
346#[cfg(test)]
351mod tests {
352 use super::*;
353
354 fn make_ae() -> GossipAntiEntropy {
355 GossipAntiEntropy::new(AntiEntropyConfig::default())
356 }
357
358 #[test]
364 fn test_upsert_creates_entry() {
365 let mut ae = make_ae();
366 ae.upsert("key/a".to_string(), 0xABCD, 1, 1000);
367 let (count, _) = ae.stats();
368 assert_eq!(count, 1);
369 }
370
371 #[test]
373 fn test_upsert_newer_version_updates() {
374 let mut ae = make_ae();
375 ae.upsert("key/a".to_string(), 0x0001, 1, 1000);
376 ae.upsert("key/a".to_string(), 0x0002, 2, 1001);
377 let digest = ae.build_digest();
378 assert_eq!(digest.entries.len(), 1);
379 assert_eq!(digest.entries[0].value_hash, 0x0002);
380 assert_eq!(digest.entries[0].version, 2);
381 }
382
383 #[test]
385 fn test_upsert_ignores_older_version() {
386 let mut ae = make_ae();
387 ae.upsert("key/a".to_string(), 0xAAAA, 5, 2000);
388 ae.upsert("key/a".to_string(), 0xBBBB, 3, 1000); ae.upsert("key/a".to_string(), 0xCCCC, 5, 2001); let digest = ae.build_digest();
391 assert_eq!(digest.entries[0].value_hash, 0xAAAA);
392 assert_eq!(digest.entries[0].version, 5);
393 }
394
395 #[test]
397 fn test_upsert_multiple_keys() {
398 let mut ae = make_ae();
399 for i in 0u64..5 {
400 ae.upsert(format!("key/{}", i), i * 100, i + 1, 1000 + i);
401 }
402 let (count, _) = ae.stats();
403 assert_eq!(count, 5);
404 }
405
406 #[test]
412 fn test_build_digest_sorted() {
413 let mut ae = make_ae();
414 ae.upsert("zebra".to_string(), 1, 1, 0);
415 ae.upsert("apple".to_string(), 2, 1, 0);
416 ae.upsert("mango".to_string(), 3, 1, 0);
417 let digest = ae.build_digest();
418 let keys: Vec<&str> = digest.entries.iter().map(|e| e.key.as_str()).collect();
419 assert_eq!(keys, vec!["apple", "mango", "zebra"]);
420 }
421
422 #[test]
424 fn test_build_digest_empty() {
425 let ae = make_ae();
426 let digest = ae.build_digest();
427 assert!(digest.entries.is_empty());
428 assert_eq!(digest.root_hash, 0);
429 }
430
431 #[test]
437 fn test_digest_matches_same_state() {
438 let mut ae1 = make_ae();
439 let mut ae2 = make_ae();
440 ae1.upsert("k1".to_string(), 111, 1, 0);
441 ae1.upsert("k2".to_string(), 222, 2, 0);
442 ae2.upsert("k1".to_string(), 111, 1, 0);
443 ae2.upsert("k2".to_string(), 222, 2, 0);
444 assert!(ae1.build_digest().matches(&ae2.build_digest()));
445 }
446
447 #[test]
449 fn test_digest_not_matches_different_hash() {
450 let mut ae1 = make_ae();
451 let mut ae2 = make_ae();
452 ae1.upsert("k1".to_string(), 111, 1, 0);
453 ae2.upsert("k1".to_string(), 999, 1, 0);
454 assert!(!ae1.build_digest().matches(&ae2.build_digest()));
455 }
456
457 #[test]
463 fn test_diff_keys_finds_missing() {
464 let mut ae1 = make_ae();
465 let mut ae2 = make_ae();
466 ae1.upsert("a".to_string(), 1, 1, 0);
467 ae1.upsert("b".to_string(), 2, 1, 0);
468 ae2.upsert("a".to_string(), 1, 1, 0);
469 let d1 = ae1.build_digest();
472 let d2 = ae2.build_digest();
473 let diff = d1.diff_keys(&d2);
474 assert_eq!(diff, vec!["b"]);
475 }
476
477 #[test]
479 fn test_diff_keys_finds_version_diff() {
480 let mut ae1 = make_ae();
481 let mut ae2 = make_ae();
482 ae1.upsert("x".to_string(), 0xAA, 3, 0);
483 ae2.upsert("x".to_string(), 0xAA, 1, 0); let diff = ae1.build_digest().diff_keys(&ae2.build_digest());
486 assert_eq!(diff, vec!["x"]);
487 }
488
489 #[test]
491 fn test_diff_keys_finds_hash_diff_same_version() {
492 let mut ae1 = make_ae();
493 let mut ae2 = make_ae();
494 ae1.upsert("x".to_string(), 0xAA, 5, 0);
495 ae2.upsert("x".to_string(), 0xBB, 5, 0); let diff = ae1.build_digest().diff_keys(&ae2.build_digest());
498 assert_eq!(diff, vec!["x"]);
499 }
500
501 #[test]
503 fn test_diff_keys_empty_when_identical() {
504 let mut ae = make_ae();
505 ae.upsert("p".to_string(), 7, 7, 7);
506 let d = ae.build_digest();
507 assert!(d.diff_keys(&d.clone()).is_empty());
508 }
509
510 #[test]
516 fn test_reconcile_sent_key_local_only() {
517 let mut ae = make_ae();
518 ae.upsert("local_only".to_string(), 1, 1, 0);
519 let local = ae.build_digest();
520 let remote = MerkleDigest::from_sorted(vec![]);
521 let result = ae.reconcile(&local, &remote);
522 assert_eq!(result.sent_keys, vec!["local_only"]);
523 assert!(result.requested_keys.is_empty());
524 assert!(result.conflict_keys.is_empty());
525 }
526
527 #[test]
529 fn test_reconcile_requested_key_remote_only() {
530 let ae = make_ae();
531 let local = MerkleDigest::from_sorted(vec![]);
532 let remote =
533 MerkleDigest::from_sorted(vec![DigestEntry::new("remote_only".to_string(), 42, 1, 0)]);
534 let result = ae.reconcile(&local, &remote);
535 assert!(result.sent_keys.is_empty());
536 assert_eq!(result.requested_keys, vec!["remote_only"]);
537 assert!(result.conflict_keys.is_empty());
538 }
539
540 #[test]
542 fn test_reconcile_conflict_same_version_diff_hash() {
543 let mut ae = make_ae();
544 ae.upsert("conflict_key".to_string(), 0xAA, 5, 0);
545 let local = ae.build_digest();
546 let remote = MerkleDigest::from_sorted(vec![DigestEntry::new(
547 "conflict_key".to_string(),
548 0xBB,
549 5,
550 0,
551 )]);
552 let result = ae.reconcile(&local, &remote);
553 assert!(result.sent_keys.is_empty());
554 assert!(result.requested_keys.is_empty());
555 assert_eq!(result.conflict_keys, vec!["conflict_key"]);
556 }
557
558 #[test]
560 fn test_reconcile_sent_local_ahead() {
561 let mut ae = make_ae();
562 ae.upsert("k".to_string(), 0xAA, 10, 0);
563 let local = ae.build_digest();
564 let remote = MerkleDigest::from_sorted(vec![DigestEntry::new("k".to_string(), 0xAA, 2, 0)]);
565 let result = ae.reconcile(&local, &remote);
566 assert_eq!(result.sent_keys, vec!["k"]);
567 assert!(result.requested_keys.is_empty());
568 }
569
570 #[test]
572 fn test_reconcile_requested_remote_ahead() {
573 let mut ae = make_ae();
574 ae.upsert("k".to_string(), 0xAA, 2, 0);
575 let local = ae.build_digest();
576 let remote =
577 MerkleDigest::from_sorted(vec![DigestEntry::new("k".to_string(), 0xBB, 10, 0)]);
578 let result = ae.reconcile(&local, &remote);
579 assert!(result.sent_keys.is_empty());
580 assert_eq!(result.requested_keys, vec!["k"]);
581 }
582
583 #[test]
585 fn test_reconcile_empty_when_identical() {
586 let mut ae = make_ae();
587 ae.upsert("same".to_string(), 0x1234, 3, 0);
588 let digest = ae.build_digest();
589 let result = ae.reconcile(&digest, &digest.clone());
590 assert_eq!(result, ReconcileResult::default());
591 }
592
593 #[test]
595 fn test_reconcile_max_diff_keys_cap() {
596 let config = AntiEntropyConfig {
597 sync_interval_secs: 30,
598 max_diff_keys: 3,
599 };
600 let mut ae = GossipAntiEntropy::new(config);
601 for i in 0u64..10 {
603 ae.upsert(format!("key/{:02}", i), i, i + 1, 0);
604 }
605 let local = ae.build_digest();
606 let remote = MerkleDigest::from_sorted(vec![]);
607 let result = ae.reconcile(&local, &remote);
608 let total =
610 result.sent_keys.len() + result.requested_keys.len() + result.conflict_keys.len();
611 assert!(total <= 3, "expected ≤3, got {}", total);
612 }
613
614 #[test]
620 fn test_apply_remote_entry_updates() {
621 let mut ae = make_ae();
622 ae.upsert("k".to_string(), 0x01, 1, 0);
623 ae.apply_remote_entry(DigestEntry::new("k".to_string(), 0x99, 5, 100));
624 let digest = ae.build_digest();
625 assert_eq!(digest.entries[0].value_hash, 0x99);
626 assert_eq!(digest.entries[0].version, 5);
627 }
628
629 #[test]
631 fn test_apply_remote_entry_rejects_older() {
632 let mut ae = make_ae();
633 ae.upsert("k".to_string(), 0xFF, 10, 0);
634 ae.apply_remote_entry(DigestEntry::new("k".to_string(), 0x00, 3, 50));
635 let digest = ae.build_digest();
636 assert_eq!(digest.entries[0].value_hash, 0xFF);
637 assert_eq!(digest.entries[0].version, 10);
638 }
639
640 #[test]
642 fn test_apply_remote_entry_new_key() {
643 let mut ae = make_ae();
644 ae.apply_remote_entry(DigestEntry::new("new_key".to_string(), 42, 1, 999));
645 let (count, _) = ae.stats();
646 assert_eq!(count, 1);
647 let digest = ae.build_digest();
648 assert_eq!(digest.entries[0].key, "new_key");
649 }
650
651 #[test]
657 fn test_stats_deterministic() {
658 let mut ae1 = make_ae();
659 let mut ae2 = make_ae();
660 ae1.upsert("alpha".to_string(), 1, 1, 0);
662 ae1.upsert("beta".to_string(), 2, 1, 0);
663 ae2.upsert("beta".to_string(), 2, 1, 0);
664 ae2.upsert("alpha".to_string(), 1, 1, 0);
665 let (c1, h1) = ae1.stats();
666 let (c2, h2) = ae2.stats();
667 assert_eq!(c1, c2);
668 assert_eq!(h1, h2);
669 }
670
671 #[test]
677 fn test_reconcile_both_empty() {
678 let ae = make_ae();
679 let empty = MerkleDigest::from_sorted(vec![]);
680 let result = ae.reconcile(&empty, &empty.clone());
681 assert_eq!(result, ReconcileResult::default());
682 }
683
684 #[test]
690 fn test_root_hash_changes_on_update() {
691 let mut ae = make_ae();
692 ae.upsert("key".to_string(), 0x1111, 1, 0);
693 let h1 = ae.build_digest().root_hash;
694 ae.upsert("key".to_string(), 0x2222, 2, 0);
695 let h2 = ae.build_digest().root_hash;
696 assert_ne!(h1, h2);
697 }
698
699 #[test]
701 fn test_reconcile_mixed_classification() {
702 let config = AntiEntropyConfig {
703 sync_interval_secs: 30,
704 max_diff_keys: 100,
705 };
706 let mut ae = GossipAntiEntropy::new(config);
707 ae.upsert("local_only".to_string(), 1, 1, 0);
709 ae.upsert("shared_ahead".to_string(), 10, 10, 0);
711 ae.upsert("shared_conflict".to_string(), 0xAA, 5, 0);
713
714 let local = ae.build_digest();
715 let remote = MerkleDigest::from_sorted(vec![
716 DigestEntry::new("remote_only".to_string(), 99, 1, 0),
717 DigestEntry::new("shared_ahead".to_string(), 10, 2, 0), DigestEntry::new("shared_conflict".to_string(), 0xBB, 5, 0), ]);
720
721 let result = ae.reconcile(&local, &remote);
722 assert!(result.sent_keys.contains(&"local_only".to_string()));
723 assert!(result.sent_keys.contains(&"shared_ahead".to_string()));
724 assert!(result.requested_keys.contains(&"remote_only".to_string()));
725 assert!(result
726 .conflict_keys
727 .contains(&"shared_conflict".to_string()));
728 }
729}