weavatrix_scan/stateful_walk/parallel/
mod.rs1use 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
67pub 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}