use std::cell::{Cell, RefCell};
use arty_executor::{TaskOptions, TaskPlacement, TaskSet};
use performables::arc::Arc;
use thread_aware::{Thread, ThreadAware};
use crate::runtime::blocking_worker::BlockingWorker;
use crate::runtime::dispatch::{DispatcherClient, WorkerIndex};
use crate::task::Builtins;
use crate::task::execution::DeferredTask;
use crate::task::join::JoinHandle;
thread_local! {
static ACTIVE_WORKER: Cell<bool> = const { Cell::new(false) };
static CURRENT_WORKER: RefCell<Option<CurrentWorker>> = const { RefCell::new(None) };
}
#[derive(Debug)]
struct CurrentWorker {
thread: Thread,
builtins: Builtins,
tasks: TaskSet,
}
#[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<BlockingWorker>,
}
impl Scheduler {
pub(crate) fn is_current_worker_thread() -> bool {
ACTIVE_WORKER.get()
}
pub(crate) fn register_current(builtins: Builtins, tasks: TaskSet) {
assert!(!ACTIVE_WORKER.replace(true), "a worker is already active on this thread");
CURRENT_WORKER.with_borrow_mut(|current| {
assert!(current.is_none(), "a worker already owns this scheduler context");
*current = Some(CurrentWorker {
thread: builtins.thread().clone(),
builtins,
tasks,
});
});
}
pub(crate) fn clear_current() {
drop(CURRENT_WORKER.with_borrow_mut(Option::take));
}
pub(crate) fn clear_worker_thread() {
Self::clear_current();
ACTIVE_WORKER.set(false);
}
pub(crate) fn begin_current_executor_shutdown(dispatcher: &DispatcherClient) {
_ = CURRENT_WORKER.try_with(|current| {
let current = current.borrow();
let Some(current) = current.as_ref() else {
return;
};
if dispatcher.owns(¤t.thread) {
current.tasks.begin_shutdown();
}
});
}
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(Builtins) -> F + Send + 'static,
F: Future<Output = R> + 'static,
R: Send + 'static,
{
if self.dispatcher.is_shutting_down() {
return JoinHandle::shutdown(self.dispatcher.remote_results());
}
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(Builtins) -> F + Send + 'static,
F: Future<Output = R> + 'static,
R: Send + 'static,
{
CURRENT_WORKER.with_borrow(|current| {
let Some(current) = current.as_ref() else {
return Err(future_factory);
};
if current.thread != self.binding.thread {
return Err(future_factory);
}
let sink = current.builtins.sink().clone();
let parent_task_enrichment = sink.transfer_context();
let builtins = current.builtins.clone();
let (result_tx, result_rx) = self.dispatcher.remote_results().channel();
let shutdown_signal = current.builtins.scheduler.dispatcher.shutdown_signal();
let inner = DeferredTask::new(future_factory, builtins, shutdown_signal);
let options = TaskOptions::observed(parent_task_enrichment, TaskPlacement::SameThread);
current.tasks.add_with_remote_result(inner, options, result_tx);
self.dispatcher.wake_worker(self.binding.worker_index);
Ok(JoinHandle::new(result_rx))
})
}
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(async move |cx| {
let mut data = data;
data.relocate(Some(&source), cx.thread());
f(data).await
})
}
pub fn spawn_everywhere<D, F, R>(&self, data: D, f: fn(D) -> F) -> Vec<JoinHandle<R>>
where
D: ThreadAware + Clone + 'static,
F: Future<Output = R> + 'static,
R: ThreadAware + 'static,
{
let source = &self.binding.thread;
let tasks = self.dispatcher.spawn_everywhere(|| {
let source = source.clone();
let mut data = data.clone();
async move |cx| {
data.relocate(Some(&source), cx.thread());
f(data).await
}
});
drop(data);
tasks
}
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<BlockingWorker> {
&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::future::Future as _;
use std::pin::pin;
use std::sync::mpsc;
use std::task::{Context, Poll, Waker};
use std::time::Duration;
use super::*;
#[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!(Scheduler::is_current_worker_thread());
CURRENT_WORKER.with_borrow(Option::is_some)
})
.unwrap();
assert!(registered);
runtime.stop().unwrap();
}
#[test]
fn current_executor_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| {
Scheduler::begin_current_executor_shutdown(&cx.scheduler().dispatcher);
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 clearing_worker_thread_retires_the_active_marker() {
ACTIVE_WORKER.set(true);
Scheduler::clear_worker_thread();
assert!(!Scheduler::is_current_worker_thread());
}
#[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((CURRENT_WORKER.with_borrow(Option::is_none), Scheduler::is_current_worker_thread()))
.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));
}
}