Skip to main content

qubit_batch/execute/
batch_outcome_builder.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::{
11    collections::HashSet,
12    time::Duration,
13};
14
15use crate::{
16    BatchOutcomeBuildError,
17    BatchTaskError,
18    BatchTaskFailure,
19};
20
21/// Builder carrying validated parts for a [`crate::BatchOutcome`].
22///
23/// The builder checks aggregate counters, failure detail count, duplicate
24/// indexes, and failed-versus-panicked detail counts before creating an
25/// outcome.
26///
27/// ```rust
28/// use qubit_batch::{
29///     BatchOutcomeBuilder,
30///     BatchTaskError,
31///     BatchTaskFailure,
32/// };
33///
34/// let outcome = BatchOutcomeBuilder::builder(2)
35///     .completed_count(2)
36///     .succeeded_count(1)
37///     .failed_count(1)
38///     .failures(vec![BatchTaskFailure::new(
39///         1,
40///         BatchTaskError::Failed("invalid row"),
41///     )])
42///     .build()
43///     .expect("outcome counters should match failure details");
44///
45/// assert_eq!(outcome.failed_count(), 1);
46/// assert_eq!(outcome.failures()[0].index(), 1);
47/// ```
48#[derive(Debug, Clone, PartialEq, Eq)]
49pub struct BatchOutcomeBuilder<E> {
50    /// Declared task count for the batch.
51    pub(crate) task_count: usize,
52    /// Number of tasks that reached a terminal outcome.
53    pub(crate) completed_count: usize,
54    /// Number of tasks that completed successfully.
55    pub(crate) succeeded_count: usize,
56    /// Number of tasks that returned their own error.
57    pub(crate) failed_count: usize,
58    /// Number of tasks that panicked.
59    pub(crate) panicked_count: usize,
60    /// Total monotonic elapsed duration for the batch.
61    pub(crate) elapsed: Duration,
62    /// Detailed failure records sorted by task index.
63    pub(crate) failures: Vec<BatchTaskFailure<E>>,
64}
65
66impl<E> BatchOutcomeBuilder<E> {
67    /// Starts building a batch outcome.
68    ///
69    /// # Parameters
70    ///
71    /// * `task_count` - Declared task count for the batch.
72    ///
73    /// # Returns
74    ///
75    /// A builder initialized with zero counters, zero elapsed time, and no
76    /// failures.
77    #[inline]
78    pub fn builder(task_count: usize) -> Self {
79        Self {
80            task_count,
81            completed_count: 0,
82            succeeded_count: 0,
83            failed_count: 0,
84            panicked_count: 0,
85            elapsed: Duration::ZERO,
86            failures: Vec::new(),
87        }
88    }
89
90    /// Sets the number of tasks that finished.
91    ///
92    /// # Parameters
93    ///
94    /// * `completed_count` - Number of tasks that reached a terminal outcome.
95    ///
96    /// # Returns
97    ///
98    /// The updated builder.
99    #[inline]
100    pub const fn completed_count(mut self, completed_count: usize) -> Self {
101        self.completed_count = completed_count;
102        self
103    }
104
105    /// Sets the number of successful tasks.
106    ///
107    /// # Parameters
108    ///
109    /// * `succeeded_count` - Number of tasks that completed successfully.
110    ///
111    /// # Returns
112    ///
113    /// The updated builder.
114    #[inline]
115    pub const fn succeeded_count(mut self, succeeded_count: usize) -> Self {
116        self.succeeded_count = succeeded_count;
117        self
118    }
119
120    /// Sets the number of tasks that returned their own error.
121    ///
122    /// # Parameters
123    ///
124    /// * `failed_count` - Number of tasks that failed with task errors.
125    ///
126    /// # Returns
127    ///
128    /// The updated builder.
129    #[inline]
130    pub const fn failed_count(mut self, failed_count: usize) -> Self {
131        self.failed_count = failed_count;
132        self
133    }
134
135    /// Sets the number of tasks that panicked.
136    ///
137    /// # Parameters
138    ///
139    /// * `panicked_count` - Number of tasks that panicked.
140    ///
141    /// # Returns
142    ///
143    /// The updated builder.
144    #[inline]
145    pub const fn panicked_count(mut self, panicked_count: usize) -> Self {
146        self.panicked_count = panicked_count;
147        self
148    }
149
150    /// Sets the total monotonic elapsed duration.
151    ///
152    /// # Parameters
153    ///
154    /// * `elapsed` - Total monotonic elapsed duration.
155    ///
156    /// # Returns
157    ///
158    /// The updated builder.
159    #[inline]
160    pub const fn elapsed(mut self, elapsed: Duration) -> Self {
161        self.elapsed = elapsed;
162        self
163    }
164
165    /// Sets the detailed failure records.
166    ///
167    /// # Parameters
168    ///
169    /// * `failures` - Detailed task failure records.
170    ///
171    /// # Returns
172    ///
173    /// The updated builder.
174    #[inline]
175    pub fn failures(mut self, failures: Vec<BatchTaskFailure<E>>) -> Self {
176        self.failures = failures;
177        self
178    }
179
180    /// Validates this builder and sorts failure records by task index.
181    ///
182    /// # Returns
183    ///
184    /// `Ok(builder)` when the counters and failure details are consistent.
185    ///
186    /// # Errors
187    ///
188    /// Returns [`BatchOutcomeBuildError`] when the counters or failure details
189    /// are inconsistent.
190    #[inline]
191    pub fn validate(mut self) -> Result<Self, BatchOutcomeBuildError> {
192        validate_outcome_invariants(
193            self.task_count,
194            self.completed_count,
195            self.succeeded_count,
196            self.failed_count,
197            self.panicked_count,
198            &self.failures,
199        )?;
200        self.failures.sort_by_key(|failure| failure.index());
201        Ok(self)
202    }
203
204    /// Validates this builder and creates a batch outcome.
205    ///
206    /// # Returns
207    ///
208    /// `Ok(outcome)` when the counters and failure details are consistent.
209    ///
210    /// # Errors
211    ///
212    /// Returns [`BatchOutcomeBuildError`] when the counters or failure details
213    /// are inconsistent.
214    #[inline]
215    pub fn build(self) -> Result<crate::BatchOutcome<E>, BatchOutcomeBuildError> {
216        self.validate().map(crate::BatchOutcome::new)
217    }
218}
219
220/// Validates all counters and failure details for a batch outcome.
221fn validate_outcome_invariants<E>(
222    task_count: usize,
223    completed_count: usize,
224    succeeded_count: usize,
225    failed_count: usize,
226    panicked_count: usize,
227    failures: &[BatchTaskFailure<E>],
228) -> Result<(), BatchOutcomeBuildError> {
229    let failure_count = failed_count.checked_add(panicked_count).ok_or(
230        BatchOutcomeBuildError::FailureCountOverflow {
231            failed_count,
232            panicked_count,
233        },
234    )?;
235    let terminal_count = succeeded_count.checked_add(failure_count).ok_or(
236        BatchOutcomeBuildError::TerminalCountOverflow {
237            succeeded_count,
238            failure_count,
239        },
240    )?;
241
242    if completed_count > task_count {
243        return Err(BatchOutcomeBuildError::CompletedCountExceeded {
244            task_count,
245            completed_count,
246        });
247    }
248    if terminal_count != completed_count {
249        return Err(BatchOutcomeBuildError::TerminalCountMismatch {
250            completed_count,
251            terminal_count,
252            succeeded_count,
253            failed_count,
254            panicked_count,
255        });
256    }
257    if failures.len() != failure_count {
258        return Err(BatchOutcomeBuildError::FailureDetailCountMismatch {
259            expected: failure_count,
260            actual: failures.len(),
261        });
262    }
263    validate_failure_details(task_count, failed_count, panicked_count, failures)
264}
265
266/// Validates detailed failure records against aggregate counters.
267fn validate_failure_details<E>(
268    task_count: usize,
269    failed_count: usize,
270    panicked_count: usize,
271    failures: &[BatchTaskFailure<E>],
272) -> Result<(), BatchOutcomeBuildError> {
273    let mut observed_failed_count = 0usize;
274    let mut observed_panicked_count = 0usize;
275    let mut observed_indexes = HashSet::with_capacity(failures.len());
276    for failure in failures {
277        if failure.index() >= task_count {
278            return Err(BatchOutcomeBuildError::FailureIndexOutOfRange {
279                index: failure.index(),
280                task_count,
281            });
282        }
283        if !observed_indexes.insert(failure.index()) {
284            return Err(BatchOutcomeBuildError::DuplicateFailureIndex {
285                index: failure.index(),
286            });
287        }
288        match failure.error() {
289            BatchTaskError::Failed(_) => observed_failed_count += 1,
290            BatchTaskError::Panicked { .. } => observed_panicked_count += 1,
291        }
292    }
293    if observed_failed_count != failed_count || observed_panicked_count != panicked_count {
294        return Err(BatchOutcomeBuildError::FailureVariantCountMismatch {
295            expected_failed: failed_count,
296            actual_failed: observed_failed_count,
297            expected_panicked: panicked_count,
298            actual_panicked: observed_panicked_count,
299        });
300    }
301    Ok(())
302}