use std::collections::BTreeMap;
use std::collections::HashMap;
use serde::{Deserialize, Serialize};
use crate::traits::{CrdtMerge, DeltaCrdt};
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct SignedSum {
pos: HashMap<String, i128>,
neg: HashMap<String, i128>,
#[serde(skip)]
pending_pos: HashMap<String, i128>,
#[serde(skip)]
pending_neg: HashMap<String, i128>,
}
impl SignedSum {
pub fn new() -> Self {
Self::default()
}
pub fn add(&mut self, node: &str, delta: i128) {
let key = node.to_string();
if delta >= 0 {
Self::add_into(&mut self.pos, key.clone(), delta);
Self::add_into(&mut self.pending_pos, key, delta);
} else {
let mag = delta.checked_neg().unwrap_or(i128::MAX);
Self::add_into(&mut self.neg, key.clone(), mag);
Self::add_into(&mut self.pending_neg, key, mag);
}
}
fn add_into(map: &mut HashMap<String, i128>, key: String, amount: i128) {
let slot = map.entry(key).or_default();
*slot = slot.saturating_add(amount);
}
pub fn value(&self) -> i128 {
let pos = Self::saturating_sum(&self.pos);
let neg = Self::saturating_sum(&self.neg);
pos.saturating_sub(neg)
}
fn saturating_sum(map: &HashMap<String, i128>) -> i128 {
map.values().fold(0i128, |acc, &v| acc.saturating_add(v))
}
}
impl CrdtMerge for SignedSum {
fn merge(&mut self, other: &Self) {
for (k, v) in &other.pos {
let slot = self.pos.entry(k.clone()).or_default();
*slot = (*slot).max(*v);
}
for (k, v) in &other.neg {
let slot = self.neg.entry(k.clone()).or_default();
*slot = (*slot).max(*v);
}
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct SignedSumDelta {
pos: HashMap<String, i128>,
neg: HashMap<String, i128>,
}
impl DeltaCrdt for SignedSum {
type Delta = SignedSumDelta;
fn take_delta(&mut self) -> Option<Self::Delta> {
if self.pending_pos.is_empty() && self.pending_neg.is_empty() {
return None;
}
Some(SignedSumDelta {
pos: std::mem::take(&mut self.pending_pos),
neg: std::mem::take(&mut self.pending_neg),
})
}
fn merge_delta(&mut self, delta: &Self::Delta) {
for (k, v) in &delta.pos {
Self::add_into(&mut self.pos, k.clone(), *v);
}
for (k, v) in &delta.neg {
Self::add_into(&mut self.neg, k.clone(), *v);
}
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct SignedSumMap<K: Ord + Clone> {
entries: BTreeMap<K, SignedSum>,
}
impl<K: Ord + Clone> SignedSumMap<K> {
pub fn new() -> Self {
Self { entries: BTreeMap::new() }
}
pub fn add(&mut self, key: K, node: &str, delta: i128) {
self.entries.entry(key).or_default().add(node, delta);
}
pub fn get(&self, key: &K) -> i128 {
self.entries.get(key).map(SignedSum::value).unwrap_or(0)
}
pub fn entries(&self) -> impl Iterator<Item = (&K, i128)> {
self.entries.iter().map(|(k, s)| (k, s.value()))
}
}
impl<K: Ord + Clone> CrdtMerge for SignedSumMap<K> {
fn merge(&mut self, other: &Self) {
for (k, v) in &other.entries {
self.entries.entry(k.clone()).or_default().merge(v);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn add_routes_sign_correctly() {
let mut s = SignedSum::new();
s.add("n1", 100);
s.add("n1", -30);
assert_eq!(s.value(), 70);
}
#[test]
fn negative_deltas_are_first_class() {
let mut s = SignedSum::new();
s.add("n1", -50);
s.add("n2", -25);
s.add("n1", 10);
assert_eq!(s.value(), -65);
}
#[test]
fn merge_takes_per_node_max() {
let mut a = SignedSum::new();
let mut b = SignedSum::new();
a.add("n1", 5);
a.add("n1", 3); b.add("n1", 5); b.add("n2", 7);
a.merge(&b);
assert_eq!(a.value(), 8 + 7);
}
#[test]
fn merge_is_idempotent() {
let mut a = SignedSum::new();
a.add("n1", 10);
a.add("n2", -4);
let before = a.clone();
a.merge(&before);
a.merge(&before);
assert_eq!(a.pos, before.pos);
assert_eq!(a.neg, before.neg);
assert_eq!(a.value(), 6);
}
#[test]
fn merge_commutative_associative_across_three_replicas() {
let build = || {
let mut r1 = SignedSum::new();
r1.add("n1", 100);
r1.add("n1", -10);
let mut r2 = SignedSum::new();
r2.add("n2", -40);
r2.add("n2", 5);
let mut r3 = SignedSum::new();
r3.add("n3", 7);
r3.add("n1", 100); (r1, r2, r3)
};
let (mut a, b, c) = build();
a.merge(&b);
a.merge(&c);
let (r1, r2, mut x) = build();
x.merge(&r1);
x.merge(&r2);
let (mut p, q, r) = build();
let mut pr = r;
pr.merge(&p);
p = q;
p.merge(&pr);
let expected = 90 - 35 + 7; assert_eq!(a.value(), expected);
assert_eq!(x.value(), expected);
assert_eq!(p.value(), expected);
assert_eq!(a.value(), x.value());
assert_eq!(x.value(), p.value());
}
#[test]
fn saturates_at_i128_max_on_accumulate() {
let mut s = SignedSum::new();
s.add("n1", i128::MAX);
s.add("n1", i128::MAX); assert_eq!(s.value(), i128::MAX);
}
#[test]
fn saturates_at_i128_min_value() {
let mut s = SignedSum::new();
s.add("n1", i128::MIN);
assert_eq!(s.value(), i128::MIN + 1);
s.add("n2", i128::MIN);
s.add("n3", -1_000_000);
assert_eq!(s.value(), i128::MIN + 1);
}
#[test]
fn value_subtraction_saturates_at_min() {
let mut s = SignedSum::new();
s.add("a", i128::MIN);
s.add("b", i128::MIN);
assert_eq!(s.value(), i128::MIN + 1);
}
#[test]
fn value_reduction_saturates_positive() {
let mut s = SignedSum::new();
s.add("n1", i128::MAX);
s.add("n2", i128::MAX);
assert_eq!(s.value(), i128::MAX);
}
#[test]
fn map_per_key_isolation_and_merge() {
let mut m: SignedSumMap<&'static str> = SignedSumMap::new();
m.add("aapl", "n1", 500);
m.add("aapl", "n1", -100);
m.add("msft", "n1", 200);
assert_eq!(m.get(&"aapl"), 400);
assert_eq!(m.get(&"msft"), 200);
assert_eq!(m.get(&"absent"), 0);
let mut m2: SignedSumMap<&'static str> = SignedSumMap::new();
m2.add("aapl", "n2", -50);
m2.add("googl", "n2", 90);
m.merge(&m2);
assert_eq!(m.get(&"aapl"), 350);
assert_eq!(m.get(&"msft"), 200);
assert_eq!(m.get(&"googl"), 90);
let collected: Vec<_> = m.entries().collect();
assert_eq!(collected, vec![(&"aapl", 350), (&"googl", 90), (&"msft", 200)]);
}
#[test]
fn delta_take_and_clear() {
let mut s = SignedSum::new();
s.add("a", 30);
s.add("a", -5);
let d = s.take_delta().expect("non-empty");
assert_eq!(d.pos.get("a"), Some(&30));
assert_eq!(d.neg.get("a"), Some(&5));
assert!(s.take_delta().is_none());
}
#[test]
fn delta_merge_matches_full_merge() {
let mut local = SignedSum::new();
local.add("a", 50);
local.add("a", -10);
let _ = local.take_delta();
let mut remote = SignedSum::new();
remote.add("a", 50);
remote.add("a", -10);
let _ = remote.take_delta();
local.add("a", 25);
local.add("a", -5);
let delta = local.take_delta().unwrap();
remote.merge_delta(&delta);
assert_eq!(remote.value(), local.value());
assert_eq!(remote.value(), 50 - 10 + 25 - 5);
}
use proptest::prelude::*;
proptest! {
#[test]
fn any_interleaving_converges(
ops in proptest::collection::vec(
(0u8..4, 0u8..4, -1_000_000i128..1_000_000i128),
0..80,
),
merge_perm in proptest::collection::vec(any::<u8>(), 0..32),
) {
let n_replicas = 4usize;
let node_name = |n: u8| format!("node-{n}");
let key_name = |k: u8| format!("key-{k}");
let mut replicas: Vec<SignedSumMap<String>> =
(0..n_replicas).map(|_| SignedSumMap::new()).collect();
for &(k, r, d) in &ops {
let r = r as usize % n_replicas;
replicas[r].add(key_name(k), &node_name(r as u8), d);
}
let mut reference: SignedSumMap<String> = SignedSumMap::new();
for r in &replicas {
reference.merge(r);
}
let mut acc = replicas[0].clone();
for r in &replicas {
acc.merge(r);
}
for &seed in &merge_perm {
acc.merge(&replicas[seed as usize % n_replicas]);
}
let mut acc2 = replicas[n_replicas - 1].clone();
for r in replicas.iter().rev() {
acc2.merge(r);
}
for k in 0u8..4 {
let key = key_name(k);
prop_assert_eq!(acc.get(&key), reference.get(&key));
prop_assert_eq!(acc2.get(&key), reference.get(&key));
}
}
}
}