swactor/tests/runtime_stress.rs
Zachery Aaron Shores-Chmielewski 3a13acc0f4 refactor(core): drop native threading for tick-driven execution
Core no longer owns or drives OS threads. The runtime is now a single
tick-driven worker whose loop an external engine hosts and advances.
This is the cutover required before the engine seam is introduced.

Removed from core:
- Runtime::run() and its owned thread pool (spawn, park/unpark, join)
- notify_worker() and worker_threads: Vec<OnceLock<Thread>> plumbing
- Placement load-aware selector and its WorkerStats-driven next_worker()
- WorkerId newtype and the address->worker routing map; AddressMap is
  now a plain AddrSet membership set
- num_threads from RuntimeConfig

Rewired for the single-worker tick API:
- python/wasm bindings, dashboard dummy node (deleted), myelin vastai
  adapter, and the runtime/test suites

Cleanup folded in during review:
- prune three never-written WorkerStats counters (cross_sends,
  messages_dropped, restarts)
- collapse the redundant tick_all params onto the WorkerContext handle


Signed-off-by: Zachery Aaron Shores-Chmielewski <zacheryasc@gmail.com>
2026-08-09 13:50:33 +04:00

243 lines
6.7 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//! Runtime Stress Tests — delivery at scale, panic isolation, and sustained throughput.
//!
//! Covers: high-volume delivery, panic isolation under load, and sustained
//! throughput with no message loss. All tests are tick-driven (single worker).
mod common;
use common::*;
use std::sync::Arc;
use std::sync::atomic::{AtomicUsize, Ordering};
// ── Tests ────────────────────────────────────────────────────────────────────
/// Single-worker runtime basics.
///
/// Story: A runtime driven by tick() does nothing before the first tick,
/// then processes messages correctly.
#[test]
fn runtime_basics() {
let rt = std_runtime(RuntimeConfig::default());
let addr = rt.spawn(PingPongActor).unwrap();
let inbox = rt.new_inbox::<Pong>().unwrap();
rt.send_to(
addr,
Ping {
reply_to: *inbox.addr(),
},
)
.unwrap();
assert!(inbox.try_recv().is_none(), "no processing before tick");
tick_n(&rt, 2);
assert!(
inbox.try_recv().is_some(),
"tick() drives processing"
);
// Delegation (spawn child from handler) works on a single worker.
let addr2 = rt.spawn(DelegatorActor).unwrap();
let done_inbox = rt.new_inbox::<Done>().unwrap();
rt.send_to(
addr2,
Forward {
value: 3,
reply_to: *done_inbox.addr(),
},
)
.unwrap();
tick_n(&rt, 3);
assert_eq!(
done_inbox.try_recv(),
Some(Done(6)),
"delegation delivers reply"
);
}
/// High-volume delivery.
///
/// Story: We throw large workloads at the runtime — 50 senders each firing
/// 100 messages at one receiver, 200 concurrent spawn+send pairs, and a
/// 50-level chain. All messages must be accounted for.
#[test]
fn high_volume_delivery() {
let cfg = || RuntimeConfig {
max_actors: 5_000,
channel_buffer_size: 10_000,
..Default::default()
};
// ── Part A: 50 senders × 100 messages → one receiver ──
{
let rt = std_runtime(cfg());
let counter = Arc::new(AtomicUsize::new(0));
let dummy = rt.new_inbox::<Pong>().unwrap();
let receiver = rt
.spawn(CountingPingActor {
counter: counter.clone(),
})
.unwrap();
let total_expected = 50 * 100;
for _ in 0..50 {
for _ in 0..100 {
rt.send_to(
receiver,
Ping {
reply_to: *dummy.addr(),
},
)
.unwrap();
}
}
tick_n(&rt, 200);
let processed = counter.load(Ordering::SeqCst);
assert_eq!(
processed, total_expected,
"all 5000 messages delivered to single receiver"
);
}
// ── Part B: 200 concurrent spawn+send pairs ──
{
let rt = std_runtime(cfg());
let counter = Arc::new(AtomicUsize::new(0));
let dummy = rt.new_inbox::<Pong>().unwrap();
for _ in 0..200 {
let a = rt
.spawn(CountingPingActor {
counter: counter.clone(),
})
.unwrap();
rt.send_to(
a,
Ping {
reply_to: *dummy.addr(),
},
)
.unwrap();
}
tick_n(&rt, 50);
let received = counter.load(Ordering::SeqCst);
assert_eq!(received, 200, "all 200 spawn+send pairs complete");
}
// ── Part C: 50-level chain ──
{
let rt = std_runtime(cfg());
let addr = rt.spawn(ChainActor).unwrap();
let inbox = rt.new_inbox::<Done>().unwrap();
rt.send_to(
addr,
ChainMsg {
remaining: 50,
depth: 0,
reply_to: *inbox.addr(),
},
)
.unwrap();
tick_n(&rt, 100);
assert_eq!(
inbox.try_recv(),
Some(Done(50)),
"50-level chain completes"
);
}
}
/// Panic isolation under load.
///
/// Story: 10 panicking actors and 10 healthy actors — every panic is isolated
/// and all 1000 healthy messages are still processed.
#[test]
fn panic_isolation_under_load() {
let rt = std_runtime(RuntimeConfig {
max_actors: 5_000,
channel_buffer_size: 10_000,
..Default::default()
});
let counter = Arc::new(AtomicUsize::new(0));
let dummy = rt.new_inbox::<Pong>().unwrap();
let mut panic_addrs = Vec::new();
let mut healthy_addrs = Vec::new();
for _ in 0..10 {
panic_addrs.push(rt.spawn(PanicActor).unwrap());
healthy_addrs.push(
rt.spawn(CountingPingActor {
counter: counter.clone(),
})
.unwrap(),
);
}
// Trigger panics and flood healthy actors.
for &addr in &panic_addrs {
rt.send_to(addr, PanicMsg).unwrap();
}
for &addr in &healthy_addrs {
for _ in 0..100 {
rt.send_to(
addr,
Ping {
reply_to: *dummy.addr(),
},
)
.unwrap();
}
}
tick_n(&rt, 200);
let expected = 10 * 100;
let processed = counter.load(Ordering::SeqCst);
assert_eq!(
processed, expected,
"all {expected} healthy messages processed despite panicking peers"
);
}
/// Sustained throughput with no message loss.
///
/// Story: We send 10 batches of 100 messages, ticking between batches.
/// Each batch must make forward progress, and after draining, all 1000
/// messages are accounted for.
#[test]
fn sustained_throughput_no_message_loss() {
let rt = std_runtime(RuntimeConfig::default());
let counter = Arc::new(AtomicUsize::new(0));
let dummy = rt.new_inbox::<Pong>().unwrap();
let addr = rt
.spawn(CountingPingActor {
counter: counter.clone(),
})
.unwrap();
for batch in 0..10 {
for _ in 0..100 {
rt.send_to(
addr,
Ping {
reply_to: *dummy.addr(),
},
)
.unwrap();
}
tick_n(&rt, 5);
let processed = counter.load(Ordering::SeqCst);
assert!(
processed > batch * 50,
"batch {batch}: expected progress, only {processed} processed"
);
}
// Drain remaining.
tick_n(&rt, 100);
let total = counter.load(Ordering::SeqCst);
assert_eq!(total, 1000, "sustained load should not drop any messages");
}