Skip to main content

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}