use std::future::Future;
use std::sync::Arc;
use std::task::{Context, Poll};
use crate::orchestration::{scope_ambient, AmbientExecutionScope};
use crate::stdlib::pool::{with_pool_registry_scope, PoolRegistry};
use pin_project_lite::pin_project;
pin_project! {
pub(crate) struct PreparedSubtask<F> {
#[pin]
inner: F,
}
}
impl<F: Future> Future for PreparedSubtask<F> {
type Output = F::Output;
fn poll(self: std::pin::Pin<&mut Self>, context: &mut Context<'_>) -> Poll<Self::Output> {
self.project().inner.poll(context)
}
}
impl<F: Future> PreparedSubtask<F> {
pub(crate) fn map_output<M, T>(self, map: M) -> PreparedSubtask<impl Future<Output = T>>
where
M: FnOnce(F::Output) -> T,
{
PreparedSubtask {
inner: async move { map(self.await) },
}
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum SubtaskPlacement {
CurrentThread,
#[default]
Worker,
}
pub const PLACEMENT_ENV: &str = "HARN_VM_SUBTASK_PLACEMENT";
pub const PLACEMENT_VALUES: &[&str] = &["worker", "current_thread"];
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct SubtaskPlacementParseError {
value: String,
}
impl std::fmt::Display for SubtaskPlacementParseError {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
formatter,
"invalid {PLACEMENT_ENV} value {:?}; expected one of {}",
self.value,
PLACEMENT_VALUES.join(", ")
)
}
}
impl std::error::Error for SubtaskPlacementParseError {}
impl SubtaskPlacement {
pub fn from_env_value(value: &str) -> Result<Self, SubtaskPlacementParseError> {
match value.trim().to_ascii_lowercase().as_str() {
"worker" => Ok(Self::Worker),
"current_thread" => Ok(Self::CurrentThread),
_ => Err(SubtaskPlacementParseError {
value: value.to_string(),
}),
}
}
fn name(self) -> &'static str {
match self {
Self::Worker => "worker",
Self::CurrentThread => "current_thread",
}
}
}
impl std::fmt::Display for SubtaskPlacement {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter.write_str(self.name())
}
}
fn placement_from_environment() -> SubtaskPlacement {
static RESOLVED: std::sync::OnceLock<SubtaskPlacement> = std::sync::OnceLock::new();
*RESOLVED.get_or_init(|| {
let Ok(value) = std::env::var(PLACEMENT_ENV) else {
return SubtaskPlacement::default();
};
SubtaskPlacement::from_env_value(&value).unwrap_or_else(|error| panic!("{error}"))
})
}
thread_local! {
static SUBTASK_PLACEMENT_CONTEXT: std::cell::RefCell<Option<SubtaskPlacement>> =
const { std::cell::RefCell::new(None) };
}
pub(crate) fn swap_subtask_placement_context(
next: Option<SubtaskPlacement>,
) -> Option<SubtaskPlacement> {
SUBTASK_PLACEMENT_CONTEXT.with(|slot| std::mem::replace(&mut *slot.borrow_mut(), next))
}
pub fn placement() -> SubtaskPlacement {
SUBTASK_PLACEMENT_CONTEXT
.with(|slot| *slot.borrow())
.unwrap_or_else(placement_from_environment)
}
pub fn scope_placement<F: Future>(
placement: SubtaskPlacement,
inner: F,
) -> impl Future<Output = F::Output> {
let mut scope = AmbientExecutionScope::capture_for_inline_subtask();
scope.set_subtask_placement(Some(placement));
scope_ambient(scope, inner)
}
pub(crate) fn prepare<F: Future>(
registry: Arc<PoolRegistry>,
future: F,
) -> PreparedSubtask<impl Future<Output = F::Output>> {
PreparedSubtask {
inner: scope_ambient(
AmbientExecutionScope::capture_for_inline_subtask(),
with_pool_registry_scope(registry, future),
),
}
}
pub(crate) fn spawn<F>(future: PreparedSubtask<F>) -> tokio::task::JoinHandle<F::Output>
where
F: Future + Send + 'static,
F::Output: Send + 'static,
{
match placement() {
SubtaskPlacement::Worker => tokio::spawn(future),
SubtaskPlacement::CurrentThread => tokio::task::spawn_local(future),
}
}
pub(crate) fn spawn_into<F>(
set: &mut tokio::task::JoinSet<F::Output>,
future: PreparedSubtask<F>,
) -> tokio::task::AbortHandle
where
F: Future + Send + 'static,
F::Output: Send + 'static,
{
match placement() {
SubtaskPlacement::Worker => set.spawn(future),
SubtaskPlacement::CurrentThread => {
let mut future = Box::pin(future);
let mut context = Context::from_waker(std::task::Waker::noop());
match future.as_mut().poll(&mut context) {
Poll::Ready(value) => set.spawn_local(async move { value }),
Poll::Pending => set.spawn_local(future),
}
}
}
}
pub(crate) fn spawn_child<F>(
registry: Arc<PoolRegistry>,
future: F,
) -> tokio::task::JoinHandle<F::Output>
where
F: Future + Send + 'static,
F::Output: Send + 'static,
{
spawn(prepare(registry, future))
}
pub(crate) fn spawn_inherited_child<F>(
registry: Arc<PoolRegistry>,
future: F,
) -> tokio::task::JoinHandle<F::Output>
where
F: Future + Send + 'static,
F::Output: Send + 'static,
{
spawn(PreparedSubtask {
inner: scope_ambient(
AmbientExecutionScope::capture_inherited(),
with_pool_registry_scope(registry, future),
),
})
}
#[cfg(test)]
#[path = "subtask/cross_thread_tests.rs"]
mod cross_thread_tests;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn placement_names_round_trip() {
assert_eq!(
SubtaskPlacement::from_env_value("worker"),
Ok(SubtaskPlacement::Worker)
);
assert_eq!(
SubtaskPlacement::from_env_value(" CURRENT_THREAD "),
Ok(SubtaskPlacement::CurrentThread)
);
assert_eq!(
SubtaskPlacement::from_env_value("sideways")
.expect_err("invalid placement must not become an absent override")
.to_string(),
"invalid HARN_VM_SUBTASK_PLACEMENT value \"sideways\"; expected one of worker, current_thread"
);
assert_eq!(SubtaskPlacement::Worker.to_string(), "worker");
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn scoped_placement_reaches_the_spawn_seam() {
assert_eq!(placement(), SubtaskPlacement::Worker);
let observed =
scope_placement(SubtaskPlacement::CurrentThread, async { placement() }).await;
assert_eq!(observed, SubtaskPlacement::CurrentThread);
assert_eq!(placement(), SubtaskPlacement::Worker);
}
#[test]
fn placement_selects_the_executor_thread() {
fn observed_thread(
runtime: &tokio::runtime::Runtime,
placement: SubtaskPlacement,
) -> std::thread::ThreadId {
runtime.block_on(async {
tokio::task::LocalSet::new()
.run_until(scope_placement(placement, async {
spawn(PreparedSubtask {
inner: async { std::thread::current().id() },
})
.await
.expect("subtask completes")
}))
.await
})
}
let runtime = tokio::runtime::Builder::new_multi_thread()
.worker_threads(2)
.enable_all()
.build()
.expect("test runtime");
let creating_thread = std::thread::current().id();
assert_ne!(
observed_thread(&runtime, SubtaskPlacement::Worker),
creating_thread
);
assert_eq!(
observed_thread(&runtime, SubtaskPlacement::CurrentThread),
creating_thread
);
}
}