1use std::future::Future;
33use std::sync::Arc;
34use std::task::{Context, Poll};
35
36use crate::orchestration::{scope_ambient, AmbientExecutionScope};
37use crate::stdlib::pool::{with_pool_registry_scope, PoolRegistry};
38use pin_project_lite::pin_project;
39
40pin_project! {
41 pub(crate) struct PreparedSubtask<F> {
49 #[pin]
50 inner: F,
51 }
52}
53
54impl<F: Future> Future for PreparedSubtask<F> {
55 type Output = F::Output;
56
57 fn poll(self: std::pin::Pin<&mut Self>, context: &mut Context<'_>) -> Poll<Self::Output> {
58 self.project().inner.poll(context)
59 }
60}
61
62impl<F: Future> PreparedSubtask<F> {
63 pub(crate) fn map_output<M, T>(self, map: M) -> PreparedSubtask<impl Future<Output = T>>
67 where
68 M: FnOnce(F::Output) -> T,
69 {
70 PreparedSubtask {
71 inner: async move { map(self.await) },
72 }
73 }
74}
75
76#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
78pub enum SubtaskPlacement {
79 CurrentThread,
83 #[default]
87 Worker,
88}
89
90pub const PLACEMENT_ENV: &str = "HARN_VM_SUBTASK_PLACEMENT";
92pub const PLACEMENT_VALUES: &[&str] = &["worker", "current_thread"];
96
97#[derive(Clone, Debug, PartialEq, Eq)]
99pub struct SubtaskPlacementParseError {
100 value: String,
101}
102
103impl std::fmt::Display for SubtaskPlacementParseError {
104 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
105 write!(
106 formatter,
107 "invalid {PLACEMENT_ENV} value {:?}; expected one of {}",
108 self.value,
109 PLACEMENT_VALUES.join(", ")
110 )
111 }
112}
113
114impl std::error::Error for SubtaskPlacementParseError {}
115
116impl SubtaskPlacement {
117 pub fn from_env_value(value: &str) -> Result<Self, SubtaskPlacementParseError> {
120 match value.trim().to_ascii_lowercase().as_str() {
121 "worker" => Ok(Self::Worker),
122 "current_thread" => Ok(Self::CurrentThread),
123 _ => Err(SubtaskPlacementParseError {
124 value: value.to_string(),
125 }),
126 }
127 }
128
129 fn name(self) -> &'static str {
130 match self {
131 Self::Worker => "worker",
132 Self::CurrentThread => "current_thread",
133 }
134 }
135}
136
137impl std::fmt::Display for SubtaskPlacement {
138 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
139 formatter.write_str(self.name())
140 }
141}
142
143fn placement_from_environment() -> SubtaskPlacement {
145 static RESOLVED: std::sync::OnceLock<SubtaskPlacement> = std::sync::OnceLock::new();
146 *RESOLVED.get_or_init(|| {
147 let Ok(value) = std::env::var(PLACEMENT_ENV) else {
148 return SubtaskPlacement::default();
149 };
150 SubtaskPlacement::from_env_value(&value).unwrap_or_else(|error| panic!("{error}"))
151 })
152}
153
154thread_local! {
155 static SUBTASK_PLACEMENT_CONTEXT: std::cell::RefCell<Option<SubtaskPlacement>> =
160 const { std::cell::RefCell::new(None) };
161}
162
163pub(crate) fn swap_subtask_placement_context(
166 next: Option<SubtaskPlacement>,
167) -> Option<SubtaskPlacement> {
168 SUBTASK_PLACEMENT_CONTEXT.with(|slot| std::mem::replace(&mut *slot.borrow_mut(), next))
169}
170
171pub fn placement() -> SubtaskPlacement {
173 SUBTASK_PLACEMENT_CONTEXT
174 .with(|slot| *slot.borrow())
175 .unwrap_or_else(placement_from_environment)
176}
177
178pub fn scope_placement<F: Future>(
182 placement: SubtaskPlacement,
183 inner: F,
184) -> impl Future<Output = F::Output> {
185 let mut scope = AmbientExecutionScope::capture_for_inline_subtask();
186 scope.set_subtask_placement(Some(placement));
187 scope_ambient(scope, inner)
188}
189
190pub(crate) fn prepare<F: Future>(
199 registry: Arc<PoolRegistry>,
200 future: F,
201) -> PreparedSubtask<impl Future<Output = F::Output>> {
202 PreparedSubtask {
203 inner: scope_ambient(
204 AmbientExecutionScope::capture_for_inline_subtask(),
205 with_pool_registry_scope(registry, future),
206 ),
207 }
208}
209
210pub(crate) fn spawn<F>(future: PreparedSubtask<F>) -> tokio::task::JoinHandle<F::Output>
212where
213 F: Future + Send + 'static,
214 F::Output: Send + 'static,
215{
216 match placement() {
217 SubtaskPlacement::Worker => tokio::spawn(future),
218 SubtaskPlacement::CurrentThread => tokio::task::spawn_local(future),
219 }
220}
221
222pub(crate) fn spawn_into<F>(
224 set: &mut tokio::task::JoinSet<F::Output>,
225 future: PreparedSubtask<F>,
226) -> tokio::task::AbortHandle
227where
228 F: Future + Send + 'static,
229 F::Output: Send + 'static,
230{
231 match placement() {
232 SubtaskPlacement::Worker => set.spawn(future),
233 SubtaskPlacement::CurrentThread => {
234 let mut future = Box::pin(future);
241 let mut context = Context::from_waker(std::task::Waker::noop());
242 match future.as_mut().poll(&mut context) {
243 Poll::Ready(value) => set.spawn_local(async move { value }),
244 Poll::Pending => set.spawn_local(future),
245 }
246 }
247 }
248}
249
250pub(crate) fn spawn_child<F>(
253 registry: Arc<PoolRegistry>,
254 future: F,
255) -> tokio::task::JoinHandle<F::Output>
256where
257 F: Future + Send + 'static,
258 F::Output: Send + 'static,
259{
260 spawn(prepare(registry, future))
261}
262
263pub(crate) fn spawn_inherited_child<F>(
266 registry: Arc<PoolRegistry>,
267 future: F,
268) -> tokio::task::JoinHandle<F::Output>
269where
270 F: Future + Send + 'static,
271 F::Output: Send + 'static,
272{
273 spawn(PreparedSubtask {
274 inner: scope_ambient(
275 AmbientExecutionScope::capture_inherited(),
276 with_pool_registry_scope(registry, future),
277 ),
278 })
279}
280
281#[cfg(test)]
282#[path = "subtask/cross_thread_tests.rs"]
283mod cross_thread_tests;
284
285#[cfg(test)]
286mod tests {
287 use super::*;
288
289 #[test]
290 fn placement_names_round_trip() {
291 assert_eq!(
292 SubtaskPlacement::from_env_value("worker"),
293 Ok(SubtaskPlacement::Worker)
294 );
295 assert_eq!(
296 SubtaskPlacement::from_env_value(" CURRENT_THREAD "),
297 Ok(SubtaskPlacement::CurrentThread)
298 );
299 assert_eq!(
300 SubtaskPlacement::from_env_value("sideways")
301 .expect_err("invalid placement must not become an absent override")
302 .to_string(),
303 "invalid HARN_VM_SUBTASK_PLACEMENT value \"sideways\"; expected one of worker, current_thread"
304 );
305 assert_eq!(SubtaskPlacement::Worker.to_string(), "worker");
306 }
307
308 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
309 async fn scoped_placement_reaches_the_spawn_seam() {
310 assert_eq!(placement(), SubtaskPlacement::Worker);
311 let observed =
312 scope_placement(SubtaskPlacement::CurrentThread, async { placement() }).await;
313 assert_eq!(observed, SubtaskPlacement::CurrentThread);
314 assert_eq!(placement(), SubtaskPlacement::Worker);
315 }
316
317 #[test]
318 fn placement_selects_the_executor_thread() {
319 fn observed_thread(
320 runtime: &tokio::runtime::Runtime,
321 placement: SubtaskPlacement,
322 ) -> std::thread::ThreadId {
323 runtime.block_on(async {
324 tokio::task::LocalSet::new()
325 .run_until(scope_placement(placement, async {
326 spawn(PreparedSubtask {
327 inner: async { std::thread::current().id() },
328 })
329 .await
330 .expect("subtask completes")
331 }))
332 .await
333 })
334 }
335
336 let runtime = tokio::runtime::Builder::new_multi_thread()
337 .worker_threads(2)
338 .enable_all()
339 .build()
340 .expect("test runtime");
341 let creating_thread = std::thread::current().id();
342
343 assert_ne!(
344 observed_thread(&runtime, SubtaskPlacement::Worker),
345 creating_thread
346 );
347 assert_eq!(
348 observed_thread(&runtime, SubtaskPlacement::CurrentThread),
349 creating_thread
350 );
351 }
352}