qubit_batch/execute/batch_executor.rs
1/*******************************************************************************
2 *
3 * Copyright (c) 2025 - 2026 Haixing Hu.
4 *
5 * SPDX-License-Identifier: Apache-2.0
6 *
7 * Licensed under the Apache License, Version 2.0.
8 *
9 ******************************************************************************/
10use std::sync::Arc;
11
12use crossbeam_queue::SegQueue;
13use qubit_function::{
14 Callable,
15 Runnable,
16};
17
18use crate::{
19 BatchExecutionError,
20 BatchOutcome,
21};
22
23use super::{
24 BatchCallResult,
25 callable_task::CallableTask,
26 for_each_task::ForEachTask,
27};
28
29/// Executes batches of fallible tasks.
30///
31/// Implementations consume the supplied iterator once, execute every observed
32/// task unless an explicitly declared count is exceeded, and return a
33/// [`BatchOutcome`] containing task-level successes, failures, panics, and
34/// elapsed time.
35///
36/// ```rust
37/// use qubit_batch::{
38/// BatchExecutor,
39/// SequentialBatchExecutor,
40/// };
41///
42/// let outcome = SequentialBatchExecutor::new()
43/// .for_each([1, 2, 3], |value| {
44/// assert!(value > 0);
45/// Ok::<(), &'static str>(())
46/// })
47/// .expect("array length should be exact");
48///
49/// assert!(outcome.is_success());
50/// ```
51pub trait BatchExecutor: Send + Sync {
52 /// Executes a batch of runnable tasks whose iterator exposes an exact
53 /// length.
54 ///
55 /// # Parameters
56 ///
57 /// * `tasks` - Task source for the batch. Its iterator must report the
58 /// remaining task count exactly.
59 ///
60 /// # Returns
61 ///
62 /// The result returned by [`Self::execute_with_count`] after deriving the
63 /// declared count from the iterator length.
64 ///
65 /// # Errors
66 ///
67 /// Returns [`BatchExecutionError`] only if the iterator violates its exact
68 /// length contract while being consumed.
69 ///
70 /// # Panics
71 ///
72 /// Panics from individual tasks are captured in [`BatchOutcome`].
73 /// Panics from the configured
74 /// [`qubit_progress::reporter::ProgressReporter`] are propagated to the
75 /// caller.
76 fn execute<T, E, I>(&self, tasks: I) -> Result<BatchOutcome<E>, BatchExecutionError<E>>
77 where
78 I: IntoIterator<Item = T>,
79 I::IntoIter: ExactSizeIterator,
80 T: Runnable<E> + Send,
81 E: Send,
82 {
83 let tasks = tasks.into_iter();
84 let count = tasks.len();
85 self.execute_with_count(tasks, count)
86 }
87
88 /// Executes a batch of runnable tasks with an explicit declared count.
89 ///
90 /// # Parameters
91 ///
92 /// * `tasks` - Task source for the batch. It may be eager or lazy.
93 /// * `count` - Declared number of tasks expected from `tasks`.
94 ///
95 /// # Returns
96 ///
97 /// `Ok(BatchOutcome)` when the declared task count matches the source, or
98 /// `Err(BatchExecutionError)` when the source yields fewer or more tasks
99 /// than declared.
100 ///
101 /// # Errors
102 ///
103 /// Returns [`BatchExecutionError`] when the source task count does not
104 /// match `count`.
105 ///
106 /// # Panics
107 ///
108 /// Panics from individual tasks are captured in [`BatchOutcome`].
109 /// Panics from the configured
110 /// [`qubit_progress::reporter::ProgressReporter`] are propagated to the
111 /// caller.
112 fn execute_with_count<T, E, I>(&self, tasks: I, count: usize) -> Result<BatchOutcome<E>, BatchExecutionError<E>>
113 where
114 I: IntoIterator<Item = T>,
115 T: Runnable<E> + Send,
116 E: Send;
117
118 /// Executes callable tasks whose iterator exposes an exact length.
119 ///
120 /// # Parameters
121 ///
122 /// * `tasks` - Callable task source for the batch. Its iterator must report
123 /// the remaining callable count exactly.
124 ///
125 /// # Returns
126 ///
127 /// A [`BatchCallResult`] containing the normal execution summary plus
128 /// optional success values indexed by callable position.
129 ///
130 /// # Errors
131 ///
132 /// Returns [`BatchExecutionError`] only if the iterator violates its exact
133 /// length contract while being consumed.
134 ///
135 /// # Panics
136 ///
137 /// Panics from individual callables are captured in the execution result.
138 /// Panics from the configured
139 /// [`qubit_progress::reporter::ProgressReporter`] are propagated to the
140 /// caller.
141 fn call<C, R, E, I>(&self, tasks: I) -> Result<BatchCallResult<R, E>, BatchExecutionError<E>>
142 where
143 I: IntoIterator<Item = C>,
144 I::IntoIter: ExactSizeIterator,
145 C: Callable<R, E> + Send,
146 R: Send,
147 E: Send,
148 {
149 let tasks = tasks.into_iter();
150 let count = tasks.len();
151 self.call_with_count(tasks, count)
152 }
153
154 /// Executes callable tasks with an explicit declared count and collects
155 /// success values by index.
156 ///
157 /// # Parameters
158 ///
159 /// * `tasks` - Callable task source for the batch.
160 /// * `count` - Declared number of callables expected from `tasks`.
161 ///
162 /// # Returns
163 ///
164 /// A [`BatchCallResult`] containing the normal execution summary plus
165 /// optional success values indexed by callable position.
166 ///
167 /// # Errors
168 ///
169 /// Returns [`BatchExecutionError`] when the source callable count does not
170 /// match `count`.
171 ///
172 /// # Panics
173 ///
174 /// Panics from individual callables are captured in the execution result.
175 /// Panics from the configured
176 /// [`qubit_progress::reporter::ProgressReporter`] are propagated to the
177 /// caller.
178 fn call_with_count<C, R, E, I>(
179 &self,
180 tasks: I,
181 count: usize,
182 ) -> Result<BatchCallResult<R, E>, BatchExecutionError<E>>
183 where
184 I: IntoIterator<Item = C>,
185 C: Callable<R, E> + Send,
186 R: Send,
187 E: Send,
188 {
189 let outputs = Arc::new(SegQueue::new());
190 // This adapter is lazy: callables are wrapped as runnable tasks only
191 // when the executor consumes the iterator. The callables themselves are
192 // still executed later by `CallableTask::run`.
193 let runnable_tasks = tasks.into_iter().enumerate().map({
194 let outputs = Arc::clone(&outputs);
195 move |(index, callable)| CallableTask::new(callable, index, Arc::clone(&outputs))
196 });
197 let outcome = self.execute_with_count(runnable_tasks, count)?;
198 let values = collect_call_outputs(outputs, count);
199 Ok(BatchCallResult::new(outcome, values))
200 }
201
202 /// Applies `action` to every item whose iterator exposes an exact length.
203 ///
204 /// # Parameters
205 ///
206 /// * `items` - Item source to transform into runnable tasks.
207 /// * `action` - Fallible action applied to each item.
208 ///
209 /// # Returns
210 ///
211 /// The result returned by [`Self::for_each_with_count`] after deriving the
212 /// declared count from the iterator length.
213 ///
214 /// # Errors
215 ///
216 /// Returns [`BatchExecutionError`] only if the iterator violates its exact
217 /// length contract while being consumed.
218 fn for_each<Item, E, I, F>(&self, items: I, action: F) -> Result<BatchOutcome<E>, BatchExecutionError<E>>
219 where
220 I: IntoIterator<Item = Item>,
221 I::IntoIter: ExactSizeIterator,
222 Item: Send,
223 F: Fn(Item) -> Result<(), E> + Send + Sync,
224 E: Send,
225 {
226 let items = items.into_iter();
227 let count = items.len();
228 self.for_each_with_count(items, count, action)
229 }
230
231 /// Applies `action` to every item using an explicit declared count.
232 ///
233 /// # Parameters
234 ///
235 /// * `items` - Item source to transform into runnable tasks.
236 /// * `count` - Declared number of items expected from `items`.
237 /// * `action` - Fallible action applied to each item.
238 ///
239 /// # Returns
240 ///
241 /// The result returned by [`Self::execute_with_count`] for the derived task
242 /// batch.
243 ///
244 /// # Errors
245 ///
246 /// Returns [`BatchExecutionError`] when the source item count does not
247 /// match `count`.
248 fn for_each_with_count<Item, E, I, F>(
249 &self,
250 items: I,
251 count: usize,
252 action: F,
253 ) -> Result<BatchOutcome<E>, BatchExecutionError<E>>
254 where
255 I: IntoIterator<Item = Item>,
256 Item: Send,
257 F: Fn(Item) -> Result<(), E> + Send + Sync,
258 E: Send,
259 {
260 let action = Arc::new(action);
261 let tasks = items
262 .into_iter()
263 .map(move |item| ForEachTask::new(item, Arc::clone(&action)));
264 self.execute_with_count(tasks, count)
265 }
266}
267
268/// Consumes shared callable outputs into an indexed value vector.
269///
270/// # Parameters
271///
272/// * `outputs` - Shared output queue filled by callable wrappers.
273/// * `count` - Declared callable count used to size the result vector.
274///
275/// # Returns
276///
277/// Optional success values indexed by callable position.
278///
279/// # Panics
280///
281/// Panics if callable wrappers still hold references to `outputs`, or if a
282/// queued output index is outside the declared batch size.
283fn collect_call_outputs<R>(outputs: Arc<SegQueue<(usize, R)>>, count: usize) -> Vec<Option<R>> {
284 let outputs = match Arc::try_unwrap(outputs) {
285 Ok(outputs) => outputs,
286 Err(_) => panic!("callable output queue should have a single owner after execution"),
287 };
288 let mut values = Vec::with_capacity(count);
289 values.resize_with(count, || None);
290 while let Some((index, value)) = outputs.pop() {
291 let slot = values
292 .get_mut(index)
293 .expect("callable index must be within the declared count");
294 *slot = Some(value);
295 }
296 values
297}