swactor/crates/simulation/tests/distribution_sim.rs
zacheryasc d817ad952f feat: distributed runtime (#30)
Major feature addition. For full details read `./docs/development_history/DISTRIBUTION.md`
2026-02-12 09:13:50 +00:00

206 lines
8.3 KiB
Rust

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"
);
}