Skip to main content

hataori/
lanes.rs

1//! Execution lanes and the coordinator/lane fan-out contract.
2//!
3//! A [`Lanes`] executor owns one execution resource per worker and lends it to a
4//! local fan-out. [`map_in_lanes`] spreads independent items over those lanes;
5//! the hybrid [`crate::pmap`] protocol runs its MPI progress loop on the calling
6//! thread as a coordinator while local batches execute on the lanes.
7//!
8//! [`crate::RayonLanes`] is the Rayon implementation over an admitted
9//! [`crate::Domain`] pool. A backend adapter supplies its own implementation so
10//! that Hataori stays the scheduler while the adapter keeps ownership of its
11//! pool and lane resources.
12
13#[cfg(all(feature = "rayon", any(feature = "mpi", feature = "rsmpi-rt")))]
14pub(crate) mod batch;
15
16use crate::domain::DomainBusy;
17use crate::map::{map, truncate_message, MapError};
18use std::fmt;
19use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
20use std::sync::{Mutex, MutexGuard, PoisonError};
21
22/// A cancellation capability shared by a coordinator and its lanes.
23///
24/// The executor creates one control per run and lends it to the coordinator and
25/// to every lane, so cancellation is visible in both directions: a coordinator
26/// that stops feeding work cancels the lanes, and an executor that observes a
27/// lane failure cancels the coordinator. Cancellation is a request: already
28/// running callback code is not stopped.
29///
30/// An adapter bridges its own cancellation state into the control it observes
31/// and must treat [`LaneControl::cancelled`] as a request to stop feeding work.
32/// A backend whose own executor can stop a run without reaching a lane callback
33/// (a failed child-session entry, for example) lends the side that waits on
34/// cancellation a [`LaneControl::with_probe`] view of the same control, so that
35/// wait also observes the backend's stop signal.
36///
37/// # Example
38///
39/// ```
40/// use hataori::LaneControl;
41///
42/// let control = LaneControl::new();
43/// assert!(!control.cancelled());
44/// control.cancel();
45/// assert!(control.cancelled());
46/// ```
47///
48/// A probe view shares the same cancellation flag and adds an external signal:
49///
50/// ```
51/// use hataori::LaneControl;
52/// use std::sync::atomic::{AtomicBool, Ordering};
53///
54/// let control = LaneControl::new();
55/// let backend_stopped = AtomicBool::new(false);
56/// let probe = || backend_stopped.load(Ordering::Acquire);
57/// let probed = control.with_probe(&probe);
58/// assert!(!probed.cancelled());
59/// backend_stopped.store(true, Ordering::Release);
60/// assert!(probed.cancelled());
61/// assert!(!control.cancelled(), "the probe is not an own-flag cancellation");
62/// probed.cancel();
63/// assert!(control.cancelled(), "views share one cancellation flag");
64/// ```
65pub struct LaneControl<'a> {
66    flag: LaneFlag<'a>,
67    probe: Option<&'a (dyn Fn() -> bool + Sync)>,
68}
69
70/// Where a control's cancellation flag lives.
71///
72/// A control created by an executor owns its flag; a probe view borrows the flag
73/// of the control it was created from, so `cancel` on either view is visible on
74/// both.
75#[derive(Debug)]
76enum LaneFlag<'a> {
77    Owned(AtomicBool),
78    Borrowed(&'a AtomicBool),
79}
80
81impl LaneFlag<'_> {
82    fn flag(&self) -> &AtomicBool {
83        match self {
84            Self::Owned(flag) => flag,
85            Self::Borrowed(flag) => flag,
86        }
87    }
88}
89
90impl fmt::Debug for LaneControl<'_> {
91    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
92        formatter
93            .debug_struct("LaneControl")
94            .field("cancelled", &self.flag.flag().load(Ordering::Acquire))
95            .field("probed", &self.probe.is_some())
96            .finish()
97    }
98}
99
100impl LaneControl<'static> {
101    /// Creates an un-cancelled control that owns its cancellation flag.
102    pub const fn new() -> Self {
103        Self {
104            flag: LaneFlag::Owned(AtomicBool::new(false)),
105            probe: None,
106        }
107    }
108}
109
110impl Default for LaneControl<'static> {
111    fn default() -> Self {
112        Self::new()
113    }
114}
115
116impl<'a> LaneControl<'a> {
117    /// Returns a view of this control that also reports `probe` as cancelled.
118    ///
119    /// The view shares this control's cancellation flag, so a lane and a
120    /// coordinator cannot diverge on an ordinary `cancel`; the probe only adds a
121    /// stop signal the executor owns. [`LaneControl::cancelled`] is evaluated as
122    /// own flag first and then the probe, so a probe must be cheap and
123    /// non-blocking.
124    pub fn with_probe(&'a self, probe: &'a (dyn Fn() -> bool + Sync)) -> Self {
125        Self {
126            flag: LaneFlag::Borrowed(self.flag.flag()),
127            probe: Some(probe),
128        }
129    }
130
131    /// Returns whether cancellation has been requested.
132    pub fn cancelled(&self) -> bool {
133        self.flag.flag().load(Ordering::Acquire) || self.probe.is_some_and(|probe| probe())
134    }
135
136    /// Requests cancellation. This never blocks and never stops running code.
137    pub fn cancel(&self) {
138        self.flag.flag().store(true, Ordering::Release);
139    }
140}
141
142/// A per-worker execution lanes executor plus a caller-thread coordinator.
143///
144/// The trait is implemented by execution backends; it has no `Send`, `Sync`, or
145/// `'static` supertrait, so an executor, its lane resources, a coordinator
146/// capture, and a coordinator result may all stay on one thread. Lane closures
147/// are shared across the lanes and therefore require `Sync`.
148///
149/// Both methods join every lane before returning, and resume a lane panic only
150/// after that join. Lanes observe cancellation through [`LaneControl`] and must
151/// stop when it is cancelled; the executor cancels the control after the
152/// coordinator returns. An executor whose backend can stop a run without
153/// reaching a lane callback lends the side that waits on cancellation a
154/// [`LaneControl::with_probe`] view, so that wait also observes the backend's
155/// own stop signal.
156///
157/// # Example
158///
159/// ```
160/// # #[cfg(feature = "rayon")]
161/// # {
162/// use hataori::{map_in_lanes, Domain, LocalMode, RayonLanes};
163/// use std::sync::Arc;
164///
165/// let pool = Arc::new(rayon::ThreadPoolBuilder::new().num_threads(2).build().unwrap());
166/// let domain = Domain::external(Arc::clone(&pool), vec![0, 1], 2).unwrap();
167/// let mut lanes = RayonLanes::new(&domain).unwrap();
168/// let doubled = map_in_lanes(&mut lanes, vec![1, 2, 3], |item, ()| {
169///     Ok::<_, std::convert::Infallible>(item * 2)
170/// })
171/// .unwrap();
172/// assert_eq!(doubled, vec![2, 4, 6]);
173/// # }
174/// ```
175pub trait Lanes {
176    /// The execution resource lent to one lane callback.
177    type Lane<'l>;
178
179    /// A lane entry, join, or cleanup failure.
180    type Error: std::error::Error + Send + Sync + 'static;
181
182    /// Returns the number of lanes the executor runs.
183    fn lane_count(&self) -> usize;
184
185    /// Runs `lane` on every lane, joins all of them, then resumes a lane panic.
186    ///
187    /// # Errors
188    ///
189    /// A lane error or panic cancels the shared control immediately, so
190    /// cooperatively waiting lanes are released before the join. Returns
191    /// [`LanesError::Lane`] when a lane callback returns an error,
192    /// [`LanesError::Executor`] when the executor itself cannot run or join a
193    /// lane, or [`LanesError::SingleLane`] when the executor has no lane to run.
194    /// A lane panic is resumed after the join and takes precedence over a lane
195    /// error.
196    fn run<E: Send>(
197        &mut self,
198        lane: impl Fn(usize, &mut Self::Lane<'_>, &LaneControl<'_>) -> Result<(), E> + Sync,
199    ) -> Result<(), LanesError<E, Self::Error>>;
200
201    /// Runs `coordinator` on the calling thread concurrently with every lane.
202    ///
203    /// The coordinator is not a lane: it gets no lane resource. Lanes and the
204    /// coordinator communicate only through caller-owned synchronization. When
205    /// the coordinator returns, errors, or panics, the control is cancelled and
206    /// every lane is joined before this method returns.
207    ///
208    /// An executor that cannot overlap a coordinator with its lanes returns
209    /// [`LanesError::SingleLane`] without running either callback, so the caller
210    /// can fall back to a non-concurrent path explicitly. An executor that
211    /// rejects being driven from one of its own workers returns
212    /// [`LanesError::Reentrant`] before running either callback.
213    ///
214    /// # Errors
215    ///
216    /// Returns [`LanesError::SingleLane`] for a non-concurrent executor,
217    /// [`LanesError::Reentrant`] for entry from one of the executor's own
218    /// workers, [`LanesError::Coordinator`] when the coordinator returns an
219    /// error, [`LanesError::Lane`] when a lane callback returns an error, or
220    /// [`LanesError::Executor`] when the executor cannot run or join a lane. A
221    /// lane panic is resumed after the join and takes precedence over every
222    /// ordinary error.
223    fn run_with_coordinator<C, E: Send>(
224        &mut self,
225        coordinator: impl FnOnce(&LaneControl<'_>) -> Result<C, E>,
226        lane: impl Fn(usize, &mut Self::Lane<'_>, &LaneControl<'_>) -> Result<(), E> + Sync,
227    ) -> Result<C, LanesError<E, Self::Error>>;
228}
229
230/// Failure from a [`Lanes`] run.
231#[derive(Debug)]
232pub enum LanesError<E, LE> {
233    /// The executor cannot overlap a coordinator with its lanes.
234    SingleLane,
235    /// A coordinator run was requested from one of the executor's own workers.
236    Reentrant,
237    /// The coordinator callback returned an error.
238    Coordinator(E),
239    /// A lane callback returned an error.
240    Lane(E),
241    /// The executor could not run, join, or clean up a lane.
242    Executor(LE),
243}
244
245impl<E: fmt::Display, LE: fmt::Display> fmt::Display for LanesError<E, LE> {
246    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
247        match self {
248            Self::SingleLane => formatter.write_str("executor has no lane for a concurrent run"),
249            Self::Reentrant => {
250                formatter.write_str("executor cannot run a coordinator from one of its workers")
251            }
252            Self::Coordinator(error) => write!(formatter, "coordinator failed: {error}"),
253            Self::Lane(error) => write!(formatter, "lane failed: {error}"),
254            Self::Executor(error) => write!(formatter, "lane executor failed: {error}"),
255        }
256    }
257}
258
259impl<E: std::error::Error + 'static, LE: std::error::Error + 'static> std::error::Error
260    for LanesError<E, LE>
261{
262    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
263        match self {
264            Self::SingleLane | Self::Reentrant => None,
265            Self::Coordinator(error) | Self::Lane(error) => Some(error),
266            Self::Executor(error) => Some(error),
267        }
268    }
269}
270
271/// A lane executor failure during a local fan-out.
272#[derive(Debug, Clone, Eq, PartialEq)]
273pub struct LaneExecutorError {
274    message: String,
275}
276
277impl LaneExecutorError {
278    pub(crate) fn new(error: &(impl fmt::Display + ?Sized)) -> Self {
279        Self {
280            message: truncate_message(error.to_string()),
281        }
282    }
283
284    pub(crate) fn from_message(message: impl fmt::Display) -> Self {
285        Self::new(&message)
286    }
287
288    /// Returns the bounded executor failure message.
289    pub fn message(&self) -> &str {
290        &self.message
291    }
292}
293
294impl fmt::Display for LaneExecutorError {
295    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
296        formatter.write_str(&self.message)
297    }
298}
299
300impl std::error::Error for LaneExecutorError {}
301
302/// Failure from a local fan-out over [`Lanes`].
303#[derive(Debug)]
304pub enum MapInError {
305    /// The domain has no explicit Rayon pool.
306    MissingPool,
307    /// The call originated on a different Rayon pool.
308    ForeignPool,
309    /// Another operation already owns the domain admission slot.
310    DomainBusy(DomainBusy),
311    /// A callback returned an error.
312    Callback(MapError),
313    /// The lane executor failed to enter or clean up a lane.
314    Executor(LaneExecutorError),
315}
316
317impl fmt::Display for MapInError {
318    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
319        match self {
320            Self::MissingPool => formatter.write_str("domain has no Rayon pool"),
321            Self::ForeignPool => formatter.write_str("map_in called from a foreign Rayon pool"),
322            Self::DomainBusy(error) => error.fmt(formatter),
323            Self::Callback(error) => error.fmt(formatter),
324            Self::Executor(error) => error.fmt(formatter),
325        }
326    }
327}
328
329impl std::error::Error for MapInError {
330    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
331        match self {
332            Self::DomainBusy(error) => Some(error),
333            Self::Callback(error) => Some(error),
334            Self::Executor(error) => Some(error),
335            Self::MissingPool | Self::ForeignPool => None,
336        }
337    }
338}
339
340impl From<DomainBusy> for MapInError {
341    fn from(error: DomainBusy) -> Self {
342        Self::DomainBusy(error)
343    }
344}
345
346impl From<MapError> for MapInError {
347    fn from(error: MapError) -> Self {
348        Self::Callback(error)
349    }
350}
351
352/// State shared by the lanes of one [`map_in_lanes`] fan-out.
353struct FanOut<T, U, E> {
354    items: Vec<Option<T>>,
355    results: Vec<Option<Result<U, E>>>,
356}
357
358impl<T, U, E> FanOut<T, U, E> {
359    fn new(items: Vec<T>) -> Self {
360        let items: Vec<Option<T>> = items.into_iter().map(Some).collect();
361        let results = (0..items.len()).map(|_| None).collect();
362        Self { items, results }
363    }
364}
365
366fn lock<T>(mutex: &Mutex<T>) -> MutexGuard<'_, T> {
367    mutex.lock().unwrap_or_else(PoisonError::into_inner)
368}
369
370/// Maps `items` through a [`Lanes`] executor with `LocalMode::Outer` semantics.
371///
372/// Every input is evaluated exactly once, results keep input order, and the
373/// lowest failing input index is reported after every item has finished.
374/// Lane callbacks pull the next input index from one shared counter and write
375/// their result into the per-index slot, so no Rayon job and no extra heap
376/// allocation is created per item. A callback panic resumes only after every
377/// lane has been joined.
378///
379/// # Errors
380///
381/// Returns [`MapInError::Callback`] with the lowest failing input index, or
382/// [`MapInError::Executor`] when the lane executor itself cannot run or join a
383/// lane.
384///
385/// # Example
386///
387/// ```
388/// # #[cfg(feature = "rayon")]
389/// # {
390/// use hataori::{map_in_lanes, Domain, RayonLanes};
391/// use std::sync::Arc;
392///
393/// let pool = Arc::new(rayon::ThreadPoolBuilder::new().num_threads(2).build().unwrap());
394/// let domain = Domain::external(Arc::clone(&pool), vec![0, 1], 2).unwrap();
395/// let mut lanes = RayonLanes::new(&domain).unwrap();
396/// let tripled = map_in_lanes(&mut lanes, vec![2, 4], |item, ()| {
397///     Ok::<_, std::convert::Infallible>(item * 3)
398/// })
399/// .unwrap();
400/// assert_eq!(tripled, vec![6, 12]);
401/// # }
402/// ```
403pub fn map_in_lanes<L, T, U, E, F>(lanes: &mut L, items: Vec<T>, f: F) -> Result<Vec<U>, MapInError>
404where
405    L: Lanes,
406    T: Send,
407    U: Send,
408    E: fmt::Display + Send,
409    F: Fn(T, &mut L::Lane<'_>) -> Result<U, E> + Sync,
410{
411    let fan_out = Mutex::new(FanOut::new(items));
412    let length = lock(&fan_out).items.len();
413    let next = AtomicUsize::new(0);
414    let outcome = lanes.run(|_lane_index, lane, control| {
415        loop {
416            if control.cancelled() {
417                break;
418            }
419            let index = next.fetch_add(1, Ordering::Relaxed);
420            if index >= length {
421                break;
422            }
423            let item = {
424                let mut state = lock(&fan_out);
425                state.items[index]
426                    .take()
427                    .expect("one lane claims each input index once")
428            };
429            // The slot guard is dropped before the callback runs, so a callback
430            // panic cannot poison the fan-out mutex.
431            let result = f(item, lane);
432            lock(&fan_out).results[index] = Some(result);
433        }
434        Ok::<(), std::convert::Infallible>(())
435    });
436    match outcome {
437        Ok(()) => {}
438        Err(LanesError::Executor(error)) => {
439            return Err(MapInError::Executor(LaneExecutorError::new(&error)))
440        }
441        Err(LanesError::SingleLane) | Err(LanesError::Reentrant) => {
442            return Err(MapInError::Executor(LaneExecutorError::from_message(
443                "executor has no lane for a lane-only run",
444            )))
445        }
446        Err(LanesError::Lane(never)) => match never {},
447        Err(LanesError::Coordinator(never)) => match never {},
448    }
449
450    let mut results = Vec::with_capacity(length);
451    for result in std::mem::take(&mut lock(&fan_out).results) {
452        match result {
453            Some(result) => results.push(result),
454            None => {
455                return Err(MapInError::Executor(LaneExecutorError::from_message(
456                    "lanes stopped before every input was evaluated",
457                )))
458            }
459        }
460    }
461    map(results, |result| result).map_err(MapInError::Callback)
462}
463
464#[cfg(test)]
465mod tests;