Skip to main content

common_batcher/
pending_worker.rs

1// Copyright 2023 Greptime Team
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use tokio::time::Instant;
16
17use crate::flush_policy::{FlushPolicy, FlushTrigger};
18use crate::flush_timer::FlushTimer;
19use crate::pending_batch::PendingBatch;
20
21/// Batch state for one grouping key, independent of the caller's event loop.
22///
23/// The caller owns channels, idle/shutdown decisions, execution limits, task
24/// spawning and business completion. Taking a batch transfers its items to the
25/// caller; no background task is started by this worker.
26pub struct PendingWorker<T, P> {
27    batch: PendingBatch<T>,
28    flush_policy: P,
29    flush_timer: FlushTimer,
30}
31
32impl<T, P> PendingWorker<T, P> {
33    /// Creates an empty worker without starting a timer or runtime task.
34    pub fn new(flush_policy: P) -> Self {
35        Self {
36            batch: PendingBatch::new(),
37            flush_policy,
38            flush_timer: FlushTimer::new(),
39        }
40    }
41
42    /// Returns whether the worker has no submissions, including zero-row items.
43    pub fn is_empty(&self) -> bool {
44        self.batch.is_empty()
45    }
46
47    /// Returns the pending row count before ownership is transferred to the caller.
48    pub fn total_rows(&self) -> usize {
49        self.batch.total_rows()
50    }
51
52    /// Waits for a timer wakeup without taking or executing the batch.
53    ///
54    /// Cancellation is safe: selecting another event does not lose items or
55    /// reset the deadline. After a wakeup, call [`Self::take_ready`] with
56    /// [`FlushTrigger::Deadline`]. An empty or unarmed worker stays pending.
57    pub async fn wait_flush(&mut self) {
58        self.flush_timer.wait().await;
59    }
60
61    /// Takes all pending submissions regardless of policy, for example on shutdown.
62    ///
63    /// Returns `None` for an empty batch. The caller decides whether to execute
64    /// inline, acquire a flush permit or discard the returned items.
65    pub fn take_pending(&mut self) -> Option<Vec<T>> {
66        self.flush_timer.set_deadline(None);
67        if self.batch.is_empty() {
68            None
69        } else {
70            Some(self.batch.take())
71        }
72    }
73}
74
75impl<T, P: FlushPolicy> PendingWorker<T, P> {
76    /// Appends one complete submission and arms the policy's deadline.
77    ///
78    /// Call [`Self::take_ready`] with [`FlushTrigger::Submission`] afterwards to
79    /// check whether this submission triggered a flush. Timer-backed policies
80    /// require a Tokio runtime with time enabled.
81    ///
82    /// # Panics
83    ///
84    /// Panics if arming a new timer outside a Tokio runtime with time enabled.
85    pub fn submit(&mut self, item: T, total_rows: usize) {
86        self.batch.push(item, total_rows, Instant::now());
87        self.refresh_deadline();
88    }
89
90    /// Takes the batch only when the policy permits flushing for this event.
91    ///
92    /// Like [`Self::submit`], this may arm a timer and requires a time-enabled
93    /// Tokio runtime when the policy starts using deadlines.
94    pub fn take_ready(&mut self, trigger: FlushTrigger) -> Option<Vec<T>> {
95        if !self.batch.is_empty()
96            && self
97                .flush_policy
98                .should_flush(&self.batch, Instant::now(), trigger)
99        {
100            self.take_pending()
101        } else {
102            self.refresh_deadline();
103            None
104        }
105    }
106
107    fn refresh_deadline(&mut self) {
108        let deadline = if self.batch.is_empty() {
109            None
110        } else {
111            self.flush_policy.deadline(&self.batch)
112        };
113        self.flush_timer.set_deadline(deadline);
114    }
115}
116
117#[cfg(test)]
118mod tests {
119    use std::future::Future;
120    use std::task::Poll;
121    use std::time::Duration;
122
123    use super::*;
124    use crate::flush_policy::timing::TimingFlushPolicy;
125
126    fn worker(rows: usize) -> PendingWorker<i32, TimingFlushPolicy> {
127        PendingWorker::new(TimingFlushPolicy::try_new(Duration::from_millis(10), rows).unwrap())
128    }
129
130    async fn assert_wait_pending<T, P>(worker: &mut PendingWorker<T, P>) {
131        let wait = worker.wait_flush();
132        tokio::pin!(wait);
133        assert!(std::future::poll_fn(|cx| Poll::Ready(wait.as_mut().poll(cx).is_pending())).await);
134    }
135
136    #[tokio::test(start_paused = true)]
137    async fn test_first_deadline_and_cancelled_wait() {
138        let mut worker = worker(100);
139        let start = Instant::now();
140        worker.submit(1, 1);
141        assert_wait_pending(&mut worker).await;
142        tokio::time::advance(Duration::from_millis(5)).await;
143        worker.submit(2, 1);
144        assert!(worker.take_ready(FlushTrigger::Submission).is_none());
145        assert_wait_pending(&mut worker).await;
146        worker.wait_flush().await;
147        assert_eq!(start + Duration::from_millis(10), Instant::now());
148        assert_eq!(Some(vec![1, 2]), worker.take_ready(FlushTrigger::Deadline));
149        assert!(worker.is_empty());
150        assert_eq!(0, worker.total_rows());
151        assert_wait_pending(&mut worker).await;
152    }
153
154    #[tokio::test(start_paused = true)]
155    async fn test_submission_does_not_consume_expired_deadline() {
156        let mut worker = worker(100);
157        worker.submit(1, 1);
158        tokio::time::advance(Duration::from_millis(10)).await;
159        worker.submit(2, 1);
160        assert!(worker.take_ready(FlushTrigger::Submission).is_none());
161        worker.wait_flush().await;
162        assert_eq!(Some(vec![1, 2]), worker.take_ready(FlushTrigger::Deadline));
163    }
164
165    #[tokio::test(start_paused = true)]
166    async fn test_size_flush_and_rearm() {
167        let mut worker = worker(2);
168        worker.submit(1, 3);
169        assert_eq!(3, worker.total_rows());
170        assert_eq!(Some(vec![1]), worker.take_ready(FlushTrigger::Submission));
171        tokio::time::advance(Duration::from_secs(1)).await;
172        assert_wait_pending(&mut worker).await;
173        let start = Instant::now();
174        worker.submit(2, 1);
175        worker.wait_flush().await;
176        assert_eq!(start + Duration::from_millis(10), Instant::now());
177        assert_eq!(Some(vec![2]), worker.take_ready(FlushTrigger::Deadline));
178    }
179
180    #[tokio::test(start_paused = true)]
181    async fn test_take_pending_without_waiting() {
182        let mut worker = worker(100);
183        assert_eq!(None, worker.take_pending());
184        worker.submit(1, 0);
185        assert!(!worker.is_empty());
186        let now = Instant::now();
187        assert_eq!(Some(vec![1]), worker.take_pending());
188        assert_eq!(now, Instant::now());
189        tokio::time::advance(Duration::from_secs(1)).await;
190        assert_wait_pending(&mut worker).await;
191    }
192
193    #[tokio::test(start_paused = true)]
194    async fn test_row_threshold_ablation() {
195        for (rows, early) in [(2, true), (100, false)] {
196            let mut worker = worker(rows);
197            let start = Instant::now();
198            worker.submit(1, 1);
199            tokio::time::advance(Duration::from_millis(5)).await;
200            worker.submit(2, 1);
201            let batch = worker.take_ready(FlushTrigger::Submission);
202            if early {
203                assert_eq!(Some(vec![1, 2]), batch);
204                assert_eq!(start + Duration::from_millis(5), Instant::now());
205            } else {
206                assert!(batch.is_none());
207                worker.wait_flush().await;
208                assert_eq!(start + Duration::from_millis(10), Instant::now());
209                assert_eq!(Some(vec![1, 2]), worker.take_ready(FlushTrigger::Deadline));
210            }
211        }
212    }
213
214    #[tokio::test]
215    async fn test_non_timing_policy() {
216        struct TwoSubmissionsPolicy;
217        impl FlushPolicy for TwoSubmissionsPolicy {
218            fn deadline<T>(&self, _: &PendingBatch<T>) -> Option<Instant> {
219                None
220            }
221            fn should_flush<T>(
222                &self,
223                batch: &PendingBatch<T>,
224                _: Instant,
225                _: FlushTrigger,
226            ) -> bool {
227                batch.len() >= 2
228            }
229        }
230        let mut worker = PendingWorker::new(TwoSubmissionsPolicy);
231        worker.submit(1, 0);
232        assert_wait_pending(&mut worker).await;
233        assert!(worker.take_ready(FlushTrigger::Submission).is_none());
234        worker.submit(2, 0);
235        assert_eq!(
236            Some(vec![1, 2]),
237            worker.take_ready(FlushTrigger::Submission)
238        );
239    }
240
241    #[tokio::test(start_paused = true)]
242    async fn test_caller_owns_event_loop() {
243        let mut worker = worker(100);
244        let (tx, mut rx) = tokio::sync::mpsc::channel(1);
245        tx.send(1).await.unwrap();
246        tokio::select! {
247            _ = worker.wait_flush() => panic!("empty worker must not wake"),
248            item = rx.recv() => worker.submit(item.unwrap(), 1),
249        }
250        assert!(worker.take_ready(FlushTrigger::Submission).is_none());
251        let start = Instant::now();
252        tokio::select! {
253            _ = worker.wait_flush() => {
254                assert_eq!(Some(vec![1]), worker.take_ready(FlushTrigger::Deadline));
255            }
256            _ = rx.recv() => panic!("sender is still open without another submission"),
257        }
258        assert_eq!(start + Duration::from_millis(10), Instant::now());
259    }
260}