use std::{
panic::{AssertUnwindSafe, catch_unwind, resume_unwind},
sync::atomic::Ordering,
};
use moirai_core::{
Priority,
error::{ExecutorError, ExecutorResult},
};
use super::super::super::{class::WorkClass, job::ScheduledJob, reduce::ReduceSlots};
use super::super::scope_state::{SchedulerScopeState, ScopedTaskCompletion};
use super::super::types::{
IndexedRegionGuard, ThreadScheduler, get_current_worker_id, is_in_indexed_region,
};
use super::super::worker::{
indexed_chunk_bounds, indexed_chunk_count, inline_map_reduce, map_reduce_range,
};
fn refusal_runs_inline(error: &ExecutorError, chunk_index: usize) -> bool {
match error {
ExecutorError::ResourceExhausted(_) => true,
ExecutorError::ShuttingDown => chunk_index > 1,
_ => false,
}
}
fn execute_catching_panic<T>(operation: impl FnOnce() -> T) -> ExecutorResult<T> {
catch_unwind(AssertUnwindSafe(operation))
.map_err(|_| ExecutorError::SpawnFailed(moirai_core::error::TaskError::Panicked))
}
impl<const BLOCKING_QUEUE_CAPACITY: usize, const SPIN_LIMIT: usize>
ThreadScheduler<BLOCKING_QUEUE_CAPACITY, SPIN_LIMIT>
{
pub fn for_each_indexed<C, F>(
&self,
priority: Priority,
locality_hint: Option<usize>,
count: usize,
task: F,
) -> ExecutorResult<()>
where
C: WorkClass,
F: Fn(usize) + Send + Sync,
{
if self.inner.shutdown.load(Ordering::Acquire) {
return Err(ExecutorError::ShuttingDown);
}
if count == 0 {
return Ok(());
}
if get_current_worker_id().is_some() || is_in_indexed_region() {
return execute_catching_panic(|| {
for index in 0..count {
task(index);
}
});
}
let chunk_count = indexed_chunk_count(count, self.worker_count());
let (_, caller_end) = indexed_chunk_bounds(count, chunk_count, 0);
if chunk_count == 1 {
return execute_catching_panic(|| {
let _region = IndexedRegionGuard::enter();
for index in 0..caller_end {
task(index);
}
});
}
let state = SchedulerScopeState::new(&self.inner.wait_lock, &self.inner.wait_signal);
let task = &task;
let mut schedule_result = Ok(());
let mut inline_result = Ok(());
for chunk_index in 1..chunk_count {
let (start, end) = indexed_chunk_bounds(count, chunk_count, chunk_index);
state.register_task();
let completion = ScopedTaskCompletion::new(&state);
let chunk_task = move |_| {
for index in start..end {
task(index);
}
};
let complete = move |succeeded: bool| completion.finish(succeeded);
if let Err(error) = self.schedule_indexed_job::<C, _, _>(
&state,
priority,
locality_hint,
chunk_task,
complete,
) {
state.forgive_refused_job();
if refusal_runs_inline(&error, chunk_index) {
self.record_admission_caller_run();
inline_result = execute_catching_panic(|| {
for index in start..end {
task(index);
}
});
if inline_result.is_err() {
break;
}
} else {
schedule_result = Err(error);
break;
}
}
}
let caller_result = if schedule_result.is_ok() && inline_result.is_ok() {
execute_catching_panic(|| {
let _region = IndexedRegionGuard::enter();
for index in 0..caller_end {
task(index);
}
})
} else {
Ok(())
};
self.drain_scope(&state);
if state.has_panicked() || inline_result.is_err() || caller_result.is_err() {
Err(ExecutorError::SpawnFailed(
moirai_core::error::TaskError::Panicked,
))
} else {
schedule_result?;
state.unrun_job_result()
}
}
pub fn map_reduce_indexed<C, T, Map, Reduce>(
&self,
priority: Priority,
locality_hint: Option<usize>,
count: usize,
identity: T,
map: Map,
reduce: Reduce,
) -> ExecutorResult<T>
where
C: WorkClass,
T: Send + Clone,
Map: Fn(usize) -> T + Send + Sync,
Reduce: Fn(T, T) -> T + Send + Sync,
{
if self.inner.shutdown.load(Ordering::Acquire) {
return Err(ExecutorError::ShuttingDown);
}
if count == 0 {
return Ok(identity);
}
if get_current_worker_id().is_some() || is_in_indexed_region() {
return inline_map_reduce(count, identity, map, reduce);
}
let chunk_count = indexed_chunk_count(count, self.worker_count());
let (_, caller_end) = indexed_chunk_bounds(count, chunk_count, 0);
if chunk_count == 1 {
let _region = IndexedRegionGuard::enter();
return inline_map_reduce(count, identity, map, reduce);
}
let state = SchedulerScopeState::new(&self.inner.wait_lock, &self.inner.wait_signal);
let slots = ReduceSlots::new(chunk_count - 1);
let map = ↦
let reduce = &reduce;
let mut schedule_result = Ok(());
let mut inline_result = Ok(());
for chunk_index in 1..chunk_count {
let (start, end) = indexed_chunk_bounds(count, chunk_count, chunk_index);
let identity_chunk = match execute_catching_panic(|| identity.clone()) {
Ok(identity) => identity,
Err(error) => {
inline_result = Err(error);
break;
}
};
state.register_task();
let completion = ScopedTaskCompletion::new(&state);
let slots_chunk = &slots;
let chunk_task = move |_| {
let accumulator = map_reduce_range(start, end, identity_chunk, map, reduce);
slots_chunk.write(chunk_index - 1, accumulator);
};
let complete = move |succeeded: bool| completion.finish(succeeded);
if let Err(error) = self.schedule_indexed_job::<C, _, _>(
&state,
priority,
locality_hint,
chunk_task,
complete,
) {
state.forgive_refused_job();
if refusal_runs_inline(&error, chunk_index) {
self.record_admission_caller_run();
inline_result = execute_catching_panic(|| {
let accumulator =
map_reduce_range(start, end, identity.clone(), map, reduce);
slots.write(chunk_index - 1, accumulator);
});
if inline_result.is_err() {
break;
}
} else {
schedule_result = Err(error);
break;
}
}
}
let caller_result = (schedule_result.is_ok() && inline_result.is_ok()).then(|| {
execute_catching_panic(|| {
let _region = IndexedRegionGuard::enter();
map_reduce_range(0, caller_end, identity.clone(), map, reduce)
})
});
self.drain_scope(&state);
if state.has_panicked() || inline_result.is_err() {
Err(ExecutorError::SpawnFailed(
moirai_core::error::TaskError::Panicked,
))
} else {
schedule_result?;
state.unrun_job_result()?;
let caller_result = caller_result
.expect("invariant: caller reduction runs after successful scheduling")?;
Ok(slots.reduce(caller_result, reduce))
}
}
fn schedule_indexed_job<'scope, C, F, Complete>(
&self,
state: &'scope SchedulerScopeState<'scope>,
priority: Priority,
locality_hint: Option<usize>,
scoped_job: F,
complete: Complete,
) -> ExecutorResult<()>
where
C: WorkClass,
F: FnOnce(usize) + Send + 'scope,
Complete: FnOnce(bool) + Send + 'scope,
{
match catch_unwind(AssertUnwindSafe(|| {
let job = unsafe { ScheduledJob::new_scoped_with_completion(scoped_job, complete) };
self.schedule_job::<C>(priority, locality_hint, job)
})) {
Ok(result) => result,
Err(payload) => {
self.drain_scope(state);
resume_unwind(payload);
}
}
}
}