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;