use std::{
sync::{
Arc,
atomic::{AtomicUsize, Ordering},
mpsc,
},
time::Duration,
};
use moirai_core::{
Priority,
error::ExecutorError,
executor::{ExecutorConfig, config::DEFAULT_LOCAL_QUEUE_INITIAL_CAPACITY},
};
use super::fixtures::{PendingTask, pending_async_state_with_first_poll_signal};
use crate::schedule::{SyncTask, ThreadScheduler};
const TEST_EVENT_DEADLINE: Duration = Duration::from_secs(5);
struct GateRelease(Option<mpsc::Sender<()>>);
impl GateRelease {
fn release(&mut self) {
self.0
.take()
.expect("gate release is sent once")
.send(())
.expect("gated worker remains alive");
}
}
impl Drop for GateRelease {
fn drop(&mut self) {
if let Some(sender) = self.0.take() {
match sender.send(()) {
Ok(()) | Err(_) => {}
}
}
}
}
#[test]
fn woken_task_completes_while_worker_injector_is_full() {
let scheduler = ThreadScheduler::<8>::from_executor_config(&ExecutorConfig {
worker_threads: 1,
max_global_queue_size: 8,
local_queue_initial_capacity: DEFAULT_LOCAL_QUEUE_INITIAL_CAPACITY,
thread_name_prefix: "wake-full-injector".into(),
..ExecutorConfig::default()
})
.expect("scheduler");
let (
PendingTask {
state,
handle,
waker,
polls,
},
first_poll_receiver,
) = pending_async_state_with_first_poll_signal(scheduler.clone(), 1789);
Arc::clone(&state).schedule().expect("first poll admits");
first_poll_receiver
.recv_timeout(TEST_EVENT_DEADLINE)
.expect("first poll must publish its waker before the event deadline");
let (entered_tx, entered_rx) = mpsc::channel::<()>();
let (release_tx, release_rx) = mpsc::channel::<()>();
let mut gate_release = GateRelease(Some(release_tx));
scheduler
.schedule::<SyncTask, _>(Priority::Normal, None, move |_worker| {
entered_tx.send(()).expect("test observer alive");
release_rx.recv().expect("release signal");
})
.expect("gate job admits");
entered_rx
.recv_timeout(TEST_EVENT_DEADLINE)
.expect("worker must enter the gate before the event deadline");
let waker = waker
.lock()
.unwrap()
.take()
.expect("first poll published its waker");
assert_eq!(polls.load(Ordering::SeqCst), 1);
let wake_phase = Arc::new(AtomicUsize::new(0));
let wake_phase_thread = Arc::clone(&wake_phase);
let (wake_ready_tx, wake_ready_rx) = mpsc::sync_channel(1);
let (wake_start_tx, wake_start_rx) = mpsc::sync_channel(0);
let (wake_done_tx, wake_done_rx) = mpsc::sync_channel(1);
let waking = std::thread::spawn(move || {
wake_phase_thread.store(1, Ordering::SeqCst);
wake_ready_tx.send(()).expect("wake-ready observer alive");
wake_start_rx.recv().expect("wake-start observer alive");
wake_phase_thread.store(2, Ordering::SeqCst);
waker.wake();
wake_phase_thread.store(3, Ordering::SeqCst);
wake_done_tx.send(()).expect("wake observer alive");
});
wake_ready_rx
.recv_timeout(TEST_EVENT_DEADLINE)
.expect("waking thread must park before the event deadline");
let filler_runs = Arc::new(AtomicUsize::new(0));
let mut saw_rejection = false;
for _ in 0..4096 {
let filler_runs = Arc::clone(&filler_runs);
let admitted = scheduler.schedule::<SyncTask, _>(Priority::Normal, None, move |_w| {
filler_runs.fetch_add(1, Ordering::SeqCst);
});
if let Err(rejection) = admitted {
assert!(matches!(rejection, ExecutorError::ResourceExhausted(_)));
saw_rejection = true;
break;
}
}
assert!(
saw_rejection,
"an 8-slot injector must fill within 4096 pushes"
);
wake_start_tx
.send(())
.expect("parked waking thread remains alive");
wake_done_rx
.recv_timeout(TEST_EVENT_DEADLINE)
.unwrap_or_else(|error| {
panic!(
"saturated wake must complete before the event deadline: {error}; phase={}, state={}, polls={}, finished={}, pending={}",
wake_phase.load(Ordering::SeqCst),
state.state.load(Ordering::SeqCst),
polls.load(Ordering::SeqCst),
handle.is_finished(),
scheduler.pending_tasks()
)
});
waking.join().expect("waking thread");
let poll_count_before_release = polls.load(Ordering::SeqCst);
let finished_before_release = handle.is_finished();
gate_release.release();
scheduler.shutdown();
assert_eq!(poll_count_before_release, 2);
assert!(
finished_before_release,
"the wake must poll inline before the gated worker is released"
);
assert_eq!(handle.join(), Some(Ok(1789)));
}