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}
58
59impl BatchStats {
60 pub fn new(
62 size: usize,
63 processing_duration: Duration,
64 success: bool,
65 item_latencies: Vec<Duration>,
66 ) -> Self {
67 Self {
68 size,
69 processing_duration,
70 success,
71 item_latencies,
72 }
73 }
74}
75
76#[derive(Debug, Clone, Copy)]
78pub(crate) struct NoopMetricsRecorder;
79
80impl MetricsRecorder for NoopMetricsRecorder {}
81
82#[cfg(test)]
83mod tests {
84 use std::sync::{Arc, Mutex};
85 use std::time::Duration;
86
87 use crate::{Batcher, BatchingPolicy, Limits, Processor};
88
89 use super::*;
90
91 #[derive(Debug, Default)]
92 struct TestRecorderInner {
93 items_received: usize,
94 items_rejected: usize,
95 batches_completed: usize,
96 batch_sizes: Vec<usize>,
97 item_latencies: Vec<Duration>,
98 channel_durations: Vec<Duration>,
99 resource_acquisitions: usize,
100 active_keys: Vec<usize>,
101 processing_concurrency: (usize, usize),
102 queue_depth: (usize, usize),
103 }
104
105 #[derive(Debug, Default)]
106 struct TestRecorder(Mutex<TestRecorderInner>);
107
108 impl MetricsRecorder for TestRecorder {
109 fn item_received(&self, channel_duration: Duration) {
110 let mut inner = self.0.lock().unwrap();
111 inner.items_received += 1;
112 inner.channel_durations.push(channel_duration);
113 }
114
115 fn item_rejected(&self) {
116 self.0.lock().unwrap().items_rejected += 1;
117 }
118
119 fn batch_completed(&self, metrics: &BatchStats) {
120 let mut inner = self.0.lock().unwrap();
121 inner.batches_completed += 1;
122 inner.batch_sizes.push(metrics.size);
123 inner
124 .item_latencies
125 .extend_from_slice(&metrics.item_latencies);
126 }
127
128 fn resource_acquisition_completed(&self, _duration: Duration, _success: bool) {
129 self.0.lock().unwrap().resource_acquisitions += 1;
130 }
131
132 fn active_keys_changed(&self, count: usize) {
133 self.0.lock().unwrap().active_keys.push(count);
134 }
135
136 fn processing_concurrency_changed(&self, total: usize, max_per_key: usize) {
137 self.0.lock().unwrap().processing_concurrency = (total, max_per_key);
138 }
139
140 fn queue_depth_changed(&self, total: usize, max_per_key: usize) {
141 self.0.lock().unwrap().queue_depth = (total, max_per_key);
142 }
143 }
144
145 #[derive(Debug, Clone)]
146 struct SimpleProcessor {
147 process_delay: Duration,
148 }
149
150 impl SimpleProcessor {
151 fn instant() -> Self {
152 Self {
153 process_delay: Duration::ZERO,
154 }
155 }
156 }
157
158 impl Processor for SimpleProcessor {
159 type Key = String;
160 type Input = String;
161 type Output = String;
162 type Error = String;
163 type Resources = ();
164
165 async fn acquire_resources(&self, _key: String) -> Result<(), String> {
166 Ok(())
167 }
168
169 async fn process(
170 &self,
171 _key: String,
172 inputs: impl Iterator<Item = String> + Send,
173 _resources: (),
174 ) -> Result<Vec<String>, String> {
175 if self.process_delay > Duration::ZERO {
176 tokio::time::sleep(self.process_delay).await;
177 }
178 Ok(inputs.map(|s| s + " done").collect())
179 }
180 }
181
182 async fn shut_down(batcher: &Batcher<SimpleProcessor>) {
183 let worker = batcher.worker_handle();
184 worker.shut_down().await;
185 tokio::time::timeout(Duration::from_secs(1), worker.wait_for_shutdown())
186 .await
187 .expect("Worker should shut down");
188 }
189
190 #[tokio::test]
191 async fn records_metrics_for_successful_batch() {
192 let recorder = Arc::new(TestRecorder::default());
193
194 let batcher = Batcher::builder()
195 .name("test")
196 .processor(SimpleProcessor::instant())
197 .limits(Limits::builder().max_batch_size(2).build())
198 .batching_policy(BatchingPolicy::Size)
199 .metrics_recorder(Arc::clone(&recorder) as Arc<dyn MetricsRecorder>)
200 .build();
201
202 let (r1, r2) = tokio::join!(
203 batcher.add("A".to_string(), "1".to_string()),
204 batcher.add("A".to_string(), "2".to_string()),
205 );
206 assert!(r1.is_ok());
207 assert!(r2.is_ok());
208
209 shut_down(&batcher).await;
210
211 let inner = recorder.0.lock().unwrap();
212 assert_eq!(inner.items_received, 2);
213 assert_eq!(inner.batches_completed, 1);
214 assert_eq!(inner.items_rejected, 0);
215 assert_eq!(inner.batch_sizes, vec![2]);
216 assert_eq!(inner.item_latencies.len(), 2);
217 assert!(inner.item_latencies.iter().all(|d| *d > Duration::ZERO));
218 assert_eq!(inner.channel_durations.len(), 2);
219 }
220
221 #[tokio::test(start_paused = true)]
222 async fn records_rejection_metrics() {
223 let recorder = Arc::new(TestRecorder::default());
224
225 let batcher = Batcher::builder()
226 .name("test")
227 .processor(SimpleProcessor {
228 process_delay: Duration::from_millis(100),
229 })
230 .limits(
231 Limits::builder()
232 .max_batch_size(1)
233 .max_key_concurrency(1)
234 .max_batch_queue_size(1)
235 .build(),
236 )
237 .batching_policy(BatchingPolicy::Size)
238 .metrics_recorder(Arc::clone(&recorder) as Arc<dyn MetricsRecorder>)
239 .build();
240
241 let (r1, r2, r3) = tokio::join!(
246 batcher.add("A".to_string(), "1".to_string()),
247 batcher.add("A".to_string(), "2".to_string()),
248 batcher.add("A".to_string(), "3".to_string()),
249 );
250
251 let results = [&r1, &r2, &r3];
252 let successes = results.iter().filter(|r| r.is_ok()).count();
253 let rejections = results.iter().filter(|r| r.is_err()).count();
254 assert_eq!(successes, 2);
255 assert_eq!(rejections, 1);
256
257 shut_down(&batcher).await;
258
259 assert_eq!(recorder.0.lock().unwrap().items_rejected, 1);
260 }
261
262 #[tokio::test]
263 async fn records_gauge_metrics() {
264 let recorder = Arc::new(TestRecorder::default());
265
266 let batcher = Batcher::builder()
267 .name("test")
268 .processor(SimpleProcessor::instant())
269 .limits(Limits::builder().max_batch_size(1).build())
270 .batching_policy(BatchingPolicy::Size)
271 .metrics_recorder(Arc::clone(&recorder) as Arc<dyn MetricsRecorder>)
272 .build();
273
274 let r = batcher.add("A".to_string(), "1".to_string()).await;
275 assert!(r.is_ok());
276
277 shut_down(&batcher).await;
278
279 let inner = recorder.0.lock().unwrap();
280 assert!(!inner.active_keys.is_empty());
281 assert!(inner.active_keys.iter().any(|&c| c > 0));
282 assert_eq!(*inner.active_keys.last().unwrap(), 0);
284 }
285}