Phase 2 surfaced the hole this closes: channel guards were held with preemption enabled, so a timeslice switch inside a channel critical section could release the pthread mutex from a different OS thread (UB). RawMutex disables preemption for the guard span and is cross-thread-release sound by construction; poison goes away with it. The strict single-class leaf rule cannot survive the migration: finalize clones the supervisor/trap senders and monitor() clones the Down sender, all under a cold lock, and those senders live in the slot - the nesting is structural. The leaf check is therefore generalized to two classes: Leaf (cold locks, free list, stack pool) and Channel. Order is Leaf -> Channel, at most one of each; both directions of violation are debug-asserted at the acquisition site. Channel critical sections call only the lock-free unpark protocol, so the order is acyclic. Tests: class-ordering unit tests in raw_mutex (allowed nesting + all three rejected shapes), plus a multi-scheduler integration test driving channels through monitor churn and actor death so any ordering regression trips the debug assert instead of deadlocking.
159 lines
4.8 KiB
Rust
159 lines
4.8 KiB
Rust
//! Channel tests. These run under the scheduler because `recv()` needs to
|
|
//! be able to park, which requires a live runtime.
|
|
|
|
use smarm::{channel, run, spawn};
|
|
use std::cell::Cell;
|
|
|
|
thread_local! {
|
|
static OUT: Cell<i64> = const { Cell::new(0) };
|
|
}
|
|
|
|
#[test]
|
|
fn send_then_recv_same_actor() {
|
|
OUT.with(|c| c.set(0));
|
|
run(|| {
|
|
let (tx, rx) = channel::<i64>();
|
|
tx.send(42).unwrap();
|
|
let v = rx.recv().unwrap();
|
|
OUT.with(|c| c.set(v));
|
|
});
|
|
assert_eq!(OUT.with(|c| c.get()), 42);
|
|
}
|
|
|
|
#[test]
|
|
fn recv_parks_until_send_from_other_actor() {
|
|
OUT.with(|c| c.set(0));
|
|
run(|| {
|
|
let (tx, rx) = channel::<i64>();
|
|
let h = spawn(move || {
|
|
// This actor blocks on an empty channel.
|
|
let v = rx.recv().unwrap();
|
|
OUT.with(|c| c.set(v));
|
|
});
|
|
// Parent runs, then yields to let the child block,
|
|
// then sends, then joins.
|
|
smarm::yield_now();
|
|
tx.send(7).unwrap();
|
|
h.join().unwrap();
|
|
});
|
|
assert_eq!(OUT.with(|c| c.get()), 7);
|
|
}
|
|
|
|
#[test]
|
|
fn multiple_messages_arrive_in_order() {
|
|
let captured: std::sync::Arc<std::sync::Mutex<Vec<i64>>> =
|
|
std::sync::Arc::new(std::sync::Mutex::new(Vec::new()));
|
|
let cap2 = captured.clone();
|
|
|
|
run(move || {
|
|
let (tx, rx) = channel::<i64>();
|
|
let h = spawn(move || {
|
|
for _ in 0..3 {
|
|
let v = rx.recv().unwrap();
|
|
cap2.lock().unwrap().push(v);
|
|
}
|
|
});
|
|
for v in 1..=3i64 {
|
|
tx.send(v).unwrap();
|
|
}
|
|
h.join().unwrap();
|
|
});
|
|
|
|
assert_eq!(*captured.lock().unwrap(), vec![1, 2, 3]);
|
|
}
|
|
|
|
#[test]
|
|
fn cloned_senders_both_deliver() {
|
|
let captured: std::sync::Arc<std::sync::Mutex<Vec<i64>>> =
|
|
std::sync::Arc::new(std::sync::Mutex::new(Vec::new()));
|
|
let cap2 = captured.clone();
|
|
|
|
run(move || {
|
|
let (tx, rx) = channel::<i64>();
|
|
let tx2 = tx.clone();
|
|
let h = spawn(move || {
|
|
for _ in 0..2 {
|
|
let v = rx.recv().unwrap();
|
|
cap2.lock().unwrap().push(v);
|
|
}
|
|
});
|
|
tx.send(10).unwrap();
|
|
tx2.send(20).unwrap();
|
|
h.join().unwrap();
|
|
});
|
|
|
|
let mut got = captured.lock().unwrap().clone();
|
|
got.sort();
|
|
assert_eq!(got, vec![10, 20]);
|
|
}
|
|
|
|
#[test]
|
|
fn recv_returns_err_when_all_senders_dropped() {
|
|
let saw_err: std::sync::Arc<std::sync::atomic::AtomicBool> =
|
|
std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false));
|
|
let saw_err2 = saw_err.clone();
|
|
|
|
run(move || {
|
|
let (tx, rx) = channel::<i64>();
|
|
let h = spawn(move || {
|
|
// Receiver waits; no message will ever come.
|
|
if rx.recv().is_err() {
|
|
saw_err2.store(true, std::sync::atomic::Ordering::SeqCst);
|
|
}
|
|
});
|
|
smarm::yield_now();
|
|
drop(tx); // last sender gone; rx.recv must return Err.
|
|
h.join().unwrap();
|
|
});
|
|
|
|
assert!(saw_err.load(std::sync::atomic::Ordering::SeqCst));
|
|
}
|
|
|
|
#[test]
|
|
fn channel_ops_interleaved_with_monitor_churn_multi_thread() {
|
|
// Regression for the RawMutex migration: monitor registration clones the
|
|
// Down sender under the target's cold (Leaf) lock, which now nests a
|
|
// Channel-class lock under it. Debug builds enforce the Leaf -> Channel
|
|
// ordering on every acquisition, so driving channels, monitors, and actor
|
|
// death concurrently across schedulers makes any ordering regression
|
|
// panic here rather than deadlock in the field.
|
|
use std::sync::atomic::{AtomicI64, Ordering};
|
|
use std::sync::Arc;
|
|
|
|
let total = Arc::new(AtomicI64::new(0));
|
|
let total2 = total.clone();
|
|
smarm::init(smarm::Config::exact(4)).run(move || {
|
|
let (tx, rx) = channel::<i64>();
|
|
let consumer = spawn(move || {
|
|
let mut sum = 0;
|
|
while let Ok(v) = rx.recv() {
|
|
sum += v;
|
|
}
|
|
OUT.with(|c| c.set(sum)); // not asserted cross-thread; see total
|
|
total2.fetch_add(sum, Ordering::Relaxed);
|
|
});
|
|
|
|
let mut handles = Vec::new();
|
|
for i in 0..32i64 {
|
|
let tx = tx.clone();
|
|
handles.push(spawn(move || {
|
|
// Short-lived target whose death fires the monitor below.
|
|
let t = spawn(move || {
|
|
tx.send(i).unwrap();
|
|
});
|
|
let m = smarm::monitor(t.pid());
|
|
t.join().unwrap();
|
|
// Down delivery exercises send-from-finalize.
|
|
let d = m.rx.recv().unwrap();
|
|
assert_eq!(d.reason, smarm::DownReason::Exit);
|
|
}));
|
|
}
|
|
drop(tx);
|
|
for h in handles {
|
|
h.join().unwrap();
|
|
}
|
|
consumer.join().unwrap();
|
|
});
|
|
assert_eq!(total.load(std::sync::atomic::Ordering::Relaxed), (0..32).sum::<i64>());
|
|
}
|