1use std::{collections::HashMap, fmt::Debug, time::Duration};
2
3use tokio::{
4 sync::{mpsc, oneshot},
5 task::JoinHandle,
6};
7use tracing::{Span, debug, info};
8
9use crate::{
10 BatchError,
11 batch::BatchItem,
12 batch_inner::Generation,
13 batch_queue::BatchQueue,
14 limits::Limits,
15 metrics::{BatchStats, MetricsRecorder},
16 policies::{BatchingPolicy, OnAdd, OnFinish, OnGenerationEvent},
17 processor::Processor,
18};
19
20pub(crate) struct Worker<P: Processor> {
21 batcher_name: String,
22
23 item_rx: mpsc::Receiver<BatchItem<P>>,
25 processor: P,
27
28 msg_tx: mpsc::Sender<Message<P>>,
30 msg_rx: mpsc::Receiver<Message<P>>,
32
33 shutdown_notifier_rx: mpsc::Receiver<ShutdownMessage>,
35
36 shutdown_notifiers: Vec<oneshot::Sender<()>>,
38
39 shutting_down: bool,
40
41 limits: Limits,
42 batching_policy: BatchingPolicy,
44
45 batch_queues: HashMap<P::Key, BatchQueue<P>>,
47
48 metrics_recorder: Box<dyn MetricsRecorder>,
49}
50
51pub(crate) enum Message<P: Processor> {
57 TimedOut(P::Key, Generation),
58 ResourcesAcquired {
59 key: P::Key,
60 generation: Generation,
61 resources: P::Resources,
62 span: Span,
63 acquisition_duration: Duration,
64 },
65 ResourceAcquisitionFailed {
66 key: P::Key,
67 generation: Generation,
68 err: BatchError<P::Error>,
69 acquisition_duration: Duration,
70 },
71 Finished {
72 key: P::Key,
73 metrics: BatchStats,
74 },
75}
76
77impl<P: Processor> Debug for Message<P> {
78 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
79 match self {
80 Message::TimedOut(key, generation) => f
81 .debug_tuple("TimedOut")
82 .field(key)
83 .field(generation)
84 .finish(),
85 Message::ResourcesAcquired {
86 key,
87 generation,
88 resources: _,
89 span: _,
90 acquisition_duration: _,
91 } => f
92 .debug_tuple("ResourcesAcquired")
93 .field(key)
94 .field(generation)
95 .field(&"<Resources>")
96 .finish(),
97 Message::ResourceAcquisitionFailed {
98 key,
99 generation,
100 err,
101 acquisition_duration: _,
102 } => f
103 .debug_tuple("ResourceAcquisitionFailed")
104 .field(key)
105 .field(generation)
106 .field(err)
107 .finish(),
108 Message::Finished { key, metrics: _ } => f.debug_tuple("Finished").field(key).finish(),
109 }
110 }
111}
112
113pub(crate) enum ShutdownMessage {
114 Register(ShutdownNotifier),
115 ShutDown,
116}
117
118pub(crate) struct ShutdownNotifier(oneshot::Sender<()>);
119
120#[derive(Debug, Clone)]
124pub struct WorkerHandle {
125 shutdown_tx: mpsc::Sender<ShutdownMessage>,
126}
127
128#[derive(Debug)]
130pub(crate) struct WorkerDropGuard {
131 handle: JoinHandle<()>,
132}
133
134impl<P: Processor> Worker<P> {
135 pub fn spawn(
136 batcher_name: String,
137 processor: P,
138 limits: Limits,
139 batching_policy: BatchingPolicy,
140 metrics_recorder: Box<dyn MetricsRecorder>,
141 ) -> (WorkerHandle, WorkerDropGuard, mpsc::Sender<BatchItem<P>>) {
142 let (item_tx, item_rx) = mpsc::channel(limits.max_items_in_system_per_key());
145 let (msg_tx, msg_rx) = mpsc::channel(limits.max_items_in_system_per_key());
146
147 let (shutdown_tx, shutdown_rx) = mpsc::channel(1);
148
149 let mut worker = Worker {
150 batcher_name,
151
152 item_rx,
153 processor,
154
155 msg_tx,
156 msg_rx,
157
158 shutdown_notifier_rx: shutdown_rx,
159 shutdown_notifiers: Vec::new(),
160
161 shutting_down: false,
162
163 limits,
164 batching_policy,
165
166 batch_queues: HashMap::new(),
167
168 metrics_recorder,
169 };
170
171 let handle = tokio::spawn(async move {
172 worker.run().await;
173 });
174
175 (
176 WorkerHandle { shutdown_tx },
177 WorkerDropGuard { handle },
178 item_tx,
179 )
180 }
181
182 fn add(&mut self, mut item: BatchItem<P>) {
184 self.metrics_recorder
185 .item_received(item.submitted_at.elapsed());
186 item.received_at = Some(tokio::time::Instant::now());
187
188 let key = item.key.clone();
189
190 let batch_queue = self.batch_queues.entry(key.clone()).or_insert_with(|| {
191 BatchQueue::new(self.batcher_name.clone(), key.clone(), self.limits)
192 });
193
194 match self.batching_policy.on_add(batch_queue) {
195 OnAdd::AddAndProcess => {
196 batch_queue.push(item);
197
198 self.process_next_batch(&key);
199 }
200 OnAdd::AddAndAcquireResources => {
201 batch_queue.push(item);
202
203 batch_queue.pre_acquire_resources(self.processor.clone(), self.msg_tx.clone());
204 }
205 OnAdd::AddAndProcessAfter(duration) => {
206 batch_queue.push(item);
207
208 batch_queue.process_after(duration, self.msg_tx.clone());
209 }
210 OnAdd::Add => {
211 batch_queue.push(item);
212 }
213 OnAdd::Reject(reason) => {
214 self.metrics_recorder.item_rejected();
215
216 if item
217 .tx
218 .send((Err(BatchError::Rejected(reason)), None))
219 .is_err()
220 {
221 debug!(
224 "Unable to send output over oneshot channel. Receiver deallocated. Batcher: {}",
225 self.batcher_name
226 );
227 }
228 }
229 }
230
231 self.report_gauges();
232 }
233
234 fn queue_mut<'q>(
237 batch_queues: &'q mut HashMap<P::Key, BatchQueue<P>>,
238 key: &P::Key,
239 ) -> &'q mut BatchQueue<P> {
240 batch_queues.get_mut(key).expect("batch queue should exist")
241 }
242
243 fn process_generation(&mut self, key: P::Key, generation: Generation) {
244 let batch_queue = Self::queue_mut(&mut self.batch_queues, &key);
245
246 batch_queue.process_generation(generation, self.processor.clone(), self.msg_tx.clone());
247 }
248
249 fn process_next_ready_batch(&mut self, key: &P::Key) {
250 let batch_queue = Self::queue_mut(&mut self.batch_queues, key);
251
252 batch_queue.process_next_ready_batch(self.processor.clone(), self.msg_tx.clone());
253 }
254
255 fn process_next_batch(&mut self, key: &P::Key) {
256 let batch_queue = Self::queue_mut(&mut self.batch_queues, key);
257
258 batch_queue.process_next_batch(self.processor.clone(), self.msg_tx.clone());
259 }
260
261 fn on_timeout(&mut self, key: P::Key, generation: Generation) {
262 let Some(batch_queue) = self.batch_queues.get_mut(&key) else {
265 debug!("Timeout for a batch queue which no longer exists. Ignoring.");
266 return;
267 };
268
269 match self.batching_policy.on_timeout(generation, batch_queue) {
270 OnGenerationEvent::Process => {
271 self.process_generation(key, generation);
272 }
273 OnGenerationEvent::DoNothing => {}
274 }
275 }
276
277 fn on_resource_acquired(
278 &mut self,
279 key: P::Key,
280 generation: Generation,
281 resources: P::Resources,
282 span: Span,
283 acquisition_duration: Duration,
284 ) {
285 self.metrics_recorder
286 .resource_acquisition_completed(acquisition_duration, true);
287
288 let batch_queue = Self::queue_mut(&mut self.batch_queues, &key);
289
290 batch_queue.resources_acquired(generation, resources, span);
291
292 match self
293 .batching_policy
294 .on_resources_acquired(generation, batch_queue)
295 {
296 OnGenerationEvent::Process => {
297 self.process_generation(key, generation);
298 }
299 OnGenerationEvent::DoNothing => {}
300 }
301 }
302
303 fn on_resource_acquisition_failed(
304 &mut self,
305 key: P::Key,
306 generation: Generation,
307 err: BatchError<P::Error>,
308 acquisition_duration: Duration,
309 ) {
310 self.metrics_recorder
311 .resource_acquisition_completed(acquisition_duration, false);
312
313 let batch_queue = Self::queue_mut(&mut self.batch_queues, &key);
314
315 batch_queue.fail_generation(generation, err);
316
317 self.process_next_and_clean_up(&key);
318 self.report_gauges();
319 }
320
321 fn on_batch_finished(&mut self, key: &P::Key, metrics: BatchStats) {
322 self.metrics_recorder.batch_completed(&metrics);
323
324 let batch_queue = Self::queue_mut(&mut self.batch_queues, key);
325
326 batch_queue.mark_processed();
327
328 self.process_next_and_clean_up(key);
329 self.report_gauges();
330 }
331
332 fn report_gauges(&self) {
333 self.metrics_recorder
334 .active_keys_changed(self.batch_queues.len());
335
336 let (total_processing, max_processing, total_queued, max_queued, total_items, max_items) =
337 self.batch_queues
338 .values()
339 .fold((0, 0, 0, 0, 0, 0), |(tp, mp, tq, mq, ti, mi), bq| {
340 let p = bq.processing();
341 let q = bq.queued();
342 let i = bq.queued_items();
343 (tp + p, mp.max(p), tq + q, mq.max(q), ti + i, mi.max(i))
344 });
345
346 self.metrics_recorder
347 .processing_concurrency_changed(total_processing, max_processing);
348 self.metrics_recorder
349 .queue_depth_changed(total_queued, max_queued);
350 self.metrics_recorder
351 .queue_items_changed(total_items, max_items);
352 }
353
354 fn process_next_and_clean_up(&mut self, key: &P::Key) {
357 let batch_queue = Self::queue_mut(&mut self.batch_queues, key);
358
359 match self.batching_policy.on_finish(batch_queue) {
360 OnFinish::ProcessNextReady => {
361 self.process_next_ready_batch(key);
362 }
363 OnFinish::ProcessNext => {
364 self.process_next_batch(key);
365 }
366 OnFinish::DoNothing => {}
367 }
368
369 if Self::queue_mut(&mut self.batch_queues, key).is_idle() {
373 self.batch_queues.remove(key);
374 }
375 }
376
377 fn ready_to_shut_down(&self) -> bool {
378 self.shutting_down
379 && self.batch_queues.values().all(|q| q.is_empty())
380 && !self.batch_queues.values().any(|q| q.is_processing())
381 }
382
383 async fn run(&mut self) {
385 loop {
386 tokio::select! {
387 Some(msg) = self.shutdown_notifier_rx.recv() => {
388 match msg {
389 ShutdownMessage::Register(notifier) => {
390 self.shutdown_notifiers.push(notifier.0);
391 }
392 ShutdownMessage::ShutDown => {
393 self.shutting_down = true;
394 }
395 }
396 }
397
398 Some(item) = self.item_rx.recv() => {
399 self.add(item);
400 }
401
402 Some(msg) = self.msg_rx.recv() => {
403 match msg {
404 Message::ResourcesAcquired { key, generation, resources, span, acquisition_duration } => {
405 self.on_resource_acquired(key, generation, resources, span, acquisition_duration);
406 }
407 Message::ResourceAcquisitionFailed { key, generation, err, acquisition_duration } => {
408 self.on_resource_acquisition_failed(key, generation, err, acquisition_duration);
409 }
410 Message::TimedOut(key, generation) => {
411 self.on_timeout(key, generation);
412 }
413 Message::Finished { key, metrics } => {
414 self.on_batch_finished(&key, metrics);
415 }
416 }
417 }
418 }
419
420 if self.ready_to_shut_down() {
421 info!("Batch worker '{}' is shutting down", &self.batcher_name);
422 return;
423 }
424 }
425 }
426}
427
428impl WorkerHandle {
429 pub async fn shut_down(&self) {
442 info!("Sending shut down signal to batch worker");
443 let _ = self.shutdown_tx.send(ShutdownMessage::ShutDown).await;
445 }
446
447 pub async fn wait_for_shutdown(&self) {
449 let (notifier_tx, notifier_rx) = oneshot::channel();
451 let _ = self
452 .shutdown_tx
453 .send(ShutdownMessage::Register(ShutdownNotifier(notifier_tx)))
454 .await;
455 let _ = notifier_rx.await;
457 }
458}
459
460impl Drop for WorkerDropGuard {
461 fn drop(&mut self) {
462 info!("Aborting batch worker");
463 self.handle.abort();
464 }
465}
466
467#[cfg(test)]
468mod test {
469 use std::sync::{Arc, Mutex};
470
471 use tokio::sync::oneshot;
472 use tracing::Span;
473
474 use super::*;
475
476 #[derive(Debug, Clone)]
477 struct SimpleBatchProcessor;
478
479 impl Processor for SimpleBatchProcessor {
480 type Key = String;
481 type Input = String;
482 type Output = String;
483 type Error = String;
484 type Resources = ();
485
486 async fn acquire_resources(&self, _key: String) -> Result<(), String> {
487 Ok(())
488 }
489
490 async fn process(
491 &self,
492 _key: String,
493 inputs: impl Iterator<Item = String> + Send,
494 _resources: (),
495 ) -> Result<Vec<String>, String> {
496 Ok(inputs.map(|s| s + " processed").collect())
497 }
498 }
499
500 fn new_worker() -> Worker<SimpleBatchProcessor> {
503 let (_item_tx, item_rx) = mpsc::channel(1);
504 let (msg_tx, msg_rx) = mpsc::channel(1);
505 let (_shutdown_tx, shutdown_rx) = mpsc::channel(1);
506
507 Worker {
508 batcher_name: "test".to_string(),
509 item_rx,
510 processor: SimpleBatchProcessor,
511 msg_tx,
512 msg_rx,
513 shutdown_notifier_rx: shutdown_rx,
514 shutdown_notifiers: Vec::new(),
515 shutting_down: false,
516 limits: Limits::builder().max_batch_size(1).build(),
517 batching_policy: BatchingPolicy::Size,
518 batch_queues: HashMap::new(),
519 metrics_recorder: Box::new(crate::metrics::NoopMetricsRecorder),
520 }
521 }
522
523 #[tokio::test]
524 async fn removes_batch_queue_when_key_becomes_idle() {
525 let mut worker = new_worker();
526
527 let (tx, rx) = oneshot::channel();
528 worker.add(BatchItem {
529 key: "K1".to_string(),
530 input: "I1".to_string(),
531 submitted_at: tokio::time::Instant::now(),
532 received_at: None,
533 tx,
534 requesting_span: Span::none(),
535 });
536
537 let output = rx.await.unwrap().0.unwrap();
539 assert_eq!(output, "I1 processed");
540
541 let msg = worker.msg_rx.recv().await.unwrap();
543 let Message::Finished { key, metrics } = msg else {
544 panic!("expected Finished message, got {:?}", msg);
545 };
546 worker.on_batch_finished(&key, metrics);
547
548 assert!(
549 worker.batch_queues.is_empty(),
550 "the batch queue for an idle key should be removed"
551 );
552 }
553
554 #[tokio::test]
555 async fn ignores_timeout_for_removed_batch_queue() {
556 let mut worker = new_worker();
560
561 worker.on_timeout("K1".to_string(), Generation::default());
562 }
563
564 #[tokio::test]
565 async fn simple_test_over_channel() {
566 let (_worker_handle, _worker_guard, item_tx) = Worker::<SimpleBatchProcessor>::spawn(
567 "test".to_string(),
568 SimpleBatchProcessor,
569 Limits::builder().max_batch_size(2).build(),
570 BatchingPolicy::Size,
571 Box::new(crate::metrics::NoopMetricsRecorder),
572 );
573
574 let rx1 = {
575 let (tx, rx) = oneshot::channel();
576 item_tx
577 .send(BatchItem {
578 key: "K1".to_string(),
579 input: "I1".to_string(),
580 submitted_at: tokio::time::Instant::now(),
581 received_at: None,
582 tx,
583 requesting_span: Span::none(),
584 })
585 .await
586 .unwrap();
587
588 rx
589 };
590
591 let rx2 = {
592 let (tx, rx) = oneshot::channel();
593 item_tx
594 .send(BatchItem {
595 key: "K1".to_string(),
596 input: "I2".to_string(),
597 submitted_at: tokio::time::Instant::now(),
598 received_at: None,
599 tx,
600 requesting_span: Span::none(),
601 })
602 .await
603 .unwrap();
604
605 rx
606 };
607
608 let o1 = rx1.await.unwrap().0.unwrap();
609 let o2 = rx2.await.unwrap().0.unwrap();
610
611 assert_eq!(o1, "I1 processed".to_string());
612 assert_eq!(o2, "I2 processed".to_string());
613 }
614
615 #[derive(Debug, Clone)]
616 struct RecordingMetrics(Arc<Mutex<(usize, usize)>>);
617
618 impl MetricsRecorder for RecordingMetrics {
619 fn queue_items_changed(&self, total: usize, max_per_key: usize) {
620 *self.0.lock().unwrap() = (total, max_per_key);
621 }
622 }
623
624 fn dummy_item(key: &str) -> BatchItem<SimpleBatchProcessor> {
625 let (tx, _rx) = oneshot::channel();
626 BatchItem {
627 key: key.to_string(),
628 input: "I".to_string(),
629 submitted_at: tokio::time::Instant::now(),
630 received_at: None,
631 tx,
632 requesting_span: Span::none(),
633 }
634 }
635
636 #[tokio::test]
637 async fn queue_items_changed_sums_and_maxes_across_keys() {
638 let mut worker = new_worker();
639 worker.limits = Limits::builder().max_batch_size(2).build();
640
641 let gauges = Arc::new(Mutex::new((0, 0)));
642 worker.metrics_recorder = Box::new(RecordingMetrics(gauges.clone()));
643
644 let limits = worker.limits;
645
646 let queue_a = worker
648 .batch_queues
649 .entry("A".to_string())
650 .or_insert_with(|| BatchQueue::new("test".to_string(), "A".to_string(), limits));
651 for _ in 0..3 {
652 queue_a.push(dummy_item("A"));
653 }
654
655 let queue_b = worker
657 .batch_queues
658 .entry("B".to_string())
659 .or_insert_with(|| BatchQueue::new("test".to_string(), "B".to_string(), limits));
660 queue_b.push(dummy_item("B"));
661
662 worker.report_gauges();
663
664 let (total, max_per_key) = *gauges.lock().unwrap();
665 assert_eq!(total, 4, "should sum items across both keys");
666 assert_eq!(
667 max_per_key, 3,
668 "should report the highest per-key item count, not batch count"
669 );
670 }
671}