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