#[cfg(not(feature = "serial"))]
mod default_runtime {
use futures::future::BoxFuture;
use pi_async_rt::rt::{
multi_thread::{MultiTaskRuntime, MultiTaskRuntimeBuilder, StealableTaskPool},
AsyncRuntime, AsyncTask, AsyncTaskPool,
};
use std::{
future::Future,
pin::Pin,
sync::{
atomic::{AtomicUsize, Ordering},
mpsc, Arc, Barrier, Mutex, Weak,
},
task::{Context, Poll, Waker},
thread,
time::{Duration, Instant},
};
const WORKER_SIZE: usize = 4;
const TASK_COUNT: usize = 4;
const CONCURRENT_WAKERS: usize = 8;
const WAKES_PER_THREAD: usize = 8;
const DUPLICATE_ENTRIES: usize = 64;
const START_TIMEOUT: Duration = Duration::from_secs(5);
const RELEASE_TIMEOUT: Duration = Duration::from_secs(2);
static ASYNC_TASK_SCHEDULING_TEST_LOCK: parking_lot::Mutex<()> =
parking_lot::Mutex::new(());
type RuntimeTask = AsyncTask<StealableTaskPool<()>, ()>;
struct WakeThenReady {
polls: Arc<AtomicUsize>,
}
impl Future for WakeThenReady {
type Output = ();
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
self.polls.fetch_add(1, Ordering::AcqRel);
cx.waker().wake_by_ref();
Poll::Ready(())
}
}
struct WakeManyThenPending {
polls: Arc<AtomicUsize>,
yielded: bool,
}
impl Future for WakeManyThenPending {
type Output = ();
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
self.polls.fetch_add(1, Ordering::AcqRel);
if self.yielded {
Poll::Ready(())
} else {
self.yielded = true;
for _ in 0..DUPLICATE_ENTRIES {
cx.waker().wake_by_ref();
}
Poll::Pending
}
}
}
struct CaptureIdleWaker {
polls: Arc<AtomicUsize>,
waker: Arc<Mutex<Option<Waker>>>,
armed: Option<mpsc::Sender<()>>,
}
impl Future for CaptureIdleWaker {
type Output = ();
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
let poll = self.polls.fetch_add(1, Ordering::AcqRel);
if poll == 0 {
*self.waker.lock().unwrap() = Some(cx.waker().clone());
self.armed.take().unwrap().send(()).unwrap();
Poll::Pending
} else {
Poll::Ready(())
}
}
}
#[derive(Clone, Copy)]
enum RunningResult {
PendingThenReady,
Ready,
}
struct BlockedRunningFuture {
polls: Arc<AtomicUsize>,
waker: Arc<Mutex<Option<Waker>>>,
entered: Option<mpsc::Sender<()>>,
release: Option<mpsc::Receiver<()>>,
dropped: Option<mpsc::Sender<()>>,
result: RunningResult,
yielded: bool,
}
impl Future for BlockedRunningFuture {
type Output = ();
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
self.polls.fetch_add(1, Ordering::AcqRel);
if self.yielded {
return Poll::Ready(());
}
*self.waker.lock().unwrap() = Some(cx.waker().clone());
self.entered.take().unwrap().send(()).unwrap();
self.release
.take()
.unwrap()
.recv_timeout(START_TIMEOUT)
.unwrap();
match self.result {
RunningResult::PendingThenReady => {
self.yielded = true;
Poll::Pending
},
RunningResult::Ready => Poll::Ready(()),
}
}
}
impl Drop for BlockedRunningFuture {
fn drop(&mut self) {
if let Some(dropped) = self.dropped.take() {
let _ = dropped.send(());
}
}
}
struct SaveWakerThenReady {
polls: Arc<AtomicUsize>,
waker: Arc<Mutex<Option<Waker>>>,
dropped: Option<mpsc::Sender<()>>,
}
impl Future for SaveWakerThenReady {
type Output = ();
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
self.polls.fetch_add(1, Ordering::AcqRel);
*self.waker.lock().unwrap() = Some(cx.waker().clone());
Poll::Ready(())
}
}
impl Drop for SaveWakerThenReady {
fn drop(&mut self) {
if let Some(dropped) = self.dropped.take() {
let _ = dropped.send(());
}
}
}
struct SlowReady {
polls: Arc<AtomicUsize>,
active: Arc<AtomicUsize>,
maximum_active: Arc<AtomicUsize>,
}
impl Future for SlowReady {
type Output = ();
fn poll(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<Self::Output> {
self.polls.fetch_add(1, Ordering::AcqRel);
let active = self.active.fetch_add(1, Ordering::AcqRel) + 1;
self.maximum_active.fetch_max(active, Ordering::AcqRel);
thread::sleep(Duration::from_millis(20));
self.active.fetch_sub(1, Ordering::AcqRel);
Poll::Ready(())
}
}
fn new_runtime() -> MultiTaskRuntime<()> {
let pool = StealableTaskPool::with(WORKER_SIZE, 1024, [1, 1], 10);
MultiTaskRuntimeBuilder::new(pool)
.thread_prefix("Async-Task-Scheduling-Redline")
.thread_stack_size(2 * 1024 * 1024)
.init_worker_size(WORKER_SIZE)
.set_worker_limit(WORKER_SIZE, WORKER_SIZE)
.set_timeout(10)
.build()
}
fn submit(
runtime: &MultiTaskRuntime<()>,
future: BoxFuture<'static, ()>,
) -> Weak<RuntimeTask> {
let task = Arc::new(AsyncTask::new(
runtime.alloc::<()>(),
runtime.shared_pool(),
5,
Some(future),
));
let task_ref = Arc::downgrade(&task);
runtime.shared_pool().push(task).unwrap();
task_ref
}
fn wait_for_poll_count(polls: &AtomicUsize, expected: usize) -> bool {
let deadline = Instant::now() + START_TIMEOUT;
while Instant::now() < deadline {
if polls.load(Ordering::Acquire) == expected {
return true;
}
thread::sleep(Duration::from_millis(1));
}
false
}
fn retained_count(tasks: &[Weak<AsyncTask<StealableTaskPool<()>, ()>>]) -> usize {
tasks.iter().filter(|task| task.upgrade().is_some()).count()
}
fn wait_for_release(
tasks: &[Weak<AsyncTask<StealableTaskPool<()>, ()>>],
) -> usize {
let deadline = Instant::now() + RELEASE_TIMEOUT;
loop {
let retained = retained_count(tasks);
if retained == 0 || Instant::now() >= deadline {
return retained;
}
thread::sleep(Duration::from_millis(1));
}
}
fn wake_concurrently(waker: &Waker) {
let barrier = Arc::new(Barrier::new(CONCURRENT_WAKERS + 1));
let mut threads = Vec::with_capacity(CONCURRENT_WAKERS);
for _ in 0..CONCURRENT_WAKERS {
let barrier = barrier.clone();
let waker = waker.clone();
threads.push(thread::spawn(move || {
barrier.wait();
for _ in 0..WAKES_PER_THREAD {
waker.wake_by_ref();
}
}));
}
barrier.wait();
for thread in threads {
thread.join().unwrap();
}
}
fn assert_released(task: Weak<RuntimeTask>, message: &str) {
let retained = wait_for_release(&[task]);
assert_eq!(retained, 0, "{}", message);
}
fn run_blocked_running_case(runtime: &MultiTaskRuntime<()>, result: RunningResult) {
let polls = Arc::new(AtomicUsize::new(0));
let waker = Arc::new(Mutex::new(None));
let (entered_tx, entered_rx) = mpsc::channel();
let (release_tx, release_rx) = mpsc::channel();
let (dropped_tx, dropped_rx) = mpsc::channel();
let task = submit(
runtime,
Box::pin(BlockedRunningFuture {
polls: polls.clone(),
waker: waker.clone(),
entered: Some(entered_tx),
release: Some(release_rx),
dropped: Some(dropped_tx),
result,
yielded: false,
}),
);
entered_rx.recv_timeout(START_TIMEOUT).unwrap();
let waker = waker.lock().unwrap().take().unwrap();
wake_concurrently(&waker);
release_tx.send(()).unwrap();
dropped_rx.recv_timeout(START_TIMEOUT).unwrap();
let expected = match result {
RunningResult::PendingThenReady => 2,
RunningResult::Ready => 1,
};
assert_eq!(
polls.load(Ordering::Acquire),
expected,
"running wake produced an incorrect number of polls"
);
for _ in 0..DUPLICATE_ENTRIES {
waker.wake_by_ref();
}
thread::sleep(Duration::from_millis(20));
assert_eq!(
polls.load(Ordering::Acquire),
expected,
"late waker re-polled a completed task"
);
drop(waker);
assert_released(task, "running-wake task remained retained");
}
#[test]
fn test_multi_thread_self_wake_ready_releases_task_without_requeue_livelock() {
let _test_lock = ASYNC_TASK_SCHEDULING_TEST_LOCK.lock();
let runtime = new_runtime();
let polls = Arc::new(AtomicUsize::new(0));
let mut tasks = Vec::with_capacity(TASK_COUNT);
for _ in 0..TASK_COUNT {
let future: BoxFuture<'static, ()> = Box::pin(WakeThenReady {
polls: polls.clone(),
});
let task = Arc::new(AsyncTask::new(
runtime.alloc::<()>(),
runtime.shared_pool(),
5,
Some(future),
));
tasks.push(Arc::downgrade(&task));
runtime.shared_pool().push(task).unwrap();
}
let all_started = wait_for_poll_count(&polls, TASK_COUNT);
let retained = if all_started {
wait_for_release(&tasks)
} else {
retained_count(&tasks)
};
let observed_polls = polls.load(Ordering::Acquire);
let _ = runtime.close();
assert!(
all_started,
"not all redline futures started before the deadline: polls={}, expected={}",
observed_polls,
TASK_COUNT
);
assert_eq!(
observed_polls,
TASK_COUNT,
"a one-shot Ready future must be polled exactly once"
);
assert_eq!(
retained,
0,
"completed self-woken tasks remained retained by runtime queues"
);
}
#[test]
fn test_multi_thread_managed_wake_coalescing_is_exact_and_bounded() {
let _test_lock = ASYNC_TASK_SCHEDULING_TEST_LOCK.lock();
let runtime = new_runtime();
let self_polls = Arc::new(AtomicUsize::new(0));
let self_task = submit(
&runtime,
Box::pin(WakeManyThenPending {
polls: self_polls.clone(),
yielded: false,
}),
);
assert!(wait_for_poll_count(&self_polls, 2));
assert_eq!(self_polls.load(Ordering::Acquire), 2);
assert_released(self_task, "self-woken Pending task remained retained");
let external_polls = Arc::new(AtomicUsize::new(0));
let external_waker = Arc::new(Mutex::new(None));
let (armed_tx, armed_rx) = mpsc::channel();
let external_task = submit(
&runtime,
Box::pin(CaptureIdleWaker {
polls: external_polls.clone(),
waker: external_waker.clone(),
armed: Some(armed_tx),
}),
);
armed_rx.recv_timeout(START_TIMEOUT).unwrap();
let external_waker = external_waker.lock().unwrap().take().unwrap();
wake_concurrently(&external_waker);
assert!(wait_for_poll_count(&external_polls, 2));
assert_eq!(external_polls.load(Ordering::Acquire), 2);
for _ in 0..DUPLICATE_ENTRIES {
external_waker.wake_by_ref();
}
thread::sleep(Duration::from_millis(20));
assert_eq!(
external_polls.load(Ordering::Acquire),
2,
"late wake after idle/queued concurrency re-polled completion"
);
drop(external_waker);
assert_released(external_task, "external-woken task remained retained");
let _ = runtime.close();
}
#[test]
fn test_multi_thread_running_wake_pending_ready_and_late_wake_are_safe() {
let _test_lock = ASYNC_TASK_SCHEDULING_TEST_LOCK.lock();
let runtime = new_runtime();
run_blocked_running_case(&runtime, RunningResult::PendingThenReady);
run_blocked_running_case(&runtime, RunningResult::Ready);
let polls = Arc::new(AtomicUsize::new(0));
let waker = Arc::new(Mutex::new(None));
let (dropped_tx, dropped_rx) = mpsc::channel();
let task = submit(
&runtime,
Box::pin(SaveWakerThenReady {
polls: polls.clone(),
waker: waker.clone(),
dropped: Some(dropped_tx),
}),
);
dropped_rx.recv_timeout(START_TIMEOUT).unwrap();
let waker = waker.lock().unwrap().take().unwrap();
wake_concurrently(&waker);
thread::sleep(Duration::from_millis(20));
assert_eq!(polls.load(Ordering::Acquire), 1);
drop(waker);
assert_released(task, "completed task was retained by its stale waker");
let _ = runtime.close();
}
#[test]
fn test_multi_thread_stale_entries_and_empty_task_are_discarded() {
let _test_lock = ASYNC_TASK_SCHEDULING_TEST_LOCK.lock();
let runtime = new_runtime();
let polls = Arc::new(AtomicUsize::new(0));
let active = Arc::new(AtomicUsize::new(0));
let maximum_active = Arc::new(AtomicUsize::new(0));
let task = Arc::new(AsyncTask::new(
runtime.alloc::<()>(),
runtime.shared_pool(),
5,
Some(Box::pin(SlowReady {
polls: polls.clone(),
active: active.clone(),
maximum_active: maximum_active.clone(),
})),
));
let task_ref = Arc::downgrade(&task);
for _ in 0..DUPLICATE_ENTRIES {
runtime.shared_pool().push(task.clone()).unwrap();
}
drop(task);
assert!(wait_for_poll_count(&polls, 1));
assert_released(task_ref, "stale duplicate entries retained a completed task");
assert_eq!(polls.load(Ordering::Acquire), 1);
assert_eq!(maximum_active.load(Ordering::Acquire), 1);
assert_eq!(active.load(Ordering::Acquire), 0);
let empty = Arc::new(AsyncTask::new(
runtime.alloc::<()>(),
runtime.shared_pool(),
5,
None,
));
let empty_ref = Arc::downgrade(&empty);
runtime.shared_pool().push(empty).unwrap();
assert_released(empty_ref, "managed task without a future was requeued");
let _ = runtime.close();
}
#[test]
#[cfg(target_arch = "x86_64")]
fn test_async_task_layout_size_and_growth_are_documented() {
let _test_lock = ASYNC_TASK_SCHEDULING_TEST_LOCK.lock();
const BASELINE_SIZE: usize = 80;
const REVIEWED_SIZE: usize = 96;
const ONE_MILLION_TASKS: usize = 1_000_000;
let actual_size = std::mem::size_of::<RuntimeTask>();
assert_eq!(actual_size, REVIEWED_SIZE);
assert_eq!(std::mem::align_of::<RuntimeTask>(), 16);
assert_eq!(actual_size - BASELINE_SIZE, 16);
assert_eq!(
(actual_size - BASELINE_SIZE) * ONE_MILLION_TASKS,
16_000_000,
);
}
}