Skip to main content

batch_aint_one/
lib.rs

1//! Batch up multiple items for processing as a single unit.
2//!
3//! _I got 99 problems, but a batch ain't one..._
4//!
5//! Sometimes it is more efficient to process many items at once rather than one at a time.
6//! Especially when the processing step has overheads which can be shared between many items.
7//!
8//! Often applications work with one item at a time, e.g. _select one row_ or _insert one row_. Many
9//! of these operations can be batched up into more efficient versions: _select many rows_ and
10//! _insert many rows_.
11//!
12//! A worker task is run in the background. Many client tasks (e.g. message handlers) can submit
13//! items to the worker and wait for them to be processed. The worker task batches together many
14//! items and processes them as one unit, before sending a result back to each calling task.
15//!
16//! See the README for an example.
17
18#![deny(missing_docs)]
19
20#[cfg(doctest)]
21use doc_comment::doctest;
22#[cfg(doctest)]
23doctest!("../README.md");
24
25/// Check an invariant which should never be violated: panic in debug builds (and tests), but
26/// only log a warning in release builds rather than bringing the process down.
27macro_rules! soft_assert {
28    ($cond:expr, $($arg:tt)+) => {
29        if !$cond {
30            tracing::warn!($($arg)+);
31            debug_assert!(false, $($arg)+);
32        }
33    };
34}
35
36mod batch;
37mod batch_inner;
38mod batch_queue;
39mod batcher;
40pub mod error;
41mod limits;
42pub mod metrics;
43mod policies;
44mod processor;
45mod timeout;
46mod worker;
47
48pub use batcher::Batcher;
49pub use error::BatchError;
50pub use limits::Limits;
51pub use metrics::{BatchStats, MetricsRecorder};
52pub use policies::{BatchingPolicy, OnFull};
53pub use processor::Processor;
54pub use worker::WorkerHandle;
55
56#[cfg(test)]
57mod tests {
58    use std::time::Duration;
59
60    use tokio::join;
61    use tracing::{Instrument, span};
62
63    use crate::{Batcher, BatchingPolicy, Limits, Processor};
64
65    #[derive(Debug, Clone)]
66    pub struct SimpleBatchProcessor(pub Duration);
67
68    impl Processor for SimpleBatchProcessor {
69        type Key = String;
70        type Input = String;
71        type Output = String;
72        type Error = String;
73        type Resources = ();
74
75        async fn acquire_resources(&self, _key: String) -> Result<(), String> {
76            tokio::time::sleep(self.0).await;
77            Ok(())
78        }
79
80        async fn process(
81            &self,
82            key: String,
83            inputs: impl Iterator<Item = String> + Send,
84            _resources: (),
85        ) -> Result<Vec<String>, String> {
86            tokio::time::sleep(self.0).await;
87            Ok(inputs.map(|s| s + " processed for " + &key).collect())
88        }
89    }
90
91    #[tokio::test]
92    #[ignore = "flaky"]
93    async fn test_tracing() {
94        use tracing::Level;
95        use tracing_capture::{CaptureLayer, SharedStorage};
96        use tracing_subscriber::layer::SubscriberExt;
97
98        let subscriber = tracing_subscriber::fmt()
99            .pretty()
100            .with_max_level(Level::INFO)
101            .with_test_writer()
102            .finish();
103        // Add the capturing layer.
104        let storage = SharedStorage::default();
105        let subscriber = subscriber.with(CaptureLayer::new(&storage));
106
107        // Capture tracing information.
108        let _guard = tracing::subscriber::set_default(subscriber);
109
110        let batcher = Batcher::builder()
111            .name("test_tracing")
112            .processor(SimpleBatchProcessor(Duration::from_millis(10)))
113            .limits(Limits::builder().max_batch_size(2).build())
114            .batching_policy(BatchingPolicy::Size)
115            .build();
116
117        let h1 = {
118            tokio_test::task::spawn({
119                let span = span!(Level::INFO, "test_handler_span1");
120
121                batcher
122                    .add("A".to_string(), "1".to_string())
123                    .instrument(span)
124            })
125        };
126        let h2 = {
127            tokio_test::task::spawn({
128                let span = span!(Level::INFO, "test_handler_span2");
129
130                batcher
131                    .add("A".to_string(), "2".to_string())
132                    .instrument(span)
133            })
134        };
135
136        let (o1, o2) = join!(h1, h2);
137
138        assert!(o1.is_ok());
139        assert!(o2.is_ok());
140
141        let worker = batcher.worker_handle();
142        worker.shut_down().await;
143        tokio::time::timeout(Duration::from_secs(1), worker.wait_for_shutdown())
144            .await
145            .expect("Worker should shut down");
146
147        let storage = storage.lock();
148
149        let outer_process_spans: Vec<_> = storage
150            .all_spans()
151            .filter(|span| span.metadata().name().contains("process batch"))
152            .collect();
153        assert_eq!(
154            outer_process_spans.len(),
155            1,
156            "should be a single outer span for processing the batch"
157        );
158        let process_span = *outer_process_spans.first().unwrap();
159
160        assert_eq!(
161            process_span["batch.size"], 2u64,
162            "batch.size shouldn't be emitted as a string",
163        );
164
165        assert_eq!(
166            process_span.follows_from().len(),
167            2,
168            "should follow from both handler spans"
169        );
170
171        let resource_spans: Vec<_> = storage
172            .all_spans()
173            .filter(|span| span.metadata().name().contains("acquire resources"))
174            .collect();
175        assert_eq!(
176            resource_spans.len(),
177            1,
178            "should be a single span for acquiring resources"
179        );
180        let resource_span_parent = resource_spans
181            .first()
182            .unwrap()
183            .parent()
184            .expect("resource span should have a parent");
185        assert_eq!(
186            resource_span_parent, process_span,
187            "resource acquisition should be a child of the outer process span"
188        );
189
190        let inner_process_spans: Vec<_> = storage
191            .all_spans()
192            .filter(|span| span.metadata().name().contains("process()"))
193            .collect();
194        assert_eq!(
195            inner_process_spans.len(),
196            1,
197            "should be a single inner span for processing the batch"
198        );
199        let inner_span_parent = inner_process_spans
200            .first()
201            .unwrap()
202            .parent()
203            .expect("resource span should have a parent");
204        assert_eq!(
205            inner_span_parent, process_span,
206            "resource acquisition should be a child of the outer process span"
207        );
208
209        let link_back_spans: Vec<_> = storage
210            .all_spans()
211            .filter(|span| span.metadata().name().contains("batch finished"))
212            .collect();
213        assert_eq!(
214            link_back_spans.len(),
215            2,
216            "should be two spans for linking back to the process span"
217        );
218
219        for span in link_back_spans {
220            assert_eq!(
221                span.follows_from().len(),
222                1,
223                "link back spans should follow from the process span"
224            );
225        }
226
227        assert_eq!(storage.all_spans().len(), 7, "should be 7 spans in total");
228    }
229}