use super::*;
#[test]
fn final_external_handle_drop_drains_and_releases_workers() {
const COMPUTE_JOBS: usize = 32;
const BLOCKING_VALUE: usize = 1_024;
let scheduler = ThreadScheduler::new(1, "test-final-scheduler-drop").unwrap();
let inner = Arc::downgrade(&scheduler.inner);
let shutdown_started = Arc::new(Barrier::new(2));
assert!(
scheduler
.inner
.shutdown_started_barrier
.set(Arc::clone(&shutdown_started))
.is_ok(),
"test installs one shutdown rendezvous"
);
let owner_drops = Arc::new(AtomicUsize::new(0));
scheduler.retain_lifetime_owner(DropProbe {
drops: Arc::clone(&owner_drops),
});
let surviving_handle = scheduler.clone();
drop(scheduler);
let (usable_sender, usable_receiver) = mpsc::sync_channel(1);
surviving_handle
.schedule::<SyncTask, _>(Priority::Normal, None, move |_| {
usable_sender.send(7).unwrap();
})
.unwrap();
assert_eq!(
usable_receiver.recv_timeout(TEST_EVENT_DEADLINE).unwrap(),
7,
"dropping a non-final handle must not stop the shared scheduler"
);
let (gate_started_sender, gate_started_receiver) = mpsc::sync_channel(2);
let (compute_release_sender, compute_release_receiver) = mpsc::sync_channel(0);
surviving_handle
.schedule::<SyncTask, _>(Priority::Critical, Some(0), move |_| {
gate_started_sender.send(()).unwrap();
compute_release_receiver.recv().unwrap();
})
.unwrap();
let (blocking_started_sender, blocking_started_receiver) = mpsc::sync_channel(1);
let (blocking_release_sender, blocking_release_receiver) = mpsc::sync_channel(0);
surviving_handle
.schedule::<BlockingTask, _>(Priority::Critical, Some(0), move |_| {
blocking_started_sender.send(()).unwrap();
blocking_release_receiver.recv().unwrap();
})
.unwrap();
gate_started_receiver
.recv_timeout(TEST_EVENT_DEADLINE)
.expect("compute gate must occupy the sole worker");
blocking_started_receiver
.recv_timeout(TEST_EVENT_DEADLINE)
.expect("blocking gate must occupy the sole blocking worker");
let completed = Arc::new(AtomicUsize::new(0));
let capture_drops = Arc::new(AtomicUsize::new(0));
for value in 1..=COMPUTE_JOBS {
let completed = Arc::clone(&completed);
let capture = DropProbe {
drops: Arc::clone(&capture_drops),
};
surviving_handle
.schedule::<SyncTask, _>(Priority::Normal, None, move |_| {
completed.fetch_add(value, Ordering::Relaxed);
drop(capture);
})
.unwrap();
}
let blocking_completed = Arc::clone(&completed);
let blocking_capture = DropProbe {
drops: Arc::clone(&capture_drops),
};
surviving_handle
.schedule::<BlockingTask, _>(Priority::Normal, None, move |_| {
blocking_completed.fetch_add(BLOCKING_VALUE, Ordering::Relaxed);
drop(blocking_capture);
})
.unwrap();
let (drop_sender, drop_receiver) = mpsc::sync_channel(1);
let drop_thread = std::thread::spawn(move || {
drop(surviving_handle);
drop_sender.send(()).unwrap();
});
shutdown_started.wait();
let live_inner = inner
.upgrade()
.expect("shutdown rendezvous retains scheduler state");
assert_eq!(
live_inner.pending_tasks.load(Ordering::Acquire),
COMPUTE_JOBS,
"all compute values must still be queued when final drop publishes shutdown"
);
assert_eq!(
live_inner.blocking_pending_tasks.load(Ordering::Acquire),
1,
"the blocking value must still be queued when final drop publishes shutdown"
);
drop(live_inner);
compute_release_sender.send(()).unwrap();
blocking_release_sender.send(()).unwrap();
drop_receiver
.recv_timeout(TEST_EVENT_DEADLINE)
.expect("final scheduler drop must synchronously release its workers");
drop_thread.join().unwrap();
assert_eq!(
completed.load(Ordering::Relaxed),
COMPUTE_JOBS * (COMPUTE_JOBS + 1) / 2 + BLOCKING_VALUE
);
assert_eq!(
capture_drops.load(Ordering::Relaxed),
COMPUTE_JOBS + 1,
"every admitted capture must be released exactly once"
);
assert_eq!(
owner_drops.load(Ordering::Relaxed),
1,
"scheduler-owned facade state must be released exactly once"
);
assert!(
inner.upgrade().is_none(),
"the final external drop must join workers and release scheduler state"
);
}
#[test]
fn concurrent_compute_and_blocking_shutdown_cannot_cross_join() {
let scheduler = ThreadScheduler::new(1, "test-concurrent-shutdown").unwrap();
let callers_ready = Arc::new(Barrier::new(3));
let (completed_sender, completed_receiver) = mpsc::sync_channel(2);
let compute_scheduler = scheduler.clone();
let compute_ready = Arc::clone(&callers_ready);
let compute_completed = completed_sender.clone();
scheduler
.schedule::<SyncTask, _>(Priority::Normal, Some(0), move |_| {
compute_ready.wait();
compute_scheduler.shutdown();
compute_completed.send(1).unwrap();
})
.unwrap();
let blocking_scheduler = scheduler.clone();
let blocking_ready = Arc::clone(&callers_ready);
scheduler
.schedule::<BlockingTask, _>(Priority::Normal, Some(0), move |_| {
blocking_ready.wait();
blocking_scheduler.shutdown();
completed_sender.send(2).unwrap();
})
.unwrap();
callers_ready.wait();
let mut completions = [
completed_receiver
.recv_timeout(TEST_EVENT_DEADLINE)
.expect("one shutdown caller must complete"),
completed_receiver
.recv_timeout(TEST_EVENT_DEADLINE)
.expect("both shutdown callers must complete"),
];
completions.sort_unstable();
assert_eq!(completions, [1, 2]);
scheduler.shutdown();
}
#[test]
fn final_compute_owner_returns_before_blocking_dependency() {
let scheduler = ThreadScheduler::new(1, "test-worker-final-owner").unwrap();
let inner = Arc::downgrade(&scheduler.inner);
let released = Arc::new((Mutex::new(false), Condvar::new()));
scheduler.retain_lifetime_owner(DropSignal {
state: Arc::clone(&released),
});
let final_handle = scheduler.clone();
let (blocking_started_sender, blocking_started_receiver) = mpsc::sync_channel(0);
let (blocking_release_sender, blocking_release_receiver) = mpsc::sync_channel(0);
let (blocking_completed_sender, blocking_completed_receiver) = mpsc::sync_channel(1);
scheduler
.schedule::<BlockingTask, _>(Priority::Normal, Some(0), move |_| {
blocking_started_sender.send(()).unwrap();
blocking_release_receiver.recv().unwrap();
blocking_completed_sender.send(()).unwrap();
})
.unwrap();
blocking_started_receiver
.recv_timeout(TEST_EVENT_DEADLINE)
.expect("blocking dependency must start before final-owner drop");
let (compute_started_sender, compute_started_receiver) = mpsc::sync_channel(0);
let (compute_release_sender, compute_release_receiver) = mpsc::sync_channel(0);
let (compute_completed_sender, compute_completed_receiver) = mpsc::sync_channel(1);
scheduler
.schedule::<SyncTask, _>(Priority::Normal, Some(0), move |_| {
compute_started_sender.send(()).unwrap();
compute_release_receiver.recv().unwrap();
drop(final_handle);
blocking_release_sender.send(()).unwrap();
compute_completed_sender.send(()).unwrap();
})
.unwrap();
compute_started_receiver
.recv_timeout(TEST_EVENT_DEADLINE)
.expect("compute final owner must start before release");
drop(scheduler);
compute_release_sender
.send(())
.expect("test controller releases the compute final owner");
compute_completed_receiver
.recv_timeout(TEST_EVENT_DEADLINE)
.expect("worker-owned shutdown must return before its peer dependency");
blocking_completed_receiver
.recv_timeout(TEST_EVENT_DEADLINE)
.expect("accepted blocking dependency must drain exactly once");
let (lock, signal) = &*released;
let dropped = lock.lock().unwrap_or_else(|poisoned| poisoned.into_inner());
let (dropped, timeout) = signal
.wait_timeout_while(dropped, TEST_EVENT_DEADLINE, |dropped| !*dropped)
.unwrap_or_else(|poisoned| poisoned.into_inner());
assert!(
!timeout.timed_out() && *dropped,
"worker-owned final drop must release scheduler state"
);
assert!(
inner.upgrade().is_none(),
"worker-owned final drop must release every scheduler Arc"
);
}
#[test]
fn scheduler_join_waits_for_queued_and_active_work() {
let scheduler = ThreadScheduler::new(2, "test-join").unwrap();
let completed = Arc::new(AtomicUsize::new(0));
let gate = Arc::new(std::sync::Mutex::new(()));
let held = gate.lock().expect("fresh test gate is never poisoned");
for _ in 0..8 {
let completed = Arc::clone(&completed);
let gate = Arc::clone(&gate);
scheduler
.schedule::<SyncTask, _>(Priority::Normal, None, move |_| {
drop(gate.lock().expect("test gate is never poisoned"));
completed.fetch_add(1, Ordering::AcqRel);
})
.unwrap();
}
assert!(scheduler.has_work());
drop(held);
scheduler.join().unwrap();
let metrics = scheduler.metrics();
scheduler.shutdown();
assert_eq!(completed.load(Ordering::Acquire), 8);
assert_eq!(metrics.pending_tasks, 0);
assert_eq!(metrics.active_workers, 0);
assert_eq!(metrics.completed_tasks, 8);
assert!(!scheduler.has_work());
}
#[test]
fn scheduler_join_waits_for_work_submitted_while_active() {
let scheduler = ThreadScheduler::new(2, "test-join-transitive").unwrap();
let completed = Arc::new(AtomicUsize::new(0));
let (started_sender, started_receiver) = mpsc::channel();
let (scheduled_sender, scheduled_receiver) = mpsc::channel();
{
let completed = Arc::clone(&completed);
scheduler
.schedule::<SyncTask, _>(Priority::Normal, None, move |_| {
started_sender.send(()).unwrap();
scheduled_receiver.recv().unwrap();
completed.fetch_add(1, Ordering::AcqRel);
})
.unwrap();
}
started_receiver.recv().unwrap();
std::thread::scope(|scope| {
let completed = Arc::clone(&completed);
let scheduler_ref = &scheduler;
scope.spawn(move || {
scheduler_ref
.schedule::<SyncTask, _>(Priority::Normal, None, move |_| {
completed.fetch_add(1, Ordering::AcqRel);
})
.unwrap();
scheduled_sender.send(()).unwrap();
});
scheduler.join().unwrap();
});
let metrics = scheduler.metrics();
scheduler.shutdown();
assert_eq!(completed.load(Ordering::Acquire), 2);
assert_eq!(metrics.pending_tasks, 0);
assert_eq!(metrics.active_workers, 0);
assert_eq!(metrics.completed_tasks, 2);
}