feat(channel): migrate Inner to RawMutex; two-class lock order (Leaf -> Channel)
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.
This commit is contained in:
@@ -108,3 +108,51 @@ fn recv_returns_err_when_all_senders_dropped() {
|
||||
|
||||
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>());
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user