qubit_batch/process/impls/sequential_batch_processor.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 sync::Arc,
12 time::Duration,
13};
14
15use qubit_function::{
16 BoxConsumer,
17 Consumer,
18};
19use qubit_progress::{
20 Progress,
21 reporter::ProgressReporter,
22};
23
24use crate::process::{
25 BatchProcessError,
26 BatchProcessResult,
27 BatchProcessState,
28 BatchProcessor,
29 PROCESS_PROGRESS_METRIC_ID,
30 PROCESS_PROGRESS_METRIC_NAME,
31};
32
33use super::SequentialBatchProcessorBuilder;
34
35/// Processes batch items sequentially by invoking a [`Consumer`] per item.
36///
37/// The processor stores the supplied consumer as a [`BoxConsumer`] and invokes it
38/// on the caller thread in input order. Consumer panics are not caught; they
39/// propagate to the caller and no [`BatchProcessResult`] is produced. Progress
40/// updates are emitted only between items.
41///
42/// # Type Parameters
43///
44/// * `Item` - Item type consumed by the stored consumer.
45///
46/// ```rust
47/// use qubit_batch::{
48/// BatchProcessor,
49/// SequentialBatchProcessor,
50/// };
51///
52/// let mut processor = SequentialBatchProcessor::new(|item: &i32| {
53/// assert!(*item > 0);
54/// });
55///
56/// let result = processor
57/// .process([1, 2, 3])
58/// .expect("array length should be exact");
59///
60/// assert!(result.is_success());
61/// ```
62pub struct SequentialBatchProcessor<Item> {
63 /// Consumer called once for each accepted item.
64 pub(crate) consumer: BoxConsumer<Item>,
65 /// Interval between progress callbacks while the batch is running.
66 pub(crate) report_interval: Duration,
67 /// Reporter receiving batch lifecycle callbacks.
68 pub(crate) reporter: Arc<dyn ProgressReporter>,
69}
70
71impl<Item> SequentialBatchProcessor<Item> {
72 /// Default interval between progress callbacks.
73 pub const DEFAULT_REPORT_INTERVAL: Duration = Duration::from_secs(5);
74
75 /// Creates a sequential consumer-backed batch processor.
76 ///
77 /// # Parameters
78 ///
79 /// * `consumer` - Consumer invoked once for each input item.
80 ///
81 /// # Returns
82 ///
83 /// A processor storing `consumer` as a [`BoxConsumer`].
84 #[inline]
85 pub fn new<C>(consumer: C) -> Self
86 where
87 C: Consumer<Item> + 'static,
88 {
89 Self::builder(consumer).build()
90 }
91
92 /// Creates a builder for configuring a sequential consumer-backed
93 /// processor.
94 ///
95 /// # Parameters
96 ///
97 /// * `consumer` - Consumer invoked once for each input item.
98 ///
99 /// # Returns
100 ///
101 /// A builder initialized with default settings.
102 #[inline]
103 pub fn builder<C>(consumer: C) -> SequentialBatchProcessorBuilder<Item>
104 where
105 C: Consumer<Item> + 'static,
106 {
107 SequentialBatchProcessorBuilder::new(consumer)
108 }
109
110 /// Returns the configured progress-report interval.
111 ///
112 /// # Returns
113 ///
114 /// The minimum time between due-based running progress callbacks.
115 #[inline]
116 pub const fn report_interval(&self) -> Duration {
117 self.report_interval
118 }
119
120 /// Returns the configured progress reporter.
121 ///
122 /// # Returns
123 ///
124 /// A shared reference to the configured progress reporter.
125 #[inline]
126 pub fn reporter(&self) -> &Arc<dyn ProgressReporter> {
127 &self.reporter
128 }
129
130 /// Returns the stored consumer.
131 ///
132 /// # Returns
133 ///
134 /// A shared reference to the boxed consumer.
135 #[inline]
136 pub const fn consumer(&self) -> &BoxConsumer<Item> {
137 &self.consumer
138 }
139
140 /// Consumes this processor and returns the stored consumer.
141 ///
142 /// # Returns
143 ///
144 /// The boxed consumer used by this processor.
145 #[inline]
146 pub fn into_consumer(self) -> BoxConsumer<Item> {
147 self.consumer
148 }
149}
150
151impl<Item> BatchProcessor<Item> for SequentialBatchProcessor<Item> {
152 type Error = BatchProcessError;
153
154 /// Processes items sequentially on the caller thread.
155 ///
156 /// # Parameters
157 ///
158 /// * `items` - Item source for the batch.
159 /// * `count` - Declared number of items expected from `items`.
160 ///
161 /// # Returns
162 ///
163 /// A result with completed and processed counts equal to the number of
164 /// consumer calls when the input source yields exactly `count` items.
165 ///
166 /// # Errors
167 ///
168 /// Returns [`BatchProcessError::CountShortfall`] when the source ends before
169 /// `count`, or [`BatchProcessError::CountExceeded`] when the source yields an
170 /// extra item. Extra items are observed but not passed to the consumer.
171 ///
172 /// # Panics
173 ///
174 /// Propagates any panic raised by the stored consumer or the configured
175 /// progress reporter.
176 fn process_with_count<I>(&mut self, items: I, count: usize) -> Result<BatchProcessResult, Self::Error>
177 where
178 I: IntoIterator<Item = Item>,
179 {
180 let state = BatchProcessState::new(count);
181 let mut progress = Progress::single_metric(
182 self.reporter.as_ref(),
183 self.report_interval,
184 PROCESS_PROGRESS_METRIC_ID,
185 PROCESS_PROGRESS_METRIC_NAME,
186 );
187 progress.report_started(|event| event.counters(state.progress_counters()));
188
189 for item in items {
190 let observed_count = state.record_item_observed();
191 if observed_count > count {
192 let failed = progress.report_failed(|event| event.counters(state.progress_counters()));
193 let result = state.to_direct_result(failed.elapsed());
194 return Err(BatchProcessError::CountExceeded {
195 expected: count,
196 observed_at_least: observed_count,
197 result,
198 });
199 }
200 state.record_item_started();
201 self.consumer.accept(&item);
202 state.record_item_processed();
203 let _ = progress.report_running_if_due(|event| event.counters(state.progress_counters()));
204 }
205
206 if state.observed_count() < count {
207 let failed = progress.report_failed(|event| event.counters(state.progress_counters()));
208 let result = state.to_direct_result(failed.elapsed());
209 Err(BatchProcessError::CountShortfall {
210 expected: count,
211 actual: state.observed_count(),
212 result,
213 })
214 } else {
215 let finished = progress.report_finished(|event| event.counters(state.progress_counters()));
216 let result = state.to_direct_result(finished.elapsed());
217 Ok(result)
218 }
219 }
220}