Skip to main content

weavatrix_scan/stateful_walk/parallel/
mod.rs

1use super::{DirectoryProcessor, StatefulWalkBuilder, StatefulWalkEntry};
2use crate::control::CancellationToken;
3use crate::runtime::ParallelRuntime;
4use crate::walk_types::{
5    DirectoryIdentity, ErrorPolicy, FileSystemId, WalkError, WalkOperation, WalkOptions,
6    WalkSkipReason,
7};
8use crate::walker::Walker;
9use std::any::Any;
10use std::collections::{HashMap, HashSet, VecDeque};
11use std::path::PathBuf;
12use std::sync::Arc;
13use std::sync::mpsc::{self, Receiver, SyncSender, sync_channel};
14use std::thread::JoinHandle;
15
16mod execution;
17mod scheduler;
18
19use execution::{run_parallel, run_serial};
20
21struct DirectoryTask<R> {
22    id: u64,
23    path: PathBuf,
24    depth: usize,
25    identity: Option<DirectoryIdentity>,
26    ancestors: Arc<HashSet<DirectoryIdentity>>,
27    read_state: R,
28}
29
30struct WorkerResult<R, E> {
31    id: u64,
32    outcome: Result<DirectoryBatch<R, E>, Box<dyn Any + Send>>,
33}
34
35struct DirectoryBatch<R, E> {
36    entries: Vec<Result<StatefulWalkEntry<E>, WalkError>>,
37    child_state: R,
38    ancestors: Arc<HashSet<DirectoryIdentity>>,
39}
40
41struct PreparedItem<E> {
42    item: Result<StatefulWalkEntry<E>, WalkError>,
43    child: Option<u64>,
44}
45
46struct DirectoryFrame<E> {
47    items: std::vec::IntoIter<PreparedItem<E>>,
48}
49
50struct OrderedStatefulScheduler<R, E> {
51    root: Arc<PathBuf>,
52    root_file_system: Option<FileSystemId>,
53    options: WalkOptions,
54    processor: Option<DirectoryProcessor<R, E>>,
55    cancellation: CancellationToken,
56    runtime: ParallelRuntime,
57    limit: usize,
58    next_id: u64,
59    queued: VecDeque<DirectoryTask<R>>,
60    outstanding: usize,
61    ready: HashMap<u64, DirectoryBatch<R, E>>,
62    result_sender: mpsc::Sender<WorkerResult<R, E>>,
63    result_receiver: mpsc::Receiver<WorkerResult<R, E>>,
64    schedule_error: Option<WalkError>,
65}
66
67/// Bounded stateful pull iterator with parallel directory processing and
68/// strict deterministic depth-first output.
69pub struct ParallelStatefulWalker<E> {
70    receiver: Option<Receiver<Result<StatefulWalkEntry<E>, WalkError>>>,
71    cancellation: CancellationToken,
72    coordinator: Option<JoinHandle<()>>,
73}
74
75impl<E> ParallelStatefulWalker<E> {
76    pub(super) fn start<R>(
77        builder: StatefulWalkBuilder<R, E>,
78        capacity: usize,
79    ) -> Result<Self, WalkError>
80    where
81        R: Clone + Send + 'static,
82        E: Default + Send + 'static,
83    {
84        let root = builder.root.clone();
85        let use_serial = builder.runtime.is_worker_thread();
86        let (sender, receiver) = sync_channel(capacity.max(1));
87        let cancellation = CancellationToken::new();
88        let coordinator_cancellation = cancellation.clone();
89        let coordinator = std::thread::Builder::new()
90            .name("weavatrix-scan-stateful".to_owned())
91            .spawn(move || {
92                if use_serial {
93                    run_serial(builder, &coordinator_cancellation, &sender);
94                } else {
95                    run_parallel(builder, &coordinator_cancellation, &sender);
96                }
97            })
98            .map_err(|source| WalkError::new(root, 0, WalkOperation::ScheduleWorker, source))?;
99        Ok(Self {
100            receiver: Some(receiver),
101            cancellation,
102            coordinator: Some(coordinator),
103        })
104    }
105
106    fn join_coordinator(&mut self) {
107        if let Some(coordinator) = self.coordinator.take() {
108            coordinator
109                .join()
110                .expect("parallel stateful coordinator panicked");
111        }
112    }
113}
114
115impl<E> Iterator for ParallelStatefulWalker<E> {
116    type Item = Result<StatefulWalkEntry<E>, WalkError>;
117
118    fn next(&mut self) -> Option<Self::Item> {
119        if let Ok(item) = self.receiver.as_ref()?.recv() {
120            Some(item)
121        } else {
122            self.receiver.take();
123            self.join_coordinator();
124            None
125        }
126    }
127}
128
129impl<E> Drop for ParallelStatefulWalker<E> {
130    fn drop(&mut self) {
131        self.receiver.take();
132        self.cancellation.cancel();
133        self.join_coordinator();
134    }
135}
136
137fn read_directory<R, E>(
138    root: &Arc<PathBuf>,
139    root_file_system: Option<FileSystemId>,
140    options: WalkOptions,
141    cancellation: &CancellationToken,
142    processor: Option<&DirectoryProcessor<R, E>>,
143    mut task: DirectoryTask<R>,
144) -> DirectoryBatch<R, E>
145where
146    R: Clone + Send + 'static,
147    E: Default + Send + 'static,
148{
149    let ancestors = Arc::clone(&task.ancestors);
150    let mut worker_options = options;
151    worker_options.error_policy = ErrorPolicy::Continue;
152    worker_options.min_depth = 0;
153    worker_options.max_open = 1;
154    worker_options.max_depth = Some(
155        options
156            .max_depth
157            .unwrap_or(task.depth.saturating_add(1))
158            .min(task.depth.saturating_add(1)),
159    );
160    let mut walker = Walker::from_known_directory_with_ancestry(
161        root,
162        task.path.clone(),
163        task.depth,
164        worker_options,
165        root_file_system,
166        task.identity,
167        task.ancestors.as_ref().clone(),
168    );
169    let mut entries = Vec::new();
170    while !cancellation.is_cancelled() {
171        let Some(item) = walker.next() else {
172            break;
173        };
174        match item {
175            Ok(mut entry) => {
176                if entry.is_dir()
177                    && entry.skip_reason() == Some(WalkSkipReason::MaxDepth)
178                    && options
179                        .max_depth
180                        .is_none_or(|maximum| entry.depth() < maximum)
181                {
182                    entry.clear_depth_skip();
183                }
184                if entry.is_dir() {
185                    walker.skip_current_dir();
186                }
187                entries.push(Ok(StatefulWalkEntry {
188                    read_children: entry.is_dir() && entry.skip_reason().is_none(),
189                    entry,
190                    state: E::default(),
191                }));
192            }
193            Err(error) => entries.push(Err(error)),
194        }
195    }
196    if let Some(processor) = processor {
197        processor(task.depth, &task.path, &mut task.read_state, &mut entries);
198    }
199    DirectoryBatch {
200        entries,
201        child_state: task.read_state,
202        ancestors,
203    }
204}
205
206fn requested_workers(runtime: &ParallelRuntime, parallelism: usize, max_open: usize) -> usize {
207    let available = runtime.parallelism();
208    let requested = if parallelism == 0 {
209        available.min(if cfg!(windows) { 16 } else { 8 })
210    } else {
211        parallelism.min(available)
212    };
213    requested.min(max_open.max(1)).max(1)
214}