Skip to main content

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}