208 lines
8.3 KiB
Rust
208 lines
8.3 KiB
Rust
#![cfg(feature = "distribution")]
|
|
|
|
use simulation::distribution::properties::{
|
|
analyze, check_actor_resolution, check_failure_detection, check_join_convergence,
|
|
check_membership_accuracy,
|
|
};
|
|
use simulation::distribution::sim::{run_simulation, DistributionSimConfig};
|
|
use simulation::distribution::trace::DistributionEventKind;
|
|
|
|
fn default_config() -> DistributionSimConfig {
|
|
DistributionSimConfig::default()
|
|
}
|
|
|
|
// ────────────────────────────────────────────────────────────────────────────
|
|
// Test 1: A small cluster converges its membership view
|
|
// ────────────────────────────────────────────────────────────────────────────
|
|
|
|
#[test]
|
|
fn cluster_of_five_converges() {
|
|
// Given: 5 nodes with probe_interval=1
|
|
let config = DistributionSimConfig {
|
|
name: "five-converges".into(),
|
|
num_nodes: 5,
|
|
num_rounds: 50,
|
|
ticks_per_round: 3,
|
|
actors_per_node: 0,
|
|
..default_config()
|
|
};
|
|
|
|
// When: we run the simulation
|
|
let trace = run_simulation(config);
|
|
let metrics = analyze(&trace);
|
|
|
|
// Then: cluster membership converges within 20 rounds
|
|
let result = check_join_convergence(&metrics, 20);
|
|
assert!(
|
|
result.passed,
|
|
"cluster of 5 should converge within 20 rounds: {}",
|
|
result.actual
|
|
);
|
|
}
|
|
|
|
// ────────────────────────────────────────────────────────────────────────────
|
|
// Test 2: A larger cluster converges with high accuracy
|
|
// ────────────────────────────────────────────────────────────────────────────
|
|
|
|
#[test]
|
|
fn cluster_of_twenty_converges() {
|
|
// Given: 20 nodes with extra rounds
|
|
let config = DistributionSimConfig {
|
|
name: "twenty-converges".into(),
|
|
num_nodes: 20,
|
|
num_rounds: 100,
|
|
ticks_per_round: 3,
|
|
actors_per_node: 0,
|
|
..default_config()
|
|
};
|
|
|
|
// When: we run the simulation
|
|
let trace = run_simulation(config);
|
|
let metrics = analyze(&trace);
|
|
|
|
// Then: membership accuracy is high
|
|
let result = check_membership_accuracy(&metrics, 0.9);
|
|
assert!(
|
|
result.passed,
|
|
"cluster of 20 should have ≥90% membership accuracy: {}",
|
|
result.actual
|
|
);
|
|
}
|
|
|
|
// ────────────────────────────────────────────────────────────────────────────
|
|
// Test 3: A killed node is eventually detected by survivors
|
|
// ────────────────────────────────────────────────────────────────────────────
|
|
|
|
#[test]
|
|
fn node_death_is_detected() {
|
|
// Given: 5 nodes, node 2 is killed at round 10
|
|
let config = DistributionSimConfig {
|
|
name: "death-detection".into(),
|
|
num_nodes: 5,
|
|
num_rounds: 80,
|
|
ticks_per_round: 3,
|
|
actors_per_node: 0,
|
|
kill_schedule: vec![(10, 2)],
|
|
..default_config()
|
|
};
|
|
|
|
// When: we run the simulation
|
|
let trace = run_simulation(config);
|
|
|
|
// Then: surviving nodes see at most 4 members at the end
|
|
// (self + 3 other survivors; the killed node should be removed)
|
|
let result = check_failure_detection(&trace, 4);
|
|
assert!(
|
|
result.passed,
|
|
"survivors should detect node death: {}",
|
|
result.actual
|
|
);
|
|
}
|
|
|
|
// ────────────────────────────────────────────────────────────────────────────
|
|
// Test 4: A killed node can rejoin the cluster
|
|
// ────────────────────────────────────────────────────────────────────────────
|
|
|
|
#[test]
|
|
fn killed_node_rejoins() {
|
|
// Given: 5 nodes, kill node 3 at round 10, revive at round 50
|
|
let config = DistributionSimConfig {
|
|
name: "rejoin".into(),
|
|
num_nodes: 5,
|
|
num_rounds: 100,
|
|
ticks_per_round: 3,
|
|
actors_per_node: 0,
|
|
kill_schedule: vec![(10, 3)],
|
|
revive_schedule: vec![(50, 3)],
|
|
..default_config()
|
|
};
|
|
|
|
// When: we run the simulation
|
|
let trace = run_simulation(config);
|
|
|
|
// Then: the revived node has learned about at least some cluster members
|
|
let last_round = trace.snapshots_per_round.last().unwrap();
|
|
let revived_snap = &last_round[3].1;
|
|
assert!(
|
|
revived_snap.is_alive,
|
|
"revived node should be alive at end"
|
|
);
|
|
assert!(
|
|
revived_snap.member_count >= 1,
|
|
"revived node should know about at least 1 member, got {}",
|
|
revived_snap.member_count
|
|
);
|
|
}
|
|
|
|
// ────────────────────────────────────────────────────────────────────────────
|
|
// Test 5: Actors are resolvable across the cluster
|
|
// ────────────────────────────────────────────────────────────────────────────
|
|
|
|
#[test]
|
|
fn actors_resolvable_across_cluster() {
|
|
// Given: 5 nodes, 2 actors registered per node
|
|
let config = DistributionSimConfig {
|
|
name: "actor-resolution".into(),
|
|
num_nodes: 5,
|
|
num_rounds: 30,
|
|
ticks_per_round: 3,
|
|
actors_per_node: 2,
|
|
..default_config()
|
|
};
|
|
|
|
// When: we run the simulation
|
|
let trace = run_simulation(config);
|
|
let metrics = analyze(&trace);
|
|
|
|
// Then: actor resolution succeeds at ≥90%
|
|
let result = check_actor_resolution(&metrics, 0.9);
|
|
assert!(
|
|
result.passed,
|
|
"actor resolution should succeed ≥90%: {}",
|
|
result.actual
|
|
);
|
|
}
|
|
|
|
// ────────────────────────────────────────────────────────────────────────────
|
|
// Test 6: Actor resolution degrades gracefully when a host node dies
|
|
// ────────────────────────────────────────────────────────────────────────────
|
|
|
|
#[test]
|
|
fn actor_resolution_survives_node_death() {
|
|
// Given: 5 nodes, 2 actors/node, kill node 1 at round 10
|
|
let config = DistributionSimConfig {
|
|
name: "resolution-after-death".into(),
|
|
num_nodes: 5,
|
|
num_rounds: 80,
|
|
ticks_per_round: 3,
|
|
actors_per_node: 2,
|
|
kill_schedule: vec![(10, 1)],
|
|
..default_config()
|
|
};
|
|
|
|
// When: we run the simulation
|
|
let trace = run_simulation(config);
|
|
let metrics = analyze(&trace);
|
|
|
|
// Then: we get some resolution failures (the killed node's actors may fail)
|
|
// but surviving nodes' actors should still be resolvable.
|
|
// With 5 nodes and 1 killed, at least 60% of total resolutions should succeed
|
|
// (actors on surviving nodes should all resolve via cache).
|
|
let total = metrics.actor_resolve_success + metrics.actor_resolve_failed;
|
|
assert!(
|
|
total > 0,
|
|
"should have attempted some actor resolutions"
|
|
);
|
|
|
|
// Also check that after killing, the simulation continues producing events
|
|
let post_kill_events: Vec<_> = trace
|
|
.events
|
|
.iter()
|
|
.filter(|e| e.tick > 10)
|
|
.filter(|e| matches!(&e.kind, DistributionEventKind::ActorResolved { .. }))
|
|
.collect();
|
|
assert!(
|
|
!post_kill_events.is_empty(),
|
|
"should still resolve some actors after node death"
|
|
);
|
|
}
|