Skip to main content

hawdb_executor/
concurrent.rs

1// Copyright 2026 Nowledge
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use hawdb_core::error::FileDescriptorError;
16use hawdb_core::temporal::{pinned_statement_clock, with_optional_statement_clock};
17use hawdb_core::{
18    HawDBError, Result as HawDBResult, RuntimeCancellationReason, RuntimeTaskContext,
19};
20use std::collections::BTreeMap;
21use std::error::Error;
22use std::fmt::{self, Display, Formatter};
23use std::num::NonZeroUsize;
24use std::panic::{catch_unwind, AssertUnwindSafe};
25use std::sync::atomic::{AtomicUsize, Ordering};
26use std::sync::mpsc::{self, RecvTimeoutError};
27use std::sync::{Arc, Condvar, Mutex, MutexGuard, OnceLock};
28use std::time::Duration;
29
30const ORDERED_STREAM_CHECK_INTERVAL: Duration = Duration::from_millis(10);
31const SHARED_EXECUTOR_WORKER_LIMIT: usize = 16;
32
33#[derive(Clone)]
34pub struct SharedExecutorPool {
35    inner: Arc<rayon::ThreadPool>,
36    worker_count: NonZeroUsize,
37}
38
39impl fmt::Debug for SharedExecutorPool {
40    fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
41        formatter
42            .debug_struct("SharedExecutorPool")
43            .field("worker_count", &self.worker_count)
44            .finish_non_exhaustive()
45    }
46}
47
48impl SharedExecutorPool {
49    #[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
50    pub fn new(_worker_count: NonZeroUsize) -> Result<Self, SharedExecutorPoolError> {
51        // Select the existing bounded sequential paths without attempting a spawn.
52        Err(SharedExecutorPoolError(
53            "browser WASM uses serial execution".into(),
54        ))
55    }
56
57    #[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))]
58    pub fn new(worker_count: NonZeroUsize) -> Result<Self, SharedExecutorPoolError> {
59        let inner = rayon::ThreadPoolBuilder::new()
60            .num_threads(worker_count.get())
61            .thread_name(|index| format!("hawdb-executor-{index}"))
62            .build()
63            .map_err(|error| SharedExecutorPoolError(error.to_string()))?;
64        Ok(Self {
65            inner: Arc::new(inner),
66            worker_count,
67        })
68    }
69
70    pub fn shared_default() -> Result<Self, SharedExecutorPoolError> {
71        Self::shared_bounded(default_shared_worker_count())
72    }
73
74    pub fn shared_bounded(worker_limit: NonZeroUsize) -> Result<Self, SharedExecutorPoolError> {
75        static SHARED: OnceLock<
76            Mutex<BTreeMap<usize, Result<SharedExecutorPool, SharedExecutorPoolError>>>,
77        > = OnceLock::new();
78        let worker_count = default_shared_worker_count().min(worker_limit);
79        let mut shared = SHARED
80            .get_or_init(|| Mutex::new(BTreeMap::new()))
81            .lock()
82            .unwrap_or_else(|poisoned| poisoned.into_inner());
83        shared
84            .entry(worker_count.get())
85            .or_insert_with(|| Self::new(worker_count))
86            .clone()
87    }
88
89    pub fn worker_count(&self) -> usize {
90        self.worker_count.get()
91    }
92}
93
94#[derive(Debug, Clone, PartialEq, Eq)]
95pub struct SharedExecutorPoolError(String);
96
97impl Display for SharedExecutorPoolError {
98    fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
99        write!(
100            formatter,
101            "failed to build shared executor pool: {}",
102            self.0
103        )
104    }
105}
106
107impl Error for SharedExecutorPoolError {}
108
109fn default_shared_worker_count() -> NonZeroUsize {
110    std::thread::available_parallelism()
111        .unwrap_or(NonZeroUsize::MIN)
112        .min(
113            NonZeroUsize::new(SHARED_EXECUTOR_WORKER_LIMIT)
114                .expect("shared worker limit is non-zero"),
115        )
116}
117
118#[derive(Debug, Clone)]
119pub struct BoundedExecutor {
120    max_parallelism: NonZeroUsize,
121    pool: Result<SharedExecutorPool, SharedExecutorPoolError>,
122}
123
124impl BoundedExecutor {
125    pub fn new(max_parallelism: NonZeroUsize) -> Self {
126        Self {
127            max_parallelism,
128            pool: SharedExecutorPool::shared_bounded(max_parallelism),
129        }
130    }
131
132    pub fn with_pool(max_parallelism: NonZeroUsize, pool: SharedExecutorPool) -> Self {
133        Self {
134            max_parallelism,
135            pool: Ok(pool),
136        }
137    }
138
139    pub fn max_parallelism(&self) -> usize {
140        self.max_parallelism.get()
141    }
142
143    pub fn is_degraded(&self) -> bool {
144        self.pool.is_err()
145    }
146
147    pub fn pool_error(&self) -> Option<&SharedExecutorPoolError> {
148        self.pool.as_ref().err()
149    }
150
151    /// Materializes one output per input in input order, regardless of completion
152    /// order. An output that is itself a `Result` does not short-circuit workers.
153    /// This bounds worker concurrency, not the collected vector's memory.
154    pub fn map_ordered<T, R, F>(self, inputs: &[T], operation: F) -> Vec<R>
155    where
156        T: Sync,
157        R: Send,
158        F: Fn(&T) -> R + Sync,
159    {
160        if inputs.is_empty() {
161            return Vec::new();
162        }
163        let Ok(pool) = &self.pool else {
164            return inputs.iter().map(operation).collect();
165        };
166
167        let worker_count = self.max_parallelism().min(inputs.len());
168        let next = AtomicUsize::new(0);
169        let outputs = Mutex::new(
170            std::iter::repeat_with(|| None)
171                .take(inputs.len())
172                .collect::<Vec<Option<R>>>(),
173        );
174
175        // Workers observe the caller's statement clock.
176        let statement_clock = pinned_statement_clock();
177        let run = || {
178            rayon::scope(|scope| {
179                for _ in 0..worker_count {
180                    scope.spawn(|_| loop {
181                        let index = next.fetch_add(1, Ordering::Relaxed);
182                        let Some(input) = inputs.get(index) else {
183                            break;
184                        };
185                        let output =
186                            with_optional_statement_clock(statement_clock, || operation(input));
187                        outputs
188                            .lock()
189                            .expect("bounded executor output lock should not be poisoned")[index] =
190                            Some(output);
191                    });
192                }
193            });
194        };
195        pool.inner.install(run);
196
197        outputs
198            .into_inner()
199            .expect("bounded executor output lock should not be poisoned")
200            .into_iter()
201            .map(|output| output.expect("each bounded executor input must produce one output"))
202            .collect()
203    }
204
205    /// The materializing map with cooperative cancellation checks. In-flight
206    /// callbacks may finish after cancellation; no partial vector is returned.
207    pub fn map_ordered_with_context<T, R, F>(
208        self,
209        inputs: &[T],
210        context: &RuntimeTaskContext,
211        operation: F,
212    ) -> Result<Vec<R>, RuntimeCancellationReason>
213    where
214        T: Sync,
215        R: Send,
216        F: Fn(&T) -> R + Sync,
217    {
218        context.checkpoint()?;
219        if inputs.is_empty() {
220            return Ok(Vec::new());
221        }
222        let Ok(pool) = &self.pool else {
223            let output = inputs
224                .iter()
225                .map(|input| {
226                    context.checkpoint()?;
227                    Ok(operation(input))
228                })
229                .collect::<Result<Vec<_>, RuntimeCancellationReason>>()?;
230            context.checkpoint()?;
231            return Ok(output);
232        };
233
234        let worker_count = self.max_parallelism().min(inputs.len());
235        let next = AtomicUsize::new(0);
236        let stopped = Mutex::new(None);
237        let outputs = Mutex::new(
238            std::iter::repeat_with(|| None)
239                .take(inputs.len())
240                .collect::<Vec<Option<R>>>(),
241        );
242
243        // Workers observe the caller's statement clock.
244        let statement_clock = pinned_statement_clock();
245        let run = || {
246            rayon::scope(|scope| {
247                for _ in 0..worker_count {
248                    scope.spawn(|_| loop {
249                        if let Err(reason) = context.checkpoint() {
250                            let mut stopped = stopped.lock().expect(
251                                "bounded executor cancellation lock should not be poisoned",
252                            );
253                            stopped.get_or_insert(reason);
254                            break;
255                        }
256                        let index = next.fetch_add(1, Ordering::Relaxed);
257                        let Some(input) = inputs.get(index) else {
258                            break;
259                        };
260                        let output =
261                            with_optional_statement_clock(statement_clock, || operation(input));
262                        outputs
263                            .lock()
264                            .expect("bounded executor output lock should not be poisoned")[index] =
265                            Some(output);
266                    });
267                }
268            });
269        };
270        pool.inner.install(run);
271
272        if let Some(reason) = *stopped
273            .lock()
274            .expect("bounded executor cancellation lock should not be poisoned")
275        {
276            return Err(reason);
277        }
278        context.checkpoint()?;
279        Ok(outputs
280            .into_inner()
281            .expect("bounded executor output lock should not be poisoned")
282            .into_iter()
283            .map(|output| output.expect("each bounded executor input must produce one output"))
284            .collect())
285    }
286
287    pub(crate) fn try_for_each_index_ordered<R, F, C>(
288        &self,
289        input_count: usize,
290        context: Option<&RuntimeTaskContext>,
291        operation: F,
292        mut consume: C,
293    ) -> HawDBResult<BoundedOrderedStreamReport>
294    where
295        R: Send,
296        F: Fn(usize) -> HawDBResult<R> + Sync,
297        C: FnMut(usize, R) -> HawDBResult<BoundedOrderedStreamControl>,
298    {
299        runtime_checkpoint(context)?;
300        if input_count == 0 {
301            return Ok(BoundedOrderedStreamReport::default());
302        }
303
304        let Ok(pool) = &self.pool else {
305            return run_sequential_index_stream(input_count, context, &operation, &mut consume);
306        };
307        let worker_count = self
308            .max_parallelism()
309            .min(pool.worker_count())
310            .min(input_count);
311        if worker_count == 1 {
312            return run_sequential_index_stream(input_count, context, &operation, &mut consume);
313        }
314
315        let window = OrderedWorkWindow {
316            state: Mutex::new(OrderedWorkState {
317                next_index: 0,
318                consumed_prefix: 0,
319                stopped: false,
320            }),
321            changed: Condvar::new(),
322        };
323        let (sender, receiver) = mpsc::sync_channel(worker_count);
324        let mut reorder = BTreeMap::new();
325        let mut report = BoundedOrderedStreamReport::default();
326
327        // Workers observe the caller's statement clock.
328        let statement_clock = pinned_statement_clock();
329        let result = pool.inner.in_place_scope(|scope| {
330            for _ in 0..worker_count {
331                let sender = sender.clone();
332                let window = &window;
333                let operation = &operation;
334                scope.spawn(move |_| {
335                    while let Some(index) =
336                        claim_ordered_index(window, input_count, worker_count, context)
337                    {
338                    if let Err(reason) = context_checkpoint(context) {
339                        let _ = sender.send(OrderedWorkerMessage::Stopped(reason));
340                        break;
341                    }
342                    let output = catch_unwind(AssertUnwindSafe(|| with_optional_statement_clock(statement_clock, || operation(index))))
343                        .unwrap_or_else(|_| {
344                            Err(HawDBError::Execution(format!(
345                                "bounded executor worker panicked at input index {index}"
346                            )))
347                        });
348                    if sender
349                        .send(OrderedWorkerMessage::Output(index, output))
350                        .is_err()
351                    {
352                        break;
353                    }
354                    }
355                });
356            }
357            drop(sender);
358
359            let mut next_expected = 0usize;
360            let result = 'receive: loop {
361                if let Err(reason) = context_checkpoint(context) {
362                    break Err(runtime_stopped_error(reason));
363                }
364                match receiver.recv_timeout(ORDERED_STREAM_CHECK_INTERVAL) {
365                    Ok(OrderedWorkerMessage::Output(index, output)) => {
366                        let output = match output {
367                            Ok(output) => output,
368                            Err(error) => break Err(error),
369                        };
370                        if reorder.insert(index, output).is_some() {
371                            break Err(HawDBError::Execution(format!(
372                                "bounded executor produced duplicate output index {index}"
373                            )));
374                        }
375                        report.peak_reorder_entries =
376                            report.peak_reorder_entries.max(reorder.len());
377
378                        while let Some(output) = reorder.remove(&next_expected) {
379                            let control = match catch_unwind(AssertUnwindSafe(|| {
380                                consume(next_expected, output)
381                            }))
382                            .unwrap_or_else(|_| {
383                                Err(HawDBError::Execution(format!(
384                                    "bounded executor consumer panicked at input index {next_expected}"
385                                )))
386                            }) {
387                                Ok(control) => control,
388                                Err(error) => break 'receive Err(error),
389                            };
390                            next_expected = next_expected.saturating_add(1);
391                            if control == BoundedOrderedStreamControl::Stop {
392                                report.stopped_early = true;
393                                break;
394                            }
395                            advance_ordered_prefix(&window, next_expected);
396                        }
397                        if report.stopped_early {
398                            break Ok(report);
399                        }
400                        if next_expected == input_count {
401                            break Ok(report);
402                        }
403                    }
404                    Ok(OrderedWorkerMessage::Stopped(reason)) => {
405                        break Err(runtime_stopped_error(reason));
406                    }
407                    Err(RecvTimeoutError::Timeout) => continue,
408                    Err(RecvTimeoutError::Disconnected) => {
409                        break Err(HawDBError::Execution(format!(
410                            "bounded executor stopped after {next_expected} of {input_count} ordered outputs"
411                        )));
412                    }
413                }
414            };
415
416            stop_ordered_window(&window);
417            if let Err(error) = &result {
418                cancel_resource_exhaustion(error, context);
419            }
420            drop(receiver);
421            result
422        });
423        match result {
424            Ok(report) => {
425                runtime_checkpoint(context)?;
426                Ok(report)
427            }
428            Err(error) => Err(error),
429        }
430    }
431}
432
433#[derive(Debug, Clone, Copy, PartialEq, Eq)]
434pub(crate) enum BoundedOrderedStreamControl {
435    Continue,
436    Stop,
437}
438
439#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
440pub(crate) struct BoundedOrderedStreamReport {
441    pub(crate) peak_reorder_entries: usize,
442    pub(crate) stopped_early: bool,
443}
444
445struct OrderedWorkWindow {
446    state: Mutex<OrderedWorkState>,
447    changed: Condvar,
448}
449
450struct OrderedWorkState {
451    next_index: usize,
452    consumed_prefix: usize,
453    stopped: bool,
454}
455
456enum OrderedWorkerMessage<R> {
457    Output(usize, HawDBResult<R>),
458    Stopped(RuntimeCancellationReason),
459}
460
461fn run_sequential_index_stream<R, F, C>(
462    input_count: usize,
463    context: Option<&RuntimeTaskContext>,
464    operation: &F,
465    consume: &mut C,
466) -> HawDBResult<BoundedOrderedStreamReport>
467where
468    F: Fn(usize) -> HawDBResult<R> + Sync,
469    C: FnMut(usize, R) -> HawDBResult<BoundedOrderedStreamControl>,
470{
471    let mut report = BoundedOrderedStreamReport::default();
472    for index in 0..input_count {
473        runtime_checkpoint(context)?;
474        let output = operation(index).inspect_err(|error| {
475            cancel_resource_exhaustion(error, context);
476        })?;
477        report.peak_reorder_entries = 1;
478        if consume(index, output).inspect_err(|error| {
479            cancel_resource_exhaustion(error, context);
480        })? == BoundedOrderedStreamControl::Stop
481        {
482            report.stopped_early = true;
483            break;
484        }
485    }
486    runtime_checkpoint(context)?;
487    Ok(report)
488}
489
490pub(crate) fn is_resource_exhaustion(error: &HawDBError) -> bool {
491    match error {
492        HawDBError::ResourceBudgetExceeded(_)
493        | HawDBError::ReadBudgetExceeded(_)
494        | HawDBError::GraphExpansionCandidateLimitExceeded { .. }
495        | HawDBError::GraphExpansionPayloadLimitExceeded { .. }
496        | HawDBError::FileDescriptors(
497            FileDescriptorError::BudgetExceeded { .. } | FileDescriptorError::OsLimit { .. },
498        ) => true,
499        HawDBError::BranchReclamationBlocked { cause, .. } => is_resource_exhaustion(cause),
500        _ => false,
501    }
502}
503
504fn cancel_resource_exhaustion(error: &HawDBError, context: Option<&RuntimeTaskContext>) {
505    if is_resource_exhaustion(error)
506        && let Some(context) = context
507    {
508        context.cancellation().cancel_for_resource_exhaustion();
509    }
510}
511
512fn claim_ordered_index(
513    window: &OrderedWorkWindow,
514    input_count: usize,
515    worker_count: usize,
516    context: Option<&RuntimeTaskContext>,
517) -> Option<usize> {
518    let mut state = lock_recover(&window.state);
519    loop {
520        if state.stopped || context_checkpoint(context).is_err() {
521            state.stopped = true;
522            window.changed.notify_all();
523            return None;
524        }
525        let window_end = state.consumed_prefix.saturating_add(worker_count);
526        if state.next_index < input_count && state.next_index < window_end {
527            let index = state.next_index;
528            state.next_index = state.next_index.saturating_add(1);
529            return Some(index);
530        }
531        if state.next_index == input_count {
532            return None;
533        }
534        let (next_state, _) = window
535            .changed
536            .wait_timeout(state, ORDERED_STREAM_CHECK_INTERVAL)
537            .unwrap_or_else(|poisoned| poisoned.into_inner());
538        state = next_state;
539    }
540}
541
542fn advance_ordered_prefix(window: &OrderedWorkWindow, consumed_prefix: usize) {
543    let mut state = lock_recover(&window.state);
544    state.consumed_prefix = state.consumed_prefix.max(consumed_prefix);
545    window.changed.notify_all();
546}
547
548fn stop_ordered_window(window: &OrderedWorkWindow) {
549    let mut state = lock_recover(&window.state);
550    state.stopped = true;
551    window.changed.notify_all();
552}
553
554fn context_checkpoint(
555    context: Option<&RuntimeTaskContext>,
556) -> Result<(), RuntimeCancellationReason> {
557    context.map_or(Ok(()), RuntimeTaskContext::checkpoint)
558}
559
560fn runtime_checkpoint(context: Option<&RuntimeTaskContext>) -> HawDBResult<()> {
561    context_checkpoint(context).map_err(runtime_stopped_error)
562}
563
564fn runtime_stopped_error(reason: RuntimeCancellationReason) -> HawDBError {
565    HawDBError::Execution(format!("runtime task stopped: {reason}"))
566}
567
568fn lock_recover<T>(mutex: &Mutex<T>) -> MutexGuard<'_, T> {
569    mutex
570        .lock()
571        .unwrap_or_else(|poisoned| poisoned.into_inner())
572}
573
574impl Default for BoundedExecutor {
575    fn default() -> Self {
576        Self::new(NonZeroUsize::MIN)
577    }
578}
579
580#[cfg(test)]
581mod tests {
582    use super::{BoundedExecutor, SharedExecutorPool, SharedExecutorPoolError};
583    use hawdb_core::{RuntimeCancellationReason, RuntimeCancellationToken, RuntimeTaskContext};
584    use std::num::NonZeroUsize;
585    use std::sync::atomic::{AtomicUsize, Ordering};
586
587    #[test]
588    fn parallel_map_preserves_input_order() {
589        let executor = BoundedExecutor::new(NonZeroUsize::new(4).unwrap());
590        let output = executor.map_ordered(&[3, 1, 4, 2], |value| value * value);
591        assert_eq!(output, vec![9, 1, 16, 4]);
592    }
593
594    #[test]
595    fn shared_pool_respects_the_requested_worker_limit() {
596        let default_workers = SharedExecutorPool::shared_default().unwrap().worker_count();
597        let bounded_workers = SharedExecutorPool::shared_bounded(NonZeroUsize::new(2).unwrap())
598            .unwrap()
599            .worker_count();
600
601        assert_eq!(bounded_workers, default_workers.min(2));
602    }
603
604    #[test]
605    fn bounded_executor_exposes_pool_degradation() {
606        let executor = BoundedExecutor {
607            max_parallelism: NonZeroUsize::new(2).unwrap(),
608            pool: Err(SharedExecutorPoolError("injected failure".to_string())),
609        };
610
611        assert!(executor.is_degraded());
612        assert!(executor.pool_error().is_some());
613        assert_eq!(executor.map_ordered(&[1, 2], |value| value * 2), [2, 4]);
614    }
615
616    #[test]
617    fn parallel_map_respects_the_worker_bound() {
618        let executor = BoundedExecutor::new(NonZeroUsize::new(2).unwrap());
619        let active = AtomicUsize::new(0);
620        let peak = AtomicUsize::new(0);
621
622        let output = executor.map_ordered(&[1, 2, 3, 4], |value| {
623            let current = active.fetch_add(1, Ordering::SeqCst) + 1;
624            peak.fetch_max(current, Ordering::SeqCst);
625            std::thread::yield_now();
626            active.fetch_sub(1, Ordering::SeqCst);
627            value * 2
628        });
629
630        assert_eq!(output, vec![2, 4, 6, 8]);
631        assert!(peak.load(Ordering::SeqCst) <= 2);
632    }
633
634    #[test]
635    fn controlled_parallel_map_stops_between_inputs() {
636        let executor = BoundedExecutor::new(NonZeroUsize::MIN);
637        let token = RuntimeCancellationToken::new();
638        let context = RuntimeTaskContext::without_deadline(token.clone());
639        let visited = AtomicUsize::new(0);
640
641        let result = executor.map_ordered_with_context(&[1, 2, 3], &context, |value| {
642            visited.fetch_add(1, Ordering::SeqCst);
643            if *value == 1 {
644                token.cancel();
645            }
646            value * 2
647        });
648
649        assert_eq!(result, Err(RuntimeCancellationReason::Cancelled));
650        assert_eq!(visited.load(Ordering::SeqCst), 1);
651    }
652
653    #[test]
654    fn map_ordered_matches_the_sequential_fallback_under_reversed_durations() {
655        let inputs = [0_usize, 1, 2, 3, 4, 5];
656        let operation = |value: &usize| {
657            std::thread::sleep(std::time::Duration::from_millis(
658                (inputs.len() - 1 - value) as u64 * 4,
659            ));
660            value * value
661        };
662        let sequential = BoundedExecutor {
663            max_parallelism: NonZeroUsize::new(2).unwrap(),
664            pool: Err(SharedExecutorPoolError("injected failure".to_string())),
665        };
666        let expected = sequential.map_ordered(&inputs, operation);
667
668        let completed = AtomicUsize::new(0);
669        let output = BoundedExecutor::new(NonZeroUsize::MIN).map_ordered(&inputs, |value| {
670            completed.fetch_add(1, Ordering::SeqCst);
671            operation(value)
672        });
673
674        assert_eq!(output, expected);
675        assert_eq!(output, inputs.map(|value| value * value));
676        assert_eq!(completed.load(Ordering::SeqCst), inputs.len());
677    }
678
679    #[test]
680    fn controlled_map_ordered_matches_the_uncontrolled_output() {
681        let inputs = [4_usize, 3, 2, 1];
682        let operation = |value: &usize| {
683            std::thread::sleep(std::time::Duration::from_millis(*value as u64 * 3));
684            value + 10
685        };
686        let uncontrolled = BoundedExecutor::new(NonZeroUsize::MIN).map_ordered(&inputs, operation);
687        let context = RuntimeTaskContext::without_deadline(RuntimeCancellationToken::new());
688
689        let controlled = BoundedExecutor::new(NonZeroUsize::MIN)
690            .map_ordered_with_context(&inputs, &context, operation);
691
692        assert_eq!(controlled, Ok(uncontrolled));
693    }
694
695    #[test]
696    fn controlled_map_ordered_rejects_a_cancelled_context_before_running_work() {
697        let token = RuntimeCancellationToken::new();
698        token.cancel();
699        let context = RuntimeTaskContext::without_deadline(token);
700        let visited = AtomicUsize::new(0);
701
702        let result = BoundedExecutor::new(NonZeroUsize::MIN).map_ordered_with_context(
703            &[1, 2, 3],
704            &context,
705            |value| {
706                visited.fetch_add(1, Ordering::SeqCst);
707                value * 2
708            },
709        );
710
711        assert_eq!(result, Err(RuntimeCancellationReason::Cancelled));
712        assert_eq!(visited.load(Ordering::SeqCst), 0);
713    }
714}