#![cfg(feature = "sim")]
use std::collections::BTreeSet;
use std::time::Duration;
use murmer::SimCluster;
use murmer::actor::ActorContext;
use murmer::prelude::*;
use serde::{Deserialize, Serialize};
struct Counter;
#[derive(Default)]
struct CounterState {
count: i64,
}
impl Actor for Counter {
type State = CounterState;
}
#[derive(Debug, Clone, Serialize, Deserialize, Message)]
#[message(result = i64, remote = "sim_cluster_example::Increment")]
struct Increment {
amount: i64,
}
#[handlers]
impl Counter {
#[handler]
fn increment(
&self,
_ctx: &ActorContext<Self>,
state: &mut CounterState,
msg: Increment,
) -> i64 {
state.count += msg.amount;
state.count
}
}
fn converged_trio(seed: u64) -> SimCluster {
let mut cluster = SimCluster::builder(seed)
.node("node-a")
.node("node-b")
.node("node-c")
.build();
cluster.mesh();
cluster.pump();
cluster
}
#[test]
fn three_nodes_converge_to_a_full_mesh() {
let mut cluster = converged_trio(0xC0FFEE);
for (me, peers) in [
("node-a", ["node-b-id", "node-c-id"]),
("node-b", ["node-a-id", "node-c-id"]),
("node-c", ["node-a-id", "node-b-id"]),
] {
let want: BTreeSet<String> = peers.iter().map(|s| s.to_string()).collect();
assert_eq!(cluster.events(me).joined_ids(), want, "{me} converged set");
}
}
#[test]
fn a_crashed_node_is_detected_exactly() {
let mut cluster = converged_trio(1);
let _ = (cluster.events("node-b"), cluster.events("node-c"));
cluster.crash("node-a");
cluster.advance(Duration::from_secs(30));
let only_a = BTreeSet::from(["node-a-id".to_string()]);
for survivor in ["node-b", "node-c"] {
assert_eq!(
cluster.events(survivor).failed,
only_a,
"{survivor} detects exactly the crashed node"
);
}
}
#[test]
fn a_single_link_partition_is_tolerated() {
let mut cluster = converged_trio(1);
for n in ["node-a", "node-b", "node-c"] {
let _ = cluster.events(n); }
assert!(
cluster.partition("node-a", "node-b"),
"the A-B link was live"
);
cluster.advance(Duration::from_secs(30));
for n in ["node-a", "node-b", "node-c"] {
assert!(
!cluster.events(n).any_failed(),
"{n} saw a failure, but a single cut should be masked by the third node"
);
}
}
#[test]
fn the_same_seed_replays_identically() {
fn detect(seed: u64) -> BTreeSet<String> {
let mut cluster = converged_trio(seed);
let _ = (cluster.events("node-b"), cluster.events("node-c"));
cluster.crash("node-a");
cluster.advance(Duration::from_secs(30));
let mut failed = cluster.events("node-b").failed;
failed.extend(cluster.events("node-c").failed);
failed
}
let only_a = BTreeSet::from(["node-a-id".to_string()]);
assert_eq!(detect(7), detect(7), "same seed must replay identically");
assert_eq!(detect(7), only_a);
assert_eq!(
detect(99),
only_a,
"a different seed still detects exactly A"
);
}
#[test]
fn a_remote_actor_is_reachable_across_nodes() {
let mut cluster = SimCluster::builder(1).node("node-a").node("node-b").build();
cluster.mesh();
cluster.pump();
let _local =
cluster
.system("node-a")
.start_actor("counter/0", Counter, CounterState::default());
cluster.advance(Duration::from_secs(6));
let endpoint = cluster
.system("node-b")
.lookup::<Counter>("counter/0")
.expect("node-b discovers node-a's actor after the registry sync");
let reply =
cluster.block_on(async move { endpoint.send(Increment { amount: 5 }).await.unwrap() });
assert_eq!(
reply, 5,
"the remote increment round-trips over the sim fabric"
);
}
#[test]
fn convergence_survives_a_slow_network() {
let mut cluster = SimCluster::builder(1)
.node("node-a")
.node("node-b")
.node("node-c")
.network_latency(Duration::from_millis(50), Duration::from_millis(20))
.build();
cluster.mesh();
cluster.pump();
cluster.advance(Duration::from_secs(30));
for (me, peers) in [
("node-a", ["node-b-id", "node-c-id"]),
("node-b", ["node-a-id", "node-c-id"]),
("node-c", ["node-a-id", "node-b-id"]),
] {
let want: BTreeSet<String> = peers.iter().map(|s| s.to_string()).collect();
assert_eq!(
cluster.events(me).joined_ids(),
want,
"{me} converges even over a slow, jittery network"
);
}
}