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}