1use std::{collections::HashMap, fmt::Debug, sync::Arc, 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: Arc<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: Arc<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, item: BatchItem<P>) {
184 self.metrics_recorder
185 .item_received(item.submitted_at.elapsed());
186
187 let key = item.key.clone();
188
189 let batch_queue = self.batch_queues.entry(key.clone()).or_insert_with(|| {
190 BatchQueue::new(self.batcher_name.clone(), key.clone(), self.limits)
191 });
192
193 match self.batching_policy.on_add(batch_queue) {
194 OnAdd::AddAndProcess => {
195 batch_queue.push(item);
196
197 self.process_next_batch(&key);
198 }
199 OnAdd::AddAndAcquireResources => {
200 batch_queue.push(item);
201
202 batch_queue.pre_acquire_resources(self.processor.clone(), self.msg_tx.clone());
203 }
204 OnAdd::AddAndProcessAfter(duration) => {
205 batch_queue.push(item);
206
207 batch_queue.process_after(duration, self.msg_tx.clone());
208 }
209 OnAdd::Add => {
210 batch_queue.push(item);
211 }
212 OnAdd::Reject(reason) => {
213 self.metrics_recorder.item_rejected();
214
215 if item
216 .tx
217 .send((Err(BatchError::Rejected(reason)), None))
218 .is_err()
219 {
220 debug!(
223 "Unable to send output over oneshot channel. Receiver deallocated. Batcher: {}",
224 self.batcher_name
225 );
226 }
227 }
228 }
229
230 self.report_gauges();
231 }
232
233 fn queue_mut<'q>(
236 batch_queues: &'q mut HashMap<P::Key, BatchQueue<P>>,
237 key: &P::Key,
238 ) -> &'q mut BatchQueue<P> {
239 batch_queues.get_mut(key).expect("batch queue should exist")
240 }
241
242 fn process_generation(&mut self, key: P::Key, generation: Generation) {
243 let batch_queue = Self::queue_mut(&mut self.batch_queues, &key);
244
245 batch_queue.process_generation(generation, self.processor.clone(), self.msg_tx.clone());
246 }
247
248 fn process_next_ready_batch(&mut self, key: &P::Key) {
249 let batch_queue = Self::queue_mut(&mut self.batch_queues, key);
250
251 batch_queue.process_next_ready_batch(self.processor.clone(), self.msg_tx.clone());
252 }
253
254 fn process_next_batch(&mut self, key: &P::Key) {
255 let batch_queue = Self::queue_mut(&mut self.batch_queues, key);
256
257 batch_queue.process_next_batch(self.processor.clone(), self.msg_tx.clone());
258 }
259
260 fn on_timeout(&mut self, key: P::Key, generation: Generation) {
261 let Some(batch_queue) = self.batch_queues.get_mut(&key) else {
264 debug!("Timeout for a batch queue which no longer exists. Ignoring.");
265 return;
266 };
267
268 match self.batching_policy.on_timeout(generation, batch_queue) {
269 OnGenerationEvent::Process => {
270 self.process_generation(key, generation);
271 }
272 OnGenerationEvent::DoNothing => {}
273 }
274 }
275
276 fn on_resource_acquired(
277 &mut self,
278 key: P::Key,
279 generation: Generation,
280 resources: P::Resources,
281 span: Span,
282 acquisition_duration: Duration,
283 ) {
284 self.metrics_recorder
285 .resource_acquisition_completed(acquisition_duration, true);
286
287 let batch_queue = Self::queue_mut(&mut self.batch_queues, &key);
288
289 batch_queue.resources_acquired(generation, resources, span);
290
291 match self
292 .batching_policy
293 .on_resources_acquired(generation, batch_queue)
294 {
295 OnGenerationEvent::Process => {
296 self.process_generation(key, generation);
297 }
298 OnGenerationEvent::DoNothing => {}
299 }
300 }
301
302 fn on_resource_acquisition_failed(
303 &mut self,
304 key: P::Key,
305 generation: Generation,
306 err: BatchError<P::Error>,
307 acquisition_duration: Duration,
308 ) {
309 self.metrics_recorder
310 .resource_acquisition_completed(acquisition_duration, false);
311
312 let batch_queue = Self::queue_mut(&mut self.batch_queues, &key);
313
314 batch_queue.fail_generation(generation, err);
315
316 self.process_next_and_clean_up(&key);
317 self.report_gauges();
318 }
319
320 fn on_batch_finished(&mut self, key: &P::Key, metrics: BatchStats) {
321 self.metrics_recorder.batch_completed(&metrics);
322
323 let batch_queue = Self::queue_mut(&mut self.batch_queues, key);
324
325 batch_queue.mark_processed();
326
327 self.process_next_and_clean_up(key);
328 self.report_gauges();
329 }
330
331 fn report_gauges(&self) {
332 self.metrics_recorder
333 .active_keys_changed(self.batch_queues.len());
334
335 let (total_processing, max_processing, total_queued, max_queued) = self
336 .batch_queues
337 .values()
338 .fold((0, 0, 0, 0), |(tp, mp, tq, mq), bq| {
339 let p = bq.processing();
340 let q = bq.queued();
341 (tp + p, mp.max(p), tq + q, mq.max(q))
342 });
343
344 self.metrics_recorder
345 .processing_concurrency_changed(total_processing, max_processing);
346 self.metrics_recorder
347 .queue_depth_changed(total_queued, max_queued);
348 }
349
350 fn process_next_and_clean_up(&mut self, key: &P::Key) {
353 let batch_queue = Self::queue_mut(&mut self.batch_queues, key);
354
355 match self.batching_policy.on_finish(batch_queue) {
356 OnFinish::ProcessNextReady => {
357 self.process_next_ready_batch(key);
358 }
359 OnFinish::ProcessNext => {
360 self.process_next_batch(key);
361 }
362 OnFinish::DoNothing => {}
363 }
364
365 if Self::queue_mut(&mut self.batch_queues, key).is_idle() {
369 self.batch_queues.remove(key);
370 }
371 }
372
373 fn ready_to_shut_down(&self) -> bool {
374 self.shutting_down
375 && self.batch_queues.values().all(|q| q.is_empty())
376 && !self.batch_queues.values().any(|q| q.is_processing())
377 }
378
379 async fn run(&mut self) {
381 loop {
382 tokio::select! {
383 Some(msg) = self.shutdown_notifier_rx.recv() => {
384 match msg {
385 ShutdownMessage::Register(notifier) => {
386 self.shutdown_notifiers.push(notifier.0);
387 }
388 ShutdownMessage::ShutDown => {
389 self.shutting_down = true;
390 }
391 }
392 }
393
394 Some(item) = self.item_rx.recv() => {
395 self.add(item);
396 }
397
398 Some(msg) = self.msg_rx.recv() => {
399 match msg {
400 Message::ResourcesAcquired { key, generation, resources, span, acquisition_duration } => {
401 self.on_resource_acquired(key, generation, resources, span, acquisition_duration);
402 }
403 Message::ResourceAcquisitionFailed { key, generation, err, acquisition_duration } => {
404 self.on_resource_acquisition_failed(key, generation, err, acquisition_duration);
405 }
406 Message::TimedOut(key, generation) => {
407 self.on_timeout(key, generation);
408 }
409 Message::Finished { key, metrics } => {
410 self.on_batch_finished(&key, metrics);
411 }
412 }
413 }
414 }
415
416 if self.ready_to_shut_down() {
417 info!("Batch worker '{}' is shutting down", &self.batcher_name);
418 return;
419 }
420 }
421 }
422}
423
424impl WorkerHandle {
425 pub async fn shut_down(&self) {
438 info!("Sending shut down signal to batch worker");
439 let _ = self.shutdown_tx.send(ShutdownMessage::ShutDown).await;
441 }
442
443 pub async fn wait_for_shutdown(&self) {
445 let (notifier_tx, notifier_rx) = oneshot::channel();
447 let _ = self
448 .shutdown_tx
449 .send(ShutdownMessage::Register(ShutdownNotifier(notifier_tx)))
450 .await;
451 let _ = notifier_rx.await;
453 }
454}
455
456impl Drop for WorkerDropGuard {
457 fn drop(&mut self) {
458 info!("Aborting batch worker");
459 self.handle.abort();
460 }
461}
462
463#[cfg(test)]
464mod test {
465 use tokio::sync::oneshot;
466 use tracing::Span;
467
468 use super::*;
469
470 #[derive(Debug, Clone)]
471 struct SimpleBatchProcessor;
472
473 impl Processor for SimpleBatchProcessor {
474 type Key = String;
475 type Input = String;
476 type Output = String;
477 type Error = String;
478 type Resources = ();
479
480 async fn acquire_resources(&self, _key: String) -> Result<(), String> {
481 Ok(())
482 }
483
484 async fn process(
485 &self,
486 _key: String,
487 inputs: impl Iterator<Item = String> + Send,
488 _resources: (),
489 ) -> Result<Vec<String>, String> {
490 Ok(inputs.map(|s| s + " processed").collect())
491 }
492 }
493
494 fn new_worker() -> Worker<SimpleBatchProcessor> {
497 let (_item_tx, item_rx) = mpsc::channel(1);
498 let (msg_tx, msg_rx) = mpsc::channel(1);
499 let (_shutdown_tx, shutdown_rx) = mpsc::channel(1);
500
501 Worker {
502 batcher_name: "test".to_string(),
503 item_rx,
504 processor: SimpleBatchProcessor,
505 msg_tx,
506 msg_rx,
507 shutdown_notifier_rx: shutdown_rx,
508 shutdown_notifiers: Vec::new(),
509 shutting_down: false,
510 limits: Limits::builder().max_batch_size(1).build(),
511 batching_policy: BatchingPolicy::Size,
512 batch_queues: HashMap::new(),
513 metrics_recorder: Arc::new(crate::metrics::NoopMetricsRecorder),
514 }
515 }
516
517 #[tokio::test]
518 async fn removes_batch_queue_when_key_becomes_idle() {
519 let mut worker = new_worker();
520
521 let (tx, rx) = oneshot::channel();
522 worker.add(BatchItem {
523 key: "K1".to_string(),
524 input: "I1".to_string(),
525 submitted_at: tokio::time::Instant::now(),
526 tx,
527 requesting_span: Span::none(),
528 });
529
530 let output = rx.await.unwrap().0.unwrap();
532 assert_eq!(output, "I1 processed");
533
534 let msg = worker.msg_rx.recv().await.unwrap();
536 let Message::Finished { key, metrics } = msg else {
537 panic!("expected Finished message, got {:?}", msg);
538 };
539 worker.on_batch_finished(&key, metrics);
540
541 assert!(
542 worker.batch_queues.is_empty(),
543 "the batch queue for an idle key should be removed"
544 );
545 }
546
547 #[tokio::test]
548 async fn ignores_timeout_for_removed_batch_queue() {
549 let mut worker = new_worker();
553
554 worker.on_timeout("K1".to_string(), Generation::default());
555 }
556
557 #[tokio::test]
558 async fn simple_test_over_channel() {
559 let (_worker_handle, _worker_guard, item_tx) = Worker::<SimpleBatchProcessor>::spawn(
560 "test".to_string(),
561 SimpleBatchProcessor,
562 Limits::builder().max_batch_size(2).build(),
563 BatchingPolicy::Size,
564 Arc::new(crate::metrics::NoopMetricsRecorder),
565 );
566
567 let rx1 = {
568 let (tx, rx) = oneshot::channel();
569 item_tx
570 .send(BatchItem {
571 key: "K1".to_string(),
572 input: "I1".to_string(),
573 submitted_at: tokio::time::Instant::now(),
574 tx,
575 requesting_span: Span::none(),
576 })
577 .await
578 .unwrap();
579
580 rx
581 };
582
583 let rx2 = {
584 let (tx, rx) = oneshot::channel();
585 item_tx
586 .send(BatchItem {
587 key: "K1".to_string(),
588 input: "I2".to_string(),
589 submitted_at: tokio::time::Instant::now(),
590 tx,
591 requesting_span: Span::none(),
592 })
593 .await
594 .unwrap();
595
596 rx
597 };
598
599 let o1 = rx1.await.unwrap().0.unwrap();
600 let o2 = rx2.await.unwrap().0.unwrap();
601
602 assert_eq!(o1, "I1 processed".to_string());
603 assert_eq!(o2, "I2 processed".to_string());
604 }
605}