1use std::{
2 fmt,
3 future::Future,
4 sync::{
5 atomic::{AtomicBool, Ordering},
6 Arc, Mutex,
7 },
8 time::{Duration, Instant},
9};
10
11use tokio::{
12 sync::{mpsc, oneshot},
13 task::JoinHandle,
14 time,
15};
16
17pub struct BatchExecutor<T> {
19 sender: mpsc::Sender<Command<T>>,
20 worker: JoinHandle<()>,
21}
22
23pub struct ChunkExecutor<T> {
25 sender: mpsc::Sender<ChunkCommand<T>>,
26 worker: JoinHandle<()>,
27}
28
29pub struct DelayExecutor {
31 trigger: mpsc::Sender<()>,
32 triggered: Arc<AtomicBool>,
33 shutdown: oneshot::Sender<()>,
34 worker: JoinHandle<Result<(), String>>,
35}
36
37pub struct LessExecutor {
39 threshold: Duration,
40 last_execution: Mutex<Option<Instant>>,
41}
42
43impl LessExecutor {
44 pub fn new(threshold: Duration) -> Self {
45 assert!(!threshold.is_zero(), "threshold must be greater than zero");
46 Self {
47 threshold,
48 last_execution: Mutex::new(None),
49 }
50 }
51
52 pub fn do_or_discard<F>(&self, execute: F) -> bool
54 where
55 F: FnOnce(),
56 {
57 let now = Instant::now();
58 {
59 let mut last = self
60 .last_execution
61 .lock()
62 .unwrap_or_else(|poisoned| poisoned.into_inner());
63 if last.is_some_and(|last| now.duration_since(last) <= self.threshold) {
64 return false;
65 }
66 *last = Some(now);
67 }
68 execute();
69 true
70 }
71}
72
73impl DelayExecutor {
74 pub fn new<F, Fut, E>(delay: Duration, mut execute: F) -> Self
75 where
76 F: FnMut() -> Fut + Send + 'static,
77 Fut: Future<Output = Result<(), E>> + Send + 'static,
78 E: fmt::Display,
79 {
80 assert!(!delay.is_zero(), "delay must be greater than zero");
81 let (trigger, mut triggers) = mpsc::channel(1);
82 let triggered = Arc::new(AtomicBool::new(false));
83 let worker_triggered = Arc::clone(&triggered);
84 let (shutdown, mut stopping) = oneshot::channel();
85 let worker = tokio::spawn(async move {
86 loop {
87 tokio::select! {
88 _ = &mut stopping => return Ok(()),
89 trigger = triggers.recv() => {
90 if trigger.is_none() {
91 return Ok(());
92 }
93 tokio::select! {
94 _ = &mut stopping => return Ok(()),
95 _ = time::sleep(delay) => {}
96 }
97 worker_triggered.store(false, Ordering::Release);
99 execute().await.map_err(|error| error.to_string())?;
100 }
101 }
102 }
103 });
104 Self {
105 trigger,
106 triggered,
107 shutdown,
108 worker,
109 }
110 }
111
112 pub fn trigger(&self) -> bool {
114 if self
115 .triggered
116 .compare_exchange(false, true, Ordering::AcqRel, Ordering::Acquire)
117 .is_err()
118 {
119 return false;
120 }
121 if self.trigger.try_send(()).is_err() {
122 self.triggered.store(false, Ordering::Release);
123 return false;
124 }
125 true
126 }
127
128 pub async fn shutdown(self, timeout: Duration) -> Result<(), DelayExecutorError> {
129 let Self {
130 trigger,
131 triggered: _,
132 shutdown,
133 mut worker,
134 } = self;
135 drop(trigger);
136 let _ = shutdown.send(());
137 match time::timeout(timeout, &mut worker).await {
138 Ok(Ok(Ok(()))) => Ok(()),
139 Ok(Ok(Err(error))) => Err(DelayExecutorError::Job(error)),
140 Ok(Err(error)) => Err(DelayExecutorError::Worker(error.to_string())),
141 Err(_) => {
142 worker.abort();
143 let _ = worker.await;
144 Err(DelayExecutorError::TimedOut(timeout))
145 }
146 }
147 }
148}
149
150#[derive(Debug, Clone, PartialEq, Eq)]
151pub enum DelayExecutorError {
152 Job(String),
153 TimedOut(Duration),
154 Worker(String),
155}
156
157impl fmt::Display for DelayExecutorError {
158 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
159 match self {
160 Self::Job(error) => write!(formatter, "delayed job failed: {error}"),
161 Self::TimedOut(timeout) => {
162 write!(formatter, "delay executor did not stop within {timeout:?}")
163 }
164 Self::Worker(error) => write!(formatter, "delay executor worker failed: {error}"),
165 }
166 }
167}
168
169impl std::error::Error for DelayExecutorError {}
170
171pub struct PeriodicExecutor {
177 shutdown: oneshot::Sender<()>,
178 worker: JoinHandle<Result<(), String>>,
179}
180
181impl PeriodicExecutor {
182 pub fn new<F, Fut, E>(interval: Duration, mut execute: F) -> Self
183 where
184 F: FnMut() -> Fut + Send + 'static,
185 Fut: Future<Output = Result<(), E>> + Send + 'static,
186 E: fmt::Display,
187 {
188 assert!(!interval.is_zero(), "interval must be greater than zero");
189 let (shutdown, mut stopping) = oneshot::channel();
190 let worker = tokio::spawn(async move {
191 let start = time::Instant::now() + interval;
192 let mut ticker = time::interval_at(start, interval);
193 ticker.set_missed_tick_behavior(time::MissedTickBehavior::Delay);
194
195 loop {
196 tokio::select! {
197 _ = &mut stopping => return Ok(()),
198 _ = ticker.tick() => {
199 execute().await.map_err(|error| error.to_string())?;
200 }
201 }
202 }
203 });
204 Self { shutdown, worker }
205 }
206
207 pub async fn shutdown(self, timeout: Duration) -> Result<(), PeriodicExecutorError> {
209 let Self {
210 shutdown,
211 mut worker,
212 } = self;
213 let _ = shutdown.send(());
214
215 match time::timeout(timeout, &mut worker).await {
216 Ok(Ok(Ok(()))) => Ok(()),
217 Ok(Ok(Err(error))) => Err(PeriodicExecutorError::Job(error)),
218 Ok(Err(error)) => Err(PeriodicExecutorError::Worker(error.to_string())),
219 Err(_) => {
220 worker.abort();
221 let _ = worker.await;
222 Err(PeriodicExecutorError::TimedOut(timeout))
223 }
224 }
225 }
226}
227
228#[derive(Debug, Clone, PartialEq, Eq)]
229pub enum PeriodicExecutorError {
230 Job(String),
231 TimedOut(Duration),
232 Worker(String),
233}
234
235impl fmt::Display for PeriodicExecutorError {
236 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
237 match self {
238 Self::Job(error) => write!(formatter, "periodic job failed: {error}"),
239 Self::TimedOut(timeout) => {
240 write!(
241 formatter,
242 "periodic executor did not stop within {timeout:?}"
243 )
244 }
245 Self::Worker(error) => write!(formatter, "periodic executor worker failed: {error}"),
246 }
247 }
248}
249
250impl std::error::Error for PeriodicExecutorError {}
251
252impl<T> BatchExecutor<T>
253where
254 T: Send + 'static,
255{
256 pub fn new<F, Fut>(max_batch_size: usize, flush_interval: Duration, execute: F) -> Self
257 where
258 F: FnMut(Vec<T>) -> Fut + Send + 'static,
259 Fut: Future<Output = ()> + Send + 'static,
260 {
261 assert!(
262 max_batch_size > 0,
263 "maximum batch size must be greater than zero"
264 );
265 assert!(
266 !flush_interval.is_zero(),
267 "flush interval must be greater than zero"
268 );
269
270 let (sender, receiver) = mpsc::channel(max_batch_size.saturating_mul(2).max(1));
271 let worker = tokio::spawn(run_worker(
272 receiver,
273 max_batch_size,
274 flush_interval,
275 execute,
276 ));
277 Self { sender, worker }
278 }
279
280 pub async fn push(&self, value: T) -> Result<(), BatchExecutorError> {
282 self.sender
283 .send(Command::Item(value))
284 .await
285 .map_err(|_| BatchExecutorError::Closed)
286 }
287
288 pub async fn flush(&self) -> Result<(), BatchExecutorError> {
290 let (sender, receiver) = oneshot::channel();
291 self.sender
292 .send(Command::Flush(sender))
293 .await
294 .map_err(|_| BatchExecutorError::Closed)?;
295 receiver.await.map_err(|_| BatchExecutorError::Closed)
296 }
297
298 pub async fn shutdown(self) -> Result<(), BatchExecutorError> {
300 let (sender, receiver) = oneshot::channel();
301 self.sender
302 .send(Command::Shutdown(sender))
303 .await
304 .map_err(|_| BatchExecutorError::Closed)?;
305 receiver.await.map_err(|_| BatchExecutorError::Closed)?;
306 self.worker
307 .await
308 .map_err(|error| BatchExecutorError::Worker(error.to_string()))
309 }
310}
311
312impl<T> ChunkExecutor<T>
313where
314 T: Send + 'static,
315{
316 pub fn new<F, Fut>(max_chunk_bytes: usize, flush_interval: Duration, execute: F) -> Self
317 where
318 F: FnMut(Vec<T>) -> Fut + Send + 'static,
319 Fut: Future<Output = ()> + Send + 'static,
320 {
321 assert!(max_chunk_bytes > 0, "chunk size must be greater than zero");
322 assert!(
323 !flush_interval.is_zero(),
324 "flush interval must be greater than zero"
325 );
326
327 let (sender, receiver) = mpsc::channel(128);
328 let worker = tokio::spawn(run_chunk_worker(
329 receiver,
330 max_chunk_bytes,
331 flush_interval,
332 execute,
333 ));
334 Self { sender, worker }
335 }
336
337 pub async fn push(&self, value: T, size: usize) -> Result<(), BatchExecutorError> {
339 self.sender
340 .send(ChunkCommand::Item { value, size })
341 .await
342 .map_err(|_| BatchExecutorError::Closed)
343 }
344
345 pub async fn flush(&self) -> Result<(), BatchExecutorError> {
347 let (sender, receiver) = oneshot::channel();
348 self.sender
349 .send(ChunkCommand::Flush(sender))
350 .await
351 .map_err(|_| BatchExecutorError::Closed)?;
352 receiver.await.map_err(|_| BatchExecutorError::Closed)
353 }
354
355 pub async fn shutdown(self) -> Result<(), BatchExecutorError> {
357 let (sender, receiver) = oneshot::channel();
358 self.sender
359 .send(ChunkCommand::Shutdown(sender))
360 .await
361 .map_err(|_| BatchExecutorError::Closed)?;
362 receiver.await.map_err(|_| BatchExecutorError::Closed)?;
363 self.worker
364 .await
365 .map_err(|error| BatchExecutorError::Worker(error.to_string()))
366 }
367}
368
369enum Command<T> {
370 Item(T),
371 Flush(oneshot::Sender<()>),
372 Shutdown(oneshot::Sender<()>),
373}
374
375enum ChunkCommand<T> {
376 Item { value: T, size: usize },
377 Flush(oneshot::Sender<()>),
378 Shutdown(oneshot::Sender<()>),
379}
380
381async fn run_worker<T, F, Fut>(
382 mut receiver: mpsc::Receiver<Command<T>>,
383 max_batch_size: usize,
384 flush_interval: Duration,
385 mut execute: F,
386) where
387 T: Send + 'static,
388 F: FnMut(Vec<T>) -> Fut + Send + 'static,
389 Fut: Future<Output = ()> + Send + 'static,
390{
391 let mut batch = Vec::with_capacity(max_batch_size);
392 let mut ticker = time::interval(flush_interval);
393 ticker.set_missed_tick_behavior(time::MissedTickBehavior::Delay);
394 ticker.tick().await;
395
396 loop {
397 tokio::select! {
398 command = receiver.recv() => {
399 match command {
400 Some(Command::Item(value)) => {
401 batch.push(value);
402 if batch.len() >= max_batch_size {
403 execute(std::mem::take(&mut batch)).await;
404 batch.reserve(max_batch_size);
405 }
406 }
407 Some(Command::Flush(done)) => {
408 flush_batch(&mut batch, &mut execute).await;
409 let _ = done.send(());
410 }
411 Some(Command::Shutdown(done)) => {
412 flush_batch(&mut batch, &mut execute).await;
413 let _ = done.send(());
414 return;
415 }
416 None => {
417 flush_batch(&mut batch, &mut execute).await;
418 return;
419 }
420 }
421 }
422 _ = ticker.tick() => {
423 flush_batch(&mut batch, &mut execute).await;
424 }
425 }
426 }
427}
428
429async fn run_chunk_worker<T, F, Fut>(
430 mut receiver: mpsc::Receiver<ChunkCommand<T>>,
431 max_chunk_bytes: usize,
432 flush_interval: Duration,
433 mut execute: F,
434) where
435 T: Send + 'static,
436 F: FnMut(Vec<T>) -> Fut + Send + 'static,
437 Fut: Future<Output = ()> + Send + 'static,
438{
439 let mut chunk = Vec::new();
440 let mut chunk_bytes = 0usize;
441 let mut ticker = time::interval(flush_interval);
442 ticker.set_missed_tick_behavior(time::MissedTickBehavior::Delay);
443 ticker.tick().await;
444
445 loop {
446 tokio::select! {
447 command = receiver.recv() => {
448 match command {
449 Some(ChunkCommand::Item { value, size }) => {
450 chunk.push(value);
451 chunk_bytes = chunk_bytes.saturating_add(size);
452 if chunk_bytes >= max_chunk_bytes {
453 execute(std::mem::take(&mut chunk)).await;
454 chunk_bytes = 0;
455 }
456 }
457 Some(ChunkCommand::Flush(done)) => {
458 flush_batch(&mut chunk, &mut execute).await;
459 chunk_bytes = 0;
460 let _ = done.send(());
461 }
462 Some(ChunkCommand::Shutdown(done)) => {
463 flush_batch(&mut chunk, &mut execute).await;
464 let _ = done.send(());
465 return;
466 }
467 None => {
468 flush_batch(&mut chunk, &mut execute).await;
469 return;
470 }
471 }
472 }
473 _ = ticker.tick() => {
474 flush_batch(&mut chunk, &mut execute).await;
475 chunk_bytes = 0;
476 }
477 }
478 }
479}
480
481async fn flush_batch<T, F, Fut>(batch: &mut Vec<T>, execute: &mut F)
482where
483 F: FnMut(Vec<T>) -> Fut,
484 Fut: Future<Output = ()>,
485{
486 if !batch.is_empty() {
487 execute(std::mem::take(batch)).await;
488 }
489}
490
491#[derive(Debug, Clone, PartialEq, Eq)]
492pub enum BatchExecutorError {
493 Closed,
494 Worker(String),
495}
496
497impl fmt::Display for BatchExecutorError {
498 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
499 match self {
500 Self::Closed => formatter.write_str("batch executor is closed"),
501 Self::Worker(error) => write!(formatter, "batch executor worker failed: {error}"),
502 }
503 }
504}
505
506impl std::error::Error for BatchExecutorError {}
507
508#[cfg(test)]
509mod tests {
510 use super::*;
511 use tokio::sync::mpsc;
512
513 #[tokio::test]
514 async fn flushes_when_the_batch_reaches_its_limit() {
515 let (batches, mut received) = mpsc::unbounded_channel();
516 let executor = BatchExecutor::new(3, Duration::from_secs(60), move |batch| {
517 let batches = batches.clone();
518 async move {
519 batches.send(batch).unwrap();
520 }
521 });
522
523 executor.push(1).await.unwrap();
524 executor.push(2).await.unwrap();
525 executor.push(3).await.unwrap();
526
527 assert_eq!(received.recv().await.unwrap(), vec![1, 2, 3]);
528 executor.shutdown().await.unwrap();
529 }
530
531 #[tokio::test]
532 async fn explicit_flush_and_shutdown_do_not_lose_work() {
533 let (batches, mut received) = mpsc::unbounded_channel();
534 let executor = BatchExecutor::new(10, Duration::from_secs(60), move |batch| {
535 let batches = batches.clone();
536 async move {
537 batches.send(batch).unwrap();
538 }
539 });
540
541 executor.push("first").await.unwrap();
542 executor.flush().await.unwrap();
543 assert_eq!(received.recv().await.unwrap(), vec!["first"]);
544
545 executor.push("second").await.unwrap();
546 executor.shutdown().await.unwrap();
547 assert_eq!(received.recv().await.unwrap(), vec!["second"]);
548 }
549
550 #[tokio::test]
551 async fn chunk_executor_flushes_on_combined_size() {
552 let (chunks, mut received) = mpsc::unbounded_channel();
553 let executor = ChunkExecutor::new(10, Duration::from_secs(60), move |chunk| {
554 let chunks = chunks.clone();
555 async move {
556 chunks.send(chunk).unwrap();
557 }
558 });
559
560 executor.push("small", 4).await.unwrap();
561 executor.push("large", 6).await.unwrap();
562 assert_eq!(received.recv().await.unwrap(), vec!["small", "large"]);
563 executor.shutdown().await.unwrap();
564 }
565
566 #[tokio::test]
567 async fn chunk_executor_flushes_on_interval_and_shutdown() {
568 let (chunks, mut received) = mpsc::unbounded_channel();
569 let executor = ChunkExecutor::new(100, Duration::from_millis(5), move |chunk| {
570 let chunks = chunks.clone();
571 async move {
572 chunks.send(chunk).unwrap();
573 }
574 });
575
576 executor.push(1, 1).await.unwrap();
577 assert_eq!(received.recv().await.unwrap(), vec![1]);
578 executor.push(2, 1).await.unwrap();
579 executor.shutdown().await.unwrap();
580 assert_eq!(received.recv().await.unwrap(), vec![2]);
581 }
582
583 #[tokio::test]
584 async fn delay_executor_coalesces_pending_triggers_and_can_run_again() {
585 let (runs, mut received) = mpsc::unbounded_channel();
586 let executor = DelayExecutor::new(Duration::from_millis(5), move || {
587 let runs = runs.clone();
588 async move {
589 runs.send(()).unwrap();
590 Ok::<_, &'static str>(())
591 }
592 });
593
594 assert!(executor.trigger());
595 assert!(!executor.trigger());
596 time::timeout(Duration::from_secs(1), received.recv())
597 .await
598 .unwrap()
599 .unwrap();
600 assert!(executor.trigger());
601 time::timeout(Duration::from_secs(1), received.recv())
602 .await
603 .unwrap()
604 .unwrap();
605 executor.shutdown(Duration::from_secs(1)).await.unwrap();
606 }
607
608 #[tokio::test]
609 async fn delay_executor_reports_job_failure() {
610 let executor = DelayExecutor::new(Duration::from_millis(1), || async {
611 Err::<(), _>("write failed")
612 });
613 assert!(executor.trigger());
614 time::sleep(Duration::from_millis(10)).await;
615 assert_eq!(
616 executor.shutdown(Duration::from_secs(1)).await,
617 Err(DelayExecutorError::Job("write failed".to_owned()))
618 );
619 }
620
621 #[test]
622 fn less_executor_discards_calls_inside_the_threshold() {
623 let executor = LessExecutor::new(Duration::from_millis(10));
624 let mut runs = 0;
625 assert!(executor.do_or_discard(|| runs += 1));
626 assert!(!executor.do_or_discard(|| runs += 1));
627 std::thread::sleep(Duration::from_millis(15));
628 assert!(executor.do_or_discard(|| runs += 1));
629 assert_eq!(runs, 2);
630 }
631
632 #[tokio::test]
633 async fn periodic_executor_runs_repeatedly_and_stops() {
634 let (runs, mut received) = mpsc::unbounded_channel();
635 let executor = PeriodicExecutor::new(Duration::from_millis(5), move || {
636 let runs = runs.clone();
637 async move {
638 runs.send(()).unwrap();
639 Ok::<_, &'static str>(())
640 }
641 });
642
643 time::timeout(Duration::from_secs(1), received.recv())
644 .await
645 .unwrap()
646 .unwrap();
647 time::timeout(Duration::from_secs(1), received.recv())
648 .await
649 .unwrap()
650 .unwrap();
651 executor.shutdown(Duration::from_secs(1)).await.unwrap();
652 }
653
654 #[tokio::test]
655 async fn periodic_executor_reports_job_failure() {
656 let executor = PeriodicExecutor::new(Duration::from_millis(1), || async {
657 Err::<(), _>("backend unavailable")
658 });
659 time::sleep(Duration::from_millis(10)).await;
660
661 assert_eq!(
662 executor.shutdown(Duration::from_secs(1)).await,
663 Err(PeriodicExecutorError::Job("backend unavailable".to_owned()))
664 );
665 }
666
667 #[tokio::test]
668 async fn periodic_executor_bounds_slow_shutdown() {
669 let (started, start_received) = oneshot::channel();
670 let mut started = Some(started);
671 let executor = PeriodicExecutor::new(Duration::from_millis(1), move || {
672 if let Some(started) = started.take() {
673 let _ = started.send(());
674 }
675 async {
676 std::future::pending::<()>().await;
677 Ok::<_, &'static str>(())
678 }
679 });
680 time::timeout(Duration::from_secs(1), start_received)
681 .await
682 .unwrap()
683 .unwrap();
684
685 assert_eq!(
686 executor.shutdown(Duration::from_millis(5)).await,
687 Err(PeriodicExecutorError::TimedOut(Duration::from_millis(5)))
688 );
689 }
690}