Files
greptimedb/src/common/batcher/src/pending_worker.rs
T

265 lines
9.5 KiB
Rust

// Copyright 2023 Greptime Team
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use tokio::time::Instant;
use crate::flush_policy::{FlushPolicy, FlushTrigger};
use crate::flush_timer::FlushTimer;
use crate::pending_batch::PendingBatch;
/// Batch state for one grouping key, independent of the caller's event loop.
///
/// The caller owns channels, idle/shutdown decisions, execution limits, task
/// spawning and business completion. Taking a batch transfers its items to the
/// caller; no background task is started by this worker.
pub struct PendingWorker<T, P> {
batch: PendingBatch<T>,
flush_policy: P,
flush_timer: FlushTimer,
}
impl<T, P> PendingWorker<T, P> {
/// Creates an empty worker without starting a timer or runtime task.
pub fn new(flush_policy: P) -> Self {
Self {
batch: PendingBatch::new(),
flush_policy,
flush_timer: FlushTimer::new(),
}
}
/// Returns whether the worker has no submissions, including zero-row items.
pub fn is_empty(&self) -> bool {
self.batch.is_empty()
}
/// Returns the pending row count before ownership is transferred to the caller.
pub fn total_rows(&self) -> usize {
self.batch.total_rows()
}
/// Waits for a timer wakeup without taking or executing the batch.
///
/// Cancellation is safe: selecting another event does not lose items or
/// reset the deadline. After a wakeup, call [`Self::take_ready`] with
/// [`FlushTrigger::Deadline`]. An empty or unarmed worker stays pending.
pub async fn wait_flush(&mut self) {
self.flush_timer.wait().await;
}
/// Takes all pending submissions regardless of policy, for example on shutdown.
///
/// Returns `None` for an empty batch. The caller decides whether to execute
/// inline, acquire a flush permit or discard the returned items.
pub fn take_pending(&mut self) -> Option<Vec<T>> {
self.flush_timer.set_deadline(None);
if self.batch.is_empty() {
None
} else {
Some(self.batch.take())
}
}
}
impl<T, P: FlushPolicy> PendingWorker<T, P> {
/// Appends one complete submission and arms the policy's deadline.
///
/// Call [`Self::take_ready`] with [`FlushTrigger::Submission`] afterwards to
/// check whether this submission triggered a flush. Timer-backed policies
/// require a Tokio runtime with time enabled.
///
/// # Panics
///
/// Panics if arming a new timer outside a Tokio runtime with time enabled.
pub fn submit(&mut self, item: T, total_rows: usize) {
self.batch.push(item, total_rows, Instant::now());
self.refresh_deadline();
}
/// Takes the batch only when the policy permits flushing for this event.
///
/// Like [`Self::submit`], this may arm a timer and requires a time-enabled
/// Tokio runtime when the policy starts using deadlines.
pub fn take_ready(&mut self, trigger: FlushTrigger) -> Option<Vec<T>> {
if !self.batch.is_empty()
&& self
.flush_policy
.should_flush(&self.batch, Instant::now(), trigger)
{
self.take_pending()
} else {
self.refresh_deadline();
None
}
}
fn refresh_deadline(&mut self) {
let deadline = if self.batch.is_empty() {
None
} else {
self.flush_policy.deadline(&self.batch)
};
self.flush_timer.set_deadline(deadline);
}
}
#[cfg(test)]
mod tests {
use std::future::Future;
use std::num::NonZeroUsize;
use std::task::Poll;
use std::time::Duration;
use super::*;
use crate::flush_policy::timing::TimingFlushPolicy;
fn worker(rows: usize) -> PendingWorker<i32, TimingFlushPolicy> {
PendingWorker::new(
TimingFlushPolicy::try_new(Duration::from_millis(10), NonZeroUsize::new(rows).unwrap())
.unwrap(),
)
}
async fn assert_wait_pending<T, P>(worker: &mut PendingWorker<T, P>) {
let wait = worker.wait_flush();
tokio::pin!(wait);
assert!(std::future::poll_fn(|cx| Poll::Ready(wait.as_mut().poll(cx).is_pending())).await);
}
#[tokio::test(start_paused = true)]
async fn test_first_deadline_and_cancelled_wait() {
let mut worker = worker(100);
let start = Instant::now();
worker.submit(1, 1);
assert_wait_pending(&mut worker).await;
tokio::time::advance(Duration::from_millis(5)).await;
worker.submit(2, 1);
assert!(worker.take_ready(FlushTrigger::Submission).is_none());
assert_wait_pending(&mut worker).await;
worker.wait_flush().await;
assert_eq!(start + Duration::from_millis(10), Instant::now());
assert_eq!(Some(vec![1, 2]), worker.take_ready(FlushTrigger::Deadline));
assert!(worker.is_empty());
assert_eq!(0, worker.total_rows());
assert_wait_pending(&mut worker).await;
}
#[tokio::test(start_paused = true)]
async fn test_submission_does_not_consume_expired_deadline() {
let mut worker = worker(100);
worker.submit(1, 1);
tokio::time::advance(Duration::from_millis(10)).await;
worker.submit(2, 1);
assert!(worker.take_ready(FlushTrigger::Submission).is_none());
worker.wait_flush().await;
assert_eq!(Some(vec![1, 2]), worker.take_ready(FlushTrigger::Deadline));
}
#[tokio::test(start_paused = true)]
async fn test_size_flush_and_rearm() {
let mut worker = worker(2);
worker.submit(1, 3);
assert_eq!(3, worker.total_rows());
assert_eq!(Some(vec![1]), worker.take_ready(FlushTrigger::Submission));
tokio::time::advance(Duration::from_secs(1)).await;
assert_wait_pending(&mut worker).await;
let start = Instant::now();
worker.submit(2, 1);
worker.wait_flush().await;
assert_eq!(start + Duration::from_millis(10), Instant::now());
assert_eq!(Some(vec![2]), worker.take_ready(FlushTrigger::Deadline));
}
#[tokio::test(start_paused = true)]
async fn test_take_pending_without_waiting() {
let mut worker = worker(100);
assert_eq!(None, worker.take_pending());
worker.submit(1, 0);
assert!(!worker.is_empty());
let now = Instant::now();
assert_eq!(Some(vec![1]), worker.take_pending());
assert_eq!(now, Instant::now());
tokio::time::advance(Duration::from_secs(1)).await;
assert_wait_pending(&mut worker).await;
}
#[tokio::test(start_paused = true)]
async fn test_row_threshold_ablation() {
for (rows, early) in [(2, true), (100, false)] {
let mut worker = worker(rows);
let start = Instant::now();
worker.submit(1, 1);
tokio::time::advance(Duration::from_millis(5)).await;
worker.submit(2, 1);
let batch = worker.take_ready(FlushTrigger::Submission);
if early {
assert_eq!(Some(vec![1, 2]), batch);
assert_eq!(start + Duration::from_millis(5), Instant::now());
} else {
assert!(batch.is_none());
worker.wait_flush().await;
assert_eq!(start + Duration::from_millis(10), Instant::now());
assert_eq!(Some(vec![1, 2]), worker.take_ready(FlushTrigger::Deadline));
}
}
}
#[tokio::test]
async fn test_non_timing_policy() {
struct TwoSubmissionsPolicy;
impl FlushPolicy for TwoSubmissionsPolicy {
fn deadline<T>(&self, _: &PendingBatch<T>) -> Option<Instant> {
None
}
fn should_flush<T>(
&self,
batch: &PendingBatch<T>,
_: Instant,
_: FlushTrigger,
) -> bool {
batch.len() >= 2
}
}
let mut worker = PendingWorker::new(TwoSubmissionsPolicy);
worker.submit(1, 0);
assert_wait_pending(&mut worker).await;
assert!(worker.take_ready(FlushTrigger::Submission).is_none());
worker.submit(2, 0);
assert_eq!(
Some(vec![1, 2]),
worker.take_ready(FlushTrigger::Submission)
);
}
#[tokio::test(start_paused = true)]
async fn test_caller_owns_event_loop() {
let mut worker = worker(100);
let (tx, mut rx) = tokio::sync::mpsc::channel(1);
tx.send(1).await.unwrap();
tokio::select! {
_ = worker.wait_flush() => panic!("empty worker must not wake"),
item = rx.recv() => worker.submit(item.unwrap(), 1),
}
assert!(worker.take_ready(FlushTrigger::Submission).is_none());
let start = Instant::now();
tokio::select! {
_ = worker.wait_flush() => {
assert_eq!(Some(vec![1]), worker.take_ready(FlushTrigger::Deadline));
}
_ = rx.recv() => panic!("sender is still open without another submission"),
}
assert_eq!(start + Duration::from_millis(10), Instant::now());
}
}