use std::collections::{BTreeMap, HashSet, VecDeque};
use std::future::Future;
use std::pin::Pin;
use std::sync::{Arc, Mutex};
use std::task::{Context, Poll, Waker};
use std::time::Duration;
use futures_util::future::LocalBoxFuture;
use futures_util::task::{ArcWake, waker};
use rand::{RngCore, SeedableRng};
use rand_chacha::ChaCha8Rng;
use crate::actor::{Actor, Handler, RemoteMessage};
use crate::endpoint::Endpoint;
use crate::runtime::{BoxFuture, Runtime, SpawnHandle};
use crate::wire::SendError;
use serde::Serialize;
use serde::de::DeserializeOwned;
type TimerId = u64;
struct Timer {
id: TimerId,
deadline: Duration,
waker: Waker,
}
struct SimShared {
now: Duration,
seed: u64,
rng: ChaCha8Rng,
inbox: Vec<BoxFuture<'static, ()>>,
timers: Vec<Timer>,
next_timer_id: TimerId,
}
#[derive(Clone)]
pub struct SimRuntime {
shared: Arc<Mutex<SimShared>>,
}
impl SimRuntime {
pub fn new(seed: u64) -> Self {
Self {
shared: Arc::new(Mutex::new(SimShared {
now: Duration::ZERO,
seed,
rng: ChaCha8Rng::seed_from_u64(seed),
inbox: Vec::new(),
timers: Vec::new(),
next_timer_id: 0,
})),
}
}
}
impl Runtime for SimRuntime {
fn spawn(&self, fut: BoxFuture<'static, ()>) -> SpawnHandle {
self.shared.lock().unwrap().inbox.push(fut);
SpawnHandle::noop()
}
fn sleep(&self, dur: Duration) -> BoxFuture<'static, ()> {
Box::pin(Sleep {
shared: self.shared.clone(),
deadline: None,
dur,
id: None,
})
}
fn now(&self) -> Duration {
self.shared.lock().unwrap().now
}
fn rng_u64(&self) -> u64 {
self.shared.lock().unwrap().rng.next_u64()
}
fn derive_seed(&self, label: &str) -> u64 {
use std::hash::{Hash, Hasher};
let seed = self.shared.lock().unwrap().seed;
let mut h = std::collections::hash_map::DefaultHasher::new();
seed.hash(&mut h);
label.hash(&mut h);
h.finish()
}
}
struct Sleep {
shared: Arc<Mutex<SimShared>>,
deadline: Option<Duration>,
dur: Duration,
id: Option<TimerId>,
}
impl Future for Sleep {
type Output = ();
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<()> {
let this = self.get_mut();
let mut s = this.shared.lock().unwrap();
let now = s.now;
let deadline = *this.deadline.get_or_insert(now + this.dur);
if now >= deadline {
Poll::Ready(())
} else {
match this.id {
Some(id) => {
if let Some(t) = s.timers.iter_mut().find(|t| t.id == id) {
t.waker = cx.waker().clone();
} else {
s.timers.push(Timer {
id,
deadline,
waker: cx.waker().clone(),
});
}
}
None => {
let id = s.next_timer_id;
s.next_timer_id += 1;
this.id = Some(id);
s.timers.push(Timer {
id,
deadline,
waker: cx.waker().clone(),
});
}
}
Poll::Pending
}
}
}
impl Drop for Sleep {
fn drop(&mut self) {
if let Some(id) = self.id
&& let Ok(mut s) = self.shared.lock()
&& let Some(pos) = s.timers.iter().position(|t| t.id == id)
{
s.timers.swap_remove(pos);
}
}
}
type TaskId = u64;
struct SimTask {
future: LocalBoxFuture<'static, ()>,
waker: Waker,
}
struct ReadyState {
queue: VecDeque<TaskId>,
queued: HashSet<TaskId>,
}
impl ReadyState {
fn push(&mut self, id: TaskId) {
if self.queued.insert(id) {
self.queue.push_back(id);
}
}
fn drain(&mut self) -> VecDeque<TaskId> {
self.queued.clear();
std::mem::take(&mut self.queue)
}
}
struct SimWaker {
id: TaskId,
ready: Arc<Mutex<ReadyState>>,
}
impl ArcWake for SimWaker {
fn wake_by_ref(arc_self: &Arc<Self>) {
arc_self.ready.lock().unwrap().push(arc_self.id);
}
}
pub trait ReadyPolicy {
fn pick(&mut self, ready: &VecDeque<TaskId>) -> usize;
}
pub struct FifoPolicy;
impl ReadyPolicy for FifoPolicy {
fn pick(&mut self, _ready: &VecDeque<TaskId>) -> usize {
0
}
}
pub struct RandomPolicy {
rng: ChaCha8Rng,
}
impl RandomPolicy {
pub fn new(seed: u64) -> Self {
Self {
rng: ChaCha8Rng::seed_from_u64(seed),
}
}
}
impl ReadyPolicy for RandomPolicy {
fn pick(&mut self, ready: &VecDeque<TaskId>) -> usize {
(self.rng.next_u64() % ready.len() as u64) as usize
}
}
struct SimExecutor {
tasks: BTreeMap<TaskId, SimTask>,
next_id: TaskId,
ready: Arc<Mutex<ReadyState>>,
}
impl SimExecutor {
fn new() -> Self {
Self {
tasks: BTreeMap::new(),
next_id: 0,
ready: Arc::new(Mutex::new(ReadyState {
queue: VecDeque::new(),
queued: HashSet::new(),
})),
}
}
fn spawn(&mut self, future: LocalBoxFuture<'static, ()>) -> TaskId {
let id = self.next_id;
self.next_id += 1;
let w = waker(Arc::new(SimWaker {
id,
ready: Arc::clone(&self.ready),
}));
self.tasks.insert(id, SimTask { future, waker: w });
self.ready.lock().unwrap().push(id);
id
}
fn poll_task(&mut self, id: TaskId) {
let Some(task) = self.tasks.get_mut(&id) else {
return;
};
let w = task.waker.clone();
let mut cx = Context::from_waker(&w);
if task.future.as_mut().poll(&mut cx).is_ready() {
self.tasks.remove(&id);
}
}
fn take_ready(&self) -> VecDeque<TaskId> {
self.ready.lock().unwrap().drain()
}
}
pub struct SimWorld {
runtime: SimRuntime,
executor: SimExecutor,
policy: Box<dyn ReadyPolicy>,
system: crate::System,
}
impl SimWorld {
pub fn new(seed: u64) -> Self {
let runtime = SimRuntime::new(seed);
let system = crate::System::with_runtime(Arc::new(runtime.clone()));
Self {
runtime,
executor: SimExecutor::new(),
policy: Box::new(FifoPolicy),
system,
}
}
pub fn system(&self) -> &crate::System {
&self.system
}
pub fn runtime(&self) -> &SimRuntime {
&self.runtime
}
pub fn send<A, M>(&mut self, ep: &Endpoint<A>, msg: M) -> Result<M::Result, SendError>
where
A: Actor + Handler<M> + 'static,
M: RemoteMessage + 'static,
M::Result: Serialize + DeserializeOwned + 'static,
{
let ep = ep.clone();
self.block_on(async move { ep.send(msg).await })
}
pub fn now(&self) -> Duration {
self.runtime.shared.lock().unwrap().now
}
#[cfg(test)]
fn timer_count(&self) -> usize {
self.runtime.shared.lock().unwrap().timers.len()
}
pub fn rng_u64(&self) -> u64 {
self.runtime.rng_u64()
}
pub fn derive_seed(&self, label: &str) -> u64 {
self.runtime.derive_seed(label)
}
pub fn set_policy(&mut self, policy: Box<dyn ReadyPolicy>) {
self.policy = policy;
}
pub fn use_random_scheduling(&mut self) {
let seed = self.derive_seed("scheduler");
self.set_policy(Box::new(RandomPolicy::new(seed)));
}
fn drain_inbox(&mut self) {
let futs: Vec<BoxFuture<'static, ()>> = {
let mut s = self.runtime.shared.lock().unwrap();
s.inbox.drain(..).collect()
};
for fut in futs {
let local: LocalBoxFuture<'static, ()> = fut;
self.executor.spawn(local);
}
}
fn inbox_nonempty(&self) -> bool {
!self.runtime.shared.lock().unwrap().inbox.is_empty()
}
fn earliest_deadline(&self) -> Option<Duration> {
self.runtime
.shared
.lock()
.unwrap()
.timers
.iter()
.map(|t| t.deadline)
.min()
}
fn set_now_and_fire(&mut self, target: Duration) {
let due: Vec<Waker> = {
let mut s = self.runtime.shared.lock().unwrap();
s.now = target;
let mut due = Vec::new();
let mut i = 0;
while i < s.timers.len() {
if s.timers[i].deadline <= target {
due.push(s.timers.swap_remove(i).waker);
} else {
i += 1;
}
}
due
};
for w in due {
w.wake();
}
}
pub fn pump(&mut self) {
loop {
self.drain_inbox();
let mut ready = self.executor.take_ready();
if ready.is_empty() {
if self.inbox_nonempty() {
continue;
}
break;
}
while !ready.is_empty() {
let idx = self.policy.pick(&ready);
let id = if idx == 0 {
ready.pop_front().expect("ready is non-empty")
} else {
ready.remove(idx).expect("policy index in bounds")
};
self.executor.poll_task(id);
ready.extend(self.executor.take_ready());
}
}
}
pub fn block_on<F>(&mut self, fut: F) -> F::Output
where
F: Future + 'static,
{
let out: Arc<Mutex<Option<F::Output>>> = Arc::new(Mutex::new(None));
let sink = out.clone();
self.executor.spawn(Box::pin(async move {
let r = fut.await;
*sink.lock().unwrap() = Some(r);
}));
loop {
self.pump();
if out.lock().unwrap().is_some() {
break;
}
match self.earliest_deadline() {
Some(deadline) => self.set_now_and_fire(deadline),
None => panic!(
"sim deadlock in block_on: all tasks stalled at t={:?} with no pending \
timers — the awaited future cannot complete (waiting on a message or reply \
that no actor will send?)",
self.now()
),
}
}
out.lock().unwrap().take().expect("block_on output was set")
}
pub fn advance(&mut self, by: Duration) {
let target = self.now() + by;
loop {
self.pump();
match self.earliest_deadline() {
Some(deadline) if deadline <= target => self.set_now_and_fire(deadline),
_ => break,
}
}
self.set_now_and_fire(target);
self.pump();
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::actor::ActorContext;
use crate::prelude::*;
use serde::{Deserialize, Serialize};
use std::time::Duration;
struct Counter;
#[derive(Default)]
struct CounterState {
count: i64,
ticks: u64,
timer: Option<ScheduleHandle>,
}
impl Actor for Counter {
type State = CounterState;
}
#[derive(Debug, Clone, Serialize, Deserialize, Message)]
#[message(result = i64, remote = "sim::Increment")]
struct Increment {
amount: i64,
}
#[derive(Debug, Clone, Serialize, Deserialize, Message)]
#[message(result = i64, remote = "sim::Get")]
struct Get;
#[derive(Debug, Clone, Serialize, Deserialize, Message)]
#[message(result = i64, remote = "sim::GetTicks")]
struct GetTicks;
#[derive(Debug, Clone, Serialize, Deserialize, Message)]
#[message(result = (), remote = "sim::Tick")]
struct Tick;
#[derive(Debug, Clone, Serialize, Deserialize, Message)]
#[message(result = (), remote = "sim::StartTicking")]
struct StartTicking {
every_ms: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize, Message)]
#[message(result = i64, remote = "sim::IncrAsync")]
struct IncrAsync {
amount: i64,
}
#[handlers]
impl Counter {
#[handler]
fn increment(
&self,
_ctx: &ActorContext<Self>,
state: &mut CounterState,
msg: Increment,
) -> i64 {
state.count += msg.amount;
state.count
}
#[handler]
fn get(&self, _ctx: &ActorContext<Self>, state: &mut CounterState, _msg: Get) -> i64 {
state.count
}
#[handler]
fn get_ticks(
&self,
_ctx: &ActorContext<Self>,
state: &mut CounterState,
_msg: GetTicks,
) -> i64 {
state.ticks as i64
}
#[handler]
fn tick(&self, _ctx: &ActorContext<Self>, state: &mut CounterState, _msg: Tick) {
state.ticks += 1;
}
#[handler]
fn start_ticking(
&self,
ctx: &ActorContext<Self>,
state: &mut CounterState,
msg: StartTicking,
) {
state.timer = Some(ctx.schedule_repeat(Duration::from_millis(msg.every_ms), Tick));
}
#[handler]
async fn incr_async(
&mut self,
_ctx: &ActorContext<Self>,
state: &mut CounterState,
msg: IncrAsync,
) -> i64 {
state.count += msg.amount;
state.count
}
}
#[test]
fn send_and_reply_under_sim() {
let mut world = SimWorld::new(1);
let ep = world
.system()
.start("counter/0", Counter, CounterState::default());
assert_eq!(world.send(&ep, Increment { amount: 5 }).unwrap(), 5);
assert_eq!(world.send(&ep, Increment { amount: 3 }).unwrap(), 8);
assert_eq!(world.send(&ep, Get).unwrap(), 8);
}
#[test]
fn virtual_time_does_not_really_sleep() {
let mut world = SimWorld::new(1);
let ep = world
.system()
.start("counter/0", Counter, CounterState::default());
let started = std::time::Instant::now();
world.advance(Duration::from_secs(3600)); assert!(
started.elapsed() < Duration::from_secs(1),
"advancing virtual time must not sleep in real time"
);
assert_eq!(world.now(), Duration::from_secs(3600));
let _ = ep;
}
#[test]
fn scheduled_repeat_fires_deterministically_on_advance() {
let mut world = SimWorld::new(7);
let ep = world
.system()
.start("counter/0", Counter, CounterState::default());
world.send(&ep, StartTicking { every_ms: 100 }).unwrap();
assert_eq!(world.send(&ep, GetTicks).unwrap(), 0);
world.advance(Duration::from_millis(350));
assert_eq!(world.send(&ep, GetTicks).unwrap(), 3);
world.advance(Duration::from_millis(100));
assert_eq!(world.send(&ep, GetTicks).unwrap(), 4);
}
#[test]
fn block_on_panics_on_deadlock() {
let mut world = SimWorld::new(1);
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
world.block_on(async {
let (_tx, rx) = tokio::sync::oneshot::channel::<()>();
let _ = rx.await;
});
}));
assert!(result.is_err(), "block_on should panic on a sim deadlock");
}
struct SlowToDie {
label: &'static str,
delay: Duration,
runtime: SimRuntime,
seen: Arc<Mutex<Vec<(String, String)>>>,
}
impl crate::lifecycle::TerminateHook for SlowToDie {
fn before_deregister(
&self,
label: &str,
reason: &TerminationReason,
) -> BoxFuture<'static, ()> {
self.seen
.lock()
.unwrap()
.push((label.to_string(), format!("{reason:?}")));
if label == self.label {
self.runtime.sleep(self.delay)
} else {
Box::pin(std::future::ready(()))
}
}
}
#[test]
fn terminate_hook_holds_a_dying_actor_registered() {
let mut world = SimWorld::new(7);
let seen = Arc::new(Mutex::new(Vec::new()));
world
.system()
.receptionist()
.set_terminate_hook(Some(Arc::new(SlowToDie {
label: "counter/dying",
delay: Duration::from_secs(5),
runtime: world.runtime().clone(),
seen: seen.clone(),
})));
let ep = world
.system()
.start("counter/dying", Counter, CounterState::default());
assert_eq!(world.send(&ep, Increment { amount: 1 }).unwrap(), 1);
world.system().stop("counter/dying");
world.pump();
assert_eq!(
&*seen.lock().unwrap(),
&[("counter/dying".to_string(), "Stopped".to_string())],
"hook should fire exactly once, with the clean-stop reason"
);
assert!(
world.system().lookup::<Counter>("counter/dying").is_some(),
"actor must stay registered while the terminate hook is pending"
);
world.advance(Duration::from_secs(4));
assert!(
world.system().lookup::<Counter>("counter/dying").is_some(),
"actor must stay registered until the hook's virtual delay elapses"
);
world.advance(Duration::from_secs(2));
assert!(
world.system().lookup::<Counter>("counter/dying").is_none(),
"actor must de-register once the terminate hook completes"
);
}
#[test]
fn terminate_hook_deregistration_time_is_deterministic() {
fn dies_at(seed: u64) -> Duration {
let mut world = SimWorld::new(seed);
world
.system()
.receptionist()
.set_terminate_hook(Some(Arc::new(SlowToDie {
label: "counter/dying",
delay: Duration::from_secs(5),
runtime: world.runtime().clone(),
seen: Arc::new(Mutex::new(Vec::new())),
})));
let _ep = world
.system()
.start("counter/dying", Counter, CounterState::default());
world.system().stop("counter/dying");
world.pump();
for _ in 0..60 {
if world.system().lookup::<Counter>("counter/dying").is_none() {
break;
}
world.advance(Duration::from_secs(1));
}
world.now()
}
assert_eq!(dies_at(1), Duration::from_secs(5));
assert_eq!(dies_at(1), dies_at(99), "seed must not change the window");
}
#[test]
fn without_a_terminate_hook_deregistration_is_immediate() {
let mut world = SimWorld::new(7);
let _ep = world
.system()
.start("counter/dying", Counter, CounterState::default());
world.system().stop("counter/dying");
world.pump();
assert!(world.system().lookup::<Counter>("counter/dying").is_none());
}
#[test]
fn replay_is_stable_across_runs() {
fn run(seed: u64) -> (i64, i64) {
let mut world = SimWorld::new(seed);
let ep = world
.system()
.start("counter/0", Counter, CounterState::default());
world.send(&ep, StartTicking { every_ms: 50 }).unwrap();
let mut total = 0;
for _ in 0..10 {
let amount = (world.rng_u64() % 7) as i64;
total += world.send(&ep, Increment { amount }).unwrap();
world.advance(Duration::from_millis(50));
}
let ticks = world.send(&ep, GetTicks).unwrap();
(total, ticks)
}
assert_eq!(run(99), run(99), "same seed must replay identically");
}
#[test]
fn listing_backfill_order_is_deterministic() {
fn collect_counts(seed: u64) -> Vec<i64> {
let mut world = SimWorld::new(seed);
let key = ReceptionKey::<Counter>::new("workers");
let mut kept = Vec::new();
for (label, count) in [("w/charlie", 3), ("w/alpha", 1), ("w/bravo", 2)] {
let ep = world.system().start(
label,
Counter,
CounterState {
count,
..Default::default()
},
);
world.system().check_in(label, key.clone());
kept.push(ep); }
let mut listing = world.system().listing(key);
let mut eps = Vec::new();
while let Some(ep) = listing.try_next() {
eps.push(ep);
}
let counts = eps
.iter()
.map(|ep| {
let ep = ep.clone();
world.block_on(async move { ep.send(Get).await.unwrap() })
})
.collect();
let _ = kept;
counts
}
let a = collect_counts(1);
let b = collect_counts(2);
assert_eq!(
a,
vec![1, 2, 3],
"backfill must follow sorted label order (alpha, bravo, charlie)"
);
assert_eq!(
a, b,
"backfill order is identical across seeds — it is deterministic, not hash-random"
);
}
#[test]
fn pool_router_fills_deterministically_under_sim() {
let mut world = SimWorld::new(5);
let key = ReceptionKey::<Counter>::new("pool");
let mut kept = Vec::new();
for label in ["p/charlie", "p/alpha", "p/bravo"] {
let ep = world
.system()
.start(label, Counter, CounterState::default());
world.system().check_in(label, key.clone());
kept.push(ep);
}
let router = PoolRouter::new(
world.system().receptionist(),
key,
RoutingStrategy::RoundRobin,
);
world.pump();
assert_eq!(router.len(), 3, "watcher ran under sim and filled the pool");
assert_eq!(
router.labels(),
vec!["p/alpha", "p/bravo", "p/charlie"],
"pool order is deterministic (sorted by label, not hash-random)"
);
let _ = kept;
}
#[test]
fn pool_router_send_async_routes_under_sim() {
let mut world = SimWorld::new(9);
let key = ReceptionKey::<Counter>::new("apool");
let mut kept = Vec::new();
for label in ["a/0", "a/1"] {
let ep = world
.system()
.start(label, Counter, CounterState::default());
world.system().check_in(label, key.clone());
kept.push(ep);
}
let router = PoolRouter::new(
world.system().receptionist(),
key,
RoutingStrategy::RoundRobin,
);
world.pump();
assert_eq!(router.len(), 2, "both async actors are in the pool");
let last = world.block_on(async move {
let mut last = 0;
for _ in 0..2 {
last = router
.send_async(IncrAsync { amount: 1 })
.await
.expect("PoolRouter::send_async routes to the AsyncHandler");
}
last
});
assert_eq!(
last, 1,
"round-robin hit each of the 2 async actors exactly once"
);
let _ = kept;
}
#[test]
fn same_seed_same_rng_sequence() {
let a = SimWorld::new(42);
let b = SimWorld::new(42);
let seq_a: Vec<u64> = (0..16).map(|_| a.rng_u64()).collect();
let seq_b: Vec<u64> = (0..16).map(|_| b.rng_u64()).collect();
assert_eq!(seq_a, seq_b, "same seed must reproduce the same RNG stream");
let c = SimWorld::new(43);
let seq_c: Vec<u64> = (0..16).map(|_| c.rng_u64()).collect();
assert_ne!(seq_a, seq_c, "different seed should diverge");
}
#[test]
fn derive_seed_is_reproducible_and_label_independent() {
let a = SimWorld::new(100);
let b = SimWorld::new(100);
assert_eq!(a.derive_seed("vfs"), b.derive_seed("vfs"));
assert_eq!(a.derive_seed("faults"), b.derive_seed("faults"));
assert_ne!(a.derive_seed("vfs"), a.derive_seed("faults"));
let c = SimWorld::new(101);
assert_ne!(a.derive_seed("vfs"), c.derive_seed("vfs"));
let d = SimWorld::new(100);
let _ = d.derive_seed("anything");
assert_eq!(
d.rng_u64(),
SimWorld::new(100).rng_u64(),
"derive_seed must not consume from the primary rng stream"
);
}
#[test]
fn repeated_poll_of_sleep_registers_at_most_one_timer() {
use futures_util::task::noop_waker;
let world = SimWorld::new(1);
let mut sleep = world.runtime().sleep(Duration::from_millis(100));
let waker = noop_waker();
let mut cx = Context::from_waker(&waker);
for _ in 0..1000 {
assert!(sleep.as_mut().poll(&mut cx).is_pending());
assert_eq!(
world.timer_count(),
1,
"each re-poll must reuse the single timer entry, not leak duplicates"
);
}
drop(sleep);
assert_eq!(world.timer_count(), 0, "drop must clean up the entry");
}
#[test]
fn sleep_dropped_before_firing_leaves_no_timer() {
use futures_util::task::noop_waker;
let world = SimWorld::new(1);
let waker = noop_waker();
let mut cx = Context::from_waker(&waker);
{
let mut sleep = world.runtime().sleep(Duration::from_secs(5));
assert!(sleep.as_mut().poll(&mut cx).is_pending());
assert_eq!(world.timer_count(), 1, "poll registers exactly one timer");
}
assert_eq!(
world.timer_count(),
0,
"a Sleep dropped before firing must remove its timer entry"
);
}
#[test]
fn many_sleeps_dropped_do_not_accumulate_timers() {
let mut world = SimWorld::new(3);
let rt = world.runtime().clone();
let shared = world.runtime().shared.clone();
let max_seen = Arc::new(Mutex::new(0usize));
let probe = max_seen.clone();
world.block_on(async move {
for _ in 0..100 {
rt.sleep(Duration::from_millis(1)).await;
let n = shared.lock().unwrap().timers.len();
let mut m = probe.lock().unwrap();
if n > *m {
*m = n;
}
}
});
assert!(
*max_seen.lock().unwrap() <= 1,
"timer set must stay bounded across many sequential sleeps, saw {}",
*max_seen.lock().unwrap()
);
assert_eq!(world.timer_count(), 0, "all timers cleaned up at the end");
}
async fn yield_once() {
struct YieldOnce(bool);
impl std::future::Future for YieldOnce {
type Output = ();
fn poll(
mut self: std::pin::Pin<&mut Self>,
cx: &mut std::task::Context<'_>,
) -> std::task::Poll<()> {
if self.0 {
std::task::Poll::Ready(())
} else {
self.0 = true;
cx.waker().wake_by_ref();
std::task::Poll::Pending
}
}
}
YieldOnce(false).await
}
#[test]
fn random_policy_reorders_ready_tasks_but_runs_them_all() {
use std::sync::{Arc, Mutex};
fn run(random: bool, seed: u64) -> Vec<u64> {
let mut world = SimWorld::new(seed);
if random {
world.use_random_scheduling();
}
let log = Arc::new(Mutex::new(Vec::new()));
for i in 0..8u64 {
let log = Arc::clone(&log);
world.runtime().spawn(Box::pin(async move {
log.lock().unwrap().push(i);
}));
}
world.pump();
Arc::try_unwrap(log).unwrap().into_inner().unwrap()
}
let fifo = run(false, 1);
assert_eq!(
fifo,
(0..8).collect::<Vec<_>>(),
"FIFO runs ready tasks in spawn order"
);
let rnd = run(true, 1);
let mut as_set = rnd.clone();
as_set.sort();
assert_eq!(
as_set,
(0..8).collect::<Vec<_>>(),
"random scheduling runs exactly the same set of tasks"
);
assert_ne!(
rnd, fifo,
"random scheduling reorders the ready set (seed 1)"
);
assert_eq!(
run(true, 1),
run(true, 1),
"same seed reproduces the same interleaving"
);
}
#[test]
fn adversarial_scheduling_reaches_an_interleaving_fifo_misses() {
use std::sync::{Arc, Mutex};
fn lost_update(seed: u64, random: bool) -> i64 {
let mut world = SimWorld::new(seed);
if random {
world.use_random_scheduling();
}
let cell = Arc::new(Mutex::new(0i64));
for _ in 0..2 {
let cell = Arc::clone(&cell);
world.runtime().spawn(Box::pin(async move {
let v = *cell.lock().unwrap();
yield_once().await;
*cell.lock().unwrap() = v + 1;
}));
}
world.pump();
*cell.lock().unwrap()
}
let fifo = lost_update(1, false);
let other = (1..50u64).find(|&s| lost_update(s, true) != fifo);
assert!(
other.is_some(),
"no adversarial seed in 1..50 reached an outcome different from FIFO's \
({fifo}) — the scheduling policy has no teeth"
);
assert_eq!(lost_update(7, true), lost_update(7, true));
}
}