feat: I/O and mutex support (v0.3)
Add epoll-based non-blocking I/O and kernel-like mutexes: - src/io.rs: Complete epoll backend with timeout & error handling - src/mutex.rs: Fair mutex with waiter queues & parking integration - Enhanced scheduler to support synchronous I/O blocking - Comprehensive test suites for I/O (epoll) and mutex behavior - Documentation: LOOM.md concurrency model & README
This commit is contained in:
324
tests/io_epoll.rs
Normal file
324
tests/io_epoll.rs
Normal file
@@ -0,0 +1,324 @@
|
||||
//! Tests for epoll-based fd readiness primitives: `wait_readable`,
|
||||
//! `wait_writable`, and the `read`/`write` sugar on top of them.
|
||||
//!
|
||||
//! Pipes are the convenient test target: cheap to create, easy to drive,
|
||||
//! and we already use `libc::pipe2` internally. Each pipe is one direction
|
||||
//! and respects `O_NONBLOCK` if we ask for it.
|
||||
|
||||
use smarm::{run, spawn, wait_readable, wait_writable, yield_now};
|
||||
use std::os::fd::RawFd;
|
||||
use std::sync::atomic::{AtomicU32, Ordering};
|
||||
use std::sync::Arc;
|
||||
use std::sync::Mutex as StdMutex;
|
||||
use std::time::Duration;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Pipe helper
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
struct Pipe {
|
||||
read: RawFd,
|
||||
write: RawFd,
|
||||
}
|
||||
|
||||
impl Pipe {
|
||||
fn new() -> Self {
|
||||
let mut fds: [libc::c_int; 2] = [0; 2];
|
||||
let r = unsafe { libc::pipe2(fds.as_mut_ptr(), libc::O_CLOEXEC | libc::O_NONBLOCK) };
|
||||
assert_eq!(r, 0, "pipe2 failed");
|
||||
Pipe {
|
||||
read: fds[0],
|
||||
write: fds[1],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for Pipe {
|
||||
fn drop(&mut self) {
|
||||
unsafe {
|
||||
libc::close(self.read);
|
||||
libc::close(self.write);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn raw_write(fd: RawFd, buf: &[u8]) -> isize {
|
||||
unsafe { libc::write(fd, buf.as_ptr() as *const _, buf.len()) }
|
||||
}
|
||||
|
||||
fn raw_read(fd: RawFd, buf: &mut [u8]) -> isize {
|
||||
unsafe { libc::read(fd, buf.as_mut_ptr() as *mut _, buf.len()) }
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// wait_readable parks until data arrives, then libc::read succeeds.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#[test]
|
||||
fn wait_readable_blocks_until_data_arrives_then_read_succeeds() {
|
||||
let captured: Arc<StdMutex<Vec<u8>>> = Arc::new(StdMutex::new(Vec::new()));
|
||||
let cap = captured.clone();
|
||||
|
||||
let p = Arc::new(Pipe::new());
|
||||
let p_reader = p.clone();
|
||||
let p_writer = p.clone();
|
||||
|
||||
run(move || {
|
||||
let reader = spawn(move || {
|
||||
// Initially the pipe is empty; this parks.
|
||||
wait_readable(p_reader.read).expect("wait_readable failed");
|
||||
// Now data should be readable.
|
||||
let mut buf = [0u8; 16];
|
||||
let n = raw_read(p_reader.read, &mut buf);
|
||||
assert!(n > 0, "read returned {}", n);
|
||||
cap.lock().unwrap().extend_from_slice(&buf[..n as usize]);
|
||||
});
|
||||
|
||||
let writer = spawn(move || {
|
||||
// Yield so the reader gets to park first.
|
||||
yield_now();
|
||||
yield_now();
|
||||
// Sleep a touch so the reader is definitely waiting in epoll.
|
||||
smarm::sleep(Duration::from_millis(5));
|
||||
let n = raw_write(p_writer.write, b"hello");
|
||||
assert_eq!(n, 5);
|
||||
});
|
||||
|
||||
reader.join().unwrap();
|
||||
writer.join().unwrap();
|
||||
});
|
||||
|
||||
assert_eq!(*captured.lock().unwrap(), b"hello");
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// The smarm::scheduler::read sugar — wait_readable + libc::read in one call.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#[test]
|
||||
fn read_sugar_returns_bytes_from_pipe() {
|
||||
let captured: Arc<StdMutex<Vec<u8>>> = Arc::new(StdMutex::new(Vec::new()));
|
||||
let cap = captured.clone();
|
||||
|
||||
let p = Arc::new(Pipe::new());
|
||||
let p_reader = p.clone();
|
||||
let p_writer = p.clone();
|
||||
|
||||
run(move || {
|
||||
let reader = spawn(move || {
|
||||
let mut buf = [0u8; 16];
|
||||
let n = smarm::scheduler::read(p_reader.read, &mut buf)
|
||||
.expect("smarm::scheduler::read failed");
|
||||
cap.lock().unwrap().extend_from_slice(&buf[..n]);
|
||||
});
|
||||
|
||||
let writer = spawn(move || {
|
||||
yield_now();
|
||||
smarm::sleep(Duration::from_millis(5));
|
||||
let _ = raw_write(p_writer.write, b"world");
|
||||
});
|
||||
|
||||
reader.join().unwrap();
|
||||
writer.join().unwrap();
|
||||
});
|
||||
|
||||
assert_eq!(*captured.lock().unwrap(), b"world");
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// wait_writable + write — though pipes are almost always writable; the
|
||||
// useful test here is that the call doesn't hang on a writable fd.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#[test]
|
||||
fn write_sugar_sends_bytes_to_pipe() {
|
||||
let counter = Arc::new(AtomicU32::new(0));
|
||||
let c = counter.clone();
|
||||
|
||||
let p = Arc::new(Pipe::new());
|
||||
let p_writer = p.clone();
|
||||
let p_reader = p.clone();
|
||||
|
||||
run(move || {
|
||||
let writer = spawn(move || {
|
||||
// Pipe is empty + has buffer space, so this returns immediately
|
||||
// after wait_writable wakes (which happens fast because the
|
||||
// kernel marks an empty pipe as immediately writable).
|
||||
let n = smarm::scheduler::write(p_writer.write, b"smarm")
|
||||
.expect("write failed");
|
||||
assert_eq!(n, 5);
|
||||
c.fetch_add(1, Ordering::SeqCst);
|
||||
});
|
||||
|
||||
let reader = spawn(move || {
|
||||
// Give the writer time.
|
||||
smarm::sleep(Duration::from_millis(10));
|
||||
let mut buf = [0u8; 16];
|
||||
let n = raw_read(p_reader.read, &mut buf);
|
||||
assert_eq!(n, 5);
|
||||
assert_eq!(&buf[..5], b"smarm");
|
||||
});
|
||||
|
||||
writer.join().unwrap();
|
||||
reader.join().unwrap();
|
||||
});
|
||||
|
||||
assert_eq!(counter.load(Ordering::SeqCst), 1);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// While an actor is parked on wait_readable, other actors keep running.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#[test]
|
||||
fn other_actors_run_while_one_is_parked_on_wait_readable() {
|
||||
let log: Arc<StdMutex<Vec<u8>>> = Arc::new(StdMutex::new(Vec::new()));
|
||||
let la = log.clone();
|
||||
let lb = log.clone();
|
||||
|
||||
let p = Arc::new(Pipe::new());
|
||||
let p_a = p.clone();
|
||||
let p_b = p.clone();
|
||||
|
||||
run(move || {
|
||||
let a = spawn(move || {
|
||||
la.lock().unwrap().push(b'A');
|
||||
wait_readable(p_a.read).unwrap();
|
||||
la.lock().unwrap().push(b'a');
|
||||
});
|
||||
|
||||
let b = spawn(move || {
|
||||
// A starts parking on the empty pipe; B should be free to do
|
||||
// its work in the meantime.
|
||||
for _ in 0..3 {
|
||||
yield_now();
|
||||
lb.lock().unwrap().push(b'B');
|
||||
}
|
||||
// Now wake A.
|
||||
let _ = raw_write(p_b.write, b"x");
|
||||
});
|
||||
|
||||
a.join().unwrap();
|
||||
b.join().unwrap();
|
||||
});
|
||||
|
||||
let v = log.lock().unwrap();
|
||||
// A goes first ('A'), then B makes progress (multiple 'B's) while A is
|
||||
// parked, then A wakes and finishes ('a').
|
||||
let pos_big_a = v.iter().position(|&c| c == b'A').unwrap();
|
||||
let pos_lit_a = v.iter().position(|&c| c == b'a').unwrap();
|
||||
let big_b_count = v.iter().filter(|&&c| c == b'B').count();
|
||||
assert_eq!(big_b_count, 3, "B should have made 3 steps: {:?}", *v);
|
||||
assert!(pos_big_a < pos_lit_a, "A pre-park before A post-park: {:?}", *v);
|
||||
// At least the last B step should be before A resumes.
|
||||
let last_big_b = v.iter().rposition(|&c| c == b'B').unwrap();
|
||||
assert!(last_big_b < pos_lit_a, "B should finish before A resumes: {:?}", *v);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Two-way pipe ping-pong via wait_readable.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#[test]
|
||||
fn ping_pong_between_two_pipes_completes() {
|
||||
// a_to_b: actor A writes, actor B reads.
|
||||
// b_to_a: actor B writes, actor A reads.
|
||||
let a_to_b = Arc::new(Pipe::new());
|
||||
let b_to_a = Arc::new(Pipe::new());
|
||||
|
||||
let counter = Arc::new(AtomicU32::new(0));
|
||||
let ca = counter.clone();
|
||||
let cb = counter.clone();
|
||||
|
||||
let a_to_b_a = a_to_b.clone();
|
||||
let a_to_b_b = a_to_b.clone();
|
||||
let b_to_a_a = b_to_a.clone();
|
||||
let b_to_a_b = b_to_a.clone();
|
||||
|
||||
run(move || {
|
||||
let a = spawn(move || {
|
||||
for _ in 0..5 {
|
||||
let _ = raw_write(a_to_b_a.write, b"x");
|
||||
wait_readable(b_to_a_a.read).unwrap();
|
||||
let mut buf = [0u8; 4];
|
||||
let _ = raw_read(b_to_a_a.read, &mut buf);
|
||||
ca.fetch_add(1, Ordering::SeqCst);
|
||||
}
|
||||
});
|
||||
|
||||
let b = spawn(move || {
|
||||
for _ in 0..5 {
|
||||
wait_readable(a_to_b_b.read).unwrap();
|
||||
let mut buf = [0u8; 4];
|
||||
let _ = raw_read(a_to_b_b.read, &mut buf);
|
||||
let _ = raw_write(b_to_a_b.write, b"y");
|
||||
cb.fetch_add(1, Ordering::SeqCst);
|
||||
}
|
||||
});
|
||||
|
||||
a.join().unwrap();
|
||||
b.join().unwrap();
|
||||
});
|
||||
|
||||
// Both sides did 5 rounds; counter is incremented by both, so total = 10.
|
||||
assert_eq!(counter.load(Ordering::SeqCst), 10);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Same fd reused across calls — DEL+ADD cycle works.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#[test]
|
||||
fn same_fd_can_be_waited_on_repeatedly() {
|
||||
let p = Arc::new(Pipe::new());
|
||||
let p_r = p.clone();
|
||||
let p_w = p.clone();
|
||||
let counter = Arc::new(AtomicU32::new(0));
|
||||
let c = counter.clone();
|
||||
|
||||
run(move || {
|
||||
let reader = spawn(move || {
|
||||
for _ in 0..4 {
|
||||
wait_readable(p_r.read).unwrap();
|
||||
let mut buf = [0u8; 4];
|
||||
let n = raw_read(p_r.read, &mut buf);
|
||||
assert!(n > 0);
|
||||
c.fetch_add(1, Ordering::SeqCst);
|
||||
}
|
||||
});
|
||||
|
||||
let writer = spawn(move || {
|
||||
for _ in 0..4 {
|
||||
yield_now();
|
||||
smarm::sleep(Duration::from_millis(2));
|
||||
let _ = raw_write(p_w.write, b"z");
|
||||
}
|
||||
});
|
||||
|
||||
reader.join().unwrap();
|
||||
writer.join().unwrap();
|
||||
});
|
||||
|
||||
assert_eq!(counter.load(Ordering::SeqCst), 4);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Sanity that wait_writable on an already-writable pipe returns promptly.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#[test]
|
||||
fn wait_writable_on_empty_pipe_returns_quickly() {
|
||||
let p = Arc::new(Pipe::new());
|
||||
let p_w = p.clone();
|
||||
|
||||
let start = std::time::Instant::now();
|
||||
run(move || {
|
||||
wait_writable(p_w.write).unwrap();
|
||||
});
|
||||
let elapsed = start.elapsed();
|
||||
assert!(
|
||||
elapsed < Duration::from_millis(200),
|
||||
"wait_writable should be fast on a writable fd, took {:?}",
|
||||
elapsed
|
||||
);
|
||||
}
|
||||
311
tests/mutex.rs
Normal file
311
tests/mutex.rs
Normal file
@@ -0,0 +1,311 @@
|
||||
//! `loom::Mutex<T>` tests. All run under the scheduler because `lock()`
|
||||
//! needs to be able to park.
|
||||
|
||||
use smarm::{run, spawn, yield_now, LockTimeout, Mutex};
|
||||
use std::sync::Arc;
|
||||
use std::sync::Mutex as StdMutex;
|
||||
use std::sync::atomic::{AtomicU32, Ordering};
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Uncontended fast path
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#[test]
|
||||
fn lock_free_mutex_succeeds() {
|
||||
let captured = Arc::new(AtomicU32::new(0));
|
||||
let c = captured.clone();
|
||||
run(move || {
|
||||
let m = Mutex::new(42u32);
|
||||
{
|
||||
let g = m.lock().unwrap();
|
||||
c.store(*g, Ordering::SeqCst);
|
||||
}
|
||||
// After drop we can lock again.
|
||||
let g2 = m.lock().unwrap();
|
||||
assert_eq!(*g2, 42);
|
||||
});
|
||||
assert_eq!(captured.load(Ordering::SeqCst), 42);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn try_lock_returns_some_when_free_none_when_held() {
|
||||
let success_flag = Arc::new(AtomicU32::new(0));
|
||||
let s = success_flag.clone();
|
||||
run(move || {
|
||||
let m = Mutex::new(0u32);
|
||||
let g = m.try_lock().expect("free");
|
||||
// Holding the guard; a second try_lock on the same actor should fail.
|
||||
assert!(m.try_lock().is_none());
|
||||
drop(g);
|
||||
// Now free again.
|
||||
let g2 = m.try_lock().expect("free again");
|
||||
drop(g2);
|
||||
s.store(1, Ordering::SeqCst);
|
||||
});
|
||||
assert_eq!(success_flag.load(Ordering::SeqCst), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn guard_mutates_value_visible_through_next_lock() {
|
||||
let final_value = Arc::new(AtomicU32::new(0));
|
||||
let f = final_value.clone();
|
||||
run(move || {
|
||||
let m = Mutex::new(0u32);
|
||||
{
|
||||
let mut g = m.lock().unwrap();
|
||||
*g = 7;
|
||||
}
|
||||
let g2 = m.lock().unwrap();
|
||||
f.store(*g2, Ordering::SeqCst);
|
||||
});
|
||||
assert_eq!(final_value.load(Ordering::SeqCst), 7);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Contention: a second actor parks until the first releases.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#[test]
|
||||
fn contended_lock_parks_until_holder_releases() {
|
||||
// Actor A locks, yields (still holding), then releases. Actor B tries
|
||||
// to lock in between — B should park, then succeed after A drops.
|
||||
let log: Arc<StdMutex<Vec<&'static str>>> = Arc::new(StdMutex::new(Vec::new()));
|
||||
let la = log.clone();
|
||||
let lb = log.clone();
|
||||
|
||||
run(move || {
|
||||
let m = Mutex::new(0u32);
|
||||
let m_a = m.clone();
|
||||
let m_b = m.clone();
|
||||
|
||||
let a = spawn(move || {
|
||||
let g = m_a.lock().unwrap();
|
||||
la.lock().unwrap().push("A_locked");
|
||||
// While holding, yield to let B run.
|
||||
yield_now();
|
||||
la.lock().unwrap().push("A_dropping");
|
||||
drop(g);
|
||||
la.lock().unwrap().push("A_dropped");
|
||||
});
|
||||
let b = spawn(move || {
|
||||
// Wait a moment to make sure A locks first.
|
||||
yield_now();
|
||||
lb.lock().unwrap().push("B_try");
|
||||
let _g = m_b.lock().unwrap();
|
||||
lb.lock().unwrap().push("B_locked");
|
||||
});
|
||||
a.join().unwrap();
|
||||
b.join().unwrap();
|
||||
});
|
||||
|
||||
let v = log.lock().unwrap();
|
||||
// A locks, B tries (parks), A drops, B gets the lock.
|
||||
let pos_a_locked = v.iter().position(|s| *s == "A_locked").unwrap();
|
||||
let pos_b_try = v.iter().position(|s| *s == "B_try").unwrap();
|
||||
let pos_a_dropped = v.iter().position(|s| *s == "A_dropped").unwrap();
|
||||
let pos_b_locked = v.iter().position(|s| *s == "B_locked").unwrap();
|
||||
|
||||
assert!(pos_a_locked < pos_b_try, "log: {:?}", *v);
|
||||
assert!(pos_b_try < pos_a_dropped, "B should attempt before A drops: {:?}", *v);
|
||||
assert!(pos_a_dropped < pos_b_locked, "B should lock only after A drops: {:?}", *v);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Timeout: B times out while A holds forever.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#[test]
|
||||
fn lock_timeout_returns_err_when_holder_never_releases() {
|
||||
let saw_err = Arc::new(std::sync::atomic::AtomicBool::new(false));
|
||||
let s = saw_err.clone();
|
||||
|
||||
run(move || {
|
||||
let m: Mutex<u32> = Mutex::new(0);
|
||||
let m_a = m.clone();
|
||||
let m_b = m.clone();
|
||||
|
||||
let a = spawn(move || {
|
||||
// Hold the lock for 100ms, blocking B's attempt with a 20ms timeout.
|
||||
let _g = m_a.lock().unwrap();
|
||||
smarm::sleep(Duration::from_millis(100));
|
||||
// _g drops here.
|
||||
});
|
||||
let b = spawn(move || {
|
||||
// Let A acquire first.
|
||||
yield_now();
|
||||
let t0 = Instant::now();
|
||||
let res = m_b.lock_timeout(Duration::from_millis(20));
|
||||
let elapsed = t0.elapsed();
|
||||
assert!(matches!(res, Err(LockTimeout)), "got {:?}", res);
|
||||
// Sanity: actually waited approximately the timeout.
|
||||
assert!(
|
||||
elapsed >= Duration::from_millis(15),
|
||||
"timed out too fast: {:?}",
|
||||
elapsed
|
||||
);
|
||||
assert!(
|
||||
elapsed < Duration::from_millis(80),
|
||||
"timed out far too slow: {:?}",
|
||||
elapsed
|
||||
);
|
||||
s.store(true, Ordering::SeqCst);
|
||||
});
|
||||
a.join().unwrap();
|
||||
b.join().unwrap();
|
||||
});
|
||||
|
||||
assert!(saw_err.load(Ordering::SeqCst));
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// FIFO fairness: when many actors queue, they get the lock in arrival order.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#[test]
|
||||
fn waiters_are_granted_the_lock_in_fifo_order() {
|
||||
let order: Arc<StdMutex<Vec<u32>>> = Arc::new(StdMutex::new(Vec::new()));
|
||||
|
||||
run({
|
||||
let order = order.clone();
|
||||
move || {
|
||||
let m: Mutex<()> = Mutex::new(());
|
||||
|
||||
// Holder: takes the lock, yields to let others queue up, then
|
||||
// releases. Each waiter records its arrival order on acquisition.
|
||||
let m_holder = m.clone();
|
||||
let holder = spawn(move || {
|
||||
let g = m_holder.lock().unwrap();
|
||||
// Let waiters pile up.
|
||||
for _ in 0..5 {
|
||||
yield_now();
|
||||
}
|
||||
drop(g);
|
||||
});
|
||||
|
||||
// Spawn 4 waiters in order 1, 2, 3, 4. Each yields once before
|
||||
// calling lock(), so we know the holder ran first.
|
||||
let mut handles = vec![holder];
|
||||
for id in 1u32..=4 {
|
||||
let m_w = m.clone();
|
||||
let o = order.clone();
|
||||
handles.push(spawn(move || {
|
||||
// Stagger the lock attempts so they arrive in order.
|
||||
for _ in 0..id {
|
||||
yield_now();
|
||||
}
|
||||
let _g = m_w.lock().unwrap();
|
||||
o.lock().unwrap().push(id);
|
||||
}));
|
||||
}
|
||||
for h in handles {
|
||||
h.join().unwrap();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let v = order.lock().unwrap().clone();
|
||||
assert_eq!(v, vec![1, 2, 3, 4], "waiters should acquire in arrival order");
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Grant-vs-timeout race: holder drops just before timer would fire — waiter
|
||||
// should get the lock, not LockTimeout.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#[test]
|
||||
fn grant_wins_when_holder_releases_before_timeout() {
|
||||
let got_lock = Arc::new(std::sync::atomic::AtomicBool::new(false));
|
||||
let g = got_lock.clone();
|
||||
|
||||
run(move || {
|
||||
let m: Mutex<u32> = Mutex::new(0);
|
||||
let m_a = m.clone();
|
||||
let m_b = m.clone();
|
||||
|
||||
let a = spawn(move || {
|
||||
let _g = m_a.lock().unwrap();
|
||||
// Hold for 10ms, well under B's 100ms timeout.
|
||||
smarm::sleep(Duration::from_millis(10));
|
||||
});
|
||||
let b = spawn(move || {
|
||||
yield_now();
|
||||
let res = m_b.lock_timeout(Duration::from_millis(100));
|
||||
if res.is_ok() {
|
||||
g.store(true, Ordering::SeqCst);
|
||||
}
|
||||
});
|
||||
a.join().unwrap();
|
||||
b.join().unwrap();
|
||||
});
|
||||
|
||||
assert!(got_lock.load(Ordering::SeqCst));
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Panic in critical section: next waiter still gets the lock (no poisoning).
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#[test]
|
||||
fn next_waiter_gets_lock_after_holder_panics() {
|
||||
let next_got_it = Arc::new(std::sync::atomic::AtomicBool::new(false));
|
||||
let n = next_got_it.clone();
|
||||
|
||||
run(move || {
|
||||
let m: Mutex<u32> = Mutex::new(7);
|
||||
let m_a = m.clone();
|
||||
let m_b = m.clone();
|
||||
|
||||
let a = spawn(move || {
|
||||
let _g = m_a.lock().unwrap();
|
||||
yield_now();
|
||||
panic!("holder dies mid-critical-section");
|
||||
});
|
||||
let b = spawn(move || {
|
||||
yield_now();
|
||||
// A is dead but its guard's Drop ran during unwind. We get the lock.
|
||||
let g = m_b.lock_timeout(Duration::from_millis(100)).unwrap();
|
||||
assert_eq!(*g, 7);
|
||||
n.store(true, Ordering::SeqCst);
|
||||
});
|
||||
let _ = a.join(); // panic — expected
|
||||
b.join().unwrap();
|
||||
});
|
||||
|
||||
assert!(next_got_it.load(Ordering::SeqCst));
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Multiple short critical sections under contention all complete (no lost
|
||||
// wakeups, no deadlock). Counts up to N from M actors.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#[test]
|
||||
fn many_actors_increment_shared_counter_via_mutex() {
|
||||
const ACTORS: u32 = 8;
|
||||
const PER_ACTOR: u32 = 50;
|
||||
|
||||
let final_value = Arc::new(AtomicU32::new(0));
|
||||
let fv = final_value.clone();
|
||||
|
||||
run(move || {
|
||||
let m: Mutex<u32> = Mutex::new(0);
|
||||
let mut handles = Vec::new();
|
||||
for _ in 0..ACTORS {
|
||||
let m_i = m.clone();
|
||||
handles.push(spawn(move || {
|
||||
for _ in 0..PER_ACTOR {
|
||||
let mut g = m_i.lock().unwrap();
|
||||
*g += 1;
|
||||
}
|
||||
}));
|
||||
}
|
||||
for h in handles {
|
||||
h.join().unwrap();
|
||||
}
|
||||
let g = m.lock().unwrap();
|
||||
fv.store(*g, Ordering::SeqCst);
|
||||
});
|
||||
|
||||
assert_eq!(final_value.load(Ordering::SeqCst), ACTORS * PER_ACTOR);
|
||||
}
|
||||
@@ -114,3 +114,96 @@ fn many_concurrent_sleepers_all_wake() {
|
||||
});
|
||||
assert_eq!(counter.load(std::sync::atomic::Ordering::SeqCst), 20);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Direct tests on the Timers data structure. No scheduler involved — these
|
||||
// cover the new Reason machinery without needing a Mutex implementation.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
use smarm::pid::Pid;
|
||||
use smarm::timer::{Reason, TimerTarget, Timers};
|
||||
use std::cell::RefCell;
|
||||
use std::rc::Rc;
|
||||
|
||||
struct RecordingTarget {
|
||||
calls: RefCell<Vec<(Pid, u64)>>,
|
||||
}
|
||||
impl TimerTarget for RecordingTarget {
|
||||
fn on_timeout(&self, pid: Pid, seq: u64) {
|
||||
self.calls.borrow_mut().push((pid, seq));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn timers_pop_due_returns_entries_in_deadline_order() {
|
||||
let mut t = Timers::new();
|
||||
let now = Instant::now();
|
||||
// Insert out of order; pop_due should hand them back sorted by deadline.
|
||||
t.insert_sleep(now + Duration::from_millis(30), Pid::new(0, 0));
|
||||
t.insert_sleep(now + Duration::from_millis(10), Pid::new(1, 0));
|
||||
t.insert_sleep(now + Duration::from_millis(20), Pid::new(2, 0));
|
||||
|
||||
// Advance past all of them.
|
||||
let due = t.pop_due(now + Duration::from_millis(50));
|
||||
let pids: Vec<u32> = due.iter().map(|e| e.pid.index()).collect();
|
||||
assert_eq!(pids, vec![1, 2, 0]);
|
||||
assert!(t.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn timers_only_pop_entries_whose_deadline_has_passed() {
|
||||
let mut t = Timers::new();
|
||||
let now = Instant::now();
|
||||
t.insert_sleep(now + Duration::from_millis(5), Pid::new(0, 0));
|
||||
t.insert_sleep(now + Duration::from_millis(100), Pid::new(1, 0));
|
||||
|
||||
let due = t.pop_due(now + Duration::from_millis(20));
|
||||
assert_eq!(due.len(), 1);
|
||||
assert_eq!(due[0].pid.index(), 0);
|
||||
assert!(!t.is_empty());
|
||||
// The unpopped entry's deadline is still visible.
|
||||
assert!(t.peek_deadline().is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn timers_mix_sleep_and_wait_timeout_reasons() {
|
||||
let mut t = Timers::new();
|
||||
let target = Rc::new(RecordingTarget { calls: RefCell::new(Vec::new()) });
|
||||
let now = Instant::now();
|
||||
|
||||
t.insert_sleep(now + Duration::from_millis(5), Pid::new(0, 0));
|
||||
t.insert(
|
||||
now + Duration::from_millis(10),
|
||||
Pid::new(1, 0),
|
||||
Reason::WaitTimeout { target: target.clone(), wait_seq: 42 },
|
||||
);
|
||||
|
||||
let due = t.pop_due(now + Duration::from_millis(20));
|
||||
assert_eq!(due.len(), 2);
|
||||
|
||||
// Order: Sleep (5ms) first, WaitTimeout (10ms) second.
|
||||
match &due[0].reason {
|
||||
Reason::Sleep => {}
|
||||
_ => panic!("first entry should be a Sleep"),
|
||||
}
|
||||
match &due[1].reason {
|
||||
Reason::WaitTimeout { wait_seq, .. } => assert_eq!(*wait_seq, 42),
|
||||
_ => panic!("second entry should be a WaitTimeout"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn same_deadline_entries_pop_in_insertion_order() {
|
||||
// The `seq` tiebreaker means inserting two entries with the same
|
||||
// deadline preserves the order they were inserted.
|
||||
let mut t = Timers::new();
|
||||
let now = Instant::now();
|
||||
let d = now + Duration::from_millis(10);
|
||||
t.insert_sleep(d, Pid::new(0, 0));
|
||||
t.insert_sleep(d, Pid::new(1, 0));
|
||||
t.insert_sleep(d, Pid::new(2, 0));
|
||||
|
||||
let due = t.pop_due(now + Duration::from_millis(20));
|
||||
let pids: Vec<u32> = due.iter().map(|e| e.pid.index()).collect();
|
||||
assert_eq!(pids, vec![0, 1, 2]);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user