use arty_executor::{TaskOptions, TaskPlacement};
use performables::arc::Arc;
use thread_aware::{Thread, ThreadAware};
use crate::runtime::dispatch::{DispatcherClient, WorkerIndex};
use crate::runtime::worker::asynchronous;
use crate::runtime::worker::blocking::Worker;
use crate::task::Context;
use crate::task::handle::JoinHandle;
use crate::task::preparation::DeferredTask;
#[derive(Debug, Clone)]
pub struct Scheduler {
pub(crate) dispatcher: DispatcherClient,
binding: Binding,
}
#[derive(Debug, Clone)]
struct Binding {
thread: Thread,
worker_index: WorkerIndex,
blocking_worker: Arc<Worker>,
}
impl Scheduler {
pub(crate) fn new(dispatcher: DispatcherClient, current: Thread) -> Self {
let worker_index = dispatcher
.worker_index(current.id())
.expect("scheduler thread must be registered with its dispatcher");
let blocking_worker = dispatcher.blocking_worker(worker_index);
Self {
dispatcher,
binding: Binding {
thread: current,
worker_index,
blocking_worker,
},
}
}
pub(crate) fn resolve_worker_index(&self, thread: &Thread) -> Option<WorkerIndex> {
self.dispatcher
.owns(thread)
.then(|| self.dispatcher.worker_index(thread.id()))
.flatten()
}
pub(crate) fn relocate_to_worker(&mut self, destination: &Thread, worker_index: WorkerIndex) {
self.binding = Binding {
thread: destination.clone(),
worker_index,
blocking_worker: self.dispatcher.blocking_worker(worker_index),
};
}
pub fn spawn<FF, F, R>(&self, future_factory: FF) -> JoinHandle<R>
where
FF: FnOnce(Context) -> F + Send + 'static,
F: Future<Output = R> + 'static,
R: Send + 'static,
{
if let Some(join_handle) = JoinHandle::try_from_shutdown(self.dispatcher.shutdown()) {
return join_handle;
}
let future_factory = match self.spawn_on_current_worker(future_factory) {
Ok(join_handle) => return join_handle,
Err(future_factory) => future_factory,
};
self.dispatcher.spawn_on_worker(self.binding.worker_index, future_factory)
}
fn spawn_on_current_worker<FF, F, R>(&self, future_factory: FF) -> Result<JoinHandle<R>, FF>
where
FF: FnOnce(Context) -> F + Send + 'static,
F: Future<Output = R> + 'static,
R: Send + 'static,
{
asynchronous::LocalWorker::with_scheduler(|current| {
let Some(current) = current else {
return Err(future_factory);
};
if current.thread() != &self.binding.thread {
return Err(future_factory);
}
let context = current.context().clone();
let (handle, result_tx) = current.results().handle();
let inner = DeferredTask::new(future_factory, context);
let options = TaskOptions::new(TaskPlacement::SameThread);
current.tasks().add(inner, options, result_tx);
self.dispatcher.wake_worker(self.binding.worker_index);
Ok(handle)
})
}
pub fn spawn_anywhere<D, F, R>(&self, data: D, f: fn(D) -> F) -> JoinHandle<R>
where
D: ThreadAware + 'static,
F: Future<Output = R> + 'static,
R: ThreadAware + 'static,
{
let source = self.binding.thread.clone();
self.dispatcher.spawn_relocating(async move |cx| {
let mut data = data;
data.relocate(Some(&source), cx.thread());
f(data).await
})
}
pub fn spawn_at<D, F, R>(&self, destination: &Thread, data: D, f: fn(D) -> F) -> JoinHandle<R>
where
D: ThreadAware + 'static,
F: Future<Output = R> + 'static,
R: ThreadAware + 'static,
{
if let Some(join_handle) = JoinHandle::try_from_shutdown(self.dispatcher.shutdown()) {
return join_handle;
}
let Some(worker_index) = self.resolve_worker_index(destination) else {
return JoinHandle::invalid_destination();
};
let source = self.binding.thread.clone();
self.dispatcher.spawn_at_worker_relocating(worker_index, move |cx| {
let mut data = data;
async move {
data.relocate(Some(&source), cx.thread());
f(data).await
}
})
}
pub fn spawn_blocking<B, R>(&self, body: B) -> JoinHandle<R>
where
B: FnOnce() -> R + Send + 'static,
R: Send + 'static,
{
self.binding.blocking_worker.spawn_blocking(body)
}
#[cfg(test)]
pub(crate) fn blocking_worker(&self) -> &Arc<Worker> {
&self.binding.blocking_worker
}
}
impl ThreadAware for Scheduler {
fn relocate(&mut self, _source: Option<&Thread>, destination: &Thread) {
if self.binding.thread == *destination {
return;
}
if let Some(index) = self.resolve_worker_index(destination) {
self.relocate_to_worker(destination, index);
}
}
}
impl AsRef<Self> for Scheduler {
fn as_ref(&self) -> &Self {
self
}
}
#[cfg(test)]
#[cfg_attr(coverage_nightly, coverage(off))] mod tests {
use std::cell::RefCell;
use std::future::Future as _;
use std::pin::pin;
use std::sync::mpsc;
use std::task::{Context, Poll, Waker};
use std::time::Duration;
use arty_executor::ShutdownCause;
use super::*;
thread_local! {
static SPAWN_ON_DROP: RefCell<Option<SpawnOnDrop>> = const { RefCell::new(None) };
}
struct SpawnOnDrop {
scheduler: Scheduler,
spawned: mpsc::Sender<JoinHandle<()>>,
}
impl Drop for SpawnOnDrop {
fn drop(&mut self) {
self.spawned.send(self.scheduler.spawn(async |_| {})).unwrap();
}
}
#[test]
fn worker_execution_registers_the_current_scheduler() {
let runtime = crate::runtime::Runtime::builder()
.workers(crate::runtime::WorkersPolicy::exactly(1))
.build()
.unwrap();
let registered = runtime
.scheduler()
.block_on(async |_| {
assert!(asynchronous::LocalWorker::is_current());
asynchronous::LocalWorker::has_scheduler_context()
})
.unwrap();
assert!(registered);
runtime.stop().unwrap();
}
#[test]
fn dispatcher_shutdown_rejects_direct_submissions_immediately() {
let runtime = crate::runtime::Runtime::builder()
.workers(crate::runtime::WorkersPolicy::exactly(1))
.build()
.unwrap();
let rejected = runtime
.scheduler()
.block_on(async |cx| {
_ = cx.scheduler().dispatcher.stop(ShutdownCause::default());
let mut task = pin!(cx.scheduler().spawn(async |_| 42));
matches!(
task.as_mut().poll(&mut Context::from_waker(Waker::noop())),
Poll::Ready(Err(error)) if error.is_shutdown()
)
})
.unwrap();
assert!(rejected);
runtime.stop().unwrap();
}
#[test]
fn worker_marker_remains_active_after_scheduler_context_is_cleared() {
struct DropProbe(mpsc::Sender<(bool, bool)>);
impl Drop for DropProbe {
fn drop(&mut self) {
self.0
.send((
!asynchronous::LocalWorker::has_scheduler_context(),
asynchronous::LocalWorker::is_current(),
))
.unwrap();
}
}
let runtime = crate::runtime::Runtime::builder()
.workers(crate::runtime::WorkersPolicy::exactly(1))
.build()
.unwrap();
let (started_tx, started_rx) = mpsc::channel();
let (cleared_tx, cleared_rx) = mpsc::channel();
runtime
.scheduler()
.block_on(async move |cx| {
let task = cx.scheduler().spawn(move |_| async move {
started_tx.send(()).unwrap();
let _probe = DropProbe(cleared_tx);
std::future::pending::<()>().await;
});
drop(task);
})
.unwrap();
started_rx.recv_timeout(Duration::from_secs(5)).unwrap();
runtime.stop().unwrap();
assert_eq!(cleared_rx.recv_timeout(Duration::from_secs(5)).unwrap(), (true, true));
}
#[test]
fn external_spawn_survives_scheduler_slot_teardown() {
let runtime = crate::runtime::Runtime::builder()
.workers(crate::runtime::WorkersPolicy::exactly(1))
.build()
.unwrap();
let scheduler = runtime.scheduler().block_on(async |cx| cx.scheduler().clone()).unwrap();
let (spawned, received) = mpsc::channel();
std::thread::spawn(move || {
SPAWN_ON_DROP.with(|owner| {
assert!(
owner
.replace(Some(SpawnOnDrop {
scheduler: scheduler.clone(),
spawned,
}))
.is_none()
);
});
scheduler.spawn(async |_| {}).join().unwrap();
})
.join()
.unwrap();
received.recv_timeout(Duration::from_secs(5)).unwrap().join().unwrap();
runtime.stop().unwrap();
}
}