mirror of
https://github.com/GreptimeTeam/greptimedb.git
synced 2026-10-26 21:56:17 +00:00
265 lines
9.5 KiB
Rust
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());
|
|
}
|
|
}
|