1use alloc::boxed::Box;
39use core::any::Any;
40use core::future::Future;
41
42use proxima_core::ProximaError;
43
44use crate::{BackgroundHandle, CoreId, Runtime, SpawnError};
45
46pub trait RuntimeExt: Runtime {
49 fn spawn<F>(&self, future: F)
53 where
54 F: Future<Output = ()> + 'static,
55 {
56 self.spawn_on_current_core(Box::pin(future));
57 }
58
59 fn core(&self, placement: impl Into<Placement>) -> OnCore<'_, Self> {
62 OnCore {
63 runtime: self,
64 placement: placement.into(),
65 }
66 }
67
68 fn blocking<R, W>(&self, work: W) -> Blocking<'_, Self, W, R>
71 where
72 R: Send + 'static,
73 W: FnOnce() -> Result<R, ProximaError> + Send + 'static,
74 {
75 Blocking {
76 runtime: self,
77 work,
78 _result: core::marker::PhantomData,
79 }
80 }
81}
82
83impl<R: Runtime + ?Sized> RuntimeExt for R {}
84
85#[derive(Debug, Clone, PartialEq, Eq)]
95pub enum Placement {
96 Only(CoreId),
98 AnyOther,
101 Any,
103 AnyOf(alloc::vec::Vec<CoreId>),
105}
106
107impl From<CoreId> for Placement {
108 fn from(core_id: CoreId) -> Self {
109 Placement::Only(core_id)
110 }
111}
112
113impl From<usize> for Placement {
114 fn from(core_id: usize) -> Self {
115 Placement::Only(CoreId(core_id))
116 }
117}
118
119impl<T: Into<CoreId>> From<alloc::vec::Vec<T>> for Placement {
120 fn from(cores: alloc::vec::Vec<T>) -> Self {
121 Placement::AnyOf(cores.into_iter().map(Into::into).collect())
122 }
123}
124
125impl<T: Into<CoreId>, const N: usize> From<[T; N]> for Placement {
126 fn from(cores: [T; N]) -> Self {
127 Placement::AnyOf(cores.into_iter().map(Into::into).collect())
128 }
129}
130
131impl Placement {
132 fn candidates(&self, current: CoreId, cores: usize) -> alloc::vec::Vec<CoreId> {
136 match self {
137 Placement::Only(core_id) => alloc::vec![*core_id],
138 Placement::AnyOf(list) => list.clone(),
139 Placement::Any => (0..cores)
140 .map(|offset| CoreId((current.0 + offset) % cores))
141 .collect(),
142 Placement::AnyOther => (1..cores)
143 .map(|offset| CoreId((current.0 + offset) % cores))
144 .collect(),
145 }
146 }
147}
148
149#[derive(Debug, Clone, Copy, PartialEq, Eq)]
155pub struct Exhausted {
156 pub attempts: u32,
159 pub last: Option<SpawnError>,
161}
162
163pub struct OnCore<'rt, R: ?Sized> {
166 runtime: &'rt R,
167 placement: Placement,
168}
169
170impl<'rt, R: Runtime + ?Sized> OnCore<'rt, R> {
171 pub fn spawn<F>(self, future: F) -> Result<(), SpawnError>
175 where
176 F: Future<Output = ()> + Send + 'static,
177 {
178 let candidates = self
179 .placement
180 .candidates(self.runtime.current_core(), self.runtime.num_cores());
181 let Some(core_id) = candidates.first().copied() else {
182 return Err(SpawnError::Disconnected);
183 };
184 self.runtime.spawn_on_core(core_id, Box::pin(future))
185 }
186
187 pub fn spawn_with<Factory, F, Decide>(
201 self,
202 factory: Factory,
203 mut decide: Decide,
204 ) -> Result<CoreId, Exhausted>
205 where
206 Factory: Fn() -> F,
207 F: Future<Output = ()> + Send + 'static,
208 Decide: FnMut(u32, &SpawnError) -> bool,
209 {
210 let candidates = self
211 .placement
212 .candidates(self.runtime.current_core(), self.runtime.num_cores());
213 let mut attempts = 0u32;
214 let mut last = None;
215 for core_id in candidates {
216 match self.runtime.spawn_on_core(core_id, Box::pin(factory())) {
217 Ok(()) => return Ok(core_id),
218 Err(err) => {
219 let keep_going = decide(attempts, &err);
220 attempts = attempts.saturating_add(1);
221 last = Some(err);
222 if !keep_going {
223 break;
224 }
225 }
226 }
227 }
228 Err(Exhausted { attempts, last })
229 }
230}
231
232pub struct Blocking<'rt, R: ?Sized, W, Out> {
234 runtime: &'rt R,
235 work: W,
236 _result: core::marker::PhantomData<fn() -> Out>,
237}
238
239impl<'rt, R, W, Out> Blocking<'rt, R, W, Out>
240where
241 R: Runtime + ?Sized,
242 Out: Send + 'static,
243 W: FnOnce() -> Result<Out, ProximaError> + Send + 'static,
244{
245 pub fn spawn(self) -> impl Future<Output = Result<Out, ProximaError>> + Send {
248 let handle: BackgroundHandle<Box<dyn Any + Send>> =
249 self.runtime.spawn_background_blocking(Box::new(move || {
250 (self.work)().map(|value| Box::new(value) as Box<dyn Any + Send>)
251 }));
252 async move {
253 let boxed = handle.await?;
254 boxed
257 .downcast::<Out>()
258 .map(|value| *value)
259 .map_err(|_| ProximaError::Config("background payload type changed".into()))
260 }
261 }
262}
263
264#[cfg(test)]
265#[allow(clippy::unwrap_used, clippy::expect_used)]
266mod tests {
267 use alloc::sync::Arc;
268 use core::sync::atomic::{AtomicUsize, Ordering};
269
270 use super::*;
271
272 fn block_on<F: Future>(future: F) -> F::Output {
276 let mut future = core::pin::pin!(future);
277 let waker = core::task::Waker::noop();
278 let mut cx = core::task::Context::from_waker(waker);
279 loop {
280 if let core::task::Poll::Ready(value) = future.as_mut().poll(&mut cx) {
281 return value;
282 }
283 }
284 }
285
286 #[derive(Default)]
287 struct StubRuntime {
288 on_core: AtomicUsize,
289 background: AtomicUsize,
290 full_for_first: AtomicUsize,
293 }
294
295 impl Runtime for StubRuntime {
296 fn spawn_on_current_core(
297 &self,
298 _f: core::pin::Pin<Box<dyn Future<Output = ()> + 'static>>,
299 ) {
300 }
301
302 fn spawn_on_core(
303 &self,
304 _core_id: CoreId,
305 _future: core::pin::Pin<Box<dyn Future<Output = ()> + Send + 'static>>,
306 ) -> Result<(), SpawnError> {
307 let attempt = self.on_core.fetch_add(1, Ordering::Relaxed);
308 if attempt < self.full_for_first.load(Ordering::Relaxed) {
309 return Err(SpawnError::InboxFull);
310 }
311 Ok(())
312 }
313
314 fn spawn_factory_on_core(
315 &self,
316 _core_id: CoreId,
317 _factory: Box<
318 dyn FnOnce() -> core::pin::Pin<Box<dyn Future<Output = ()> + 'static>> + Send,
319 >,
320 ) -> Result<(), SpawnError> {
321 Ok(())
322 }
323
324 fn spawn_background_blocking(
325 &self,
326 work: Box<dyn FnOnce() -> Result<Box<dyn Any + Send>, ProximaError> + Send>,
327 ) -> BackgroundHandle<Box<dyn Any + Send>> {
328 self.background.fetch_add(1, Ordering::Relaxed);
329 let result = work();
330 Box::pin(async move { result })
331 }
332
333 fn timer_at(
334 &self,
335 _deadline: std::time::Instant,
336 ) -> core::pin::Pin<Box<dyn Future<Output = ()>>> {
337 Box::pin(async {})
338 }
339
340 fn num_cores(&self) -> usize {
341 1
342 }
343
344 fn current_core(&self) -> CoreId {
345 CoreId(0)
346 }
347 }
348
349 #[test]
353 fn builder_is_reachable_through_a_trait_object() {
354 let stub = Arc::new(StubRuntime::default());
355 let erased: Arc<dyn Runtime> = stub.clone();
356
357 erased.core(CoreId(0)).spawn(async {}).expect("chosen core");
358 assert_eq!(stub.on_core.load(Ordering::Relaxed), 1);
359 }
360
361 #[test]
364 fn any_other_excludes_the_current_core() {
365 let candidates = Placement::AnyOther.candidates(CoreId(2), 4);
366 assert_eq!(candidates, alloc::vec![CoreId(3), CoreId(0), CoreId(1)]);
367 assert!(!candidates.contains(&CoreId(2)));
368 }
369
370 #[test]
371 fn any_includes_the_current_core_last_in_wrap_order() {
372 assert_eq!(
373 Placement::Any.candidates(CoreId(2), 4),
374 alloc::vec![CoreId(2), CoreId(3), CoreId(0), CoreId(1)]
375 );
376 }
377
378 #[test]
381 fn any_other_on_one_core_yields_no_candidate() {
382 assert!(Placement::AnyOther.candidates(CoreId(0), 1).is_empty());
383
384 let stub = Arc::new(StubRuntime::default());
385 let erased: Arc<dyn Runtime> = stub.clone();
386 let outcome = erased.core(Placement::AnyOther).spawn(async {});
387
388 assert_eq!(outcome, Err(SpawnError::Disconnected));
389 assert_eq!(stub.on_core.load(Ordering::Relaxed), 0);
390 }
391
392 #[test]
393 fn a_bare_index_is_a_specific_core() {
394 assert_eq!(Placement::from(3usize), Placement::Only(CoreId(3)));
395 }
396
397 #[test]
403 fn a_bare_literal_array_is_a_candidate_list() {
404 let stub = Arc::new(StubRuntime::default());
405 let erased: Arc<dyn Runtime> = stub.clone();
406
407 erased
408 .core([1, 2, 3])
409 .spawn(async {})
410 .expect("first candidate");
411 assert_eq!(stub.on_core.load(Ordering::Relaxed), 1);
412 }
413
414 #[test]
415 fn arrays_and_vecs_of_either_element_type_become_candidate_lists() {
416 let expected = Placement::AnyOf(alloc::vec![CoreId(1), CoreId(2)]);
417
418 assert_eq!(Placement::from([1usize, 2]), expected);
419 assert_eq!(Placement::from([CoreId(1), CoreId(2)]), expected);
420 assert_eq!(Placement::from(alloc::vec![1usize, 2]), expected);
421 assert_eq!(Placement::from(alloc::vec![CoreId(1), CoreId(2)]), expected);
422 }
423
424 #[test]
428 fn a_full_core_falls_through_to_the_next_candidate() {
429 let stub = Arc::new(StubRuntime::default());
430 stub.full_for_first.store(2, Ordering::Relaxed);
431 let erased: Arc<dyn Runtime> = stub.clone();
432
433 let outcome = erased
434 .core([0, 1, 2])
435 .spawn_with(|| async {}, |_, err| *err == SpawnError::InboxFull);
436
437 assert_eq!(outcome, Ok(CoreId(2)), "reports which core took it");
438 assert_eq!(
439 stub.on_core.load(Ordering::Relaxed),
440 3,
441 "two full, third accepted"
442 );
443 }
444
445 #[test]
448 fn a_decline_stops_before_the_remaining_candidates() {
449 let stub = Arc::new(StubRuntime::default());
450 stub.full_for_first.store(3, Ordering::Relaxed);
451 let erased: Arc<dyn Runtime> = stub.clone();
452
453 let outcome = erased.core([0, 1, 2]).spawn_with(|| async {}, |_, _| false);
454
455 assert_eq!(
456 outcome,
457 Err(Exhausted {
458 attempts: 1,
459 last: Some(SpawnError::InboxFull)
460 })
461 );
462 assert_eq!(
463 stub.on_core.load(Ordering::Relaxed),
464 1,
465 "stopped after the first"
466 );
467 }
468
469 #[test]
473 fn no_candidates_reports_zero_attempts_and_no_cause() {
474 let stub = Arc::new(StubRuntime::default());
475 let erased: Arc<dyn Runtime> = stub.clone();
476
477 let outcome = erased
478 .core(Placement::AnyOther)
479 .spawn_with(|| async {}, |_, _| true);
480
481 assert_eq!(
482 outcome,
483 Err(Exhausted {
484 attempts: 0,
485 last: None
486 })
487 );
488 assert_eq!(stub.on_core.load(Ordering::Relaxed), 0);
489 }
490
491 #[test]
494 fn every_candidate_full_reports_exhaustion_not_the_last_error() {
495 let stub = Arc::new(StubRuntime::default());
496 stub.full_for_first.store(9, Ordering::Relaxed);
497 let erased: Arc<dyn Runtime> = stub.clone();
498
499 let outcome = erased.core([0, 1, 2]).spawn_with(|| async {}, |_, _| true);
500
501 assert_eq!(
502 outcome,
503 Err(Exhausted {
504 attempts: 3,
505 last: Some(SpawnError::InboxFull)
506 })
507 );
508 }
509
510 #[test]
511 fn blocking_returns_the_concrete_type_not_an_any_box() {
512 let stub = Arc::new(StubRuntime::default());
513 let erased: Arc<dyn Runtime> = stub.clone();
514
515 let fut = erased.blocking(|| Ok(7u32)).spawn();
516 let value = block_on(fut).expect("background work");
517
518 assert_eq!(value, 7u32);
519 assert_eq!(stub.background.load(Ordering::Relaxed), 1);
520 }
521}