use std::time::Duration;
use rand::{
distributions::{Alphanumeric, DistString},
Rng, SeedableRng,
};
use reconcile::{clock::NodeId, replicated_map::Config, ClusterKey, Fingerprint, ReplicatedMap};
async fn wait_until<F: FnMut() -> bool>(mut f: F) -> bool {
for _ in 0..100 {
tokio::time::sleep(Duration::from_millis(10)).await;
if f() {
return true;
}
}
false
}
macro_rules! assert_until {
( $x:expr ) => {
assert!(wait_until(|| $x).await, stringify!($x))
};
}
async fn wait_until_slow<F: FnMut() -> bool>(mut f: F) -> bool {
for _ in 0..1000 {
tokio::time::sleep(Duration::from_millis(10)).await;
if f() {
return true;
}
}
false
}
macro_rules! assert_until_slow {
( $x:expr ) => {
assert!(wait_until_slow(|| $x).await, stringify!($x))
};
}
#[tokio::test(flavor = "multi_thread")]
async fn test() {
let port = 8080;
let net = "127.0.0.1/8".parse().unwrap();
let addr1 = "127.0.0.44".parse().unwrap();
let addr2 = "127.0.0.45".parse().unwrap();
let cfg1 = Config::default()
.with_port(port)
.with_listen_addr(addr1)
.with_net(net)
.with_insecure_no_key();
let cfg2 = Config::default()
.with_port(port)
.with_listen_addr(addr2)
.with_net(net)
.with_insecure_no_key();
let mut rng = rand::rngs::StdRng::seed_from_u64(42);
let key_values: [(String, String); 1000] = core::array::from_fn(|_| {
let key: String = Alphanumeric.sample_string(&mut rng, 100);
let value: String = Alphanumeric.sample_string(&mut rng, 100);
(key, value)
});
let store1 = ReplicatedMap::new(cfg1)
.await
.expect("bind failed")
.with_seed(addr2);
store1.insert_bulk(&key_values);
let start_fingerprint = store1.fingerprint(..);
let store2 = ReplicatedMap::new(cfg2)
.await
.expect("bind failed")
.with_seed(addr1);
assert_eq!(store2.fingerprint(..), Fingerprint::ZERO);
assert_eq!(store1.fingerprint(..), start_fingerprint);
let task2 = tokio::spawn(store2.clone().run());
let task1 = tokio::spawn(store1.clone().run());
assert_until!(store2.fingerprint(..) == start_fingerprint);
assert_eq!(store1.fingerprint(..), start_fingerprint);
let key = "42".to_string();
let value = "Hello, World!".to_string();
store2.insert(key.clone(), value.clone());
assert_until!(store1.get(&key).as_deref() == Some(&value));
store1.remove(&key);
assert_until!(store2.get(&key).is_none());
let key = "42".to_string();
for i in 0..20 {
let first = format!("first-{i}");
let second = format!("second-{i}");
if rng.gen() {
store1.insert(key.clone(), first.clone());
assert_until!(store2.get(&key).as_deref() == Some(&first));
store2.insert(key.clone(), second.clone());
assert_until!(store1.get(&key).as_deref() == Some(&second));
assert_until!(store2.get(&key).as_deref() == Some(&second));
} else if rng.gen() {
store2.insert(key.clone(), first.clone());
assert_until!(store1.get(&key).as_deref() == Some(&first));
store1.insert(key.clone(), second.clone());
assert_until!(store1.get(&key).as_deref() == Some(&second));
assert_until!(store2.get(&key).as_deref() == Some(&second));
} else if rng.gen() {
store1.insert(key.clone(), first.clone());
assert_until!(store2.get(&key).as_deref() == Some(&first));
store2.remove(&key);
assert_until!(store1.get(&key).is_none());
assert_until!(store2.get(&key).is_none());
} else {
store1.insert(key.clone(), first.clone());
assert_until!(store2.get(&key).as_deref() == Some(&first));
store1.remove(&key);
assert_until!(store2.get(&key).is_none());
store2.insert(key.clone(), second.clone());
assert_until!(store1.get(&key).as_deref() == Some(&second));
assert_until!(store2.get(&key).as_deref() == Some(&second));
}
}
let key = "43".to_string();
let value1 = "Hello, World!".to_string();
let value2 = "Goodbye!".to_string();
store1.insert(key.clone(), value1.clone());
assert_until!(store2.get(&key).as_deref() == Some(&value1));
store2.remove(&key);
assert_until!(store1.get(&key).is_none());
store1.insert(key.clone(), value2.clone());
assert_until!(store2.get(&key).as_deref() == Some(&value2));
task2.abort();
task1.abort();
}
#[tokio::test(flavor = "multi_thread")]
async fn get_mut_edit_propagates_to_peers() {
let port = 8089;
let net = "127.0.0.1/8".parse().unwrap();
let addr1 = "127.0.0.100".parse().unwrap();
let addr2 = "127.0.0.101".parse().unwrap();
let cfg1 = Config::default()
.with_port(port)
.with_listen_addr(addr1)
.with_net(net)
.with_insecure_no_key();
let cfg2 = Config::default()
.with_port(port)
.with_listen_addr(addr2)
.with_net(net)
.with_insecure_no_key();
let store1 = ReplicatedMap::new(cfg1)
.await
.expect("bind failed")
.with_seed(addr2);
let store2 = ReplicatedMap::new(cfg2)
.await
.expect("bind failed")
.with_seed(addr1);
let task1 = tokio::spawn(store1.clone().run());
let task2 = tokio::spawn(store2.clone().run());
let key = "k".to_string();
let before = "before".to_string();
let after = "after".to_string();
store1.insert(key.clone(), before.clone());
assert_until!(store2.get(&key).as_deref() == Some(&before));
store1.get_mut(&key, |v| {
if let Some(v) = v {
*v = after.clone();
}
});
assert_eq!(store1.get(&key).as_deref(), Some(&after));
assert_until!(store2.get(&key).as_deref() == Some(&after));
task1.abort();
task2.abort();
}
#[tokio::test(flavor = "multi_thread")]
async fn authenticated_nodes_converge() {
let port = 8081;
let net = "127.0.0.1/8".parse().unwrap();
let addr1 = "127.0.0.46".parse().unwrap();
let addr2 = "127.0.0.47".parse().unwrap();
let key = [0x42u8; 32];
let cfg1 = Config::default()
.with_port(port)
.with_listen_addr(addr1)
.with_net(net)
.with_cluster_key(ClusterKey::new(key));
let cfg2 = Config::default()
.with_port(port)
.with_listen_addr(addr2)
.with_net(net)
.with_cluster_key(ClusterKey::new(key));
let mut rng = rand::rngs::StdRng::seed_from_u64(42);
let key_values: [(String, String); 1000] = core::array::from_fn(|_| {
let key: String = Alphanumeric.sample_string(&mut rng, 100);
let value: String = Alphanumeric.sample_string(&mut rng, 100);
(key, value)
});
let store1 = ReplicatedMap::new(cfg1)
.await
.expect("bind failed")
.with_seed(addr2);
store1.insert_bulk(&key_values);
let start_fingerprint = store1.fingerprint(..);
let store2 = ReplicatedMap::new(cfg2)
.await
.expect("bind failed")
.with_seed(addr1);
let task2 = tokio::spawn(store2.clone().run());
let task1 = tokio::spawn(store1.clone().run());
assert_until!(store2.fingerprint(..) == start_fingerprint);
let key = "auth-key".to_string();
let value = "authenticated value".to_string();
store2.insert(key.clone(), value.clone());
assert_until!(store1.get(&key).as_deref() == Some(&value));
task2.abort();
task1.abort();
}
#[tokio::test(flavor = "multi_thread")]
async fn concurrent_writes_converge() {
let port = 8083;
let net = "127.0.0.1/8".parse().unwrap();
let addr1 = "127.0.0.80".parse().unwrap();
let addr2 = "127.0.0.81".parse().unwrap();
let cfg1 = Config::default()
.with_port(port)
.with_listen_addr(addr1)
.with_net(net)
.with_node_id(NodeId::new(1))
.with_insecure_no_key();
let cfg2 = Config::default()
.with_port(port)
.with_listen_addr(addr2)
.with_net(net)
.with_node_id(NodeId::new(2))
.with_insecure_no_key();
let store1 = ReplicatedMap::<String, String>::new(cfg1)
.await
.expect("bind failed")
.with_seed(addr2);
let store2 = ReplicatedMap::<String, String>::new(cfg2)
.await
.expect("bind failed")
.with_seed(addr1);
let task1 = tokio::spawn(store1.clone().run());
let task2 = tokio::spawn(store2.clone().run());
let key = "contended".to_string();
for i in 0..50 {
store1.insert(key.clone(), format!("from-1-{i}"));
store2.insert(key.clone(), format!("from-2-{i}"));
}
assert_until!(store1.fingerprint(..) == store2.fingerprint(..));
let v1 = store1.get(&key).map(|g| g.clone());
let v2 = store2.get(&key).map(|g| g.clone());
assert_eq!(
v1, v2,
"replicas disagree on the contended key: {v1:?} vs {v2:?}"
);
assert!(v1.is_some(), "the contended key vanished entirely");
task1.abort();
task2.abort();
}
#[tokio::test(flavor = "multi_thread")]
async fn tombstone_is_retained_until_peer_acknowledges() {
let port = 8084;
let net = "127.0.0.1/8".parse().unwrap();
let addr1 = "127.0.0.72".parse().unwrap();
let addr2 = "127.0.0.73".parse().unwrap();
let cfg1 = Config::default()
.with_port(port)
.with_listen_addr(addr1)
.with_net(net)
.with_insecure_no_key();
let cfg2 = Config::default()
.with_port(port)
.with_listen_addr(addr2)
.with_net(net)
.with_insecure_no_key();
let store1 = ReplicatedMap::<i32, i32>::new(cfg1)
.await
.expect("bind failed")
.with_seed(addr2)
.with_tombstone_timeout(Duration::from_millis(50));
let store2 = ReplicatedMap::<i32, i32>::new(cfg2)
.await
.expect("bind failed")
.with_seed(addr1)
.with_tombstone_timeout(Duration::from_millis(50));
let task1 = tokio::spawn(store1.clone().run());
let task2 = tokio::spawn(store2.clone().run());
store1.insert(1, 11);
assert_until!(store2.get(&1).as_deref() == Some(&11));
store2.insert(2, 22);
assert_until!(store1.get(&2).as_deref() == Some(&22));
task2.abort();
store1.remove(&1);
assert!(store1.get(&1).is_none());
let fingerprint_with_tombstone = store1.fingerprint(..);
tokio::time::sleep(Duration::from_millis(1500)).await;
assert_eq!(
store1.fingerprint(..),
fingerprint_with_tombstone,
"tombstone was garbage-collected before the partitioned peer acknowledged it (resurrection hazard)"
);
store1.forget_peer(addr2);
tokio::time::sleep(Duration::from_millis(1500)).await;
assert_ne!(
store1.fingerprint(..),
fingerprint_with_tombstone,
"tombstone was not collected after the silent peer was decommissioned"
);
task1.abort();
}
#[tokio::test(flavor = "multi_thread")]
async fn deleted_value_is_not_resurrected_by_returning_peer() {
let port = 8085;
let net = "127.0.0.1/8".parse().unwrap();
let addr1 = "127.0.0.70".parse().unwrap();
let addr2 = "127.0.0.71".parse().unwrap();
let cfg1 = Config::default()
.with_port(port)
.with_listen_addr(addr1)
.with_net(net)
.with_insecure_no_key();
let cfg2 = Config::default()
.with_port(port)
.with_listen_addr(addr2)
.with_net(net)
.with_insecure_no_key();
let store1 = ReplicatedMap::<i32, i32>::new(cfg1)
.await
.expect("bind failed")
.with_seed(addr2)
.with_tombstone_timeout(Duration::from_millis(50));
let store2 = ReplicatedMap::<i32, i32>::new(cfg2)
.await
.expect("bind failed")
.with_seed(addr1)
.with_tombstone_timeout(Duration::from_millis(50));
let task1 = tokio::spawn(store1.clone().run());
let task2 = tokio::spawn(store2.clone().run());
store1.insert(1, 11);
assert_until!(store2.get(&1).as_deref() == Some(&11));
store2.insert(2, 22);
assert_until!(store1.get(&1).as_deref() == Some(&11));
assert_until!(store1.get(&2).as_deref() == Some(&22));
task2.abort();
assert_eq!(store2.get(&1).as_deref(), Some(&11));
store1.remove(&1);
tokio::time::sleep(Duration::from_millis(1500)).await;
assert!(store1.get(&1).is_none());
let task2 = tokio::spawn(store2.clone().run());
assert_until!(store2.get(&1).is_none());
tokio::time::sleep(Duration::from_millis(300)).await;
assert!(
store1.get(&1).is_none(),
"deleted value was resurrected by the returning partitioned peer"
);
assert!(
store2.get(&1).is_none(),
"deletion did not reach the returning peer"
);
task1.abort();
task2.abort();
}
#[tokio::test(flavor = "multi_thread")]
async fn test_malformed_datagram_does_not_crash() {
let port = 8082;
let net = "127.0.0.1/8".parse().unwrap();
let addr1 = "127.0.0.46".parse().unwrap();
let addr2 = "127.0.0.47".parse().unwrap();
let cfg1 = Config::default()
.with_port(port)
.with_listen_addr(addr1)
.with_net(net)
.with_insecure_no_key();
let cfg2 = Config::default()
.with_port(port)
.with_listen_addr(addr2)
.with_net(net)
.with_insecure_no_key();
let store1 = ReplicatedMap::new(cfg1)
.await
.expect("bind failed")
.with_seed(addr2);
let store2 = ReplicatedMap::new(cfg2)
.await
.expect("bind failed")
.with_seed(addr1);
let task1 = tokio::spawn(store1.clone().run());
let task2 = tokio::spawn(store2.clone().run());
let attacker = tokio::net::UdpSocket::bind("127.0.0.1:0").await.unwrap();
attacker.send_to(&[0x02], (addr1, port)).await.unwrap();
attacker.send_to(&[0x02], (addr2, port)).await.unwrap();
let key = "key".to_string();
let value = "value".to_string();
store1.insert(key.clone(), value.clone());
assert_until!(store2.get(&key).as_deref() == Some(&value));
task2.abort();
task1.abort();
}
#[cfg(feature = "encryption")]
#[tokio::test(flavor = "multi_thread")]
async fn encrypted_nodes_converge() {
let port = 8083;
let net = "127.0.0.1/8".parse().unwrap();
let addr1 = "127.0.0.48".parse().unwrap();
let addr2 = "127.0.0.49".parse().unwrap();
let key = [0x42u8; 32];
let cfg1 = Config::default()
.with_port(port)
.with_listen_addr(addr1)
.with_net(net)
.with_cluster_key(ClusterKey::new(key))
.with_encryption();
let cfg2 = Config::default()
.with_port(port)
.with_listen_addr(addr2)
.with_net(net)
.with_cluster_key(ClusterKey::new(key))
.with_encryption();
let mut rng = rand::rngs::StdRng::seed_from_u64(42);
let key_values: [(String, String); 1000] = core::array::from_fn(|_| {
let key: String = Alphanumeric.sample_string(&mut rng, 100);
let value: String = Alphanumeric.sample_string(&mut rng, 100);
(key, value)
});
let store1 = ReplicatedMap::new(cfg1)
.await
.expect("bind failed")
.with_seed(addr2);
store1.insert_bulk(&key_values);
let start_fingerprint = store1.fingerprint(..);
let store2 = ReplicatedMap::new(cfg2)
.await
.expect("bind failed")
.with_seed(addr1);
let task2 = tokio::spawn(store2.clone().run());
let task1 = tokio::spawn(store1.clone().run());
assert_until!(store2.fingerprint(..) == start_fingerprint);
let key = "enc-key".to_string();
let value = "encrypted value".to_string();
store2.insert(key.clone(), value.clone());
assert_until!(store1.get(&key).as_deref() == Some(&value));
task2.abort();
task1.abort();
}
#[cfg(feature = "encryption")]
#[tokio::test(flavor = "multi_thread")]
async fn encrypted_node_with_wrong_key_is_rejected() {
let port = 8084;
let net = "127.0.0.1/8".parse().unwrap();
let addr1 = "127.0.0.50".parse().unwrap();
let addr2 = "127.0.0.51".parse().unwrap();
let cfg1 = Config::default()
.with_port(port)
.with_listen_addr(addr1)
.with_net(net)
.with_cluster_key(ClusterKey::new([0x42u8; 32]))
.with_encryption();
let cfg2 = Config::default()
.with_port(port)
.with_listen_addr(addr2)
.with_net(net)
.with_cluster_key(ClusterKey::new([0x99u8; 32])) .with_encryption();
let store1 = ReplicatedMap::new(cfg1)
.await
.expect("bind failed")
.with_seed(addr2);
store1.insert("secret".to_string(), "value".to_string());
let start_fingerprint = store1.fingerprint(..);
let store2 = ReplicatedMap::<String, String>::new(cfg2)
.await
.expect("bind failed")
.with_seed(addr1);
let task2 = tokio::spawn(store2.clone().run());
let task1 = tokio::spawn(store1.clone().run());
assert!(
!wait_until(|| store2.fingerprint(..) == start_fingerprint).await,
"node with the wrong key must not converge"
);
task2.abort();
task1.abort();
}
#[tokio::test(flavor = "multi_thread")]
async fn cross_net_reconciliation() {
let port = 8085;
let net_a = "127.0.0.0/30".parse().unwrap();
let net_b = "127.0.1.0/30".parse().unwrap();
let addr1 = "127.0.0.1".parse().unwrap();
let addr2 = "127.0.1.1".parse().unwrap();
let cfg1 = Config::default()
.with_port(port)
.with_listen_addr(addr1)
.with_net(net_a)
.with_net(net_b)
.with_remote_interval(1)
.with_remote_fanout(1)
.with_insecure_no_key();
let cfg2 = Config::default()
.with_port(port)
.with_listen_addr(addr2)
.with_net(net_b)
.with_net(net_a)
.with_remote_interval(1)
.with_remote_fanout(1)
.with_insecure_no_key();
let store1 = ReplicatedMap::new(cfg1)
.await
.expect("bind failed")
.with_seed(addr2);
store1.insert("key".to_string(), "value".to_string());
let start_fingerprint = store1.fingerprint(..);
let store2 = ReplicatedMap::<String, String>::new(cfg2)
.await
.expect("bind failed")
.with_seed(addr1);
assert_eq!(store2.fingerprint(..), Fingerprint::ZERO);
let task2 = tokio::spawn(store2.clone().run());
let task1 = tokio::spawn(store1.clone().run());
assert_until!(store2.get(&"key".to_string()).as_deref() == Some(&"value".to_string()));
assert_until!(store2.fingerprint(..) == start_fingerprint);
task1.abort();
task2.abort();
}
#[tokio::test(flavor = "multi_thread")]
async fn cross_net_discovery_without_seed() {
let port = 8086;
let net_a = "127.0.2.0/30".parse().unwrap();
let net_b = "127.0.3.0/30".parse().unwrap();
let addr1 = "127.0.2.1".parse().unwrap();
let addr2 = "127.0.3.1".parse().unwrap();
let peer2_host = "127.0.3.1/32".parse().unwrap();
let peer1_host = "127.0.2.1/32".parse().unwrap();
let cfg1 = Config::default()
.with_port(port)
.with_listen_addr(addr1)
.with_net(net_a)
.with_net(peer2_host)
.with_remote_interval(1)
.with_remote_fanout(1)
.with_insecure_no_key();
let cfg2 = Config::default()
.with_port(port)
.with_listen_addr(addr2)
.with_net(net_b)
.with_net(peer1_host)
.with_remote_interval(1)
.with_remote_fanout(1)
.with_insecure_no_key();
let store1 = ReplicatedMap::new(cfg1).await.expect("bind failed");
store1.insert("k".to_string(), "v".to_string());
let start_fingerprint = store1.fingerprint(..);
let store2 = ReplicatedMap::<String, String>::new(cfg2)
.await
.expect("bind failed");
let task2 = tokio::spawn(store2.clone().run());
let task1 = tokio::spawn(store1.clone().run());
assert_until!(store2.fingerprint(..) == start_fingerprint);
task1.abort();
task2.abort();
}
#[tokio::test(flavor = "multi_thread")]
async fn runtime_add_net_enables_discovery_and_convergence() {
let port = 8087;
let net_a = "127.0.4.0/30".parse().unwrap();
let net_b = "127.0.5.0/30".parse().unwrap();
let addr1 = "127.0.4.1".parse().unwrap();
let addr2 = "127.0.5.1".parse().unwrap();
let peer2_host = "127.0.5.1/32".parse().unwrap();
let peer1_host = "127.0.4.1/32".parse().unwrap();
let cfg1 = Config::default()
.with_port(port)
.with_listen_addr(addr1)
.with_net(net_a)
.with_remote_interval(1)
.with_remote_fanout(1)
.with_insecure_no_key();
let cfg2 = Config::default()
.with_port(port)
.with_listen_addr(addr2)
.with_net(net_b)
.with_remote_interval(1)
.with_remote_fanout(1)
.with_insecure_no_key();
let store1 = ReplicatedMap::new(cfg1).await.expect("bind failed");
store1.insert("k".to_string(), "v".to_string());
let start_fingerprint = store1.fingerprint(..);
let store2 = ReplicatedMap::<String, String>::new(cfg2)
.await
.expect("bind failed");
let task2 = tokio::spawn(store2.clone().run());
let task1 = tokio::spawn(store1.clone().run());
assert!(
!wait_until(|| store2.fingerprint(..) == start_fingerprint).await,
"nodes converged before the peer network was declared"
);
assert!(store1.add_net(peer2_host));
assert!(store2.add_net(peer1_host));
assert_until!(store2.fingerprint(..) == start_fingerprint);
task1.abort();
task2.abort();
}
#[tokio::test(flavor = "multi_thread")]
async fn unclassified_peer_is_still_reconciled() {
let port = 8088;
let foreign_net = "127.0.7.0/30".parse().unwrap();
let addr1 = "127.0.6.1".parse().unwrap();
let addr2 = "127.0.6.2".parse().unwrap();
let cfg1 = Config::default()
.with_port(port)
.with_listen_addr(addr1)
.with_net(foreign_net)
.with_remote_interval(1)
.with_remote_fanout(1)
.with_insecure_no_key();
let cfg2 = Config::default()
.with_port(port)
.with_listen_addr(addr2)
.with_net(foreign_net)
.with_remote_interval(1)
.with_remote_fanout(1)
.with_insecure_no_key();
let store1 = ReplicatedMap::new(cfg1)
.await
.expect("bind failed")
.with_seed(addr2);
store1.insert("k".to_string(), "v".to_string());
let start_fingerprint = store1.fingerprint(..);
let store2 = ReplicatedMap::<String, String>::new(cfg2)
.await
.expect("bind failed")
.with_seed(addr1);
assert_eq!(store1.local_net(), "127.0.6.1/32".parse().unwrap());
let task2 = tokio::spawn(store2.clone().run());
let task1 = tokio::spawn(store1.clone().run());
assert_until!(store2.fingerprint(..) == start_fingerprint);
task1.abort();
task2.abort();
}
#[tokio::test]
async fn runtime_config_setters() {
let addr = "127.0.8.1".parse().unwrap();
let net_c = "127.0.8.0/30".parse().unwrap(); let net_d = "127.0.9.0/30".parse().unwrap(); let host_route = "127.0.8.1/32".parse().unwrap();
let store = ReplicatedMap::<i32, i32>::new(
Config::default()
.with_port(0)
.with_listen_addr(addr)
.with_net(net_c)
.with_insecure_no_key(),
)
.await
.expect("bind failed");
assert_eq!(store.nets(), vec![net_c]);
assert_eq!(store.local_net(), net_c);
assert!(store.add_net(net_d));
assert_eq!(store.local_net(), net_c);
assert_eq!(store.nets(), vec![net_c, net_d]);
assert!(store.add_net(net_d));
assert_eq!(store.nets().len(), 2, "add_net must be idempotent");
assert!(store.remove_net(net_c));
assert_eq!(store.nets(), vec![net_d]);
assert_eq!(store.local_net(), host_route);
assert!(
!store.remove_net(net_c),
"removing an absent net returns false"
);
store.set_nets(&[net_c]);
assert_eq!(store.nets(), vec![net_c]);
assert_eq!(store.local_net(), net_c);
for i in 0..(reconcile::replicated_map::MAX_NETS - 1) {
let n = format!("127.1.{i}.0/30").parse().unwrap();
assert!(store.add_net(n));
}
assert_eq!(store.nets().len(), reconcile::replicated_map::MAX_NETS);
let overflow = "127.2.0.0/30".parse().unwrap();
assert!(
!store.add_net(overflow),
"add_net past MAX_NETS must return false"
);
assert_eq!(store.nets().len(), reconcile::replicated_map::MAX_NETS);
store.set_remote_interval(3);
store.set_remote_fanout(5);
store.set_reconcile_interval(Duration::from_millis(200));
store.set_tombstone_timeout(Duration::from_millis(500));
}
#[cfg(feature = "internal-testing")]
#[tokio::test(flavor = "multi_thread")]
async fn stale_datagram_outside_freshness_window_is_rejected() {
use reconcile::testing::seal_datagram;
let port = 8092;
let net = "127.0.0.1/8".parse().unwrap();
let addr_victim = "127.0.9.1".parse().unwrap();
let key = [0xBBu8; 32];
let cfg = Config::default()
.with_port(port)
.with_listen_addr(addr_victim)
.with_net(net)
.with_cluster_key(ClusterKey::new(key));
let store = ReplicatedMap::<i32, i32>::new(cfg)
.await
.expect("bind failed");
store.just_insert(0, 99);
let task = tokio::spawn(store.clone().run());
tokio::time::sleep(Duration::from_millis(20)).await;
let sender_sock = tokio::net::UdpSocket::bind("127.0.0.1:0").await.unwrap();
let target = format!("{}:{}", addr_victim, port);
let one_hour_ago_ms = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.saturating_sub(Duration::from_secs(3600))
.as_millis() as u64;
let mut payload = Vec::new();
payload.extend_from_slice(&2u32.to_le_bytes()); payload.extend_from_slice(&0i32.to_le_bytes()); payload.extend_from_slice(&0u64.to_le_bytes());
let stale_datagram = seal_datagram(key, 1, one_hour_ago_ms, &payload);
sender_sock.send_to(&stale_datagram, &target).await.unwrap();
tokio::time::sleep(Duration::from_millis(100)).await;
assert!(
!task.is_finished(),
"engine must still be running after stale datagram"
);
assert_eq!(store.get(&0).as_deref(), Some(&99));
task.abort();
}
#[cfg(feature = "internal-testing")]
#[tokio::test(flavor = "multi_thread")]
async fn replayed_sealed_datagram_is_rejected() {
use reconcile::testing::seal_datagram;
let port = 8093;
let net = "127.0.0.1/8".parse().unwrap();
let addr_victim = "127.0.10.1".parse().unwrap();
let key = [0xDDu8; 32];
let cfg = Config::default()
.with_port(port)
.with_listen_addr(addr_victim)
.with_net(net)
.with_cluster_key(ClusterKey::new(key));
let store = ReplicatedMap::<i32, i32>::new(cfg)
.await
.expect("bind failed");
let task = tokio::spawn(store.clone().run());
tokio::time::sleep(Duration::from_millis(20)).await;
let sender_sock = tokio::net::UdpSocket::bind("127.0.0.1:0").await.unwrap();
let target = format!("{}:{}", addr_victim, port);
let now_ms = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_millis() as u64;
let mut payload = Vec::new();
payload.extend_from_slice(&2u32.to_le_bytes()); payload.extend_from_slice(&0i32.to_le_bytes()); payload.extend_from_slice(&0u64.to_le_bytes());
let datagram = seal_datagram(key, 1, now_ms, &payload);
sender_sock.send_to(&datagram, &target).await.unwrap();
tokio::time::sleep(Duration::from_millis(50)).await;
sender_sock.send_to(&datagram, &target).await.unwrap();
tokio::time::sleep(Duration::from_millis(50)).await;
assert!(
!task.is_finished(),
"engine must still be running after replay"
);
task.abort();
}
#[cfg(feature = "internal-testing")]
#[tokio::test(flavor = "multi_thread")]
async fn decommissioned_peer_replay_is_rejected() {
use reconcile::testing::{members_snapshot, seal_datagram};
let port = 8094;
let net = "127.0.0.1/8".parse().unwrap();
let addr_victim: std::net::IpAddr = "127.0.11.1".parse().unwrap();
let addr_sender: std::net::IpAddr = "127.0.11.2".parse().unwrap();
let key = [0xEEu8; 32];
let cfg = Config::default()
.with_port(port)
.with_listen_addr(addr_victim)
.with_net(net)
.with_cluster_key(ClusterKey::new(key));
let store = ReplicatedMap::<i32, i32>::new(cfg)
.await
.expect("bind failed");
let task = tokio::spawn(store.clone().run());
tokio::time::sleep(Duration::from_millis(20)).await;
let sender_sock = tokio::net::UdpSocket::bind(std::net::SocketAddr::new(addr_sender, 0))
.await
.unwrap();
let target = format!("{}:{}", addr_victim, port);
let now_ms = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_millis() as u64;
let mut payload = Vec::new();
payload.extend_from_slice(&2u32.to_le_bytes()); payload.extend_from_slice(&0i32.to_le_bytes()); payload.extend_from_slice(&0u64.to_le_bytes());
let captured = seal_datagram(key, 1, now_ms, &payload);
sender_sock.send_to(&captured, &target).await.unwrap();
tokio::time::sleep(Duration::from_millis(50)).await;
assert!(
members_snapshot(&store).contains(&addr_sender),
"sender must have joined members after first delivery"
);
store.forget_peer(addr_sender);
assert!(
!members_snapshot(&store).contains(&addr_sender),
"sender must be gone from members after decommission"
);
sender_sock.send_to(&captured, &target).await.unwrap();
tokio::time::sleep(Duration::from_millis(50)).await;
assert!(
!members_snapshot(&store).contains(&addr_sender),
"decommissioned peer must not be re-added to members by a replayed datagram"
);
assert!(!task.is_finished(), "engine must still be running");
task.abort();
}
#[tokio::test(flavor = "multi_thread")]
async fn tombstone_gc_converges_in_3_node_cluster_mesh() {
let port = 8120;
let net = "127.0.0.1/8".parse().unwrap();
let addr1 = "127.0.0.110".parse().unwrap();
let addr2 = "127.0.0.111".parse().unwrap();
let addr3 = "127.0.0.112".parse().unwrap();
let mk = |addr| {
Config::default()
.with_port(port)
.with_listen_addr(addr)
.with_net(net)
.with_reconcile_interval(Duration::from_millis(100))
.with_insecure_no_key()
};
let store1 = ReplicatedMap::<i32, i32>::new(mk(addr1))
.await
.expect("bind failed")
.with_seed(addr2)
.with_seed(addr3)
.with_tombstone_timeout(Duration::from_millis(200));
let store2 = ReplicatedMap::<i32, i32>::new(mk(addr2))
.await
.expect("bind failed")
.with_seed(addr1)
.with_seed(addr3)
.with_tombstone_timeout(Duration::from_millis(200));
let store3 = ReplicatedMap::<i32, i32>::new(mk(addr3))
.await
.expect("bind failed")
.with_seed(addr1)
.with_seed(addr2)
.with_tombstone_timeout(Duration::from_millis(200));
let task1 = tokio::spawn(store1.clone().run());
let task2 = tokio::spawn(store2.clone().run());
let task3 = tokio::spawn(store3.clone().run());
store1.insert(1, 11);
store2.insert(2, 22);
store3.insert(3, 33);
assert_until!(store1.get(&2).as_deref() == Some(&22) && store1.get(&3).as_deref() == Some(&33));
assert_until!(store2.get(&1).as_deref() == Some(&11) && store2.get(&3).as_deref() == Some(&33));
assert_until!(store3.get(&1).as_deref() == Some(&11) && store3.get(&2).as_deref() == Some(&22));
store1.remove(&1);
assert_until!(store2.get(&1).is_none() && store3.get(&1).is_none());
let fp_tombstone = store1.fingerprint(..);
assert_until!(store2.fingerprint(..) == fp_tombstone && store3.fingerprint(..) == fp_tombstone);
assert_until_slow!(store1.fingerprint(..) != fp_tombstone);
assert_until_slow!(store2.fingerprint(..) != fp_tombstone);
assert_until_slow!(store3.fingerprint(..) != fp_tombstone);
let fp_collected = store1.fingerprint(..);
assert_until_slow!(
store2.fingerprint(..) == fp_collected && store3.fingerprint(..) == fp_collected
);
task1.abort();
task2.abort();
task3.abort();
}
#[tokio::test(flavor = "multi_thread")]
async fn tombstone_gc_converges_in_3_node_cluster_line() {
let port = 8121;
let net = "127.0.0.1/8".parse().unwrap();
let addr1 = "127.0.0.113".parse().unwrap();
let addr2 = "127.0.0.114".parse().unwrap();
let addr3 = "127.0.0.115".parse().unwrap();
let mk = |addr| {
Config::default()
.with_port(port)
.with_listen_addr(addr)
.with_net(net)
.with_reconcile_interval(Duration::from_millis(100))
.with_insecure_no_key()
};
let store1 = ReplicatedMap::<i32, i32>::new(mk(addr1))
.await
.expect("bind failed")
.with_seed(addr2)
.with_tombstone_timeout(Duration::from_millis(200));
let store2 = ReplicatedMap::<i32, i32>::new(mk(addr2))
.await
.expect("bind failed")
.with_seed(addr1)
.with_seed(addr3)
.with_tombstone_timeout(Duration::from_millis(200));
let store3 = ReplicatedMap::<i32, i32>::new(mk(addr3))
.await
.expect("bind failed")
.with_seed(addr2)
.with_tombstone_timeout(Duration::from_millis(200));
let task1 = tokio::spawn(store1.clone().run());
let task2 = tokio::spawn(store2.clone().run());
let task3 = tokio::spawn(store3.clone().run());
store1.insert(1, 11);
store3.insert(3, 33);
assert_until!(store1.get(&3).as_deref() == Some(&33));
assert_until!(store3.get(&1).as_deref() == Some(&11));
assert_until!(store2.get(&1).as_deref() == Some(&11) && store2.get(&3).as_deref() == Some(&33));
store1.remove(&1);
assert_until!(store2.get(&1).is_none() && store3.get(&1).is_none());
let fp_tombstone = store1.fingerprint(..);
assert_until!(store2.fingerprint(..) == fp_tombstone && store3.fingerprint(..) == fp_tombstone);
assert_until_slow!(store1.fingerprint(..) != fp_tombstone);
assert_until_slow!(store2.fingerprint(..) != fp_tombstone);
assert_until_slow!(store3.fingerprint(..) != fp_tombstone);
let fp_collected = store1.fingerprint(..);
assert_until_slow!(
store2.fingerprint(..) == fp_collected && store3.fingerprint(..) == fp_collected
);
task1.abort();
task2.abort();
task3.abort();
}