1#![deny(missing_docs)]
19
20#[cfg(doctest)]
21use doc_comment::doctest;
22#[cfg(doctest)]
23doctest!("../README.md");
24
25macro_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, MetricsRecorderFactory};
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 let storage = SharedStorage::default();
105 let subscriber = subscriber.with(CaptureLayer::new(&storage));
106
107 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}