1use std::{fmt::Debug, time::Duration};
4
5pub trait MetricsRecorder: Debug + Send + Sync + 'static {
14 fn item_received(&self, _channel_duration: Duration) {}
19
20 fn item_rejected(&self) {}
22
23 fn batch_completed(&self, _metrics: &BatchStats) {}
25
26 fn resource_acquisition_completed(&self, _duration: Duration, _success: bool) {}
28
29 fn active_keys_changed(&self, _count: usize) {}
31
32 fn processing_concurrency_changed(&self, _total: usize, _max_per_key: usize) {}
37
38 fn queue_depth_changed(&self, _total: usize, _max_per_key: usize) {}
43}
44
45#[derive(Debug, Clone)]
47#[non_exhaustive]
48pub struct BatchStats {
49 pub size: usize,
51 pub processing_duration: Duration,
53 pub success: bool,
55 pub item_latencies: Vec<Duration>,
57 pub queue_durations: Vec<Duration>,
63}
64
65impl BatchStats {
66 pub fn new(
68 size: usize,
69 processing_duration: Duration,
70 success: bool,
71 item_latencies: Vec<Duration>,
72 queue_durations: Vec<Duration>,
73 ) -> Self {
74 Self {
75 size,
76 processing_duration,
77 success,
78 item_latencies,
79 queue_durations,
80 }
81 }
82}
83
84pub trait MetricsRecorderFactory: Debug + Send + Sync + 'static {
86 fn create_recorder(&self, batcher_name: &str) -> Box<dyn MetricsRecorder>;
88}
89
90#[derive(Debug, Clone, Copy)]
92pub(crate) struct NoopMetricsRecorder;
93
94impl MetricsRecorder for NoopMetricsRecorder {}
95
96#[cfg(test)]
97mod tests {
98 use std::sync::{Arc, Mutex};
99 use std::time::Duration;
100
101 use crate::{Batcher, BatchingPolicy, Limits, Processor};
102
103 use super::*;
104
105 #[derive(Debug, Default)]
106 struct TestRecorderInner {
107 items_received: usize,
108 items_rejected: usize,
109 batches_completed: usize,
110 batch_sizes: Vec<usize>,
111 item_latencies: Vec<Duration>,
112 channel_durations: Vec<Duration>,
113 resource_acquisitions: usize,
114 active_keys: Vec<usize>,
115 processing_concurrency: (usize, usize),
116 queue_depth: (usize, usize),
117 }
118
119 #[derive(Debug)]
120 struct TestRecorder(Arc<Mutex<TestRecorderInner>>);
121
122 #[derive(Debug)]
123 struct TestRecorderFactory(Arc<Mutex<TestRecorderInner>>);
124
125 impl MetricsRecorderFactory for TestRecorderFactory {
126 fn create_recorder(&self, _batcher_name: &str) -> Box<dyn MetricsRecorder> {
127 Box::new(TestRecorder(self.0.clone()))
128 }
129 }
130
131 fn test_metrics() -> (Arc<Mutex<TestRecorderInner>>, Box<dyn MetricsRecorderFactory>) {
132 let state = Arc::new(Mutex::new(TestRecorderInner::default()));
133 let factory = Box::new(TestRecorderFactory(state.clone()));
134 (state, factory)
135 }
136
137 impl MetricsRecorder for TestRecorder {
138 fn item_received(&self, channel_duration: Duration) {
139 let mut inner = self.0.lock().unwrap();
140 inner.items_received += 1;
141 inner.channel_durations.push(channel_duration);
142 }
143
144 fn item_rejected(&self) {
145 self.0.lock().unwrap().items_rejected += 1;
146 }
147
148 fn batch_completed(&self, metrics: &BatchStats) {
149 let mut inner = self.0.lock().unwrap();
150 inner.batches_completed += 1;
151 inner.batch_sizes.push(metrics.size);
152 inner
153 .item_latencies
154 .extend_from_slice(&metrics.item_latencies);
155 }
156
157 fn resource_acquisition_completed(&self, _duration: Duration, _success: bool) {
158 self.0.lock().unwrap().resource_acquisitions += 1;
159 }
160
161 fn active_keys_changed(&self, count: usize) {
162 self.0.lock().unwrap().active_keys.push(count);
163 }
164
165 fn processing_concurrency_changed(&self, total: usize, max_per_key: usize) {
166 self.0.lock().unwrap().processing_concurrency = (total, max_per_key);
167 }
168
169 fn queue_depth_changed(&self, total: usize, max_per_key: usize) {
170 self.0.lock().unwrap().queue_depth = (total, max_per_key);
171 }
172 }
173
174 #[derive(Debug, Clone)]
175 struct SimpleProcessor {
176 process_delay: Duration,
177 }
178
179 impl SimpleProcessor {
180 fn instant() -> Self {
181 Self {
182 process_delay: Duration::ZERO,
183 }
184 }
185 }
186
187 impl Processor for SimpleProcessor {
188 type Key = String;
189 type Input = String;
190 type Output = String;
191 type Error = String;
192 type Resources = ();
193
194 async fn acquire_resources(&self, _key: String) -> Result<(), String> {
195 Ok(())
196 }
197
198 async fn process(
199 &self,
200 _key: String,
201 inputs: impl Iterator<Item = String> + Send,
202 _resources: (),
203 ) -> Result<Vec<String>, String> {
204 if self.process_delay > Duration::ZERO {
205 tokio::time::sleep(self.process_delay).await;
206 }
207 Ok(inputs.map(|s| s + " done").collect())
208 }
209 }
210
211 async fn shut_down(batcher: &Batcher<SimpleProcessor>) {
212 let worker = batcher.worker_handle();
213 worker.shut_down().await;
214 tokio::time::timeout(Duration::from_secs(1), worker.wait_for_shutdown())
215 .await
216 .expect("Worker should shut down");
217 }
218
219 #[tokio::test]
220 async fn records_metrics_for_successful_batch() {
221 let (state, factory) = test_metrics();
222
223 let batcher = Batcher::builder()
224 .name("test")
225 .processor(SimpleProcessor::instant())
226 .limits(Limits::builder().max_batch_size(2).build())
227 .batching_policy(BatchingPolicy::Size)
228 .metrics(factory)
229 .build();
230
231 let (r1, r2) = tokio::join!(
232 batcher.add("A".to_string(), "1".to_string()),
233 batcher.add("A".to_string(), "2".to_string()),
234 );
235 assert!(r1.is_ok());
236 assert!(r2.is_ok());
237
238 shut_down(&batcher).await;
239
240 let inner = state.lock().unwrap();
241 assert_eq!(inner.items_received, 2);
242 assert_eq!(inner.batches_completed, 1);
243 assert_eq!(inner.items_rejected, 0);
244 assert_eq!(inner.batch_sizes, vec![2]);
245 assert_eq!(inner.item_latencies.len(), 2);
246 assert!(inner.item_latencies.iter().all(|d| *d > Duration::ZERO));
247 assert_eq!(inner.channel_durations.len(), 2);
248 }
249
250 #[tokio::test(start_paused = true)]
251 async fn records_rejection_metrics() {
252 let (state, factory) = test_metrics();
253
254 let batcher = Batcher::builder()
255 .name("test")
256 .processor(SimpleProcessor {
257 process_delay: Duration::from_millis(100),
258 })
259 .limits(
260 Limits::builder()
261 .max_batch_size(1)
262 .max_key_concurrency(1)
263 .max_batch_queue_size(1)
264 .build(),
265 )
266 .batching_policy(BatchingPolicy::Size)
267 .metrics(factory)
268 .build();
269
270 let (r1, r2, r3) = tokio::join!(
275 batcher.add("A".to_string(), "1".to_string()),
276 batcher.add("A".to_string(), "2".to_string()),
277 batcher.add("A".to_string(), "3".to_string()),
278 );
279
280 let results = [&r1, &r2, &r3];
281 let successes = results.iter().filter(|r| r.is_ok()).count();
282 let rejections = results.iter().filter(|r| r.is_err()).count();
283 assert_eq!(successes, 2);
284 assert_eq!(rejections, 1);
285
286 shut_down(&batcher).await;
287
288 assert_eq!(state.lock().unwrap().items_rejected, 1);
289 }
290
291 #[tokio::test]
292 async fn records_gauge_metrics() {
293 let (state, factory) = test_metrics();
294
295 let batcher = Batcher::builder()
296 .name("test")
297 .processor(SimpleProcessor::instant())
298 .limits(Limits::builder().max_batch_size(1).build())
299 .batching_policy(BatchingPolicy::Size)
300 .metrics(factory)
301 .build();
302
303 let r = batcher.add("A".to_string(), "1".to_string()).await;
304 assert!(r.is_ok());
305
306 shut_down(&batcher).await;
307
308 let inner = state.lock().unwrap();
309 assert!(!inner.active_keys.is_empty());
310 assert!(inner.active_keys.iter().any(|&c| c > 0));
311 assert_eq!(*inner.active_keys.last().unwrap(), 0);
313 }
314}