common_batcher/flush_policy/
timing.rs1use std::time::Duration;
16
17use tokio::time::Instant;
18
19use crate::flush_policy::{FlushPolicy, FlushTrigger};
20use crate::pending_batch::PendingBatch;
21
22#[derive(Debug, Clone, Copy)]
27pub struct TimingFlushPolicy {
28 flush_interval: Duration,
29 max_batch_rows: usize,
30}
31
32impl TimingFlushPolicy {
33 pub fn try_new(flush_interval: Duration, max_batch_rows: usize) -> Option<Self> {
35 if max_batch_rows == 0 || !Self::validate(flush_interval) {
36 return None;
37 }
38 Some(Self {
39 flush_interval,
40 max_batch_rows,
41 })
42 }
43
44 pub fn validate(flush_interval: Duration) -> bool {
46 !flush_interval.is_zero() && Instant::now().checked_add(flush_interval).is_some()
47 }
48
49 pub fn reached_row_threshold<T>(&self, batch: &PendingBatch<T>) -> bool {
51 !batch.is_empty() && batch.total_rows() >= self.max_batch_rows
52 }
53}
54
55impl FlushPolicy for TimingFlushPolicy {
56 fn deadline<T>(&self, batch: &PendingBatch<T>) -> Option<Instant> {
61 batch
62 .first_submitted_at()
63 .map(|first| first.checked_add(self.flush_interval).unwrap_or(first))
64 }
65
66 fn should_flush<T>(
68 &self,
69 batch: &PendingBatch<T>,
70 now: Instant,
71 trigger: FlushTrigger,
72 ) -> bool {
73 !batch.is_empty()
74 && (self.reached_row_threshold(batch)
75 || (trigger == FlushTrigger::Deadline
76 && self.deadline(batch).is_some_and(|deadline| now >= deadline)))
77 }
78}
79
80#[cfg(test)]
81mod tests {
82 use super::*;
83
84 #[test]
85 fn test_validate_matches_construction() {
86 for interval in [Duration::ZERO, Duration::from_secs(1), Duration::MAX] {
87 assert_eq!(
88 TimingFlushPolicy::validate(interval),
89 TimingFlushPolicy::try_new(interval, 1).is_some()
90 );
91 }
92 }
93
94 #[test]
95 fn test_first_submission_deadline() {
96 let policy = TimingFlushPolicy::try_new(Duration::from_millis(10), 100).unwrap();
97 let mut batch = PendingBatch::new();
98 let first = Instant::now();
99 assert_eq!(policy.deadline(&batch), None);
100 assert!(!policy.should_flush(&batch, first, FlushTrigger::Deadline));
101 batch.push(1, 1, first);
102 batch.push(2, 1, first + Duration::from_millis(9));
103 assert_eq!(
104 policy.deadline(&batch),
105 Some(first + Duration::from_millis(10))
106 );
107 assert!(!policy.should_flush(
108 &batch,
109 first + Duration::from_millis(9),
110 FlushTrigger::Deadline
111 ));
112 assert!(policy.should_flush(
113 &batch,
114 first + Duration::from_millis(10),
115 FlushTrigger::Deadline
116 ));
117 assert!(!policy.reached_row_threshold(&batch));
118 assert!(!policy.should_flush(
119 &batch,
120 first + Duration::from_millis(10),
121 FlushTrigger::Submission,
122 ));
123 assert_eq!(batch.take(), vec![1, 2]);
124 let next = first + Duration::from_millis(20);
125 batch.push(3, 0, next);
126 assert!(!policy.should_flush(&batch, next, FlushTrigger::Deadline));
127 assert!(policy.should_flush(
128 &batch,
129 next + Duration::from_millis(10),
130 FlushTrigger::Deadline
131 ));
132 }
133
134 #[test]
135 fn test_row_threshold_preserves_complete_submissions() {
136 let policy = TimingFlushPolicy::try_new(Duration::from_secs(1), 3).unwrap();
137 let now = Instant::now();
138 for rows in [2, 3, 4] {
139 let mut batch = PendingBatch::new();
140 batch.push(vec![0; rows], rows, now);
141 assert_eq!(
142 policy.should_flush(&batch, now, FlushTrigger::Submission),
143 rows >= 3
144 );
145 assert_eq!(policy.reached_row_threshold(&batch), rows >= 3);
146 assert_eq!(batch.take(), vec![vec![0; rows]]);
147 }
148 }
149
150 #[test]
151 fn test_invalid_interval() {
152 assert!(TimingFlushPolicy::try_new(Duration::from_secs(1), 0).is_none());
153 let max_batch_rows = 1;
154 assert!(TimingFlushPolicy::try_new(Duration::ZERO, max_batch_rows).is_none());
155 assert!(TimingFlushPolicy::try_new(Duration::MAX, max_batch_rows).is_none());
156 }
157}