feat(flow): merge intermediate aggregate states (#9168)

I hereby agree to the terms of the [GreptimeDB
CLA](https://github.com/GreptimeTeam/.github/blob/main/CLA.md).

## Refer to a related PR or issue link (optional)

Third PR in the stack extracted from #9035: #9161 → #9165 → this PR →
#9171. Based on `split/9035-exact-reads`; the diff contains only
aggregate-state merging.

## What's changed and what's your intention?

The incremental flow path already joins the sink state for ordinary
scalar aggregates. Extend that existing mechanism to aggregate families
that store intermediate state, so supported incremental flows do not
need a full snapshot only because the aggregate state cannot be merged.

- Merge existing sink state with the newly computed delta for HLL,
`stddev_state`, and UDDSketch, in addition to the existing scalar
aggregate path.
- Preserve input/output aliases and aggregate parameters when
reconstructing the final result, including repeated aliases for the same
aggregate.
- Correct NULL group-key matching in the state join and keep NULL-only
source results visible.
- Keep the existing full-snapshot/base-repair recovery for uncertain
writes or unsupported plans; this PR does not weaken that fallback.

Focused regressions cover populated old/new states, sink misses, NULL
group keys, repeated aliases, parameters, schema shape, and state-family
rejection paths. Rust formatting, compilation, and Flow unit tests
passed on the pre-stack base; CI is running on the current stacked head.

## PR Checklist

- [x] I have written the necessary rustdoc comments.
- [x] I have added the necessary unit tests and integration tests.
- [ ] This PR requires documentation updates.
- [x] API changes are backward compatible.
- [x] Schema or data changes are backward compatible.
This commit is contained in:
discord9
2026-09-17 13:00:38 +08:00
committed by GitHub
52 changed files with 4569 additions and 930 deletions
+8 -1
View File
@@ -6,9 +6,13 @@ on:
- opened
- reopened
- edited
- synchronize
- ready_for_review
merge_group:
types: [checks_requested]
concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
permissions:
@@ -18,6 +22,9 @@ permissions:
jobs:
check:
name: Semantic PR title
# Validate before queue admission; report a skipped check for merge groups.
if: github.event_name == 'pull_request_target'
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
Generated
+11
View File
@@ -2317,6 +2317,15 @@ dependencies = [
"zeroize",
]
[[package]]
name = "common-batcher"
version = "1.3.0-alpha.1"
dependencies = [
"dashmap",
"futures",
"tokio",
]
[[package]]
name = "common-catalog"
version = "1.3.0-alpha.1"
@@ -5669,6 +5678,7 @@ dependencies = [
"catalog",
"client",
"common-base",
"common-batcher",
"common-catalog",
"common-config",
"common-datasource",
@@ -13596,6 +13606,7 @@ dependencies = [
"chrono",
"client",
"common-base",
"common-batcher",
"common-catalog",
"common-decimal",
"common-error",
+2
View File
@@ -8,6 +8,7 @@ members = [
"src/client",
"src/cmd",
"src/common/base",
"src/common/batcher",
"src/common/catalog",
"src/common/config",
"src/common/datasource",
@@ -287,6 +288,7 @@ cli = { path = "src/cli" }
client = { path = "src/client" }
cmd = { path = "src/cmd", default-features = false }
common-base = { path = "src/common/base" }
common-batcher = { path = "src/common/batcher" }
common-catalog = { path = "src/common/catalog" }
common-config = { path = "src/common/config" }
common-datasource = { path = "src/common/datasource" }
+19
View File
@@ -0,0 +1,19 @@
[package]
name = "common-batcher"
version.workspace = true
edition.workspace = true
license.workspace = true
[lib]
path = "src/lib.rs"
[lints]
workspace = true
[dependencies]
dashmap.workspace = true
futures.workspace = true
tokio.workspace = true
[dev-dependencies]
tokio = { workspace = true, features = ["test-util"] }
+73
View File
@@ -0,0 +1,73 @@
// 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 std::sync::Arc;
use tokio::sync::{AcquireError, OwnedSemaphorePermit, Semaphore};
/// Shared execution budget. One permit represents one complete flush, not one RPC.
#[derive(Clone)]
pub struct FlushLimiter {
semaphore: Arc<Semaphore>,
}
impl FlushLimiter {
/// Returns `None` if the limit is zero or exceeds Tokio's supported semaphore capacity.
pub fn try_new(max_concurrent_flushes: usize) -> Option<Self> {
let permits = max_concurrent_flushes;
((1..=Semaphore::MAX_PERMITS).contains(&permits)).then(|| Self {
semaphore: Arc::new(Semaphore::new(permits)),
})
}
/// The caller must retain the returned permit until its flush finishes.
pub async fn acquire(&self) -> Result<OwnedSemaphorePermit, AcquireError> {
self.semaphore.clone().acquire_owned().await
}
}
#[cfg(test)]
mod tests {
use std::future::Future;
use super::*;
#[test]
fn test_invalid_capacity() {
for capacity in [0, Semaphore::MAX_PERMITS + 1, usize::MAX] {
assert!(FlushLimiter::try_new(capacity).is_none());
}
for capacity in [1, Semaphore::MAX_PERMITS] {
assert!(FlushLimiter::try_new(capacity).is_some());
}
}
#[tokio::test]
async fn test_clones_share_flush_budget() {
let limiter = FlushLimiter::try_new(1).unwrap();
let other = limiter.clone();
let permit = limiter.acquire().await.unwrap();
let waiting = other.acquire();
tokio::pin!(waiting);
// Poll admission without a sleep or an assumption about scheduler timing.
assert!(
std::future::poll_fn(|cx| {
std::task::Poll::Ready(waiting.as_mut().poll(cx).is_pending())
})
.await
);
drop(permit);
let _next = waiting.await.unwrap();
}
}
+43
View File
@@ -0,0 +1,43 @@
// 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.
pub mod timing;
use tokio::time::Instant;
use crate::pending_batch::PendingBatch;
/// The event that caused the worker to reconsider flushing.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FlushTrigger {
/// A complete submission was appended to the batch.
Submission,
/// The flush timer reached its deadline.
Deadline,
}
/// Decides when a pending batch is ready without owning its payloads or execution.
///
/// A deadline wakeup must either allow flushing or produce a future deadline;
/// otherwise the caller would repeatedly poll an expired timer.
pub trait FlushPolicy {
/// Returns the next timer deadline, or `None` when no timer is needed.
fn deadline<T>(&self, batch: &PendingBatch<T>) -> Option<Instant>;
/// Returns whether the batch should be flushed for this event at `now`.
///
/// Workers never flush empty batches, regardless of this decision.
fn should_flush<T>(&self, batch: &PendingBatch<T>, now: Instant, trigger: FlushTrigger)
-> bool;
}
@@ -0,0 +1,157 @@
// 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 std::time::Duration;
use tokio::time::Instant;
use crate::flush_policy::{FlushPolicy, FlushTrigger};
use crate::pending_batch::PendingBatch;
/// Decides whether the first-submission deadline or row threshold requires a flush.
///
/// The row threshold is checked after appending a complete submission. It is not a
/// hard batch-size limit and never splits a submission.
#[derive(Debug, Clone, Copy)]
pub struct TimingFlushPolicy {
flush_interval: Duration,
max_batch_rows: usize,
}
impl TimingFlushPolicy {
/// Creates a policy, rejecting zero limits, zero intervals, and unrepresentable deadlines.
pub fn try_new(flush_interval: Duration, max_batch_rows: usize) -> Option<Self> {
if max_batch_rows == 0 || !Self::validate(flush_interval) {
return None;
}
Some(Self {
flush_interval,
max_batch_rows,
})
}
/// Checks that the interval is nonzero and its deadline is representable.
pub fn validate(flush_interval: Duration) -> bool {
!flush_interval.is_zero() && Instant::now().checked_add(flush_interval).is_some()
}
/// Checks only the row trigger when a caller drives deadline events separately.
pub fn reached_row_threshold<T>(&self, batch: &PendingBatch<T>) -> bool {
!batch.is_empty() && batch.total_rows() >= self.max_batch_rows
}
}
impl FlushPolicy for TimingFlushPolicy {
/// Returns the deadline anchored to the first submission, or `None` for an empty batch.
///
/// An unrepresentable deadline falls back to the first submission time so that
/// extreme timestamps cannot leave a nonempty batch waiting indefinitely.
fn deadline<T>(&self, batch: &PendingBatch<T>) -> Option<Instant> {
batch
.first_submitted_at()
.map(|first| first.checked_add(self.flush_interval).unwrap_or(first))
}
/// Returns whether a nonempty batch has reached either trigger.
fn should_flush<T>(
&self,
batch: &PendingBatch<T>,
now: Instant,
trigger: FlushTrigger,
) -> bool {
!batch.is_empty()
&& (self.reached_row_threshold(batch)
|| (trigger == FlushTrigger::Deadline
&& self.deadline(batch).is_some_and(|deadline| now >= deadline)))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_validate_matches_construction() {
for interval in [Duration::ZERO, Duration::from_secs(1), Duration::MAX] {
assert_eq!(
TimingFlushPolicy::validate(interval),
TimingFlushPolicy::try_new(interval, 1).is_some()
);
}
}
#[test]
fn test_first_submission_deadline() {
let policy = TimingFlushPolicy::try_new(Duration::from_millis(10), 100).unwrap();
let mut batch = PendingBatch::new();
let first = Instant::now();
assert_eq!(policy.deadline(&batch), None);
assert!(!policy.should_flush(&batch, first, FlushTrigger::Deadline));
batch.push(1, 1, first);
batch.push(2, 1, first + Duration::from_millis(9));
assert_eq!(
policy.deadline(&batch),
Some(first + Duration::from_millis(10))
);
assert!(!policy.should_flush(
&batch,
first + Duration::from_millis(9),
FlushTrigger::Deadline
));
assert!(policy.should_flush(
&batch,
first + Duration::from_millis(10),
FlushTrigger::Deadline
));
assert!(!policy.reached_row_threshold(&batch));
assert!(!policy.should_flush(
&batch,
first + Duration::from_millis(10),
FlushTrigger::Submission,
));
assert_eq!(batch.take(), vec![1, 2]);
let next = first + Duration::from_millis(20);
batch.push(3, 0, next);
assert!(!policy.should_flush(&batch, next, FlushTrigger::Deadline));
assert!(policy.should_flush(
&batch,
next + Duration::from_millis(10),
FlushTrigger::Deadline
));
}
#[test]
fn test_row_threshold_preserves_complete_submissions() {
let policy = TimingFlushPolicy::try_new(Duration::from_secs(1), 3).unwrap();
let now = Instant::now();
for rows in [2, 3, 4] {
let mut batch = PendingBatch::new();
batch.push(vec![0; rows], rows, now);
assert_eq!(
policy.should_flush(&batch, now, FlushTrigger::Submission),
rows >= 3
);
assert_eq!(policy.reached_row_threshold(&batch), rows >= 3);
assert_eq!(batch.take(), vec![vec![0; rows]]);
}
}
#[test]
fn test_invalid_interval() {
assert!(TimingFlushPolicy::try_new(Duration::from_secs(1), 0).is_none());
let max_batch_rows = 1;
assert!(TimingFlushPolicy::try_new(Duration::ZERO, max_batch_rows).is_none());
assert!(TimingFlushPolicy::try_new(Duration::MAX, max_batch_rows).is_none());
}
}
+141
View File
@@ -0,0 +1,141 @@
// 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 std::pin::Pin;
use tokio::time::{Instant, Sleep, sleep_until};
/// Maintains one reusable timer for the current flush deadline.
///
/// Creating a timer does not require a Tokio runtime. Arming it requires a Tokio
/// runtime with time enabled. Disabling retains the allocation for the next arm.
#[derive(Default)]
pub struct FlushTimer {
deadline: Option<Instant>,
sleep: Option<Pin<Box<Sleep>>>,
}
impl FlushTimer {
/// Creates a disabled timer without allocating a Tokio sleep.
pub fn new() -> Self {
Self::default()
}
/// Arms or disables the timer, resetting its sleep only when the deadline changes.
///
/// # Panics
///
/// Arming for the first time panics outside a Tokio runtime with time enabled.
pub fn set_deadline(&mut self, deadline: Option<Instant>) {
if self.deadline == deadline {
return;
}
self.deadline = deadline;
if let Some(deadline) = deadline {
match &mut self.sleep {
Some(sleep) => sleep.as_mut().reset(deadline),
None => self.sleep = Some(Box::pin(sleep_until(deadline))),
}
}
}
/// Waits until the armed deadline, or indefinitely while disabled.
///
/// Cancelling this wait does not change the deadline. An elapsed timer stays
/// ready until the caller changes or disables its deadline.
pub async fn wait(&mut self) {
if self.deadline.is_some()
&& let Some(sleep) = &mut self.sleep
{
sleep.as_mut().await;
} else {
std::future::pending::<()>().await;
}
}
}
#[cfg(test)]
mod tests {
use std::future::{Future, poll_fn};
use std::task::Poll;
use std::time::Duration;
use super::*;
async fn is_ready(timer: &mut FlushTimer) -> bool {
let wait = timer.wait();
tokio::pin!(wait);
poll_fn(|cx| Poll::Ready(wait.as_mut().poll(cx).is_ready())).await
}
#[test]
fn test_create_without_runtime() {
let mut timer = FlushTimer::new();
timer.set_deadline(None);
assert!(timer.sleep.is_none());
}
#[tokio::test(start_paused = true)]
async fn test_deadline_and_cancelled_wait() {
let mut timer = FlushTimer::new();
assert!(!is_ready(&mut timer).await);
let deadline = Instant::now() + Duration::from_millis(10);
timer.set_deadline(Some(deadline));
assert!(!is_ready(&mut timer).await);
tokio::time::advance(Duration::from_millis(9)).await;
// Each poll drops its wait future. Repeating the same deadline must not
// move the timer forward or replace its sleep allocation.
let sleep = timer.sleep.as_ref().unwrap().as_ref().get_ref() as *const Sleep;
timer.set_deadline(Some(deadline));
assert_eq!(
sleep,
timer.sleep.as_ref().unwrap().as_ref().get_ref() as *const Sleep
);
assert!(!is_ready(&mut timer).await);
tokio::time::advance(Duration::from_millis(1)).await;
assert!(is_ready(&mut timer).await);
}
#[tokio::test(start_paused = true)]
async fn test_disable_and_rearm_reuses_sleep() {
let mut timer = FlushTimer::new();
timer.set_deadline(Some(Instant::now()));
assert!(is_ready(&mut timer).await);
let sleep = timer.sleep.as_ref().unwrap().as_ref().get_ref() as *const Sleep;
timer.set_deadline(None);
assert!(!is_ready(&mut timer).await);
assert!(!is_ready(&mut timer).await);
timer.set_deadline(Some(Instant::now() + Duration::from_millis(5)));
assert_eq!(
sleep,
timer.sleep.as_ref().unwrap().as_ref().get_ref() as *const Sleep
);
assert!(!is_ready(&mut timer).await);
tokio::time::advance(Duration::from_millis(5)).await;
assert!(is_ready(&mut timer).await);
}
#[tokio::test(start_paused = true)]
async fn test_change_armed_deadline() {
let mut timer = FlushTimer::new();
let now = Instant::now();
timer.set_deadline(Some(now + Duration::from_millis(5)));
assert!(!is_ready(&mut timer).await);
timer.set_deadline(Some(now + Duration::from_millis(10)));
tokio::time::advance(Duration::from_millis(5)).await;
assert!(!is_ready(&mut timer).await);
timer.set_deadline(Some(Instant::now()));
assert!(is_ready(&mut timer).await);
}
}
+27
View File
@@ -0,0 +1,27 @@
// 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.
//! Protocol-independent building blocks for timing-based batching.
//!
//! Payload preparation, grouping keys, write execution and request completion
//! remain the caller's responsibility.
pub mod flush_limiter;
pub mod flush_policy;
pub mod flush_timer;
pub mod notifier;
pub mod pending_batch;
pub mod pending_worker;
pub mod request_limiter;
pub mod worker_registry;
+145
View File
@@ -0,0 +1,145 @@
// 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 std::future::Future;
use std::num::NonZeroUsize;
use futures::{StreamExt, stream};
use tokio::sync::Semaphore;
use tokio::sync::mpsc::error::TrySendError;
use tokio::sync::mpsc::{self, Receiver, Sender};
/// Best-effort notification admission, independent of the write result.
///
/// The caller owns the notification payload, delivery handler and failure metrics.
pub struct Notifier<T> {
sender: Sender<T>,
}
impl<T> Clone for Notifier<T> {
fn clone(&self) -> Self {
Self {
sender: self.sender.clone(),
}
}
}
impl<T> Notifier<T> {
/// Returns `None` if the queue capacity is zero or exceeds Tokio's supported capacity.
pub fn try_new(capacity: usize) -> Option<(Self, Receiver<T>)> {
if capacity == 0 || capacity > Semaphore::MAX_PERMITS {
return None;
}
let (sender, receiver) = mpsc::channel(capacity);
Some((Self { sender }, receiver))
}
/// Configured queue capacity, independent of its current occupancy.
pub fn max_capacity(&self) -> usize {
self.sender.max_capacity()
}
/// Never waits for delivery. Returns the payload on a full or closed queue
/// so the caller can record domain-specific diagnostics.
pub fn try_notify(&self, notification: T) -> Result<(), TrySendError<T>> {
self.sender.try_send(notification)
}
}
/// Delivers queued notifications with bounded concurrency.
///
/// The caller schedules this future on its runtime. Closing all senders drains
/// queued and in-flight notifications. Handler failures are handled by the caller;
/// this worker does not retry or propagate them into write completion.
pub async fn run_notifier<T, F, Fut>(
receiver: Receiver<T>,
max_concurrent_notifications: NonZeroUsize,
handler: F,
) where
F: FnMut(T) -> Fut,
Fut: Future<Output = ()>,
{
stream::unfold(receiver, |mut receiver| async move {
receiver.recv().await.map(|item| (item, receiver))
})
.for_each_concurrent(max_concurrent_notifications.get(), handler)
.await;
}
#[cfg(test)]
mod tests {
use std::future::{Future, poll_fn};
use std::num::NonZeroUsize;
use std::sync::Arc;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::task::Poll;
use tokio::sync::Semaphore;
use tokio::sync::mpsc::error::TrySendError;
use crate::notifier::{Notifier, run_notifier};
#[test]
fn test_admission() {
assert!(Notifier::<usize>::try_new(0).is_none());
assert!(Notifier::<usize>::try_new(usize::MAX).is_none());
let (notifier, receiver) = Notifier::try_new(1).unwrap();
let other = notifier.clone();
assert_eq!(other.max_capacity(), 1);
assert_eq!(notifier.try_notify(1), Ok(()));
assert_eq!(other.try_notify(2), Err(TrySendError::Full(2)));
drop(receiver);
assert_eq!(other.try_notify(3), Err(TrySendError::Closed(3)));
}
#[tokio::test]
async fn test_concurrency_and_drain() {
// One-factor comparison: only delivery concurrency changes.
for _ in 0..2 {
for concurrency in [1, 2] {
let (notifier, receiver) = Notifier::try_new(3).unwrap();
for item in 0..3 {
notifier.try_notify(item).unwrap();
}
drop(notifier);
let started = Arc::new(AtomicUsize::new(0));
let completed = Arc::new(AtomicUsize::new(0));
let gate = Arc::new(Semaphore::new(0));
let worker =
run_notifier(receiver, NonZeroUsize::new(concurrency).unwrap(), |_| {
let started = started.clone();
let completed = completed.clone();
let gate = gate.clone();
async move {
started.fetch_add(1, Ordering::SeqCst);
gate.acquire().await.unwrap().forget();
completed.fetch_add(1, Ordering::SeqCst);
}
});
tokio::pin!(worker);
poll_fn(|cx| {
assert!(worker.as_mut().poll(cx).is_pending());
Poll::Ready(())
})
.await;
assert_eq!(started.load(Ordering::SeqCst), concurrency);
assert_eq!(completed.load(Ordering::SeqCst), 0);
gate.add_permits(3);
worker.await;
assert_eq!(started.load(Ordering::SeqCst), 3);
assert_eq!(completed.load(Ordering::SeqCst), 3);
}
}
}
}
+115
View File
@@ -0,0 +1,115 @@
// 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;
/// Accumulates complete submissions in arrival order, without interpreting their data.
#[derive(Debug)]
pub struct PendingBatch<T> {
items: Vec<T>,
total_rows: usize,
first_submitted_at: Option<Instant>,
}
impl<T> Default for PendingBatch<T> {
fn default() -> Self {
Self {
items: Vec::new(),
total_rows: 0,
first_submitted_at: None,
}
}
}
impl<T> PendingBatch<T> {
/// Creates an empty batch.
pub fn new() -> Self {
Self::default()
}
/// Appends a complete submission without moving the first submission's timestamp.
pub fn push(&mut self, item: T, total_rows: usize, now: Instant) {
self.first_submitted_at.get_or_insert(now);
self.items.push(item);
// Saturation preserves threshold decisions even for caller-supplied oversized weights.
self.total_rows = self.total_rows.saturating_add(total_rows);
}
/// Returns the accumulated row count, saturated at `usize::MAX`.
pub fn total_rows(&self) -> usize {
self.total_rows
}
/// Returns the number of complete submissions.
pub fn len(&self) -> usize {
self.items.len()
}
/// Returns whether there are no submissions, including zero-row submissions.
pub fn is_empty(&self) -> bool {
self.items.is_empty()
}
/// Returns the time the first submission was appended to the current batch.
pub fn first_submitted_at(&self) -> Option<Instant> {
self.first_submitted_at
}
/// Takes all submissions in arrival order and resets the batch's accounting.
pub fn take(&mut self) -> Vec<T> {
self.total_rows = 0;
self.first_submitted_at = None;
std::mem::take(&mut self.items)
}
}
#[cfg(test)]
mod tests {
use std::time::Duration;
use super::*;
#[test]
fn test_complete_submissions_and_reset() {
let mut batch = PendingBatch::new();
let first = Instant::now();
assert!(batch.is_empty());
assert_eq!(batch.first_submitted_at(), None);
batch.push(vec![1, 2], 2, first);
batch.push(vec![3, 4, 5], 3, first + Duration::from_secs(1));
assert_eq!(batch.len(), 2);
assert_eq!(batch.total_rows(), 5);
assert_eq!(batch.first_submitted_at(), Some(first));
assert_eq!(batch.take(), vec![vec![1, 2], vec![3, 4, 5]]);
assert!(batch.is_empty());
assert_eq!(batch.total_rows(), 0);
assert_eq!(batch.first_submitted_at(), None);
let next = first + Duration::from_secs(2);
batch.push(vec![6], 1, next);
assert_eq!(batch.first_submitted_at(), Some(next));
}
#[test]
fn test_zero_rows_and_saturating_count() {
let mut batch = PendingBatch::new();
let now = Instant::now();
batch.push(1, 0, now);
assert!(!batch.is_empty());
assert_eq!(batch.total_rows(), 0);
batch.push(2, usize::MAX, now);
batch.push(3, 1, now);
assert_eq!(batch.total_rows(), usize::MAX);
assert_eq!(batch.take(), vec![1, 2, 3]);
}
}
+260
View File
@@ -0,0 +1,260 @@
// 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::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), rows).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());
}
}
+115
View File
@@ -0,0 +1,115 @@
// 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 std::sync::Arc;
use tokio::sync::{AcquireError, OwnedSemaphorePermit, Semaphore};
/// Limits unfinished original requests independently of their number of submissions.
///
/// Acquire once for each original request, then clone its permit into all of that
/// request's submissions. Capacity is released when the last owner drops it.
#[derive(Clone)]
pub struct RequestLimiter {
semaphore: Arc<Semaphore>,
}
impl RequestLimiter {
/// Returns `None` if the limit is zero or exceeds Tokio's supported semaphore capacity.
pub fn try_new(max_inflight_requests: usize) -> Option<Self> {
let permits = max_inflight_requests;
((1..=Semaphore::MAX_PERMITS).contains(&permits)).then(|| Self {
semaphore: Arc::new(Semaphore::new(permits)),
})
}
/// Waits for one request slot and returns a permit shared by its submissions.
///
/// Cancelling a pending acquisition does not consume a slot. Once acquired,
/// callers must retain the permit until every submission has finished, even
/// if the original caller stops waiting for its response.
pub async fn acquire(&self) -> Result<Arc<OwnedSemaphorePermit>, AcquireError> {
self.semaphore.clone().acquire_owned().await.map(Arc::new)
}
}
#[cfg(test)]
mod tests {
use std::future::{Future, poll_fn};
use std::pin::Pin;
use std::task::Poll;
use tokio::sync::Semaphore;
use crate::request_limiter::RequestLimiter;
async fn is_pending<F: Future>(mut future: Pin<&mut F>) -> bool {
poll_fn(|cx| Poll::Ready(future.as_mut().poll(cx).is_pending())).await
}
#[test]
fn test_capacity_boundaries() {
for capacity in [1, Semaphore::MAX_PERMITS] {
assert!(RequestLimiter::try_new(capacity).is_some());
}
for capacity in [0, Semaphore::MAX_PERMITS + 1, usize::MAX] {
assert!(RequestLimiter::try_new(capacity).is_none());
}
}
#[tokio::test]
async fn test_last_submission_releases_request_slot() {
let limiter = RequestLimiter::try_new(1).unwrap();
let other = limiter.clone();
let request = limiter.acquire().await.unwrap();
let first_submission = request.clone();
let second_submission = request.clone();
let mut waiting = Box::pin(other.acquire());
assert!(is_pending(waiting.as_mut()).await);
// The caller and one submission finish, but another submission remains.
drop(request);
drop(first_submission);
assert!(is_pending(waiting.as_mut()).await);
drop(second_submission);
let next = waiting.await.unwrap();
assert_eq!(next.num_permits(), 1);
}
#[tokio::test]
async fn test_cancelled_acquisition_does_not_leak_capacity() {
let limiter = RequestLimiter::try_new(1).unwrap();
let permit = limiter.acquire().await.unwrap();
let mut cancelled = Box::pin(limiter.acquire());
assert!(is_pending(cancelled.as_mut()).await);
drop(cancelled);
let mut next = Box::pin(limiter.acquire());
assert!(is_pending(next.as_mut()).await);
drop(permit);
let next = next.await.unwrap();
drop(next);
let _available = limiter.acquire().await.unwrap();
}
#[tokio::test]
async fn test_independent_requests_consume_separate_slots() {
let limiter = RequestLimiter::try_new(2).unwrap();
let first = limiter.acquire().await.unwrap();
let second = limiter.acquire().await.unwrap();
let mut third = Box::pin(limiter.acquire());
assert!(is_pending(third.as_mut()).await);
drop(first);
let _third = third.await.unwrap();
drop(second);
}
}
+265
View File
@@ -0,0 +1,265 @@
// 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 std::hash::Hash;
use dashmap::DashMap;
use dashmap::mapref::entry::Entry;
use tokio::sync::mpsc::{self, Receiver, Sender};
/// Registers worker senders by key, without owning workers or their execution.
///
/// Closed senders are replaced on lookup/creation. Cleanup checks channel
/// identity under the same lock as replacement, so an old worker cannot remove
/// a replacement registered under its key.
pub struct WorkerRegistry<K, T> {
workers: DashMap<K, Sender<T>>,
}
impl<K: Eq + Hash, T> Default for WorkerRegistry<K, T> {
fn default() -> Self {
Self {
workers: DashMap::new(),
}
}
}
impl<K: Eq + Hash, T> WorkerRegistry<K, T> {
pub fn new() -> Self {
Self::default()
}
/// Returns a live sender, if one is currently registered.
/// The receiver can close after this method returns; callers must handle a
/// failed send and retry worker lookup without discarding the unsent item.
pub async fn get(&self, key: &K) -> Option<Sender<T>> {
self.workers
.get(key)
.filter(|tx| !tx.is_closed())
.map(|tx| tx.value().clone())
}
/// Returns the registered sender and, only when created, its receiver.
///
/// Start the new worker before the next await so cancellation cannot leave
/// a registered channel without a consumer. Existing channels retain their
/// original capacity. New channel capacity must satisfy `mpsc::channel`.
pub async fn get_or_create(&self, key: K, capacity: usize) -> (Sender<T>, Option<Receiver<T>>) {
let mut receiver = None;
let sender = self
.get_or_insert_with(key, || {
let (sender, rx) = mpsc::channel(capacity);
receiver = Some(rx);
sender
})
.await;
(sender, receiver)
}
/// Reuses a live sender or atomically creates its replacement.
///
/// `create` runs synchronously under the registry shard lock. It should only
/// prepare the sender and capture any initialization state (such as the
/// receiver) for the caller; start the worker after this method returns.
/// Do not block or reenter the registry from `create`.
pub async fn get_or_insert_with<F>(&self, key: K, create: F) -> Sender<T>
where
F: FnOnce() -> Sender<T>,
{
if let Some(tx) = self.get(&key).await {
return tx;
}
match self.workers.entry(key) {
Entry::Occupied(mut entry) => {
if entry.get().is_closed() {
entry.insert(create());
}
entry.get().clone()
}
Entry::Vacant(entry) => entry.insert(create()).value().clone(),
}
}
/// Removes the key only if it still points to this worker's channel.
pub async fn remove_if_same(&self, key: &K, tx: &Sender<T>) -> bool {
self.workers
.remove_if(key, |_, current| current.same_channel(tx))
.is_some()
}
/// Number of registered entries, including senders whose receivers closed.
pub async fn len(&self) -> usize {
self.workers.len()
}
pub async fn is_empty(&self) -> bool {
self.workers.is_empty()
}
}
#[cfg(test)]
mod tests {
use std::sync::Arc;
use std::sync::atomic::{AtomicUsize, Ordering};
use tokio::sync::{Barrier, mpsc};
use crate::worker_registry::WorkerRegistry;
#[tokio::test]
async fn test_live_lookup_uses_read_lock() {
let registry = Arc::new(WorkerRegistry::<_, ()>::new());
let (sender, _receiver) = registry.get_or_create(1, 1).await;
let guard = registry.workers.get(&1).unwrap();
let (result_tx, result_rx) = std::sync::mpsc::channel();
let worker_registry = registry.clone();
let runtime = tokio::runtime::Handle::current();
let thread = std::thread::spawn(move || {
let result = runtime.block_on(worker_registry.get_or_create(1, 1));
let _ = result_tx.send(result);
});
// Release the read guard before joining, even if lookup needs a write lock.
let result = result_rx.recv_timeout(std::time::Duration::from_secs(5));
drop(guard);
thread.join().unwrap();
let (reused, receiver) = result.expect("live lookup must not wait for an exclusive lock");
assert!(sender.same_channel(&reused));
assert!(receiver.is_none());
}
#[tokio::test]
async fn test_reuses_live_sender_and_returns_receiver_to_caller() {
let registry = WorkerRegistry::new();
assert!(registry.is_empty().await);
assert!(registry.get(&1).await.is_none());
let mut receiver = None;
let first = registry
.get_or_insert_with(1, || {
let (tx, rx) = mpsc::channel(1);
receiver = Some(rx);
tx
})
.await;
let second = registry
.get_or_insert_with(1, || panic!("live worker must be reused"))
.await;
assert!(first.same_channel(&second));
assert!(registry.get(&1).await.unwrap().same_channel(&first));
first.send(7).await.unwrap();
assert_eq!(Some(7), receiver.as_mut().unwrap().recv().await);
assert_eq!(1, registry.len().await);
assert!(registry.remove_if_same(&1, &first).await);
assert!(!registry.remove_if_same(&1, &first).await);
assert!(registry.is_empty().await);
}
#[tokio::test]
async fn test_old_cleanup_does_not_remove_replacement() {
let registry = WorkerRegistry::<_, ()>::new();
let (first, mut receiver) = mpsc::channel(1);
registry.get_or_insert_with("table", || first.clone()).await;
receiver.close();
assert!(registry.get(&"table").await.is_none());
// Closed entries remain accounted until replacement or explicit removal.
assert_eq!(1, registry.len().await);
let (second, _receiver) = mpsc::channel(1);
registry
.get_or_insert_with("table", || second.clone())
.await;
assert!(!registry.remove_if_same(&"table", &first).await);
assert!(registry.get(&"table").await.unwrap().same_channel(&second));
assert_eq!(1, registry.len().await);
assert!(registry.remove_if_same(&"table", &second).await);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_concurrent_lookup_initializes_once() {
let registry = Arc::new(WorkerRegistry::<_, ()>::new());
let barrier = Arc::new(Barrier::new(3));
let count = Arc::new(AtomicUsize::new(0));
let mut tasks = Vec::new();
for _ in 0..2 {
let registry = registry.clone();
let barrier = barrier.clone();
let count = count.clone();
tasks.push(tokio::spawn(async move {
barrier.wait().await;
let mut receiver = None;
let tx = registry
.get_or_insert_with(1, || {
count.fetch_add(1, Ordering::Relaxed);
let (tx, rx) = mpsc::channel(1);
receiver = Some(rx);
tx
})
.await;
// Keep the newly created receiver alive while both lookups run.
barrier.wait().await;
(tx, receiver)
}));
}
barrier.wait().await;
barrier.wait().await;
let first = tasks.remove(0).await.unwrap();
let second = tasks.remove(0).await.unwrap();
assert!(first.0.same_channel(&second.0));
assert_eq!(1, count.load(Ordering::Relaxed));
assert_eq!(1, registry.len().await);
}
#[tokio::test]
async fn test_channel_creation_replacement_and_cleanup() {
let registry = WorkerRegistry::<_, usize>::new();
let (first, receiver) = registry.get_or_create(1, 2).await;
let mut receiver = receiver.unwrap();
let (reused, absent) = registry.get_or_create(1, 3).await;
assert!(absent.is_none());
assert!(first.same_channel(&reused));
assert_eq!(2, reused.max_capacity());
first.send(7).await.unwrap();
assert_eq!(Some(7), receiver.recv().await);
receiver.close();
let (replacement, receiver) = registry.get_or_create(1, 3).await;
assert!(receiver.is_some());
assert_eq!(3, replacement.max_capacity());
assert!(!first.same_channel(&replacement));
assert!(!registry.remove_if_same(&1, &first).await);
assert!(registry.get(&1).await.unwrap().same_channel(&replacement));
assert!(registry.remove_if_same(&1, &replacement).await);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_concurrent_channel_creation_returns_one_receiver() {
let registry = Arc::new(WorkerRegistry::<_, ()>::new());
let barrier = Arc::new(Barrier::new(3));
let mut tasks = Vec::new();
for _ in 0..2 {
let registry = registry.clone();
let barrier = barrier.clone();
tasks.push(tokio::spawn(async move {
barrier.wait().await;
let channel = registry.get_or_create(1, 2).await;
barrier.wait().await;
channel
}));
}
barrier.wait().await;
barrier.wait().await;
let first = tasks.remove(0).await.unwrap();
let second = tasks.remove(0).await.unwrap();
assert!(first.0.same_channel(&second.0));
assert_ne!(first.1.is_some(), second.1.is_some());
assert_eq!(1, registry.len().await);
}
}
+74 -1
View File
@@ -37,6 +37,35 @@ use vec1::{Vec1, vec1};
use crate::error;
use crate::error::{DecodeFlightDataSnafu, InvalidFlightDataSnafu, Result};
/// Encodes a batch into the schema, record-batch header and payload used by bulk inserts.
///
/// Uses the default Flight compression and rejects dictionary batches, whose
/// additional Flight messages cannot be represented by this three-part format.
pub fn record_batch_to_ipc(
record_batch: DfRecordBatch,
) -> Result<(ProstBytes, ProstBytes, ProstBytes)> {
let mut encoder = FlightEncoder::default();
let schema = encoder.encode_schema(record_batch.schema().as_ref());
let mut iter = encoder
.encode(FlightMessage::RecordBatch(record_batch))
.into_iter();
let flight_data = iter.next().context(InvalidFlightDataSnafu {
reason: "Failed to encode empty flight data",
})?;
if iter.next().is_some() {
return error::NotSupportedSnafu {
feat: "bulk insert RecordBatch with dictionary arrays",
}
.fail();
}
Ok((
schema.data_header,
flight_data.data_header,
flight_data.data_body,
))
}
/// Flight metadata key used to carry flow query extensions as JSON pairs.
pub const FLOW_EXTENSIONS_METADATA_KEY: &str = "x-greptime-flow-extensions";
/// Flight metadata key used to carry query snapshot read upper bounds as JSON.
@@ -374,8 +403,52 @@ mod test {
use datatypes::arrow::buffer::OffsetBuffer;
use datatypes::arrow::datatypes::{DataType, Field, Schema};
use super::*;
use crate::Error;
use crate::flight::*;
#[test]
fn test_record_batch_to_ipc_preserves_wire_bytes() {
let schema = Arc::new(Schema::new(vec![Field::new("n", DataType::Int32, true)]));
for values in [vec![], vec![Some(1), None, Some(3)]] {
let batch =
DfRecordBatch::try_new(schema.clone(), vec![Arc::new(Int32Array::from(values))])
.unwrap();
// Reproduce the previous Prom encoding sequence and compare all bytes.
let mut encoder = FlightEncoder::default();
let expected_schema = encoder.encode_schema(batch.schema().as_ref());
let messages = encoder.encode(FlightMessage::RecordBatch(batch.clone()));
assert_eq!(messages.len(), 1);
let expected_batch = messages.first();
assert_eq!(
record_batch_to_ipc(batch).unwrap(),
(
expected_schema.data_header,
expected_batch.data_header.clone(),
expected_batch.data_body.clone(),
)
);
}
}
#[test]
fn test_record_batch_to_ipc_rejects_dictionary() {
let schema = Arc::new(Schema::new(vec![Field::new_dictionary(
"tag",
DataType::UInt32,
DataType::Utf8,
true,
)]));
let dictionary = DictionaryArray::new(
UInt32Array::from_value(0, 3),
Arc::new(StringArray::from_iter_values(["x"])),
);
let batch = DfRecordBatch::try_new(schema, vec![Arc::new(dictionary)]).unwrap();
assert!(matches!(
record_batch_to_ipc(batch),
Err(Error::NotSupported { feat })
if feat == "bulk insert RecordBatch with dictionary arrays"
));
}
#[test]
fn test_try_decode() -> Result<()> {
+61 -17
View File
@@ -31,6 +31,7 @@ use common_procedure::{
EventTrigger, LockKey, PoisonKey, PoisonKeys, Procedure, ProcedureId, Status, StringKey,
};
use common_telemetry::{error, info, warn};
use common_wal::options::WalOptions;
use serde::{Deserialize, Serialize};
use snafu::{ResultExt, ensure};
use store_api::metadata::ColumnMetadata;
@@ -47,12 +48,12 @@ use crate::ddl::event::table::{
TableDdlEvent, TableDdlEventType, TableDdlLocator, alter_table_kind_name,
};
use crate::ddl::utils::{
MultipleResults, extract_column_metadatas, handle_multiple_results, map_to_procedure_error,
sync_follower_regions,
MultipleResults, extract_column_metadatas, extract_region_wal_options, handle_multiple_results,
map_to_procedure_error, sync_follower_regions,
};
use crate::error::{
AbortProcedureSnafu, ConvertAlterTableRequestSnafu, NoLeaderSnafu, PutPoisonSnafu, Result,
RetryLaterSnafu, UnsupportedSnafu,
RetryLaterSnafu, UnexpectedSnafu, UnsupportedSnafu,
};
use crate::key::table_info::TableInfoValue;
use crate::key::{DeserializedValueWithBytes, RegionDistribution};
@@ -148,11 +149,11 @@ impl AlterTableProcedure {
// Safety: Checked in `AlterTableProcedure::new`.
let alter_kind = self.data.task.alter_table.kind.as_ref().unwrap();
if enables_skip_wal(alter_kind) {
if sets_skip_wal(alter_kind) {
ensure!(
only_enables_skip_wal(alter_kind),
only_sets_skip_wal(alter_kind),
UnsupportedSnafu {
operation: "combining skip_wal = 'true' with other table options".to_string()
operation: "combining skip_wal with other table options".to_string()
}
);
let engine = table_info_value.table_info.meta.engine.as_str();
@@ -162,7 +163,8 @@ impl AlterTableProcedure {
operation: format!("setting skip_wal on {engine} engine tables")
}
);
// Persist the irreversible intent before any region stops writing WAL.
// Keep skip-WAL changes serialized with region migration and use the same
// route snapshot for validation and RPC delivery.
let (physical_table_id, physical_table_route) = self
.context
.table_metadata_manager
@@ -199,6 +201,37 @@ impl AlterTableProcedure {
table_id: physical_table_id
}
);
// validate_alter_table_expr() has already parsed this single skip-WAL option.
let Some(skip_wal) = skip_wal_value(alter_kind) else {
return UnexpectedSnafu {
err_msg: "missing or invalid skip_wal value after validation".to_string(),
}
.fail();
};
if !skip_wal {
let datanode_table_values = self
.context
.table_metadata_manager
.datanode_table_manager()
.regions(physical_table_id, &physical_table_route)
.await?;
let region_wal_options = extract_region_wal_options(&datanode_table_values)?;
ensure!(
current_region_ids.iter().all(|region_id| {
matches!(
region_wal_options
.get(&region_id.region_number())
.cloned()
.unwrap_or_default(),
WalOptions::RaftEngine | WalOptions::Kafka(_)
)
}),
UnsupportedSnafu {
operation: "setting skip_wal = 'false' without an existing WAL provider"
.to_string()
}
);
}
self.data.region_distribution =
Some(region_distribution(&physical_table_route.region_routes));
}
@@ -265,8 +298,8 @@ impl AlterTableProcedure {
MultipleResults::PartialRetryable(error) => Err(error),
MultipleResults::PartialNonRetryable(error)
| MultipleResults::AllNonRetryable(error) => {
// The metadata already enables skip-WAL. Retry the idempotent request
// so later attempts can update the remaining replicas.
// The metadata already contains the requested skip-WAL value. Retry the
// idempotent request so later attempts can update the remaining replicas.
Err(BoxedError::new(error)).context(RetryLaterSnafu {
clean_poisons: true,
})
@@ -450,24 +483,35 @@ impl AlterTableProcedure {
}
}
fn enables_skip_wal(alter_kind: &Kind) -> bool {
fn sets_skip_wal(alter_kind: &Kind) -> bool {
let Kind::SetTableOptions(SetTableOptions { table_options }) = alter_kind else {
return false;
};
table_options
.iter()
.any(|option| option.key == SKIP_WAL_KEY && option.value == "true")
.any(|option| option.key == SKIP_WAL_KEY)
}
pub(crate) fn only_enables_skip_wal(alter_kind: &Kind) -> bool {
pub(crate) fn only_sets_skip_wal(alter_kind: &Kind) -> bool {
let Kind::SetTableOptions(SetTableOptions { table_options }) = alter_kind else {
return false;
};
table_options.len() == 1
&& table_options[0].key == SKIP_WAL_KEY
&& table_options[0].value == "true"
table_options.len() == 1 && table_options[0].key == SKIP_WAL_KEY
}
fn skip_wal_value(alter_kind: &Kind) -> Option<bool> {
let Kind::SetTableOptions(SetTableOptions { table_options }) = alter_kind else {
return None;
};
let [option] = table_options.as_slice() else {
return None;
};
if option.key != SKIP_WAL_KEY {
return None;
}
option.value.parse().ok()
}
fn is_metadata_only_alter(alter_kind: &Kind) -> Result<bool> {
@@ -487,7 +531,7 @@ enum AlterTableFlow {
impl AlterTableFlow {
fn from_kind(kind: &Kind) -> Result<Self> {
Ok(if only_enables_skip_wal(kind) {
Ok(if only_sets_skip_wal(kind) {
Self::MetadataFirst
} else if is_metadata_only_alter(kind)? {
Self::MetadataOnly
@@ -611,7 +655,7 @@ pub struct AlterTableData {
table_info_value: Option<DeserializedValueWithBytes<TableInfoValue>>,
/// Region distribution for table in case we need to update region options.
region_distribution: Option<RegionDistribution>,
/// Region locks held by irreversible region-option alters.
/// Region locks held by skip-WAL alters.
#[serde(default)]
region_locks: Vec<RegionId>,
}
@@ -174,7 +174,7 @@ impl AlterTableExecutor {
.await
}
/// Alters all replicas for the irreversible skip-WAL flow.
/// Alters all replicas for the skip-WAL flow.
pub(crate) async fn on_alter_skip_wal_regions(
&self,
node_manager: &NodeManagerRef,
+304 -31
View File
@@ -19,7 +19,7 @@ use std::sync::Arc;
use api::region::RegionResponse;
use api::v1::alter_table_expr::Kind;
use api::v1::region::sync_request::ManifestInfo;
use api::v1::region::{RegionRequest, region_request};
use api::v1::region::{RegionRequest, alter_request, region_request};
use api::v1::{
AddColumn, AddColumns, AlterTableExpr, ColumnDataType, ColumnDef as PbColumnDef, DropColumn,
DropColumns, SemanticType, SetTableOptions,
@@ -30,6 +30,7 @@ use common_error::status_code::StatusCode;
use common_procedure::store::poison_store::PoisonStore;
use common_procedure::{Procedure, ProcedureId, Status};
use common_procedure_test::{MockContextProvider, execute_procedure_until_done};
use common_wal::options::{KafkaWalOptions, WalOptions};
use datatypes::prelude::ConcreteDataType;
use datatypes::schema::ColumnSchema;
use store_api::metadata::ColumnMetadata;
@@ -164,6 +165,29 @@ fn assert_alter_request(
assert_eq!(req.region_id, expected_region_id);
}
fn assert_skip_wal_alter_request(
peer: Peer,
request: RegionRequest,
expected_peer_id: u64,
expected_region_id: RegionId,
expected_skip_wal: bool,
) {
assert_eq!(peer.id, expected_peer_id);
let Some(region_request::Body::Alter(req)) = request.body else {
unreachable!();
};
assert_eq!(req.region_id, expected_region_id);
let Some(alter_request::Kind::SetTableOptions(options)) = req.kind else {
unreachable!();
};
assert_eq!(1, options.table_options.len());
assert_eq!(SKIP_WAL_KEY, options.table_options[0].key);
assert_eq!(
expected_skip_wal.to_string(),
options.table_options[0].value
);
}
fn assert_sync_request(
peer: Peer,
request: RegionRequest,
@@ -729,39 +753,42 @@ async fn test_skip_wal_rejects_file_engine_table() {
fn test_skip_wal_holds_region_locks() {
let table_id = 1024;
let region_ids = vec![RegionId::new(table_id, 2), RegionId::new(table_id, 1)];
let task = AlterTableTask {
alter_table: AlterTableExpr {
catalog_name: DEFAULT_CATALOG_NAME.to_string(),
schema_name: DEFAULT_SCHEMA_NAME.to_string(),
table_name: "foo".to_string(),
kind: Some(Kind::SetTableOptions(SetTableOptions {
table_options: vec![api::v1::Option {
key: SKIP_WAL_KEY.to_string(),
value: "true".to_string(),
}],
})),
},
};
let context = new_ddl_context(Arc::new(MockDatanodeManager::new(())));
let procedure = AlterTableProcedure::new_with_region_locks(
table_id,
task,
region_ids.clone(),
context.clone(),
)
.unwrap();
for value in ["true", "false"] {
let task = AlterTableTask {
alter_table: AlterTableExpr {
catalog_name: DEFAULT_CATALOG_NAME.to_string(),
schema_name: DEFAULT_SCHEMA_NAME.to_string(),
table_name: "foo".to_string(),
kind: Some(Kind::SetTableOptions(SetTableOptions {
table_options: vec![api::v1::Option {
key: SKIP_WAL_KEY.to_string(),
value: value.to_string(),
}],
})),
},
};
let procedure = AlterTableProcedure::new_with_region_locks(
table_id,
task,
region_ids.clone(),
context.clone(),
)
.unwrap();
let lock_key = procedure.lock_key();
for region_id in &region_ids {
assert!(
lock_key
.keys_to_lock()
.any(|key| key == &RegionLock::Write(*region_id).into())
);
let lock_key = procedure.lock_key();
for region_id in &region_ids {
assert!(
lock_key
.keys_to_lock()
.any(|key| key == &RegionLock::Write(*region_id).into())
);
}
let recovered =
AlterTableProcedure::from_json(&procedure.dump().unwrap(), context.clone()).unwrap();
assert_eq!(lock_key, recovered.lock_key());
}
let recovered = AlterTableProcedure::from_json(&procedure.dump().unwrap(), context).unwrap();
assert_eq!(lock_key, recovered.lock_key());
}
#[tokio::test]
@@ -875,6 +902,252 @@ async fn test_skip_wal_rejects_no_leader_before_updating_metadata() {
assert!(!table_info.meta.options.skip_wal);
}
#[tokio::test]
async fn test_enable_wal_rejects_noop_provider_before_updating_metadata() {
let ddl_context = new_ddl_context(Arc::new(MockDatanodeManager::new(())));
let table_name = "foo";
let table_id = 1024;
let mut task = test_create_table_task(table_name, table_id);
task.table_info.meta.options.skip_wal = true;
task.table_info
.meta
.options
.extra_options
.insert(SKIP_WAL_KEY.to_string(), "true".to_string());
let table_route = prepare_table_route(table_id);
let region_locks = table_route
.region_routes()
.unwrap()
.iter()
.map(|route| route.region.id)
.collect();
let region_wal_options = HashMap::from([
(1, WalOptions::Noop),
(2, WalOptions::Noop),
(3, WalOptions::Noop),
]);
ddl_context
.table_metadata_manager
.create_table_metadata(task.table_info, table_route, region_wal_options)
.await
.unwrap();
let alter_task = AlterTableTask {
alter_table: AlterTableExpr {
catalog_name: DEFAULT_CATALOG_NAME.to_string(),
schema_name: DEFAULT_SCHEMA_NAME.to_string(),
table_name: table_name.to_string(),
kind: Some(Kind::SetTableOptions(SetTableOptions {
table_options: vec![api::v1::Option {
key: SKIP_WAL_KEY.to_string(),
value: "false".to_string(),
}],
})),
},
};
let mut procedure = AlterTableProcedure::new_with_region_locks(
table_id,
alter_task,
region_locks,
ddl_context.clone(),
)
.unwrap();
let error = procedure.on_prepare().await.unwrap_err();
assert_matches!(error, Error::Unsupported { .. });
assert!(
error
.to_string()
.contains("without an existing WAL provider")
);
let table_info = ddl_context
.table_metadata_manager
.table_info_manager()
.get(table_id)
.await
.unwrap()
.unwrap()
.into_inner()
.table_info;
assert!(table_info.meta.options.skip_wal);
}
#[tokio::test]
async fn test_enable_wal_accepts_legacy_raft_regions_without_wal_options() {
let ddl_context = new_ddl_context(Arc::new(MockDatanodeManager::new(())));
let table_name = "legacy_raft";
let table_id = 1024;
let mut task = test_create_table_task(table_name, table_id);
task.table_info.meta.options.skip_wal = true;
task.table_info
.meta
.options
.extra_options
.insert(SKIP_WAL_KEY.to_string(), "true".to_string());
let table_route = prepare_table_route(table_id);
let region_locks = table_route
.region_routes()
.unwrap()
.iter()
.map(|route| route.region.id)
.collect();
ddl_context
.table_metadata_manager
.create_table_metadata(task.table_info, table_route, HashMap::new())
.await
.unwrap();
let alter_task = AlterTableTask {
alter_table: AlterTableExpr {
catalog_name: DEFAULT_CATALOG_NAME.to_string(),
schema_name: DEFAULT_SCHEMA_NAME.to_string(),
table_name: table_name.to_string(),
kind: Some(Kind::SetTableOptions(SetTableOptions {
table_options: vec![api::v1::Option {
key: SKIP_WAL_KEY.to_string(),
value: "false".to_string(),
}],
})),
},
};
let mut procedure = AlterTableProcedure::new_with_region_locks(
table_id,
alter_task,
region_locks,
ddl_context.clone(),
)
.unwrap();
procedure.on_prepare().await.unwrap();
procedure.on_update_metadata().await.unwrap();
let table_info = ddl_context
.table_metadata_manager
.table_info_manager()
.get(table_id)
.await
.unwrap()
.unwrap()
.into_inner()
.table_info;
assert!(!table_info.meta.options.skip_wal);
}
#[tokio::test]
async fn test_enable_wal_updates_metadata_and_region_request() {
let (tx, mut rx) = mpsc::channel(8);
let node_manager = Arc::new(MockDatanodeManager::new(DatanodeWatcher::new(tx)));
let ddl_context = new_ddl_context(node_manager);
let table_name = "foo";
let table_id = 1024;
let mut task = test_create_table_task(table_name, table_id);
task.table_info.meta.options.skip_wal = true;
task.table_info
.meta
.options
.extra_options
.insert(SKIP_WAL_KEY.to_string(), "true".to_string());
let table_route = prepare_table_route(table_id);
let region_locks = table_route
.region_routes()
.unwrap()
.iter()
.map(|route| route.region.id)
.collect();
let region_wal_options = HashMap::from([
(1, WalOptions::RaftEngine),
(
2,
WalOptions::Kafka(KafkaWalOptions::new("topic-2".to_string())),
),
(
3,
WalOptions::Kafka(KafkaWalOptions::new("topic-3".to_string())),
),
]);
ddl_context
.table_metadata_manager
.create_table_metadata(task.table_info, table_route, region_wal_options)
.await
.unwrap();
let alter_task = AlterTableTask {
alter_table: AlterTableExpr {
catalog_name: DEFAULT_CATALOG_NAME.to_string(),
schema_name: DEFAULT_SCHEMA_NAME.to_string(),
table_name: table_name.to_string(),
kind: Some(Kind::SetTableOptions(SetTableOptions {
table_options: vec![api::v1::Option {
key: SKIP_WAL_KEY.to_string(),
value: "false".to_string(),
}],
})),
},
};
let mut procedure = AlterTableProcedure::new_with_region_locks(
table_id,
alter_task,
region_locks,
ddl_context.clone(),
)
.unwrap();
procedure.on_prepare().await.unwrap();
let persisted: serde_json::Value = serde_json::from_str(&procedure.dump().unwrap()).unwrap();
assert_eq!("UpdateMetadata", persisted["state"]);
let Some(alter_request::Kind::SetTableOptions(options)) =
procedure.make_region_alter_kind().unwrap()
else {
unreachable!();
};
assert_eq!(1, options.table_options.len());
assert_eq!(SKIP_WAL_KEY, options.table_options[0].key);
assert_eq!("false", options.table_options[0].value);
procedure.on_update_metadata().await.unwrap();
let table_info = ddl_context
.table_metadata_manager
.table_info_manager()
.get(table_id)
.await
.unwrap()
.unwrap()
.into_inner()
.table_info;
assert!(!table_info.meta.options.skip_wal);
assert_eq!(
Some("false"),
table_info
.meta
.options
.extra_options
.get(SKIP_WAL_KEY)
.map(String::as_str)
);
let provider = Arc::new(MockContextProvider::default());
procedure
.submit_alter_region_requests(ProcedureId::random(), provider.as_ref())
.await
.unwrap();
let mut requests = Vec::new();
while let Ok(request) = rx.try_recv() {
requests.push(request);
}
requests.sort_unstable_by_key(|(peer, _)| peer.id);
let expected = [
(1, RegionId::new(table_id, 1)),
(2, RegionId::new(table_id, 2)),
(3, RegionId::new(table_id, 3)),
(4, RegionId::new(table_id, 2)),
(5, RegionId::new(table_id, 1)),
];
assert_eq!(expected.len(), requests.len());
for ((peer, request), (peer_id, region_id)) in requests.into_iter().zip(expected) {
assert_skip_wal_alter_request(peer, request, peer_id, region_id, false);
}
}
#[tokio::test]
async fn test_skip_wal_updates_metadata_before_all_replicas() {
let (tx, mut rx) = mpsc::channel(8);
+2 -2
View File
@@ -35,7 +35,7 @@ use table::table_name::TableName;
use crate::ddl::alter_database::AlterDatabaseProcedure;
use crate::ddl::alter_logical_tables::AlterLogicalTablesProcedure;
use crate::ddl::alter_table::{AlterTableProcedure, RegionRouteChanged, only_enables_skip_wal};
use crate::ddl::alter_table::{AlterTableProcedure, RegionRouteChanged, only_sets_skip_wal};
use crate::ddl::comment_on::CommentOnProcedure;
use crate::ddl::create_database::{CreateDatabaseMetadataCommitterRef, CreateDatabaseProcedure};
use crate::ddl::create_flow::CreateFlowProcedure;
@@ -433,7 +433,7 @@ impl DdlManager {
.alter_table
.kind
.as_ref()
.is_some_and(only_enables_skip_wal);
.is_some_and(only_sets_skip_wal);
let mut route_change_retries = 0;
loop {
+59 -1
View File
@@ -13,7 +13,7 @@
// limitations under the License.
use arrow_array::{
ArrayRef, PrimitiveArray, TimestampMicrosecondArray, TimestampMillisecondArray,
Array, ArrayRef, PrimitiveArray, TimestampMicrosecondArray, TimestampMillisecondArray,
TimestampNanosecondArray, TimestampSecondArray,
};
use arrow_schema::DataType;
@@ -179,8 +179,24 @@ pub fn timestamp_array_to_primitive(
Some((ts_primitive, *unit))
}
/// Appends non-null timestamps in the source array's native time unit.
///
/// Returns `None` for a non-timestamp array without changing `timestamps`.
pub fn append_timestamps(ts_array: &ArrayRef, timestamps: &mut Vec<i64>) -> Option<()> {
let (values, _) = timestamp_array_to_primitive(ts_array)?;
if values.null_count() == 0 {
timestamps.extend_from_slice(values.values());
} else {
timestamps.extend(values.iter().flatten());
}
Some(())
}
#[cfg(test)]
mod tests {
use std::sync::Arc;
use arrow_array::Int64Array;
use common_time::timezone::set_default_timezone;
use super::*;
@@ -213,4 +229,46 @@ mod tests {
assert_eq!(ts, ts.as_scalar_ref());
assert_eq!(ts, ts.to_owned_scalar());
}
#[test]
fn test_append_timestamps() {
let cases = [
vec![Some(i64::MIN), Some(-1), Some(0), Some(i64::MAX)],
vec![Some(-1), None, Some(2), None],
vec![None, None],
vec![],
];
for values in cases {
let arrays: [ArrayRef; 4] = [
Arc::new(TimestampSecondArray::from(values.clone())),
Arc::new(TimestampMillisecondArray::from(values.clone())),
Arc::new(TimestampMicrosecondArray::from(values.clone())),
Arc::new(TimestampNanosecondArray::from(values.clone())),
];
let mut expected = vec![42];
expected.extend(values.iter().flatten().copied());
for array in arrays {
for _ in 0..2 {
let (primitive, _) = timestamp_array_to_primitive(&array).unwrap();
let mut reference = vec![42];
reference.extend(primitive.iter().flatten());
let mut timestamps = vec![42];
assert_eq!(append_timestamps(&array, &mut timestamps), Some(()));
assert_eq!(timestamps, expected);
assert_eq!(timestamps, reference);
}
}
}
}
#[test]
fn test_append_timestamps_invalid_array_preserves_prefix() {
for values in [vec![], vec![Some(1), None]] {
let array: ArrayRef = Arc::new(Int64Array::from(values));
let mut timestamps = vec![42, -1];
assert_eq!(append_timestamps(&array, &mut timestamps), None);
assert_eq!(timestamps, vec![42, -1]);
}
}
}
+3 -1
View File
@@ -510,6 +510,7 @@ impl BatchingTask {
debug!("Generate new query: {}", new_query.plan);
let res = self
.execute_logical_plan_unlocked(
engine,
frontend_client,
&new_query.plan,
&new_query.dirty_restore,
@@ -640,6 +641,7 @@ impl BatchingTask {
/// Executes the insert plan. Caller must reach this through the serialized path.
async fn execute_logical_plan_unlocked(
&self,
engine: &QueryEngineRef,
frontend_client: &Arc<FrontendClient>,
plan: &LogicalPlan,
dirty_restore: &DirtyRestore,
@@ -676,7 +678,7 @@ impl BatchingTask {
// For incremental-mode SQL queries, attempt to rewrite the delta aggregate
// plan into a safe delta-LEFT-JOIN-sink form before deciding on extensions.
let incremental_plan = if coverage.is_incremental_delta() {
self.prepare_plan_for_incremental(&plan).await?
self.prepare_plan_for_incremental(engine, &plan).await?
} else {
None
};
+3
View File
@@ -18,6 +18,7 @@ use common_error::ext::BoxedError;
use common_telemetry::debug;
use common_telemetry::tracing::warn;
use datafusion_expr::{DmlStatement, LogicalPlan};
use query::QueryEngineRef;
use query::options::{
FLOW_INCREMENTAL_AFTER_SEQS, FLOW_INCREMENTAL_MODE, FLOW_INCREMENTAL_MODE_MEMTABLE_ONLY,
FLOW_INCREMENTAL_MODE_SEQUENCE_RANGE, FLOW_SINK_TABLE_ID,
@@ -119,6 +120,7 @@ impl BatchingTask {
/// incremental safe without a rewrite, so they return `Some(original_plan)`.
pub(super) async fn prepare_plan_for_incremental(
&self,
engine: &QueryEngineRef,
plan: &LogicalPlan,
) -> Result<Option<LogicalPlan>, Error> {
let is_incremental_sql = {
@@ -219,6 +221,7 @@ impl BatchingTask {
let rewritten_inner = match rewrite_incremental_aggregate_with_sink_merge(
&inner_plan,
&analysis,
engine,
sink_table,
&self.config.sink_table_name,
None,
+13 -168
View File
@@ -24,8 +24,8 @@ use common_error::ext::BoxedError;
use common_error::mock::MockError;
use common_error::status_code::StatusCode;
use common_query::Output;
use common_recordbatch::RecordBatch;
use common_recordbatch::adapter::{RecordBatchMetrics, RegionWatermarkEntry};
use common_recordbatch::{RecordBatch, RecordBatchStreamWrapper};
use datatypes::data_type::ConcreteDataType as CDT;
use datatypes::schema::ColumnSchema;
use datatypes::vectors::{
@@ -601,47 +601,6 @@ impl crate::batching_mode::frontend_client::GrpcQueryHandlerWithBoxedError
}
}
/// Injected frontend handler that serves the exact delta with an empty
/// (zero-row) stream whose terminal metrics still prove a complete region
/// watermark. It fails the test when the dispatched request is not the
/// expected `sequence_range` read after the stored checkpoint.
struct ExactEmptyDeltaHandler {
invoked: Arc<AtomicBool>,
}
#[async_trait::async_trait]
impl crate::batching_mode::frontend_client::GrpcQueryHandlerWithBoxedError
for ExactEmptyDeltaHandler
{
async fn do_query(
&self,
_query: api::v1::greptime_request::Request,
ctx: QueryContextRef,
) -> std::result::Result<Output, BoxedError> {
self.invoked.store(true, Ordering::SeqCst);
assert_eq!(ctx.extension(FLOW_INCREMENTAL_MODE), Some("sequence_range"));
assert_eq!(
ctx.extension(FLOW_INCREMENTAL_AFTER_SEQS),
Some("{\"1\":10}")
);
// Reuse the existing record-batch wrapper stream: an empty futures
// stream produces no rows, while the public metrics field carries the
// complete terminal watermark of the finished exact scan.
let wrapper = RecordBatchStreamWrapper::new(
aggregate_time_window_sink_schema(),
futures::stream::empty::<common_recordbatch::error::Result<RecordBatch>>(),
);
wrapper.metrics.store(Some(Arc::new(RecordBatchMetrics {
region_watermarks: vec![RegionWatermarkEntry {
region_id: 1,
watermark: Some(12),
}],
..Default::default()
})));
Ok(Output::new_with_stream(Box::pin(wrapper)))
}
}
struct CaptureScheduledNowHandler {
expected_extension: String,
captured_sql: Arc<std::sync::Mutex<Option<String>>>,
@@ -1139,35 +1098,6 @@ fn test_apply_query_result_to_state_advances_incremental_subset() {
);
}
#[test]
fn test_apply_query_result_to_state_empty_delta_complete_watermark_keeps_incremental() {
// An empty (zero-row) delta result is not a missing proof: the terminal
// watermark of the completed exact scan still advances the checkpoint and
// keeps incremental mode.
let query_ctx = QueryContext::arc();
let (_tx, rx) = tokio::sync::oneshot::channel();
let mut state = TaskState::new(query_ctx, rx);
state.advance_checkpoints(HashMap::from([(1_u64, 10_u64)]));
let result = output_with_region_watermarks([(1_u64, Some(12_u64))]);
let decision = BatchingTask::apply_query_result_to_state(
&mut state,
&result,
std::time::Duration::from_millis(1),
&QueryCoverage::IncrementalDelta,
);
assert_eq!(
decision,
FlowCheckpointDecision::AdvancedIncremental {
participating_regions: 1,
watermarks: 1,
}
);
assert_eq!(state.checkpoint_mode(), CheckpointMode::Incremental);
assert_eq!(state.checkpoints(), &BTreeMap::from([(1_u64, 12_u64)]));
}
#[test]
fn test_scoped_base_repair_with_dirty_backlog_starts_fenced_repair_from_full_snapshot() {
let query_ctx = QueryContext::arc();
@@ -2260,6 +2190,7 @@ async fn test_exact_required_executed_failure_selects_full_snapshot_repair() {
QueryOptions::default(),
));
task.execute_logical_plan_unlocked(
&query_engine,
&frontend_client,
&plan_info.plan,
&plan_info.dirty_restore,
@@ -2294,100 +2225,6 @@ async fn test_exact_required_executed_failure_selects_full_snapshot_repair() {
assert!(!repair.plan.to_string().contains("Left Join"));
}
#[tokio::test]
async fn test_exact_required_empty_delta_complete_watermark_advances_checkpoint() {
let TestTaskParts {
mut task,
query_engine,
..
} = new_time_window_test_task_with_query(
"SELECT max(number) AS number, date_bin(INTERVAL '5 second', ts) AS time_window FROM numbers_with_ts GROUP BY time_window",
)
.await;
configure_source_capability(&query_engine, "mito", true).await;
register_twe_sink(&query_engine, "missing_sink", 9201);
Arc::get_mut(&mut task.config)
.expect("test task config should be uniquely owned")
.exact_sequence_range_required = true;
{
let mut state = task.state.write().unwrap();
state.advance_checkpoints(HashMap::from([(1_u64, 10_u64)]));
state
.dirty_time_windows
.add_window(Timestamp::new_second(0), Some(Timestamp::new_second(5)));
}
let plan_info = task
.gen_insert_plan_unlocked(&query_engine, Some(1))
.await
.unwrap()
.expect("dirty incremental flow must generate an exact delta plan");
assert!(matches!(
plan_info.coverage,
QueryCoverage::IncrementalDelta
));
let handler_invoked = Arc::new(AtomicBool::new(false));
let handler: Arc<dyn crate::batching_mode::frontend_client::GrpcQueryHandlerWithBoxedError> =
Arc::new(ExactEmptyDeltaHandler {
invoked: handler_invoked.clone(),
});
let frontend_client = Arc::new(FrontendClient::from_grpc_handler(
Arc::downgrade(&handler),
QueryOptions::default(),
));
let (affected_rows, _) = task
.execute_logical_plan_unlocked(
&frontend_client,
&plan_info.plan,
&plan_info.dirty_restore,
&plan_info.coverage,
)
.await
.expect("the dispatched exact delta must execute through the injected frontend handler")
.expect("executed exact delta must report its affected rows");
assert!(
handler_invoked.load(Ordering::SeqCst),
"the injected frontend handler must receive the exact delta"
);
assert_eq!(
affected_rows, 0,
"an empty exact delta must report zero rows"
);
{
let state = task.state.read().unwrap();
assert_eq!(state.checkpoint_mode(), CheckpointMode::Incremental);
assert_eq!(state.checkpoints(), &BTreeMap::from([(1_u64, 12_u64)]));
assert!(state.dirty_time_windows.is_empty());
}
// Without a new dirty signal the incremental flow skips the next round
// instead of re-reading the delta or falling back to a full snapshot.
let skipped = task
.gen_insert_plan_unlocked(&query_engine, Some(1))
.await
.unwrap();
assert!(
skipped.is_none(),
"without a dirty signal the incremental flow must skip the round"
);
// New dirty work still advances through the exact delta, not through a
// filtered base repair or an unfiltered full snapshot.
task.state
.write()
.unwrap()
.dirty_time_windows
.add_window(Timestamp::new_second(5), Some(Timestamp::new_second(10)));
let followup = task
.gen_insert_plan_unlocked(&query_engine, Some(1))
.await
.unwrap()
.expect("new dirty work must still generate an exact delta plan");
assert!(matches!(followup.coverage, QueryCoverage::IncrementalDelta));
}
#[tokio::test]
async fn test_exact_required_incomplete_proof_selects_base_recomputation() {
let TestTaskParts {
@@ -2623,6 +2460,7 @@ async fn test_exact_required_unsupported_plan_keeps_exact_retry_state() {
for _ in 0..2 {
let err = task
.execute_logical_plan_unlocked(
&query_engine,
&frontend_client,
&dml_plan,
&DirtyRestore::Unscoped(dirty_range(10, 15)),
@@ -2708,7 +2546,10 @@ async fn test_prepare_plan_for_incremental_disables_on_non_aggregate() {
CheckpointMode::Incremental
);
let incremental_plan = task.prepare_plan_for_incremental(&dml_plan).await.unwrap();
let incremental_plan = task
.prepare_plan_for_incremental(&query_engine, &dml_plan)
.await
.unwrap();
assert!(incremental_plan.is_none());
let state = task.state.read().unwrap();
assert!(state.is_incremental_disabled());
@@ -2785,6 +2626,7 @@ async fn test_unsafe_incremental_plan_skip_restores_dirty_without_query() {
let result = task
.execute_logical_plan_unlocked(
&query_engine,
&Arc::new(frontend_client),
&dml_plan,
&dirty_restore,
@@ -2871,7 +2713,7 @@ async fn test_prepare_plan_for_incremental_group_by_without_merge_columns_uses_o
.advance_checkpoints(HashMap::from([(1_u64, 10_u64)]));
let incremental_plan = task
.prepare_plan_for_incremental(&dml_plan)
.prepare_plan_for_incremental(&query_engine, &dml_plan)
.await
.unwrap()
.expect("plain GROUP BY is incremental-safe without a rewrite");
@@ -2916,7 +2758,10 @@ async fn test_auto_created_sql_aggregate_sink_reaches_incremental_safe() {
.write()
.unwrap()
.advance_checkpoints(HashMap::from([(1_u64, 10_u64)]));
let incremental_plan = task.prepare_plan_for_incremental(&dml_plan).await.unwrap();
let incremental_plan = task
.prepare_plan_for_incremental(&query_engine, &dml_plan)
.await
.unwrap();
let incremental_safe = incremental_plan.is_some();
assert!(incremental_safe);
+221 -96
View File
@@ -66,6 +66,9 @@ mod test;
/// `max(numbers_with_ts.number)`.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct IncrementalAggregateMergeColumn {
/// Delta-plan field containing the aggregate result/state column. Repeated
/// projections intentionally share this field while using distinct outputs.
pub input_field_name: String,
/// Final output/sink field name for the aggregate result/state column.
///
pub output_field_name: String,
@@ -73,16 +76,17 @@ pub struct IncrementalAggregateMergeColumn {
}
impl IncrementalAggregateMergeColumn {
/// Create a new merge column.
/// Create a new merge column whose delta and output fields have the same name.
pub fn new(output_field_name: String, merge_op: IncrementalAggregateMergeOp) -> Self {
Self {
input_field_name: output_field_name.clone(),
output_field_name,
merge_op,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum IncrementalAggregateMergeOp {
Sum,
Min,
@@ -92,6 +96,10 @@ pub enum IncrementalAggregateMergeOp {
BitAnd,
BitOr,
BitXor,
StateDeltaMerge {
function_name: &'static str,
params: Vec<Expr>,
},
}
/// Analysis result for an incremental aggregate plan.
@@ -113,24 +121,6 @@ pub struct IncrementalAggregateAnalysis {
pub unsupported_exprs: Vec<String>,
}
/// Recursively find all `Expr::Column` names inside an expression tree.
/// Only recurses into wrappers that are merge-transparent.
/// Non-transparent wrappers (e.g., `ScalarFunction`, `Negative`, `Cast`) are
/// intentionally not recursed into since their merge semantics would be
/// incorrect.
///
/// `Cast`/`TryCast` are intentionally opaque: merging already-casted aggregate
/// outputs is not generally equivalent to casting the final merged aggregate.
fn find_column_names(expr: &Expr, names: &mut Vec<String>) {
match expr {
Expr::Column(col) => {
names.push(col.name.clone());
}
Expr::Alias(alias) => find_column_names(&alias.expr, names),
_ => {}
}
}
fn unqualified_col(name: impl Into<String>) -> Expr {
Expr::Column(Column::from_name(name.into()))
}
@@ -230,8 +220,8 @@ fn check_input_plan_shape(plan: &LogicalPlan) -> Result<(), String> {
#[derive(Debug, Default)]
struct OutputProjectionInfo {
has_top_level_projection: bool,
output_aliases: HashMap<String, String>,
duplicate_aggregate_aliases: BTreeSet<String>,
/// Aggregate expression name and projected output field, in projection order.
aggregate_outputs: Vec<(String, String)>,
literal_columns: HashSet<String>,
output_field_names: Vec<String>,
}
@@ -265,46 +255,34 @@ fn collect_output_projection_info(plan: &LogicalPlan) -> OutputProjectionInfo {
..Default::default()
};
let mut output_aliases = HashMap::new();
if let LogicalPlan::Projection(projection) = plan {
for expr in &projection.expr {
match expr {
Expr::Alias(alias) => {
// Alias resolution has three cases:
// - 0 Column refs (e.g., literal `42 AS lit`): record literal output
// - 1 Column ref: record the mapping (e.g., `sum(x) AS total`)
// - >1 Column refs (e.g., `COALESCE(sum(x), sum(y))`):
// skip — ambiguous merge semantics
// Only a direct aggregate output column has the same
// merge semantics as the original resolver. In particular,
// do not mine aggregate columns through CAST/TryCast or
// other output wrappers.
let alias_name = alias.name.clone();
let mut col_names = Vec::new();
find_column_names(&alias.expr, &mut col_names);
match col_names.len() {
0 if is_passthrough_output_column(&alias_name, alias.expr.as_ref()) => {
projection_info.literal_columns.insert(alias_name);
}
1 => {
if let Some(col_name) = col_names.into_iter().next() {
if let Some(existing_alias) = output_aliases.get(&col_name) {
if existing_alias != &alias_name {
projection_info.duplicate_aggregate_aliases.insert(format!(
"same aggregate output {col_name} is used by multiple aliases: {existing_alias}, {alias_name}"
));
}
} else {
output_aliases.insert(col_name, alias_name);
}
}
}
_ => {}
if let Expr::Column(column) = alias.expr.as_ref() {
projection_info
.aggregate_outputs
.push((column.name.clone(), alias_name));
} else if let Expr::Alias(inner_alias) = alias.expr.as_ref()
&& inner_alias.name.eq_ignore_ascii_case("count(*)")
&& let Expr::Column(column) = inner_alias.expr.as_ref()
{
projection_info
.aggregate_outputs
.push((column.name.clone(), alias_name));
} else if is_passthrough_output_column(&alias_name, alias.expr.as_ref()) {
projection_info.literal_columns.insert(alias_name);
}
// If >1 column references detected (e.g., COALESCE(sum(x), sum(y))),
// intentionally skip alias mapping — the merge semantics are ambiguous.
}
Expr::Column(col) => {
output_aliases
.entry(col.name.clone())
.or_insert(col.name.clone());
projection_info
.aggregate_outputs
.push((col.name.clone(), col.name.clone()));
}
Expr::Literal(_, _) => {
projection_info
@@ -326,7 +304,6 @@ fn collect_output_projection_info(plan: &LogicalPlan) -> OutputProjectionInfo {
.insert(AUTO_CREATED_PLACEHOLDER_TS_COL.to_string());
}
projection_info.output_aliases = output_aliases;
projection_info
}
@@ -362,6 +339,12 @@ fn merge_op_for_aggregate_expr(aggr_expr: &Expr) -> Result<IncrementalAggregateM
return Err(format!("unsupported aggregate NULL treatment: {aggr_expr}"));
}
let state_delta_merge = |function_name, params| {
Ok(IncrementalAggregateMergeOp::StateDeltaMerge {
function_name,
params,
})
};
match aggr_func.func.name().to_ascii_lowercase().as_str() {
"sum" | "count" => Ok(IncrementalAggregateMergeOp::Sum),
"min" => Ok(IncrementalAggregateMergeOp::Min),
@@ -371,28 +354,52 @@ fn merge_op_for_aggregate_expr(aggr_expr: &Expr) -> Result<IncrementalAggregateM
"bit_and" => Ok(IncrementalAggregateMergeOp::BitAnd),
"bit_or" => Ok(IncrementalAggregateMergeOp::BitOr),
"bit_xor" => Ok(IncrementalAggregateMergeOp::BitXor),
// Preserve state-family parameters; value coercion is handled by the aggregate.
"hll" if aggr_func.params.args.len() == 1 => state_delta_merge("__hll_delta_merge", vec![]),
"stddev_pop_state" if aggr_func.params.args.len() == 1 => {
state_delta_merge("__stddev_pop_state_delta_merge", vec![])
}
"uddsketch_state" if aggr_func.params.args.len() == 3 => {
let [bucket_size, error_rate, _] = aggr_func.params.args.as_slice() else {
unreachable!();
};
if !matches!(bucket_size, Expr::Literal(ScalarValue::Int64(Some(_)), _))
|| !matches!(error_rate, Expr::Literal(ScalarValue::Float64(Some(_)), _))
{
return Err(aggr_expr.to_string());
}
state_delta_merge(
"__uddsketch_state_delta_merge",
vec![bucket_size.clone(), error_rate.clone()],
)
}
_ => Err(aggr_expr.to_string()),
}
}
fn resolve_aggregate_output_field_name(
fn resolve_aggregate_output_fields(
aggr_expr: &Expr,
projection_info: &OutputProjectionInfo,
output_field_name_set: &HashSet<String>,
) -> Option<String> {
) -> Vec<(String, String)> {
// qualified_name() returns (Option<String>, String) where the second
// element is the unqualified column/alias name. This relies on
// DataFusion's internal naming convention: aggregate expressions
// emit a column named after the aggregate itself (e.g. "SUM(x)"),
// which matches what the projection aliases reference.
// emit a column named after the aggregate itself (e.g. "SUM(x)").
// Keep every matching projection occurrence because DataFusion can share
// one aggregate input field for identical expressions.
let raw_name = aggr_expr.qualified_name().1;
if let Some(alias) = projection_info.output_aliases.get(&raw_name) {
Some(alias.clone())
} else if !projection_info.has_top_level_projection && output_field_name_set.contains(&raw_name)
{
Some(raw_name)
if projection_info.has_top_level_projection {
projection_info
.aggregate_outputs
.iter()
.filter(|(input_name, _)| input_name == &raw_name)
.cloned()
.collect()
} else if output_field_name_set.contains(&raw_name) {
vec![(raw_name.clone(), raw_name)]
} else {
None
vec![]
}
}
@@ -478,7 +485,6 @@ pub fn analyze_incremental_aggregate_plan(
.map(|name| format!("duplicate output field name: {name}"))
.collect::<Vec<_>>();
unsupported_exprs.push(reason);
unsupported_exprs.extend(projection_info.duplicate_aggregate_aliases.iter().cloned());
return Ok(Some(IncrementalAggregateAnalysis {
group_key_names,
merge_columns: vec![],
@@ -512,7 +518,6 @@ pub fn analyze_incremental_aggregate_plan(
aggregate,
&group_key_names,
));
unsupported_exprs.extend(projection_info.duplicate_aggregate_aliases.iter().cloned());
for aggr_expr in aggr_exprs {
let merge_op = match merge_op_for_aggregate_expr(&aggr_expr) {
Ok(merge_op) => merge_op,
@@ -521,18 +526,37 @@ pub fn analyze_incremental_aggregate_plan(
continue;
}
};
let Some(output_field_name) = resolve_aggregate_output_field_name(
&aggr_expr,
&projection_info,
&output_field_name_set,
) else {
let aggregate_outputs =
resolve_aggregate_output_fields(&aggr_expr, &projection_info, &output_field_name_set);
if aggregate_outputs.is_empty() {
unsupported_exprs.push(aggr_expr.to_string());
continue;
}
let Some((_, input_field_name)) = aggregate_outputs.first() else {
continue;
};
merge_columns.push(IncrementalAggregateMergeColumn::new(
output_field_name,
merge_op,
));
let input_field_name = input_field_name.clone();
for (_, output_field_name) in aggregate_outputs {
merge_columns.push(IncrementalAggregateMergeColumn {
input_field_name: input_field_name.clone(),
output_field_name,
merge_op: merge_op.clone(),
});
}
}
if projection_info.has_top_level_projection {
let output_positions = projection_info
.output_field_names
.iter()
.enumerate()
.map(|(position, name)| (name.as_str(), position))
.collect::<HashMap<_, _>>();
merge_columns.sort_by_key(|column| {
output_positions
.get(column.output_field_name.as_str())
.copied()
.unwrap_or(usize::MAX)
});
}
unsupported_exprs.extend(
find_uncovered_output_fields(&projection_info, &group_key_names, &merge_columns)
@@ -591,6 +615,7 @@ pub fn analyze_incremental_aggregate_plan(
pub async fn rewrite_incremental_aggregate_with_sink_merge(
delta_plan: &LogicalPlan,
analysis: &IncrementalAggregateAnalysis,
engine: &QueryEngineRef,
sink_table: TableRef,
sink_table_name: &TableName,
sink_dirty_filter: Option<Expr>,
@@ -625,6 +650,12 @@ pub async fn rewrite_incremental_aggregate_with_sink_merge(
let delta_alias = "__flow_delta";
let sink_alias = "__flow_sink";
let state_merge = analysis.merge_columns.iter().any(|column| {
matches!(
&column.merge_op,
IncrementalAggregateMergeOp::StateDeltaMerge { .. }
)
});
let mut selected_columns = analysis.group_key_names.clone();
selected_columns.extend(
analysis
@@ -632,8 +663,18 @@ pub async fn rewrite_incremental_aggregate_with_sink_merge(
.iter()
.map(|c| c.output_field_name.clone()),
);
let mut delta_selected_columns = selected_columns.clone();
let mut selected_column_names = HashSet::new();
selected_columns.retain(|name| selected_column_names.insert(name.clone()));
let mut delta_selected_columns = analysis.group_key_names.clone();
delta_selected_columns.extend(
analysis
.merge_columns
.iter()
.map(|c| c.input_field_name.clone()),
);
delta_selected_columns.extend(analysis.literal_columns.iter().cloned());
let mut delta_selected_column_names = HashSet::new();
delta_selected_columns.retain(|name| delta_selected_column_names.insert(name.clone()));
let delta_selected_exprs = delta_selected_columns
.iter()
@@ -745,20 +786,25 @@ pub async fn rewrite_incremental_aggregate_with_sink_merge(
.map(|c| (&c.output_field_name, c))
.collect::<HashMap<_, _>>();
let mut projection_exprs = Vec::with_capacity(analysis.output_field_names.len());
let mut ordinary_exprs = Vec::with_capacity(analysis.output_field_names.len());
let mut state_aggr_exprs = Vec::new();
for output_field_name in &analysis.output_field_names {
if group_key_names.contains(output_field_name)
|| literal_columns.contains(output_field_name)
{
projection_exprs.push(
qualified_col(delta_alias, output_field_name.clone()).alias(output_field_name),
);
let expr =
qualified_col(delta_alias, output_field_name.clone()).alias(output_field_name);
ordinary_exprs.push(expr);
} else if let Some(merge_col) = merge_columns.get(output_field_name) {
projection_exprs.push(build_left_join_merge_expr(
delta_alias,
sink_alias,
merge_col,
)?);
if matches!(
&merge_col.merge_op,
IncrementalAggregateMergeOp::StateDeltaMerge { .. }
) {
state_aggr_exprs.push(build_state_delta_merge_expr(engine, merge_col)?);
} else {
let expr = build_left_join_merge_expr(delta_alias, sink_alias, merge_col)?;
ordinary_exprs.push(expr);
}
} else {
return InvalidQuerySnafu {
reason: format!(
@@ -769,15 +815,88 @@ pub async fn rewrite_incremental_aggregate_with_sink_merge(
}
}
LogicalPlanBuilder::from(joined)
.project(projection_exprs)
.with_context(|_| DatafusionSnafu {
context: "Failed to build projection merge plan for incremental sink merge".to_string(),
})?
.build()
.with_context(|_| DatafusionSnafu {
context: "Failed to finalize incremental aggregate sink merge plan".to_string(),
if state_merge {
let aggregated = LogicalPlanBuilder::from(joined)
.aggregate(ordinary_exprs, state_aggr_exprs)
.with_context(|_| DatafusionSnafu {
context: "Failed to aggregate state delta merge plan".to_string(),
})?
.build()
.with_context(|_| DatafusionSnafu {
context: "Failed to build state delta merge plan".to_string(),
})?;
let output_exprs = analysis
.output_field_names
.iter()
.cloned()
.map(unqualified_col)
.collect::<Vec<_>>();
LogicalPlanBuilder::from(aggregated)
.project(output_exprs)
.with_context(|_| DatafusionSnafu {
context: "Failed to project state delta merge plan".to_string(),
})?
.build()
.with_context(|_| DatafusionSnafu {
context: "Failed to finalize incremental aggregate sink merge plan".to_string(),
})
} else {
LogicalPlanBuilder::from(joined)
.project(ordinary_exprs)
.with_context(|_| DatafusionSnafu {
context: "Failed to build projection merge plan for incremental sink merge"
.to_string(),
})?
.build()
.with_context(|_| DatafusionSnafu {
context: "Failed to finalize incremental aggregate sink merge plan".to_string(),
})
}
}
fn build_state_delta_merge_expr(
engine: &QueryEngineRef,
merge_col: &IncrementalAggregateMergeColumn,
) -> Result<Expr, Error> {
let (function_name, params) = match &merge_col.merge_op {
IncrementalAggregateMergeOp::StateDeltaMerge {
function_name,
params,
} => (*function_name, params.clone()),
_ => {
return InvalidQuerySnafu {
reason: "non-state aggregate passed to state delta merge".to_string(),
}
.fail();
}
};
let Some(udaf) = engine
.engine_state()
.aggr_function(function_name)
.or_else(|| {
engine
.engine_state()
.session_state()
.aggregate_functions()
.get(function_name)
.map(|udaf| udaf.as_ref().clone())
})
else {
return InvalidQuerySnafu {
reason: format!("Aggregate function {function_name} is not registered"),
}
.fail();
};
let mut args = params;
args.push(qualified_col(
"__flow_delta",
merge_col.input_field_name.clone(),
));
args.push(qualified_col(
"__flow_sink",
merge_col.output_field_name.clone(),
));
Ok(udaf.call(args).alias(merge_col.output_field_name.clone()))
}
fn build_left_join_merge_expr(
@@ -785,9 +904,9 @@ fn build_left_join_merge_expr(
sink_alias: &str,
merge_col: &IncrementalAggregateMergeColumn,
) -> Result<Expr, Error> {
let left = qualified_col(delta_alias, merge_col.output_field_name.clone());
let left = qualified_col(delta_alias, merge_col.input_field_name.clone());
let right = qualified_col(sink_alias, merge_col.output_field_name.clone());
let merged = match merge_col.merge_op {
let merged = match merge_col.merge_op.clone() {
IncrementalAggregateMergeOp::Sum => when(is_null(left.clone()), right.clone())
.when(is_null(right.clone()), left.clone())
.otherwise(binary_expr(left.clone(), Operator::Plus, right.clone()))
@@ -836,6 +955,12 @@ fn build_left_join_merge_expr(
.with_context(|_| DatafusionSnafu {
context: "Failed to build BIT_XOR merge expression".to_string(),
})?,
IncrementalAggregateMergeOp::StateDeltaMerge { .. } => {
return InvalidQuerySnafu {
reason: "state aggregate must be built with its delta UDAF".to_string(),
}
.fail();
}
};
Ok(merged.alias(merge_col.output_field_name.clone()))
}
+363 -18
View File
@@ -15,10 +15,14 @@
use std::sync::Arc;
use catalog::RegisterTableRequest;
use common_recordbatch::RecordBatch;
use common_query::OutputData;
use common_recordbatch::recordbatch::merge_record_batches;
use common_recordbatch::{RecordBatch, util};
use common_time::Timestamp;
use datafusion_common::tree_node::TreeNode as _;
use datafusion_expr::{GroupingSet, TableScanBuilder};
use datatypes::arrow::array::{Array, AsArray};
use datatypes::arrow::datatypes::{Float64Type, Int64Type, UInt64Type};
use datatypes::prelude::{ConcreteDataType, MutableVector, Scalar, ScalarVectorBuilder, VectorRef};
use datatypes::schema::{ColumnSchema, Schema};
use datatypes::timestamp::TimestampMillisecond;
@@ -1104,13 +1108,16 @@ async fn test_rewrite_incremental_aggregate_allows_alias_wrapped_scan() {
let query_engine = create_test_query_engine();
let ctx = QueryContext::arc();
let sql = "SELECT max(n.number) AS number, n.ts FROM numbers_with_ts AS n GROUP BY n.ts";
let plan = sql_to_df_plan(ctx, query_engine, sql, false).await.unwrap();
let plan = sql_to_df_plan(ctx, query_engine.clone(), sql, false)
.await
.unwrap();
let analysis = analyze_incremental_aggregate_plan(&plan).unwrap().unwrap();
assert!(analysis.unsupported_exprs.is_empty());
let rewritten = rewrite_incremental_aggregate_with_sink_merge(
&plan,
&analysis,
&query_engine,
single_row_u32_table("alias_wrapped_sink", vec!["ts", "number"]),
&[
"greptime".to_string(),
@@ -1362,6 +1369,7 @@ async fn test_analyze_incremental_aggregate_plan_allows_literal_outputs() {
let rewritten = rewrite_incremental_aggregate_with_sink_merge(
&plan,
&analysis,
&query_engine,
sink_table.clone(),
&sink_table_name,
None,
@@ -1435,7 +1443,9 @@ async fn test_rewrite_incremental_aggregate_preserves_non_identifier_aliases() {
let query_engine = create_test_query_engine();
let ctx = QueryContext::arc();
let sql = "SELECT max(number) AS \"max value\", number, 42 AS \"literal value\" FROM numbers_with_ts GROUP BY number";
let plan = sql_to_df_plan(ctx, query_engine, sql, false).await.unwrap();
let plan = sql_to_df_plan(ctx, query_engine.clone(), sql, false)
.await
.unwrap();
let analysis = analyze_incremental_aggregate_plan(&plan).unwrap().unwrap();
assert!(analysis.unsupported_exprs.is_empty());
assert_eq!(
@@ -1447,6 +1457,7 @@ async fn test_rewrite_incremental_aggregate_preserves_non_identifier_aliases() {
let rewritten = rewrite_incremental_aggregate_with_sink_merge(
&plan,
&analysis,
&query_engine,
sink_table,
&[
"greptime".to_string(),
@@ -1520,24 +1531,34 @@ async fn test_datafusion_rejects_duplicate_output_names() {
}
#[tokio::test]
async fn test_analyze_incremental_aggregate_plan_rejects_same_aggregate_multiple_aliases() {
let query_engine = create_test_query_engine();
let ctx = QueryContext::arc();
let sql = "SELECT sum(number) AS a, sum(number) AS b, ts FROM numbers_with_ts GROUP BY ts";
let plan = sql_to_df_plan(ctx, query_engine, sql, false).await.unwrap();
async fn test_analyze_incremental_aggregate_plan_supports_same_aggregate_multiple_aliases() {
let analysis = analyze_test_sql(
"SELECT sum(number) AS a, sum(number) AS b, ts FROM numbers_with_ts GROUP BY ts",
)
.await;
let analysis = analyze_incremental_aggregate_plan(&plan).unwrap().unwrap();
assert!(analysis.unsupported_exprs.is_empty());
assert_eq!(analysis.merge_columns.len(), 2);
assert_eq!(analysis.merge_columns[0].input_field_name, "a");
assert_eq!(analysis.merge_columns[1].input_field_name, "a");
assert!(
analysis
.unsupported_exprs
.merge_columns
.iter()
.any(|expr| expr.contains("same aggregate output")
&& expr.contains("a")
&& expr.contains("b")),
"same aggregate with multiple aliases should be unsupported until explicit reproduction is implemented: {:?}",
analysis.unsupported_exprs
.all(|column| { column.merge_op == IncrementalAggregateMergeOp::Sum })
);
assert!(
analysis
.merge_columns
.iter()
.any(|column| column.output_field_name == "a")
);
assert!(
analysis
.merge_columns
.iter()
.any(|column| column.output_field_name == "b")
);
assert!(analysis.merge_columns.is_empty());
}
#[test]
@@ -1581,6 +1602,322 @@ async fn test_analyze_incremental_aggregate_plan_rejects_avg() {
assert!(!analysis.unsupported_exprs.is_empty());
}
#[tokio::test]
async fn test_rewrite_incremental_aggregate_merges_populated_mixed_state_families() {
let query_engine = create_test_query_engine();
let old_sql = "SELECT hll(CAST(number AS VARCHAR)) AS hll_a, hll(CAST(number AS VARCHAR)) AS hll_b, uddsketch_state(128, 0.000001, number) AS percentile_a, uddsketch_state(256, 0.02, number) AS percentile_b, stddev_pop_state(number) AS stddev_state, sum(number) AS total, CASE WHEN number <= 5 THEN CAST(NULL AS BIGINT) ELSE 2 END AS grp FROM numbers_with_ts WHERE number <= 3 OR number = 6 GROUP BY grp";
let new_sql = "SELECT hll(CAST(number AS VARCHAR)) AS hll_a, hll(CAST(number AS VARCHAR)) AS hll_b, uddsketch_state(128, 0.000001, number) AS percentile_a, uddsketch_state(256, 0.02, number) AS percentile_b, stddev_pop_state(number) AS stddev_state, sum(number) AS total, CASE WHEN number <= 5 THEN CAST(NULL AS BIGINT) WHEN number <= 8 THEN 2 ELSE 3 END AS grp FROM numbers_with_ts WHERE number >= 4 AND number != 6 GROUP BY grp";
let old_plan = sql_to_df_plan(QueryContext::arc(), query_engine.clone(), old_sql, false)
.await
.unwrap();
let old_output = query_engine
.execute(old_plan, QueryContext::arc())
.await
.unwrap();
let OutputData::Stream(old_stream) = old_output.data else {
panic!("expected old aggregate execution to be a stream");
};
let old_batches = util::collect(old_stream).await.unwrap();
let old_schema = old_batches.first().unwrap().schema.clone();
let old_batch = merge_record_batches(old_schema, &old_batches).unwrap();
assert_eq!(
old_batch.num_rows(),
2,
"old state must have one row per group"
);
let empty_state_sink_batch = old_batch.clone();
let sink_table = MemTable::table("state_merge_sink", old_batch);
let sink_table_name = [
"greptime".to_string(),
"public".to_string(),
"state_merge_sink".to_string(),
];
let new_plan = sql_to_df_plan(QueryContext::arc(), query_engine.clone(), new_sql, false)
.await
.unwrap();
let analysis = analyze_incremental_aggregate_plan(&new_plan)
.unwrap()
.unwrap();
assert!(analysis.unsupported_exprs.is_empty());
let rewritten = rewrite_incremental_aggregate_with_sink_merge(
&new_plan,
&analysis,
&query_engine,
sink_table,
&sink_table_name,
None,
)
.await
.unwrap();
assert_eq!(
analysis.output_field_names,
vec![
"hll_a",
"hll_b",
"percentile_a",
"percentile_b",
"stddev_state",
"total",
"grp",
],
"repeated HLL aliases must preserve output order"
);
let output = query_engine
.execute(rewritten, QueryContext::arc())
.await
.unwrap();
let OutputData::Stream(stream) = output.data else {
panic!("expected rewritten plan to execute as a stream");
};
let batches = util::collect(stream).await.unwrap();
let merged_schema = batches.first().unwrap().schema.clone();
let merged_batch = merge_record_batches(merged_schema, &batches).unwrap();
assert_eq!(
merged_batch.num_rows(),
3,
"rewrite must produce one row per group"
);
let merged_table = MemTable::table("merged_states", merged_batch);
query_engine
.engine_state()
.catalog_manager()
.as_any()
.downcast_ref::<catalog::memory::MemoryCatalogManager>()
.unwrap()
.register_table_sync(RegisterTableRequest {
catalog: "greptime".to_string(),
schema: "public".to_string(),
table_name: "merged_states".to_string(),
table_id: 4097,
table: merged_table,
})
.unwrap();
let checks = "SELECT grp, sum(total) AS total, hll_count(hll_merge(hll_a)) AS hll_a, hll_count(hll_merge(hll_b)) AS hll_b, stddev_pop_calc(stddev_pop_merge(stddev_state)) AS stddev, uddsketch_calc(0.5, uddsketch_merge(128, 0.000001, percentile_a)) AS p50_a, uddsketch_calc(0.5, uddsketch_merge(256, 0.02, percentile_b)) AS p50_b FROM merged_states GROUP BY grp ORDER BY grp NULLS FIRST";
let checks_plan = sql_to_df_plan(QueryContext::arc(), query_engine.clone(), checks, false)
.await
.unwrap();
let checks_output = query_engine
.execute(checks_plan, QueryContext::arc())
.await
.unwrap();
let OutputData::Stream(checks_stream) = checks_output.data else {
panic!("expected state check execution to be a stream");
};
let checks_batches = util::collect(checks_stream).await.unwrap();
let checks_schema = checks_batches.first().unwrap().schema.clone();
let checks = merge_record_batches(checks_schema, &checks_batches).unwrap();
assert_eq!(checks.num_rows(), 3);
assert_eq!(
checks
.schema
.column_schemas()
.iter()
.map(|column| column.name.as_str())
.collect::<Vec<_>>(),
vec!["grp", "total", "hll_a", "hll_b", "stddev", "p50_a", "p50_b"]
);
let group = checks.column(0).as_primitive::<Int64Type>();
let total = checks.column(1).as_primitive::<UInt64Type>();
let hll_a = checks.column(2).as_primitive::<UInt64Type>();
let hll_b = checks.column(3).as_primitive::<UInt64Type>();
let stddev = checks.column(4).as_primitive::<Float64Type>();
let p50_a = checks.column(5).as_primitive::<Float64Type>();
let p50_b = checks.column(6).as_primitive::<Float64Type>();
assert_eq!(group.null_count(), 1);
assert_eq!(total.null_count(), 0);
assert_eq!(hll_a.null_count(), 0);
assert_eq!(hll_b.null_count(), 0);
assert_eq!(stddev.null_count(), 0);
assert_eq!(p50_a.null_count(), 0);
assert_eq!(p50_b.null_count(), 0);
for (index, expected) in [
(None, 15_u64, 5_u64, 2_f64.sqrt(), 3_f64, 3_f64),
(
Some(2_i64),
21_u64,
3_u64,
(2_f64 / 3.0).sqrt(),
7_f64,
7_f64,
),
(Some(3_i64), 19_u64, 2_u64, 0.5_f64, 10_f64, 10_f64),
]
.into_iter()
.enumerate()
{
assert_eq!(
(!group.is_null(index)).then(|| group.value(index)),
expected.0
);
assert_eq!(total.value(index), expected.1);
assert_eq!(hll_a.value(index), expected.2);
assert_eq!(hll_b.value(index), expected.2);
assert!((stddev.value(index) - expected.3).abs() < 1e-12);
// UDDSketch's relative-error bounds are 1e-6 and 2e-2 respectively.
assert!((p50_a.value(index) - expected.4).abs() <= expected.4 * 0.000001);
assert!((p50_b.value(index) - expected.5).abs() <= expected.5 * 0.02);
}
let empty_state_plan = sql_to_df_plan(
QueryContext::arc(),
query_engine.clone(),
"SELECT hll(CAST(NULL AS VARCHAR)) AS hll_a, hll(CAST(NULL AS VARCHAR)) AS hll_b, uddsketch_state(128, 0.000001, CAST(NULL AS DOUBLE)) AS percentile_a, uddsketch_state(256, 0.02, CAST(NULL AS DOUBLE)) AS percentile_b, stddev_pop_state(CAST(NULL AS DOUBLE)) AS stddev_state, sum(number) AS total, CASE WHEN number <= 5 THEN CAST(NULL AS BIGINT) WHEN number <= 8 THEN 2 ELSE 3 END AS grp FROM numbers_with_ts WHERE number = 10 GROUP BY grp",
false,
)
.await
.unwrap();
let empty_state_analysis = analyze_incremental_aggregate_plan(&empty_state_plan)
.unwrap()
.unwrap();
assert!(empty_state_analysis.unsupported_exprs.is_empty());
let empty_state_rewritten = rewrite_incremental_aggregate_with_sink_merge(
&empty_state_plan,
&empty_state_analysis,
&query_engine,
MemTable::table("empty_state_sink", empty_state_sink_batch),
&[
"greptime".to_string(),
"public".to_string(),
"empty_state_sink".to_string(),
],
None,
)
.await
.unwrap();
let empty_state_output = query_engine
.execute(empty_state_rewritten, QueryContext::arc())
.await
.unwrap();
let OutputData::Stream(empty_state_stream) = empty_state_output.data else {
panic!("expected empty state merge to execute as a stream");
};
let empty_state_batches = util::collect(empty_state_stream).await.unwrap();
let empty_state_schema = empty_state_batches.first().unwrap().schema.clone();
let empty_state_batch = merge_record_batches(empty_state_schema, &empty_state_batches).unwrap();
assert_eq!(empty_state_batch.num_rows(), 1);
let group_index = empty_state_batch
.schema
.column_index_by_name("grp")
.unwrap();
let total_index = empty_state_batch
.schema
.column_index_by_name("total")
.unwrap();
let group = empty_state_batch
.column(group_index)
.as_primitive::<Int64Type>();
let total = empty_state_batch
.column(total_index)
.as_primitive::<UInt64Type>();
assert!(!group.is_null(0));
assert_eq!(group.value(0), 3);
assert!(!total.is_null(0));
assert_eq!(total.value(0), 10);
let empty_state_table = MemTable::table("empty_states", empty_state_batch);
query_engine
.engine_state()
.catalog_manager()
.as_any()
.downcast_ref::<catalog::memory::MemoryCatalogManager>()
.unwrap()
.register_table_sync(RegisterTableRequest {
catalog: "greptime".to_string(),
schema: "public".to_string(),
table_name: "empty_states".to_string(),
table_id: 4098,
table: empty_state_table,
})
.unwrap();
let empty_checks_plan = sql_to_df_plan(
QueryContext::arc(),
query_engine.clone(),
"SELECT hll_count(hll_merge(hll_a)) AS hll, stddev_pop_calc(stddev_pop_merge(stddev_state)) AS stddev, uddsketch_calc(0.5, uddsketch_merge(128, 0.000001, percentile_a)) AS p50_a, uddsketch_calc(0.5, uddsketch_merge(256, 0.02, percentile_b)) AS p50_b FROM empty_states",
false,
)
.await
.unwrap();
let empty_checks_output = query_engine
.execute(empty_checks_plan, QueryContext::arc())
.await
.unwrap();
let OutputData::Stream(empty_checks_stream) = empty_checks_output.data else {
panic!("expected empty state check to execute as a stream");
};
let empty_checks_batches = util::collect(empty_checks_stream).await.unwrap();
let empty_checks_schema = empty_checks_batches.first().unwrap().schema.clone();
let empty_checks = merge_record_batches(empty_checks_schema, &empty_checks_batches).unwrap();
assert_eq!(empty_checks.num_rows(), 1);
assert_eq!(
empty_checks.column(0).as_primitive::<UInt64Type>().value(0),
0
);
assert!(
empty_checks
.column(1)
.as_primitive::<Float64Type>()
.is_null(0)
);
assert!(
empty_checks
.column(2)
.as_primitive::<Float64Type>()
.is_null(0)
);
assert!(
empty_checks
.column(3)
.as_primitive::<Float64Type>()
.is_null(0)
);
}
#[tokio::test]
async fn test_analyze_incremental_aggregate_plan_state_producer_metadata_and_rejections() {
let analysis = analyze_test_sql(
"SELECT hll(CAST(number AS VARCHAR)) AS hll_state, \
stddev_pop_state(number) AS stddev_state, \
uddsketch_state(128, 0.000001, number) AS percentile_state, ts \
FROM numbers_with_ts GROUP BY ts",
)
.await;
assert!(analysis.unsupported_exprs.is_empty());
let percentile = analysis
.merge_columns
.iter()
.find(|column| column.output_field_name == "percentile_state")
.unwrap();
let IncrementalAggregateMergeOp::StateDeltaMerge {
function_name,
params,
} = &percentile.merge_op
else {
panic!("expected UDDSketch state delta merge");
};
assert_eq!(*function_name, "__uddsketch_state_delta_merge");
assert_eq!(params.len(), 2);
assert!(matches!(
params[0],
Expr::Literal(ScalarValue::Int64(Some(128)), _)
));
assert!(matches!(
params[1],
Expr::Literal(ScalarValue::Float64(Some(rate)), _) if rate == 0.000001
));
for sql in [
"SELECT uddsketch_state(CAST(number AS BIGINT), 0.01, number) AS state, ts FROM numbers_with_ts GROUP BY ts",
"SELECT uddsketch_state(128, NULL, number) AS state, ts FROM numbers_with_ts GROUP BY ts",
"SELECT uddsketch_state(NULL, 0.01, number) AS state, ts FROM numbers_with_ts GROUP BY ts",
"SELECT avg(number) AS state, ts FROM numbers_with_ts GROUP BY ts",
"SELECT stddev_pop(number) AS state, ts FROM numbers_with_ts GROUP BY ts",
] {
let analysis = analyze_test_sql(sql).await;
assert!(!analysis.unsupported_exprs.is_empty(), "must reject {sql}");
}
}
#[tokio::test]
async fn test_analyze_incremental_aggregate_plan_rejects_distinct() {
let query_engine = create_test_query_engine();
@@ -1638,6 +1975,7 @@ async fn test_rewrite_incremental_aggregate_with_left_join() {
let rewritten = rewrite_incremental_aggregate_with_sink_merge(
&plan,
&analysis,
&query_engine,
sink_table.clone(),
&sink_table_name,
None,
@@ -1699,6 +2037,7 @@ async fn test_rewrite_incremental_aggregate_filters_sink_dirty_time_window() {
let rewritten = rewrite_incremental_aggregate_with_sink_merge(
&plan,
&analysis,
&query_engine,
sink_table.clone(),
&sink_table_name,
Some(sink_filter.clone()),
@@ -1749,7 +2088,9 @@ async fn test_rewrite_incremental_aggregate_rejects_empty_group_keys() {
let query_engine = create_test_query_engine();
let ctx = QueryContext::arc();
let sql = "SELECT max(number) AS number FROM numbers_with_ts";
let plan = sql_to_df_plan(ctx, query_engine, sql, false).await.unwrap();
let plan = sql_to_df_plan(ctx, query_engine.clone(), sql, false)
.await
.unwrap();
let analysis = IncrementalAggregateAnalysis {
group_key_names: vec![],
merge_columns: vec![IncrementalAggregateMergeColumn::new(
@@ -1770,6 +2111,7 @@ async fn test_rewrite_incremental_aggregate_rejects_empty_group_keys() {
let err = rewrite_incremental_aggregate_with_sink_merge(
&plan,
&analysis,
&query_engine,
sink_table,
&sink_table_name,
None,
@@ -1788,7 +2130,9 @@ async fn test_rewrite_incremental_aggregate_preserves_raw_aggregate_field_name()
let query_engine = create_test_query_engine();
let ctx = QueryContext::arc();
let sql = "SELECT max(number), number FROM numbers_with_ts GROUP BY number";
let plan = sql_to_df_plan(ctx, query_engine, sql, false).await.unwrap();
let plan = sql_to_df_plan(ctx, query_engine.clone(), sql, false)
.await
.unwrap();
let analysis = analyze_incremental_aggregate_plan(&plan).unwrap().unwrap();
assert!(analysis.unsupported_exprs.is_empty());
@@ -1802,6 +2146,7 @@ async fn test_rewrite_incremental_aggregate_preserves_raw_aggregate_field_name()
let rewritten = rewrite_incremental_aggregate_with_sink_merge(
&plan,
&analysis,
&query_engine,
sink_table.clone(),
&sink_table_name,
None,
+1
View File
@@ -29,6 +29,7 @@ cache.workspace = true
catalog.workspace = true
client.workspace = true
common-base.workspace = true
common-batcher.workspace = true
common-catalog.workspace = true
common-config.workspace = true
common-datasource.workspace = true
+17 -1
View File
@@ -567,7 +567,23 @@ mod tests {
// returns `None`; in these cases no request can wait for a pending-row
// flush, so the timeout must not be raised.
type KnobMutator = fn(&mut FrontendOptions);
let cases: [(&str, KnobMutator); 4] = [
let cases: [(&str, KnobMutator); 9] = [
("oversized flush concurrency", |opts| {
opts.prom_store.max_concurrent_flushes = usize::MAX
}),
("oversized worker channel", |opts| {
opts.prom_store.worker_channel_capacity = usize::MAX
}),
("oversized inflight limit", |opts| {
opts.prom_store.max_inflight_requests = usize::MAX
}),
("oversized flow queue", |opts| {
opts.prom_store.flow_notification_queue_capacity =
std::num::NonZeroUsize::new(usize::MAX).unwrap()
}),
("unrepresentable deadline", |opts| {
opts.prom_store.pending_rows_flush_interval = Duration::MAX
}),
("zero max_batch_rows", |opts| {
opts.prom_store.max_batch_rows = 0
}),
+48 -8
View File
@@ -15,8 +15,10 @@
use std::num::NonZeroUsize;
use std::time::Duration;
use common_batcher::flush_policy::timing::TimingFlushPolicy;
use serde::{Deserialize, Serialize};
use servers::prom_remote_write::validation::PromValidationMode;
use tokio::sync::Semaphore;
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
pub struct PromStoreOptions {
@@ -64,18 +66,20 @@ fn default_flow_notification_queue_capacity() -> NonZeroUsize {
}
impl PromStoreOptions {
/// Returns whether the pending rows batcher can be enabled with these
/// options. Mirrors the enablement conditions of
/// `PendingRowsBatcher::try_new` in the servers crate, which returns
/// `None` when any of these knobs is zero.
/// Returns whether batching is enabled and its controls pass construction validation.
pub fn pending_rows_batching_enabled(&self) -> bool {
self.enable
&& self.with_metric_engine
&& !self.pending_rows_flush_interval.is_zero()
&& TimingFlushPolicy::validate(self.pending_rows_flush_interval)
&& self.max_batch_rows > 0
&& self.max_concurrent_flushes > 0
&& self.worker_channel_capacity > 0
&& self.max_inflight_requests > 0
&& [
self.max_concurrent_flushes,
self.worker_channel_capacity,
self.max_inflight_requests,
self.flow_notification_queue_capacity.get(),
]
.into_iter()
.all(|capacity| (1..=Semaphore::MAX_PERMITS).contains(&capacity))
}
}
@@ -107,6 +111,42 @@ mod tests {
default_worker_channel_capacity,
};
#[test]
fn test_batching_capacity_boundaries() {
let max = tokio::sync::Semaphore::MAX_PERMITS;
let options = PromStoreOptions {
pending_rows_flush_interval: Duration::from_secs(5),
max_batch_rows: usize::MAX,
max_concurrent_flushes: max,
worker_channel_capacity: max,
max_inflight_requests: max,
flow_notification_queue_capacity: std::num::NonZeroUsize::new(max).unwrap(),
..Default::default()
};
assert!(options.pending_rows_batching_enabled());
let cases = [
PromStoreOptions {
max_concurrent_flushes: max + 1,
..options.clone()
},
PromStoreOptions {
worker_channel_capacity: max + 1,
..options.clone()
},
PromStoreOptions {
max_inflight_requests: max + 1,
..options.clone()
},
PromStoreOptions {
flow_notification_queue_capacity: std::num::NonZeroUsize::new(max + 1).unwrap(),
..options
},
];
for options in cases {
assert!(!options.pending_rows_batching_enabled());
}
}
#[test]
fn test_prom_store_options() {
let default = PromStoreOptions::default();
+18 -6
View File
@@ -34,6 +34,7 @@ use common_meta::cache_invalidator::CacheInvalidatorRef;
use common_meta::ddl::DdlContext;
use common_meta::ddl::allocator::region_routes::RegionRoutesAllocatorRef;
use common_meta::ddl::allocator::wal_options::WalOptionsAllocatorRef;
use common_meta::ddl::utils::get_region_wal_options;
use common_meta::ddl_manager::{RepartitionProcedureFactory, RepartitionSource};
use common_meta::instruction::CacheIdent;
use common_meta::key::datanode_table::RegionInfo;
@@ -402,16 +403,27 @@ impl Context {
let datanode_table_value =
get_datanode_table_value(&self.table_metadata_manager, table_id, datanode_id).await?;
let RegionInfo {
region_options,
region_wal_options,
..
} = &datanode_table_value.region_info;
let RegionInfo { region_options, .. } = &datanode_table_value.region_info;
let mut region_wal_options = get_region_wal_options(
&self.table_metadata_manager,
current_table_route_value,
table_id,
)
.await
.context(error::TableMetadataManagerSnafu)?;
// Legacy regions without a persisted WAL option use RaftEngine. Only
// existing routes get this default; allocated regions must supply one.
for route in current_table_route_value.region_routes().unwrap() {
region_wal_options
.entry(route.region.id.region_number())
.or_default();
}
// Merge and validate the new region wal options.
let validated_region_wal_options =
crate::procedure::repartition::utils::merge_and_validate_region_wal_options(
region_wal_options,
&region_wal_options,
new_region_wal_options,
&new_region_routes,
table_id,
@@ -19,6 +19,7 @@ use common_meta::ddl::create_table::executor::CreateTableExecutor;
use common_meta::ddl::create_table::template::{
CreateRequestBuilder, build_template_from_raw_table_info_for_physical_table,
};
use common_meta::ddl::utils::get_region_wal_options;
use common_meta::lock_key::TableLock;
use common_meta::node_manager::NodeManagerRef;
use common_meta::peer::PeerAllocContext;
@@ -28,6 +29,7 @@ use common_meta::wal_provider::{
};
use common_procedure::{Context as ProcedureContext, Status};
use common_telemetry::{debug, info};
use common_wal::options::WalOptions;
use serde::{Deserialize, Deserializer, Serialize};
use snafu::{OptionExt, ResultExt};
use store_api::region_request::RegionRequirements;
@@ -165,6 +167,29 @@ impl ExecutePlan {
)
.await
.context(error::AllocateRegionRoutesSnafu { table_id })?;
let skip_wal = table_info_value.table_info.meta.options.skip_wal;
let allocate_noop = if skip_wal {
let table_route_value = ctx.get_table_route_value().await?;
let region_wal_options =
get_region_wal_options(&ctx.table_metadata_manager, &table_route_value, table_id)
.await
.context(error::AllocateWalOptionsSnafu { table_id })?;
// A table disabled after creation keeps its real providers. A table
// created with skip_wal has Noop providers and must keep them.
!table_route_value
.region_routes()
.unwrap()
.iter()
.all(|route| {
region_wal_options
.get(&route.region.id.region_number())
.is_none_or(|option| {
matches!(option, WalOptions::RaftEngine | WalOptions::Kafka(_))
})
})
} else {
false
};
let mut wal_options = ctx
.wal_options_allocator
.allocate(
@@ -172,7 +197,7 @@ impl ExecutePlan {
.iter()
.map(|r| r.region_id.region_number())
.collect::<Vec<_>>(),
table_info_value.table_info.meta.options.skip_wal,
allocate_noop,
)
.await
.context(error::AllocateWalOptionsSnafu { table_id })?;
@@ -414,14 +439,19 @@ mod tests {
use std::sync::Arc;
use api::v1::region::region_request::Body;
use common_meta::ddl::allocator::wal_options::WalOptionsAllocator;
use common_meta::ddl::test_util::datanode_handler::DatanodeWatcher;
use common_meta::key::TableMetadataManagerRef;
use common_meta::key::datanode_table::DatanodeTableKey;
use common_meta::key::table_route::TableRouteValue;
use common_meta::key::test_utils::new_test_table_info_with_name;
use common_meta::peer::Peer;
use common_meta::rpc::router::{Region, RegionRoute};
use common_meta::test_util::MockDatanodeManager;
use common_procedure::{ContextProvider, ProcedureId, ProcedureState};
use common_procedure_test::MockContextProvider;
use common_wal::options::{KafkaWalOptions, WAL_OPTIONS_KEY, WalOptions};
use store_api::mito_engine_options::SKIP_WAL_KEY;
use store_api::storage::RegionId;
use tokio::sync::{mpsc, watch};
use uuid::Uuid;
@@ -510,6 +540,29 @@ mod tests {
region_wal_options: RegionWalOptions,
}
struct TestKafkaWalOptionsAllocator;
#[async_trait::async_trait]
impl WalOptionsAllocator for TestKafkaWalOptionsAllocator {
async fn allocate(
&self,
region_numbers: &[RegionNumber],
skip_wal: bool,
) -> common_meta::error::Result<RegionWalOptions> {
Ok(region_numbers
.iter()
.map(|&region_number| {
let options = if skip_wal {
WalOptions::Noop
} else {
WalOptions::Kafka(KafkaWalOptions::new("new-topic".to_string()))
};
(region_number, options)
})
.collect())
}
}
#[async_trait::async_trait]
impl ContextProvider for ConcurrentTableRouteUpdateProvider {
async fn procedure_state(
@@ -804,6 +857,101 @@ mod tests {
);
}
async fn check_execute_plan_skip_wal_provider(existing_wal_options: Option<WalOptions>) {
let env = TestingEnv::new();
let table_id = 1024;
let routes = create_current_region_routes(table_id, &[1]);
let mut table_info = new_test_table_info_with_name(table_id, "test_table");
table_info.meta.column_ids = vec![0, 1, 2];
table_info.meta.options.skip_wal = true;
let stored_wal_options = existing_wal_options
.clone()
.map(|options| HashMap::from([(1, options)]))
.unwrap_or_default();
env.table_metadata_manager
.create_table_metadata(
table_info,
TableRouteValue::physical(routes),
stored_wal_options,
)
.await
.unwrap();
let (sender, mut receiver) = mpsc::channel(1);
let node_manager = Arc::new(MockDatanodeManager::new(DatanodeWatcher::new(sender)));
let mut ctx = new_parent_context(&env, node_manager, table_id);
if matches!(&existing_wal_options, Some(WalOptions::Kafka(_))) {
ctx.wal_options_allocator = Arc::new(TestKafkaWalOptionsAllocator);
}
ctx.persistent_ctx.plans = vec![RepartitionPlanEntry {
group_id: Uuid::new_v4(),
source_regions: vec![],
target_regions: vec![create_target_region_descriptor(table_id, 2, "x", 0, 100)],
allocated_region_ids: vec![RegionId::new(table_id, 2)],
pending_deallocate_region_ids: vec![],
transition_map: vec![],
original_target_routes: vec![],
}];
let mut state = ExecutePlan;
state
.next(&mut ctx, &TestingEnv::procedure_context())
.await
.unwrap();
let (_, request) = receiver.recv().await.unwrap();
let Some(Body::Create(create)) = request.body else {
unreachable!()
};
assert_eq!(
Some("true"),
create.options.get(SKIP_WAL_KEY).map(String::as_str)
);
let new_wal_options: WalOptions =
serde_json::from_str(create.options.get(WAL_OPTIONS_KEY).unwrap()).unwrap();
let expected = match &existing_wal_options {
Some(WalOptions::Noop) => WalOptions::Noop,
Some(WalOptions::Kafka(_)) => WalOptions::Kafka(KafkaWalOptions {
topic: "new-topic".to_string(),
initial_pruned_entry_id: Some(0),
}),
None | Some(WalOptions::RaftEngine) => WalOptions::RaftEngine,
};
assert_eq!(expected, new_wal_options);
let route = ctx.get_table_route_value().await.unwrap();
let wal_options = get_region_wal_options(&ctx.table_metadata_manager, &route, table_id)
.await
.unwrap();
assert_eq!(Some(&expected), wal_options.get(&2));
let legacy_default = WalOptions::RaftEngine;
assert_eq!(
Some(existing_wal_options.as_ref().unwrap_or(&legacy_default)),
wal_options.get(&1)
);
}
#[tokio::test]
async fn test_execute_plan_keeps_real_wal_provider_for_skipped_table() {
check_execute_plan_skip_wal_provider(Some(WalOptions::RaftEngine)).await;
}
#[tokio::test]
async fn test_execute_plan_keeps_legacy_raft_provider_for_skipped_table() {
check_execute_plan_skip_wal_provider(None).await;
}
#[tokio::test]
async fn test_execute_plan_keeps_kafka_wal_provider_for_skipped_table() {
check_execute_plan_skip_wal_provider(Some(WalOptions::Kafka(KafkaWalOptions::new(
"existing-topic".to_string(),
))))
.await;
}
#[tokio::test]
async fn test_execute_plan_keeps_noop_for_table_created_without_wal() {
check_execute_plan_skip_wal_provider(Some(WalOptions::Noop)).await;
}
#[test]
fn test_allocate_region_state_backward_compatibility() {
// Arrange
+19 -10
View File
@@ -284,6 +284,22 @@ mod test {
use crate::test_util::{TestEnv, alter_logical_region_request, create_logical_region_request};
async fn check_alter_physical_skip_wal(
engine: &super::MetricEngineInner,
region_id: RegionId,
skip_wal: bool,
) {
let request = RegionAlterRequest {
kind: AlterKind::SetRegionOptions {
options: vec![SetRegionOption::SkipWal(skip_wal)],
},
};
engine
.alter_physical_region(region_id, request)
.await
.unwrap();
}
#[tokio::test]
async fn test_alter_region() {
let env = TestEnv::new().await;
@@ -304,16 +320,9 @@ mod test {
"Alter request to physical region is forbidden".to_string()
);
// skip WAL on the physical region should be forwarded to the data region
let alter_region_option_request = RegionAlterRequest {
kind: AlterKind::SetRegionOptions {
options: vec![SetRegionOption::SkipWal],
},
};
engine_inner
.alter_physical_region(physical_region_id, alter_region_option_request)
.await
.unwrap();
// skip-WAL changes on the physical region should be forwarded to the data region
check_alter_physical_skip_wal(&engine_inner, physical_region_id, true).await;
check_alter_physical_skip_wal(&engine_inner, physical_region_id, false).await;
// alter logical region
let metadata_region = env.metadata_region();
+194 -29
View File
@@ -16,7 +16,10 @@ use std::sync::Arc;
use std::time::Duration;
use api::v1::Rows;
use common_error::ext::ErrorExt;
use common_wal::options::{WAL_OPTIONS_KEY, WalOptions};
use rstest::rstest;
use rstest_reuse::{self, apply};
use store_api::logstore::provider::Provider;
use store_api::mito_engine_options::SKIP_WAL_KEY;
use store_api::region_engine::{RegionEngine, RegionRole};
@@ -29,9 +32,19 @@ use store_api::storage::{RegionId, ScanRequest};
use crate::config::MitoConfig;
use crate::engine::listener::AlterFlushListener;
use crate::test_util::{
CreateRequestBuilder, TestEnv, build_rows, flush_region, put_rows, rows_schema,
CreateRequestBuilder, LogStoreFactory, TestEnv, build_rows, flush_region,
kafka_log_store_factory, multiple_log_store_factories, prepare_test_for_kafka_log_store,
put_rows, raft_engine_log_store_factory, rows_schema,
};
fn set_skip_wal_request(skip_wal: bool) -> RegionRequest {
RegionRequest::Alter(RegionAlterRequest {
kind: AlterKind::SetRegionOptions {
options: vec![SetRegionOption::SkipWal(skip_wal)],
},
})
}
#[tokio::test]
async fn test_close_region_skip_wal_with_pending_data() {
test_close_region_skip_wal(true).await;
@@ -76,25 +89,11 @@ async fn test_alter_skip_wal_stops_wal_and_flushes_on_close() {
assert!(!before_alter.version.memtables.is_empty());
engine
.handle_request(
region_id,
RegionRequest::Alter(RegionAlterRequest {
kind: AlterKind::SetRegionOptions {
options: vec![SetRegionOption::SkipWal],
},
}),
)
.handle_request(region_id, set_skip_wal_request(true))
.await
.unwrap();
engine
.handle_request(
region_id,
RegionRequest::Alter(RegionAlterRequest {
kind: AlterKind::SetRegionOptions {
options: vec![SetRegionOption::SkipWal],
},
}),
)
.handle_request(region_id, set_skip_wal_request(true))
.await
.unwrap();
@@ -170,6 +169,176 @@ async fn test_alter_skip_wal_stops_wal_and_flushes_on_close() {
);
}
#[apply(multiple_log_store_factories)]
async fn test_alter_skip_wal_round_trip_reuses_provider(factory: Option<LogStoreFactory>) {
let Some(factory) = factory else {
return;
};
let mut env = TestEnv::with_prefix("alter-skip-wal-round-trip")
.await
.with_log_store_factory(factory.clone());
let engine = env.create_engine(MitoConfig::default()).await;
let region_id = RegionId::new(1, 1);
let topic = prepare_test_for_kafka_log_store(&factory).await;
let request = CreateRequestBuilder::new().kafka_topic(topic).build();
let schema = rows_schema(&request);
engine
.handle_request(region_id, RegionRequest::Create(request))
.await
.unwrap();
let region = engine.get_region(region_id).unwrap();
let provider = region.provider.clone();
assert!(!matches!(&provider, Provider::Noop));
put_rows(
&engine,
region_id,
Rows {
schema: schema.clone(),
rows: build_rows(0, 1),
},
)
.await;
let last_entry_id = region.version_control.current().last_entry_id;
engine
.handle_request(region_id, set_skip_wal_request(true))
.await
.unwrap();
put_rows(
&engine,
region_id,
Rows {
schema: schema.clone(),
rows: build_rows(1, 2),
},
)
.await;
assert_eq!(
last_entry_id,
region.version_control.current().last_entry_id
);
for _ in 0..2 {
engine
.handle_request(region_id, set_skip_wal_request(false))
.await
.unwrap();
}
assert!(!region.version().options.skip_wal);
assert_eq!(&provider, &region.provider);
put_rows(
&engine,
region_id,
Rows {
schema,
rows: build_rows(2, 3),
},
)
.await;
assert!(region.version_control.current().last_entry_id > last_entry_id);
}
#[apply(multiple_log_store_factories)]
async fn test_create_skipped_region_with_real_wal_can_reenable(factory: Option<LogStoreFactory>) {
let Some(factory) = factory else {
return;
};
let mut env = TestEnv::with_prefix("create-skipped-with-real-wal")
.await
.with_log_store_factory(factory.clone());
let engine = env.create_engine(MitoConfig::default()).await;
let region_id = RegionId::new(1, 1);
let topic = prepare_test_for_kafka_log_store(&factory).await;
let request = CreateRequestBuilder::new()
.kafka_topic(topic)
.insert_option(SKIP_WAL_KEY, "true")
.build();
let schema = rows_schema(&request);
engine
.handle_request(region_id, RegionRequest::Create(request))
.await
.unwrap();
let region = engine.get_region(region_id).unwrap();
let provider = region.provider.clone();
assert!(!matches!(&provider, Provider::Noop));
assert!(region.version().options.skip_wal);
let last_entry_id = region.version_control.current().last_entry_id;
put_rows(
&engine,
region_id,
Rows {
schema: schema.clone(),
rows: build_rows(0, 1),
},
)
.await;
assert_eq!(
last_entry_id,
region.version_control.current().last_entry_id
);
engine
.handle_request(region_id, set_skip_wal_request(false))
.await
.unwrap();
assert!(!region.version().options.skip_wal);
assert_eq!(&provider, &region.provider);
put_rows(
&engine,
region_id,
Rows {
schema,
rows: build_rows(1, 2),
},
)
.await;
assert!(region.version_control.current().last_entry_id > last_entry_id);
}
#[tokio::test]
async fn test_alter_skip_wal_rejects_noop_provider() {
let mut env = TestEnv::with_prefix("alter-skip-wal-noop").await;
let engine = env.create_engine(MitoConfig::default()).await;
let region_id = RegionId::new(1, 1);
let mut request = CreateRequestBuilder::new().build();
request.options.insert(
WAL_OPTIONS_KEY.to_string(),
serde_json::to_string(&WalOptions::Noop).unwrap(),
);
request
.options
.insert(SKIP_WAL_KEY.to_string(), "true".to_string());
engine
.handle_request(region_id, RegionRequest::Create(request))
.await
.unwrap();
let region = engine.get_region(region_id).unwrap();
let provider = region.provider.clone();
assert!(matches!(&provider, Provider::Noop));
engine
.handle_request(region_id, set_skip_wal_request(true))
.await
.unwrap();
assert!(region.version().options.skip_wal);
assert_eq!(&provider, &region.provider);
let error = engine
.handle_request(region_id, set_skip_wal_request(false))
.await
.unwrap_err();
assert!(error.output_msg().contains("uses the Noop WAL provider"));
assert!(region.version().options.skip_wal);
assert_eq!(&provider, &region.provider);
}
#[tokio::test]
async fn test_alter_skip_wal_on_follower_survives_promotion() {
let mut env = TestEnv::with_prefix("alter-skip-wal-follower").await;
@@ -186,14 +355,7 @@ async fn test_alter_skip_wal_on_follower_survives_promotion() {
.set_region_role(region_id, RegionRole::Follower)
.unwrap();
engine
.handle_request(
region_id,
RegionRequest::Alter(RegionAlterRequest {
kind: AlterKind::SetRegionOptions {
options: vec![SetRegionOption::SkipWal],
},
}),
)
.handle_request(region_id, set_skip_wal_request(true))
.await
.unwrap();
@@ -201,6 +363,12 @@ async fn test_alter_skip_wal_on_follower_survives_promotion() {
assert!(region.is_follower());
assert!(region.version().options.skip_wal);
engine
.handle_request(region_id, set_skip_wal_request(false))
.await
.unwrap();
assert!(!region.version().options.skip_wal);
engine
.set_region_role(region_id, RegionRole::Leader)
.unwrap();
@@ -214,10 +382,7 @@ async fn test_alter_skip_wal_on_follower_survives_promotion() {
},
)
.await;
assert_eq!(
last_entry_id,
region.version_control.current().last_entry_id
);
assert!(region.version_control.current().last_entry_id > last_entry_id);
}
async fn test_close_region_skip_wal(insert: bool) {
+56 -25
View File
@@ -23,6 +23,7 @@ use common_telemetry::{error, info};
use humantime_serde::re::humantime;
use snafu::{ResultExt, ensure};
use store_api::logstore::LogStore;
use store_api::logstore::provider::Provider;
use store_api::metadata::{
InvalidSetRegionOptionRequestSnafu, MetadataError, RegionMetadata, RegionMetadataBuilder,
RegionMetadataRef,
@@ -50,16 +51,19 @@ impl<S: LogStore> RegionWorkerLoop<S> {
request: RegionAlterRequest,
sender: OptionOutputTx,
) {
let skip_wal_only = only_enables_skip_wal(&request.kind);
let (region, is_follower) = match self.regions.writable_non_staging_region(region_id) {
Ok(region) => (region, false),
Err(_) if skip_wal_only => match self.regions.follower_region(region_id) {
Ok(region) => (region, true),
Err(e) => {
sender.send(Err(e));
return;
let requested_skip_wal = skip_wal_value(&request.kind);
let (region, follower_skip_wal) = match self.regions.writable_non_staging_region(region_id)
{
Ok(region) => (region, None),
Err(_) if requested_skip_wal.is_some() => {
match self.regions.follower_region(region_id) {
Ok(region) => (region, requested_skip_wal),
Err(e) => {
sender.send(Err(e));
return;
}
}
},
}
Err(e) => {
sender.send(Err(e));
return;
@@ -68,13 +72,20 @@ impl<S: LogStore> RegionWorkerLoop<S> {
info!("Try to alter region: {}, request: {:?}", region_id, request);
// Followers only accept skip-WAL, which is an in-memory option change and must
// Followers only accept skip-WAL changes, which are in-memory option changes and must
// never enter the leader path that may flush memtables.
if is_follower {
if let Some(skip_wal) = follower_skip_wal {
if let Err(e) = validate_skip_wal_change(&region, skip_wal) {
sender.send(Err(e).context(InvalidMetadataSnafu));
return;
}
let mut options = region.version().options.clone();
if !options.skip_wal {
info!("Stop writing WAL for follower region: {}", region_id);
options.skip_wal = true;
if options.skip_wal != skip_wal {
info!(
"Set skip_wal for follower region: {}, previous: {} new: {}",
region_id, options.skip_wal, skip_wal
);
options.skip_wal = skip_wal;
region.version_control.alter_options(options);
}
sender.send(Ok(0));
@@ -317,10 +328,14 @@ impl<S: LogStore> RegionWorkerLoop<S> {
current_options.preserve_row_sequence = new_preserve;
}
}
SetRegionOption::SkipWal => {
if !current_options.skip_wal {
info!("Stop writing WAL for region: {}", region.region_id);
current_options.skip_wal = true;
SetRegionOption::SkipWal(skip_wal) => {
validate_skip_wal_change(region, skip_wal)?;
if current_options.skip_wal != skip_wal {
info!(
"Set skip_wal for region: {}, previous: {} new: {}",
region.region_id, current_options.skip_wal, skip_wal
);
current_options.skip_wal = skip_wal;
}
}
}
@@ -352,12 +367,28 @@ impl<S: LogStore> RegionWorkerLoop<S> {
}
}
fn only_enables_skip_wal(kind: &AlterKind) -> bool {
matches!(
kind,
AlterKind::SetRegionOptions { options }
if matches!(options.as_slice(), [SetRegionOption::SkipWal])
)
fn skip_wal_value(kind: &AlterKind) -> Option<bool> {
let AlterKind::SetRegionOptions { options } = kind else {
return None;
};
let [SetRegionOption::SkipWal(skip_wal)] = options.as_slice() else {
return None;
};
Some(*skip_wal)
}
fn validate_skip_wal_change(
region: &MitoRegionRef,
skip_wal: bool,
) -> std::result::Result<(), MetadataError> {
ensure!(
skip_wal || !matches!(&region.provider, Provider::Noop),
store_api::metadata::InvalidRegionRequestSnafu {
region_id: region.region_id,
err: "cannot enable WAL because the region uses the Noop WAL provider".to_string(),
}
);
Ok(())
}
/// Returns the new region options if there are updates to the options.
@@ -382,7 +413,7 @@ fn new_region_options_on_empty_memtable(
| SetRegionOption::Ttl(_)
| SetRegionOption::Twsc(_, _)
| SetRegionOption::AutoFlushInterval(_)
| SetRegionOption::SkipWal => (),
| SetRegionOption::SkipWal(_) => (),
SetRegionOption::Format(format_str) => {
// Safety: handle_alter_region_options_fast() has validated this.
let new_format = format_str.parse::<FormatType>().unwrap();
+2 -21
View File
@@ -20,7 +20,6 @@ use api::v1::region::{
BulkInsertRequest, RegionRequest, RegionRequestHeader, bulk_insert_request, region_request,
};
use api::v1::{ArrowIpc, PartitionExprVersion};
use arrow::array::Array;
use arrow::record_batch::RecordBatch;
use bytes::Bytes;
use common_base::AffectedRows;
@@ -28,12 +27,13 @@ use common_grpc::FlightData;
use common_grpc::flight::{FlightEncoder, FlightMessage};
use common_telemetry::error;
use common_telemetry::tracing_context::TracingContext;
use snafu::{OptionExt, ResultExt, ensure};
use snafu::{ResultExt, ensure};
use store_api::storage::RegionId;
use table::TableRef;
use table::metadata::TableInfoRef;
use crate::insert::Inserter;
use crate::req_convert::insert::extract_timestamps;
use crate::{error, metrics};
impl Inserter {
@@ -319,22 +319,3 @@ impl Inserter {
});
}
}
/// Calculate the timestamp range of record batch. Return `None` if record batch is empty.
fn extract_timestamps(rb: &RecordBatch, timestamp_index_name: &str) -> error::Result<Vec<i64>> {
let ts_col = rb
.column_by_name(timestamp_index_name)
.context(error::ColumnNotFoundSnafu {
msg: timestamp_index_name,
})?;
if rb.num_rows() == 0 {
return Ok(vec![]);
}
let (primitive, _) =
datatypes::timestamp::timestamp_array_to_primitive(ts_col).with_context(|| {
error::InvalidTimeIndexTypeSnafu {
ty: ts_col.data_type().clone(),
}
})?;
Ok(primitive.iter().flatten().collect())
}
+2
View File
@@ -17,6 +17,7 @@ mod fill_impure_default;
mod row_to_region;
mod stmt_to_region;
mod table_to_region;
mod timestamps;
use api::v1::SemanticType;
pub use column_to_row::ColumnToRow;
@@ -26,6 +27,7 @@ use snafu::{OptionExt, ResultExt};
pub use stmt_to_region::StatementToRegion;
use table::metadata::TableInfo;
pub use table_to_region::TableToRegion;
pub use timestamps::extract_timestamps;
use crate::error::{ColumnNotFoundSnafu, MissingTimeIndexColumnSnafu, Result};
@@ -0,0 +1,38 @@
// 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 arrow::record_batch::RecordBatch;
use datatypes::timestamp::append_timestamps;
use snafu::OptionExt;
use crate::error::{self, Result};
/// Extracts non-null timestamps in the source column's native time unit.
pub fn extract_timestamps(rb: &RecordBatch, timestamp_index_name: &str) -> Result<Vec<i64>> {
let ts_col = rb
.column_by_name(timestamp_index_name)
.context(error::ColumnNotFoundSnafu {
msg: timestamp_index_name,
})?;
if rb.num_rows() == 0 {
return Ok(vec![]);
}
let mut timestamps = Vec::with_capacity(rb.num_rows());
append_timestamps(ts_col, &mut timestamps).with_context(|| {
error::InvalidTimeIndexTypeSnafu {
ty: ts_col.data_type().clone(),
}
})?;
Ok(timestamps)
}
+217 -2
View File
@@ -557,7 +557,9 @@ impl Stream for SeriesDivideStream {
}
error => return Poll::Ready(error),
};
self.buffer.push(batch);
if batch.num_rows() != 0 {
self.buffer.push(batch);
}
continue;
}
}
@@ -627,7 +629,8 @@ impl SeriesDivideStream {
#[cfg(test)]
mod test {
use datafusion::arrow::array::{
DictionaryArray, Int32Array, LargeStringArray, StringArray, StringViewArray, UInt32Array,
DictionaryArray, Int32Array, Int64Array, LargeStringArray, StringArray, StringViewArray,
UInt32Array,
};
use datafusion::arrow::datatypes::{DataType, Field, Int32Type, Schema, UInt32Type};
use datafusion::common::ToDFSchema;
@@ -1248,4 +1251,216 @@ mod test {
assert!(tsid_array.iter().all(|v| v == Some(expected_tsid)));
}
}
/// The input stream may legally start with a batch that has zero rows.
/// `SeriesDivideStream` must skip it and keep working on the real rows that
/// follow (here: two batches of the very same tagged series).
#[tokio::test]
async fn test_stream_leading_empty_batch_then_same_tags() {
let schema = Arc::new(Schema::new(vec![
Field::new("host", DataType::Utf8, false),
Field::new("path", DataType::Utf8, false),
Field::new(
"time_index",
DataType::Timestamp(datafusion::arrow::datatypes::TimeUnit::Millisecond, None),
false,
),
]));
let make_batch = |hosts: Vec<&str>, paths: Vec<&str>, times: Vec<i64>| {
RecordBatch::try_new(
schema.clone(),
vec![
Arc::new(StringArray::from(hosts)),
Arc::new(StringArray::from(paths)),
Arc::new(datafusion::arrow::array::TimestampMillisecondArray::from(
times,
)),
],
)
.unwrap()
};
let memory_exec: Arc<dyn ExecutionPlan> = Arc::new(DataSourceExec::new(Arc::new(
MemorySourceConfig::try_new(
&[vec![
RecordBatch::new_empty(schema.clone()),
make_batch(
vec!["server1", "server1"],
vec!["/var/log", "/var/log"],
vec![1000, 2000],
),
make_batch(vec!["server1"], vec!["/var/log"], vec![3000]),
]],
schema.clone(),
None,
)
.unwrap(),
)));
let divide_exec = Arc::new(SeriesDivideExec {
tag_columns: vec!["host".to_string(), "path".to_string()],
time_index_column: "time_index".to_string(),
input: memory_exec,
metric: ExecutionPlanMetricsSet::new(),
});
let mut stream = divide_exec
.execute(0, SessionContext::default().task_ctx())
.unwrap();
let batch = stream.next().await.unwrap().unwrap();
assert_eq!(batch.num_rows(), 3);
let expected = [
("server1", "/var/log", 1000i64),
("server1", "/var/log", 2000),
("server1", "/var/log", 3000),
];
for (row, (host, path, time)) in expected.into_iter().enumerate() {
assert_eq!(
string_array_value_at_index(batch.column(0), row),
Some(host)
);
assert_eq!(
string_array_value_at_index(batch.column(1), row),
Some(path)
);
assert_eq!(
batch
.column(2)
.as_any()
.downcast_ref::<datafusion::arrow::array::TimestampMillisecondArray>()
.unwrap()
.value(row),
time
);
}
assert!(stream.next().await.is_none());
}
/// Several leading empty batches (and one empty batch in the middle of a
/// series) must not disturb series division: the two series are still
/// emitted separately, including the last one flushed at end of input.
#[tokio::test]
async fn test_stream_repeated_empty_batches_and_two_series() {
let schema = Arc::new(Schema::new(vec![
Field::new("host", DataType::Utf8, false),
Field::new("value", DataType::Int64, false),
Field::new(
"time_index",
DataType::Timestamp(datafusion::arrow::datatypes::TimeUnit::Millisecond, None),
false,
),
]));
let make_batch = |hosts: Vec<&str>, values: Vec<i64>| {
RecordBatch::try_new(
schema.clone(),
vec![
Arc::new(StringArray::from(hosts)),
Arc::new(Int64Array::from(values.clone())),
Arc::new(datafusion::arrow::array::TimestampMillisecondArray::from(
values.iter().map(|v| v * 1000).collect::<Vec<_>>(),
)),
],
)
.unwrap()
};
let memory_exec: Arc<dyn ExecutionPlan> = Arc::new(DataSourceExec::new(Arc::new(
MemorySourceConfig::try_new(
&[vec![
RecordBatch::new_empty(schema.clone()),
RecordBatch::new_empty(schema.clone()),
make_batch(vec!["server1", "server1"], vec![1, 2]),
RecordBatch::new_empty(schema.clone()),
make_batch(vec!["server1"], vec![3]),
make_batch(vec!["server2", "server2"], vec![4, 5]),
]],
schema.clone(),
None,
)
.unwrap(),
)));
let divide_exec = Arc::new(SeriesDivideExec {
tag_columns: vec!["host".to_string()],
time_index_column: "time_index".to_string(),
input: memory_exec,
metric: ExecutionPlanMetricsSet::new(),
});
let mut stream = divide_exec
.execute(0, SessionContext::default().task_ctx())
.unwrap();
for (expected_host, expected_values) in
[("server1", vec![1i64, 2, 3]), ("server2", vec![4, 5])]
{
let batch = stream.next().await.unwrap().unwrap();
assert_eq!(batch.num_rows(), expected_values.len());
for (row, value) in expected_values.into_iter().enumerate() {
assert_eq!(
string_array_value_at_index(batch.column(0), row),
Some(expected_host)
);
assert_eq!(
batch
.column(1)
.as_any()
.downcast_ref::<Int64Array>()
.unwrap()
.value(row),
value
);
}
}
assert!(stream.next().await.is_none());
}
/// A stream that only ever yields zero-row batches must terminate with no
/// batches instead of failing.
#[tokio::test]
async fn test_stream_all_empty_batches_yields_nothing() {
let schema = Arc::new(Schema::new(vec![
Field::new("host", DataType::Utf8, false),
Field::new("value", DataType::Int64, false),
Field::new(
"time_index",
DataType::Timestamp(datafusion::arrow::datatypes::TimeUnit::Millisecond, None),
false,
),
]));
let memory_exec: Arc<dyn ExecutionPlan> = Arc::new(DataSourceExec::new(Arc::new(
MemorySourceConfig::try_new(
&[vec![
RecordBatch::new_empty(schema.clone()),
RecordBatch::new_empty(schema.clone()),
RecordBatch::new_empty(schema.clone()),
]],
schema.clone(),
None,
)
.unwrap(),
)));
let divide_exec = Arc::new(SeriesDivideExec {
tag_columns: vec!["host".to_string()],
time_index_column: "time_index".to_string(),
input: memory_exec,
metric: ExecutionPlanMetricsSet::new(),
});
let mut stream = divide_exec
.execute(0, SessionContext::default().task_ctx())
.unwrap();
assert!(stream.next().await.is_none());
let collected =
datafusion::physical_plan::collect(divide_exec, SessionContext::default().task_ctx())
.await
.unwrap();
assert!(collected.is_empty());
}
}
+1
View File
@@ -36,6 +36,7 @@ bytes.workspace = true
catalog.workspace = true
chrono.workspace = true
common-base.workspace = true
common-batcher.workspace = true
common-catalog.workspace = true
common-decimal.workspace = true
common-error.workspace = true
+292 -226
View File
@@ -23,22 +23,27 @@ use api::v1::region::{
BulkInsertRequest, RegionRequest, RegionRequestHeader, bulk_insert_request, region_request,
};
use api::v1::{ArrowIpc, ColumnSchema, RowInsertRequests, Rows};
use arrow::array::Array;
use arrow::compute::{concat_batches, filter_record_batch};
use arrow::datatypes::{DataType as ArrowDataType, Schema as ArrowSchema, TimeUnit};
use arrow::record_batch::RecordBatch;
use async_trait::async_trait;
use bytes::Bytes;
use catalog::CatalogManagerRef;
use common_grpc::flight::{FlightEncoder, FlightMessage};
use common_batcher::flush_limiter::FlushLimiter;
use common_batcher::flush_policy::FlushTrigger;
use common_batcher::flush_policy::timing::TimingFlushPolicy;
use common_batcher::notifier::{Notifier, run_notifier};
use common_batcher::pending_worker::PendingWorker as PendingCore;
use common_batcher::request_limiter::RequestLimiter;
use common_batcher::worker_registry::WorkerRegistry;
use common_grpc::error::Error as GrpcError;
use common_grpc::flight::record_batch_to_ipc;
use common_meta::cache::TableFlownodeSetCacheRef;
use common_meta::node_manager::NodeManagerRef;
use common_query::prelude::{GREPTIME_PHYSICAL_TABLE, greptime_timestamp, greptime_value};
use common_runtime::spawn_global;
use common_telemetry::tracing_context::TracingContext;
use common_telemetry::{debug, error, warn};
use dashmap::DashMap;
use dashmap::mapref::entry::Entry;
use futures::StreamExt;
use datatypes::timestamp::append_timestamps;
use metric_engine::batch_modifier::{TagColumnInfo, modify_batch_sparse};
use partition::manager::PartitionRuleManagerRef;
use partition::partition::PartitionRuleRef;
@@ -81,7 +86,7 @@ pub fn pending_rows_batch_sync_enabled() -> bool {
}
const WORKER_IDLE_TIMEOUT_MULTIPLIER: u32 = 3;
const PHYSICAL_REGION_ESSENTIAL_COLUMN_COUNT: usize = 3;
const MAX_CONCURRENT_FLOW_NOTIFICATIONS: usize = 8;
const MAX_CONCURRENT_FLOW_NOTIFICATIONS: NonZeroUsize = NonZeroUsize::new(8).unwrap();
#[async_trait]
pub trait PendingRowsSchemaAlterer: Send + Sync {
/// Batch-create multiple logical tables that are missing.
@@ -292,15 +297,13 @@ struct TableResolutionPlan {
struct PendingBatch {
tables: HashMap<TableId, TableBatch>,
total_row_count: usize,
db_string: String,
ctx: QueryContextRef,
waiters: Vec<FlushWaiter>,
}
struct FlushWaiter {
response_tx: oneshot::Sender<std::result::Result<(), Arc<Error>>>,
_permit: OwnedSemaphorePermit,
_permit: Arc<OwnedSemaphorePermit>,
}
struct FlushBatch {
@@ -322,7 +325,7 @@ enum WorkerCommand {
total_rows: usize,
ctx: QueryContextRef,
response_tx: oneshot::Sender<std::result::Result<(), Arc<Error>>>,
_permit: OwnedSemaphorePermit,
_permit: Arc<OwnedSemaphorePermit>,
},
#[cfg(test)]
Ack { ack_tx: oneshot::Sender<()> },
@@ -344,15 +347,15 @@ fn batch_key_from_ctx(ctx: &QueryContextRef) -> BatchKey {
/// Prometheus remote write pending rows batcher.
pub struct PendingRowsBatcher {
workers: Arc<DashMap<BatchKey, PendingWorker>>,
workers: Arc<WorkerRegistry<BatchKey, WorkerCommand>>,
flush_interval: Duration,
max_batch_rows: usize,
flush_policy: TimingFlushPolicy,
partition_manager: PartitionRuleManagerRef,
node_manager: NodeManagerRef,
catalog_manager: CatalogManagerRef,
flow_notification_tx: mpsc::Sender<FlowNotification>,
flush_semaphore: Arc<Semaphore>,
inflight_semaphore: Arc<Semaphore>,
flow_notification_tx: Notifier<FlowNotification>,
flush_limiter: FlushLimiter,
request_limiter: RequestLimiter,
worker_channel_capacity: usize,
prom_store_with_metric_engine: bool,
schema_alterer: PendingRowsSchemaAltererRef,
@@ -376,24 +379,21 @@ impl PendingRowsBatcher {
max_inflight_requests: usize,
flow_notification_queue_capacity: NonZeroUsize,
) -> Option<Arc<Self>> {
// Disable the batcher if flush is disabled or configuration is invalid.
// Zero values for these knobs either cause panics (e.g., zero-capacity channels)
// or deadlocks (e.g., semaphores with no permits).
if flush_interval.is_zero()
|| max_batch_rows == 0
|| max_concurrent_flushes == 0
|| worker_channel_capacity == 0
|| max_inflight_requests == 0
{
if worker_channel_capacity == 0 || worker_channel_capacity > Semaphore::MAX_PERMITS {
return None;
}
let flush_policy = TimingFlushPolicy::try_new(flush_interval, max_batch_rows)?;
let flush_limiter = FlushLimiter::try_new(max_concurrent_flushes)?;
let request_limiter = RequestLimiter::try_new(max_inflight_requests)?;
let (flow_notification_tx, flow_notification_rx) =
Notifier::try_new(flow_notification_queue_capacity.get())?;
let (shutdown, _) = broadcast::channel(1);
let pending_rows_batch_sync = pending_rows_batch_sync_enabled();
let workers = Arc::new(DashMap::new());
PENDING_WORKERS.set(workers.len() as i64);
let (flow_notification_tx, flow_notification_rx) =
mpsc::channel(flow_notification_queue_capacity.get());
let workers = Arc::new(WorkerRegistry::new());
PENDING_WORKERS.set(0);
start_flow_notification_worker(
flow_notification_rx,
table_flownode_set_cache,
@@ -403,15 +403,15 @@ impl PendingRowsBatcher {
Some(Arc::new(Self {
workers,
flush_interval,
max_batch_rows,
flush_policy,
partition_manager,
node_manager,
catalog_manager,
flow_notification_tx,
prom_store_with_metric_engine,
schema_alterer,
flush_semaphore: Arc::new(Semaphore::new(max_concurrent_flushes)),
inflight_semaphore: Arc::new(Semaphore::new(max_inflight_requests)),
flush_limiter,
request_limiter,
worker_channel_capacity,
pending_rows_batch_sync,
shutdown,
@@ -433,9 +433,8 @@ impl PendingRowsBatcher {
let _timer = PENDING_ROWS_BATCH_INGEST_STAGE_ELAPSED
.with_label_values(&["submit_acquire_inflight_permit"])
.start_timer();
self.inflight_semaphore
.clone()
.acquire_owned()
self.request_limiter
.acquire()
.await
.map_err(|_| error::BatcherChannelClosedSnafu.build())?
};
@@ -457,7 +456,7 @@ impl PendingRowsBatcher {
.start_timer();
for _ in 0..2 {
let worker = self.get_or_spawn_worker(batch_key.clone());
let worker = self.get_or_spawn_worker(batch_key.clone()).await;
let Some(worker_cmd) = cmd.take() else {
break;
};
@@ -470,7 +469,8 @@ impl PendingRowsBatcher {
self.workers.as_ref(),
&batch_key,
&worker.tx,
);
)
.await;
}
}
}
@@ -832,39 +832,24 @@ impl PendingRowsBatcher {
Ok(aligned_batches)
}
fn get_or_spawn_worker(&self, key: BatchKey) -> PendingWorker {
if let Some(worker) = self.workers.get(&key)
&& !worker.tx.is_closed()
{
return worker.clone();
}
let entry = self.workers.entry(key.clone());
match entry {
Entry::Occupied(mut worker) => {
if worker.get().tx.is_closed() {
let new_worker = self.spawn_worker(key);
worker.insert(new_worker.clone());
PENDING_WORKERS.set(self.workers.len() as i64);
new_worker
} else {
worker.get().clone()
}
}
Entry::Vacant(vacant) => {
let worker = self.spawn_worker(key);
vacant.insert(worker.clone());
PENDING_WORKERS.set(self.workers.len() as i64);
worker
}
async fn get_or_spawn_worker(&self, key: BatchKey) -> PendingWorker {
let (tx, receiver) = self
.workers
.get_or_create(key.clone(), self.worker_channel_capacity)
.await;
if let Some(rx) = receiver {
self.spawn_worker(key, tx.clone(), rx);
PENDING_WORKERS.set(self.workers.len().await as i64);
}
PendingWorker { tx }
}
fn spawn_worker(&self, key: BatchKey) -> PendingWorker {
let (tx, rx) = mpsc::channel(self.worker_channel_capacity);
let worker = PendingWorker { tx: tx.clone() };
fn spawn_worker(
&self,
key: BatchKey,
tx: mpsc::Sender<WorkerCommand>,
rx: mpsc::Receiver<WorkerCommand>,
) {
let worker_idle_timeout = self
.flush_interval
.checked_mul(WORKER_IDLE_TIMEOUT_MULTIPLIER)
@@ -872,7 +857,7 @@ impl PendingRowsBatcher {
start_worker(
key,
worker.tx.clone(),
tx,
self.workers.clone(),
rx,
self.shutdown.clone(),
@@ -880,13 +865,10 @@ impl PendingRowsBatcher {
self.node_manager.clone(),
self.catalog_manager.clone(),
self.flow_notification_tx.clone(),
self.flush_interval,
worker_idle_timeout,
self.max_batch_rows,
self.flush_semaphore.clone(),
self.flush_policy,
self.flush_limiter.clone(),
);
worker
}
}
@@ -901,10 +883,8 @@ impl PendingBatch {
let db_string = ctx.get_db_string();
Self {
tables: HashMap::new(),
total_row_count: 0,
db_string,
ctx,
waiters: Vec::new(),
}
}
@@ -929,22 +909,21 @@ impl PendingBatch {
fn start_worker(
key: BatchKey,
worker_tx: mpsc::Sender<WorkerCommand>,
workers: Arc<DashMap<BatchKey, PendingWorker>>,
workers: Arc<WorkerRegistry<BatchKey, WorkerCommand>>,
mut rx: mpsc::Receiver<WorkerCommand>,
shutdown: broadcast::Sender<()>,
partition_manager: PartitionRuleManagerRef,
node_manager: NodeManagerRef,
catalog_manager: CatalogManagerRef,
flow_notification_tx: mpsc::Sender<FlowNotification>,
flush_interval: Duration,
flow_notification_tx: Notifier<FlowNotification>,
worker_idle_timeout: Duration,
max_batch_rows: usize,
flush_semaphore: Arc<Semaphore>,
flush_policy: TimingFlushPolicy,
flush_limiter: FlushLimiter,
) {
tokio::spawn(async move {
// The business batch and pending flush are populated and drained together.
let mut batch = None;
let flush_timer = tokio::time::sleep(flush_interval);
tokio::pin!(flush_timer);
let mut pending_flush = PendingCore::new(flush_policy);
let mut shutdown_rx = shutdown.subscribe();
let idle_deadline = tokio::time::Instant::now() + worker_idle_timeout;
let idle_timer = tokio::time::sleep_until(idle_deadline);
@@ -955,43 +934,37 @@ fn start_worker(
cmd = rx.recv() => {
match cmd {
Some(WorkerCommand::Submit { table_batches, total_rows, ctx, response_tx, _permit }) => {
idle_timer.as_mut().reset(tokio::time::Instant::now() + worker_idle_timeout);
let submitted_at = tokio::time::Instant::now();
idle_timer.as_mut().reset(submitted_at + worker_idle_timeout);
if batch.is_none() {
// Anchor the flush deadline to this batch's first submission,
// rather than to the worker's creation time.
flush_timer
.as_mut()
.reset(tokio::time::Instant::now() + flush_interval);
}
pending_flush.submit(
FlushWaiter { response_tx, _permit },
total_rows,
);
let pending_batch = batch.get_or_insert_with(||{
PENDING_BATCHES.inc();
PendingBatch::new(ctx)
});
pending_batch.waiters.push(FlushWaiter { response_tx, _permit });
for (table_name, table_id, record_batch) in table_batches {
pending_batch.add_table_batch(table_name, table_id, record_batch);
}
pending_batch.total_row_count += total_rows;
PENDING_ROWS.add(total_rows as i64);
if pending_batch.total_row_count >= max_batch_rows
&& let Some(flush) = drain_batch(&mut batch) {
spawn_flush(
flush,
partition_manager.clone(),
node_manager.clone(),
catalog_manager.clone(),
flow_notification_tx.clone(),
flush_semaphore.clone(),
).await;
if let Some(flush) = drain_batch(&mut batch, &mut pending_flush, Some(FlushTrigger::Submission)) {
spawn_flush(
flush,
partition_manager.clone(),
node_manager.clone(),
catalog_manager.clone(),
flow_notification_tx.clone(),
flush_limiter.clone(),
).await;
}
}
None => {
if let Some(flush) = drain_batch(&mut batch) {
if let Some(flush) = drain_batch(&mut batch, &mut pending_flush, None) {
flush_batch_with_managers(
flush,
partition_manager.clone(),
@@ -1010,7 +983,7 @@ fn start_worker(
}
_ = &mut idle_timer => {
if !should_close_worker_on_idle_timeout(
batch.as_ref().map_or(0, |batch| batch.total_row_count),
pending_flush.total_rows(),
rx.len(),
) {
idle_timer
@@ -1027,20 +1000,20 @@ fn start_worker(
);
break;
}
_ = &mut flush_timer, if batch.is_some() => {
if let Some(flush) = drain_batch(&mut batch) {
_ = pending_flush.wait_flush() => {
if let Some(flush) = drain_batch(&mut batch, &mut pending_flush, Some(FlushTrigger::Deadline)) {
spawn_flush(
flush,
partition_manager.clone(),
node_manager.clone(),
catalog_manager.clone(),
flow_notification_tx.clone(),
flush_semaphore.clone(),
flush_limiter.clone(),
).await;
}
}
_ = shutdown_rx.recv() => {
if let Some(flush) = drain_batch(&mut batch) {
if let Some(flush) = drain_batch(&mut batch, &mut pending_flush, None) {
flush_batch_with_managers(
flush,
partition_manager.clone(),
@@ -1054,41 +1027,46 @@ fn start_worker(
}
}
remove_worker_if_same_channel(workers.as_ref(), &key, &worker_tx);
remove_worker_if_same_channel(workers.as_ref(), &key, &worker_tx).await;
});
}
fn remove_worker_if_same_channel(
workers: &DashMap<BatchKey, PendingWorker>,
async fn remove_worker_if_same_channel(
workers: &WorkerRegistry<BatchKey, WorkerCommand>,
key: &BatchKey,
worker_tx: &mpsc::Sender<WorkerCommand>,
) -> bool {
if let Some(worker) = workers.get(key)
&& worker.tx.same_channel(worker_tx)
{
drop(worker);
workers.remove(key);
PENDING_WORKERS.set(workers.len() as i64);
return true;
if workers.remove_if_same(key, worker_tx).await {
PENDING_WORKERS.set(workers.len().await as i64);
true
} else {
false
}
false
}
fn should_close_worker_on_idle_timeout(total_row_count: usize, queued_requests: usize) -> bool {
total_row_count == 0 && queued_requests == 0
}
fn drain_batch(batch: &mut Option<PendingBatch>) -> Option<FlushBatch> {
/// Transfers the ready batch, or drains unconditionally when no trigger is given.
/// Execution and flush permits remain owned by the caller.
fn drain_batch(
batch: &mut Option<PendingBatch>,
pending_flush: &mut PendingCore<FlushWaiter, TimingFlushPolicy>,
trigger: Option<FlushTrigger>,
) -> Option<FlushBatch> {
let total_row_count = pending_flush.total_rows();
let waiters = match trigger {
Some(trigger) => pending_flush.take_ready(trigger)?,
None => pending_flush.take_pending()?,
};
let batch = batch.take()?;
let total_row_count = batch.total_row_count;
if total_row_count == 0 {
return None;
}
let table_batches = batch.tables.into_values().collect();
let waiters = batch.waiters;
PENDING_ROWS.sub(total_row_count as i64);
PENDING_BATCHES.dec();
@@ -1107,10 +1085,10 @@ async fn spawn_flush(
partition_manager: PartitionRuleManagerRef,
node_manager: NodeManagerRef,
catalog_manager: CatalogManagerRef,
flow_notification_tx: mpsc::Sender<FlowNotification>,
semaphore: Arc<Semaphore>,
flow_notification_tx: Notifier<FlowNotification>,
flush_limiter: FlushLimiter,
) {
match semaphore.acquire_owned().await {
match flush_limiter.acquire().await {
Ok(permit) => {
tokio::spawn(async move {
let _permit = permit;
@@ -1323,7 +1301,7 @@ async fn flush_batch_with_managers(
partition_manager: PartitionRuleManagerRef,
node_manager: NodeManagerRef,
catalog_manager: CatalogManagerRef,
flow_notification_tx: mpsc::Sender<FlowNotification>,
flow_notification_tx: Notifier<FlowNotification>,
) {
let partition_provider = PartitionManagerPhysicalFlushAdapter { partition_manager };
let node_requester = NodeManagerPhysicalFlushAdapter {
@@ -1345,7 +1323,7 @@ async fn flush_batch(
partition_manager: &(impl PhysicalFlushPartitionProvider + ?Sized),
node_manager: &(impl PhysicalFlushNodeRequester + ?Sized),
catalog_manager: &(impl PhysicalFlushCatalogProvider + ?Sized),
flow_notification_tx: mpsc::Sender<FlowNotification>,
flow_notification_tx: Notifier<FlowNotification>,
) {
let FlushBatch {
table_batches,
@@ -1403,21 +1381,13 @@ fn extract_timestamps(table_batch: &TableBatch) -> Vec<i64> {
let mut timestamps = Vec::with_capacity(table_batch.row_count);
for batch in &table_batch.batches {
let timestamp_column = batch.batch.column(batch.timestamp_index);
let Some((timestamp_values, _)) =
datatypes::timestamp::timestamp_array_to_primitive(timestamp_column)
else {
let Some(()) = append_timestamps(timestamp_column, &mut timestamps) else {
error!(
"Failed to extract timestamps from record batch, table_id: {}, timestamp_index: {}",
table_batch.table_id, batch.timestamp_index
);
continue;
};
if timestamp_values.null_count() == 0 {
timestamps.extend_from_slice(timestamp_values.values());
} else {
timestamps.extend(timestamp_values.iter().flatten());
}
}
timestamps
}
@@ -1428,10 +1398,10 @@ struct FlowNotification {
}
fn try_enqueue_flow_notification(
tx: &mpsc::Sender<FlowNotification>,
tx: &Notifier<FlowNotification>,
notification: FlowNotification,
) -> bool {
match tx.try_send(notification) {
match tx.try_notify(notification) {
Ok(()) => true,
Err(mpsc::error::TrySendError::Full(notification)) => {
FLOW_NOTIFICATION_DROPPED.with_label_values(&["full"]).inc();
@@ -1456,7 +1426,7 @@ fn try_enqueue_flow_notification(
}
}
fn enqueue_flow_notifications(table_batches: Vec<TableBatch>, tx: &mpsc::Sender<FlowNotification>) {
fn enqueue_flow_notifications(table_batches: Vec<TableBatch>, tx: &Notifier<FlowNotification>) {
for table_batch in table_batches {
let timestamps = extract_timestamps(&table_batch);
if timestamps.is_empty() {
@@ -1517,14 +1487,17 @@ fn start_flow_notification_worker(
table_flownode_set_cache: TableFlownodeSetCacheRef,
node_manager: NodeManagerRef,
) {
common_runtime::spawn_global(async move {
tokio_stream::wrappers::ReceiverStream::new(notification_rx)
.for_each_concurrent(MAX_CONCURRENT_FLOW_NOTIFICATIONS, |notification| {
spawn_global(async move {
run_notifier(
notification_rx,
MAX_CONCURRENT_FLOW_NOTIFICATIONS,
|notification| {
let table_flownode_set_cache = table_flownode_set_cache.clone();
let node_manager = node_manager.clone();
handle_flow_notification(notification, table_flownode_set_cache, node_manager)
})
.await;
},
)
.await;
});
}
@@ -1534,7 +1507,7 @@ fn notify_flow_dirty_windows_after_flush(
table_flownode_set_cache: TableFlownodeSetCacheRef,
node_manager: NodeManagerRef,
) {
let (tx, rx) = mpsc::channel(table_batches.len().max(1));
let (tx, rx) = Notifier::try_new(table_batches.len().max(1)).unwrap();
start_flow_notification_worker(rx, table_flownode_set_cache, node_manager);
enqueue_flow_notifications(table_batches, &tx);
}
@@ -1804,7 +1777,7 @@ fn encode_region_write_requests(
let _timer = PENDING_ROWS_BATCH_FLUSH_STAGE_ELAPSED
.with_label_values(&["flush_physical_encode_ipc"])
.start_timer();
record_batch_to_ipc(resolved.planned.batch)?
record_batch_to_ipc(resolved.planned.batch).map_err(map_ipc_error)?
};
let request = RegionRequest {
@@ -1847,36 +1820,24 @@ fn notify_waiters(waiters: Vec<FlushWaiter>, result: Result<()>) {
}
}
fn record_batch_to_ipc(record_batch: RecordBatch) -> Result<(Bytes, Bytes, Bytes)> {
let mut encoder = FlightEncoder::default();
let schema = encoder.encode_schema(record_batch.schema().as_ref());
let mut iter = encoder
.encode(FlightMessage::RecordBatch(record_batch))
.into_iter();
let Some(flight_data) = iter.next() else {
return Err(Error::Internal {
err_msg: "Failed to encode empty flight data".to_string(),
});
};
if iter.next().is_some() {
return Err(Error::NotSupported {
feat: "bulk insert RecordBatch with dictionary arrays".to_string(),
});
fn map_ipc_error(error: GrpcError) -> Error {
match error {
GrpcError::NotSupported { feat } => Error::NotSupported { feat },
GrpcError::InvalidFlightData { reason, .. } => Error::Internal { err_msg: reason },
error => Error::Internal {
err_msg: error.to_string(),
},
}
Ok((
schema.data_header,
flight_data.data_header,
flight_data.data_body,
))
}
#[cfg(test)]
mod tests {
use std::any::Any;
use std::collections::{HashMap, HashSet};
use std::future::{Future, poll_fn};
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::{Arc, Mutex};
use std::task::Poll;
use std::time::Duration;
use api::region::RegionResponse;
@@ -1912,7 +1873,6 @@ mod tests {
};
use common_query::request::QueryRequest;
use common_recordbatch::SendableRecordBatchStream;
use dashmap::DashMap;
use datatypes::schema::{ColumnSchema as DtColumnSchema, Schema as DtSchema};
use moka::future::CacheBuilder;
use partition::cache::new_partition_info_cache;
@@ -1927,21 +1887,22 @@ mod tests {
use tokio::sync::{Notify, Semaphore, broadcast, mpsc, oneshot};
use tokio::time::{advance, sleep};
use super::{
BatchKey, Error, FlushBatch, FlushRegionWrite, FlushWaiter, PendingBatch,
PendingRowsBatcher, PendingWorker, PhysicalFlushCatalogProvider,
use crate::error;
use crate::metrics::FLOW_NOTIFICATION_DROPPED;
use crate::pending_rows_batcher::{
BatchKey, Error, FlushBatch, FlushLimiter, FlushRegionWrite, FlushTrigger, FlushWaiter,
Notifier, PendingBatch, PendingCore, PendingRowsBatcher, PhysicalFlushCatalogProvider,
PhysicalFlushNodeRequester, PhysicalFlushPartitionProvider, PhysicalTableMetadata,
PlannedRegionBatch, RecordBatchWithTsIdx, ResolvedRegionBatch, TableBatch, WorkerCommand,
batch_key_from_ctx, columns_taxonomy, drain_batch, encode_region_write_requests,
extract_timestamps, flush_batch, flush_batch_physical, flush_region_writes_concurrently,
greptime_timestamp, notify_flow_dirty_windows_after_flush, plan_region_batches,
PlannedRegionBatch, RecordBatchWithTsIdx, RequestLimiter, ResolvedRegionBatch, TableBatch,
TimingFlushPolicy, WorkerCommand, WorkerRegistry, batch_key_from_ctx, columns_taxonomy,
drain_batch, encode_region_write_requests, extract_timestamps, flush_batch,
flush_batch_physical, flush_region_writes_concurrently, greptime_timestamp,
notify_flow_dirty_windows_after_flush, notify_waiters, plan_region_batches,
remove_worker_if_same_channel, should_close_worker_on_idle_timeout,
should_dispatch_concurrently, start_flow_notification_worker, start_worker,
strip_partition_columns_from_batch, transform_logical_batches_to_physical,
try_enqueue_flow_notification,
};
use crate::error;
use crate::metrics::FLOW_NOTIFICATION_DROPPED;
use crate::prom_row_builder::rows_to_aligned_record_batch;
fn mock_rows(row_count: usize, schema_name: &str) -> Rows {
@@ -2165,8 +2126,8 @@ mod tests {
#[test]
fn test_flow_notification_queue_drops_when_full() {
let (tx, mut rx) = mpsc::channel(1);
let notification = |table_id| super::FlowNotification {
let (tx, mut rx) = Notifier::try_new(1).unwrap();
let notification = |table_id| crate::pending_rows_batcher::FlowNotification {
table_id,
timestamps: vec![table_id as i64],
};
@@ -2178,6 +2139,12 @@ mod tests {
assert_eq!(1, rx.try_recv().unwrap().table_id);
assert_eq!(dropped_before + 1, dropped.get());
let closed = FLOW_NOTIFICATION_DROPPED.with_label_values(&["closed"]);
let closed_before = closed.get();
drop(rx);
assert!(!try_enqueue_flow_notification(&tx, notification(3)));
assert_eq!(closed_before + 1, closed.get());
}
fn mock_physical_table_metadata(table_id: TableId) -> PhysicalTableMetadata {
@@ -2375,11 +2342,20 @@ mod tests {
assert_eq!(2, table_rows[0].1.rows.len());
}
#[test]
fn test_drain_batch_takes_initialized_pending_batch_from_option() {
#[tokio::test]
async fn test_drain_batch_takes_initialized_pending_batch_from_option() {
let ctx = session::context::QueryContext::arc();
let (response_tx, _response_rx) = oneshot::channel();
let permit = Arc::new(Semaphore::new(1)).try_acquire_owned().unwrap();
let mut pending_flush =
PendingCore::new(TimingFlushPolicy::try_new(Duration::from_secs(10), 1).unwrap());
pending_flush.submit(
FlushWaiter {
response_tx,
_permit: Arc::new(permit),
},
1,
);
let mut batch = Some(PendingBatch {
tables: HashMap::from([(
42,
@@ -2390,24 +2366,77 @@ mod tests {
row_count: 1,
},
)]),
total_row_count: 1,
db_string: ctx.get_db_string(),
ctx: ctx.clone(),
waiters: vec![FlushWaiter {
response_tx,
_permit: permit,
}],
});
let flush = drain_batch(&mut batch).unwrap();
let flush = drain_batch(
&mut batch,
&mut pending_flush,
Some(FlushTrigger::Submission),
)
.unwrap();
assert!(batch.is_none());
assert!(pending_flush.is_empty());
assert_eq!(0, pending_flush.total_rows());
assert_eq!(1, flush.waiters.len());
assert_eq!(1, flush.total_row_count);
assert_eq!(1, flush.table_batches.len());
assert_eq!(ctx.get_db_string(), flush.db_string);
assert_eq!(ctx.current_catalog(), flush.ctx.current_catalog());
}
#[tokio::test]
async fn test_drain_batch_preserves_unready_state_and_clears_zero_rows() {
for total_rows in [0, 1] {
let mut pending_flush =
PendingCore::new(TimingFlushPolicy::try_new(Duration::from_secs(10), 2).unwrap());
let mut batch = Some(PendingBatch::new(session::context::QueryContext::arc()));
let semaphore = Arc::new(Semaphore::new(1));
let (response_tx, mut response_rx) = oneshot::channel();
pending_flush.submit(
FlushWaiter {
response_tx,
_permit: Arc::new(semaphore.clone().acquire_owned().await.unwrap()),
},
total_rows,
);
assert!(
drain_batch(
&mut batch,
&mut pending_flush,
Some(FlushTrigger::Submission)
)
.is_none()
);
assert!(batch.is_some());
assert!(!pending_flush.is_empty());
assert_eq!(total_rows, pending_flush.total_rows());
assert_eq!(0, semaphore.available_permits());
let drained = drain_batch(&mut batch, &mut pending_flush, None);
assert!(batch.is_none());
assert!(pending_flush.is_empty());
assert_eq!(0, pending_flush.total_rows());
if total_rows == 0 {
assert!(drained.is_none());
assert_eq!(1, semaphore.available_permits());
assert!(matches!(
response_rx.try_recv(),
Err(oneshot::error::TryRecvError::Closed)
));
} else {
let drained = drained.unwrap();
assert_eq!(1, drained.total_row_count);
assert_eq!(1, drained.waiters.len());
assert_eq!(0, semaphore.available_permits());
drop(drained);
assert_eq!(1, semaphore.available_permits());
}
}
}
#[test]
fn test_pending_batch_keeps_same_name_batches_with_distinct_table_ids() {
let ctx = session::context::QueryContext::arc();
@@ -2572,8 +2601,8 @@ mod tests {
fn mock_flow_notification_sender(
cache: TableFlownodeSetCacheRef,
node_manager: NodeManagerRef,
) -> mpsc::Sender<super::FlowNotification> {
let (tx, rx) = mpsc::channel(16);
) -> Notifier<crate::pending_rows_batcher::FlowNotification> {
let (tx, rx) = Notifier::try_new(16).unwrap();
start_flow_notification_worker(rx, cache, node_manager);
tx
}
@@ -2930,9 +2959,25 @@ mod tests {
assert_eq!(batches[&batch_key_from_ctx(&skip_wal_ctx)], 2);
}
#[test]
fn test_remove_worker_if_same_channel_removes_matching_entry() {
let workers = DashMap::new();
#[tokio::test]
async fn test_cancelled_waiter_retains_request_slot_until_notification() {
let limiter = RequestLimiter::try_new(1).unwrap();
let (response_tx, response_rx) = oneshot::channel();
let waiter = FlushWaiter {
response_tx,
_permit: limiter.acquire().await.unwrap(),
};
drop(response_rx);
let next = limiter.acquire();
tokio::pin!(next);
assert!(poll_fn(|cx| Poll::Ready(next.as_mut().poll(cx).is_pending())).await);
notify_waiters(vec![waiter], Ok(()));
let _permit = next.await.unwrap();
}
#[tokio::test]
async fn test_remove_worker_if_same_channel_removes_matching_entry() {
let workers = WorkerRegistry::new();
let key = BatchKey {
catalog: "greptime".to_string(),
schema: "public".to_string(),
@@ -2941,15 +2986,15 @@ mod tests {
};
let (tx, _rx) = mpsc::channel::<WorkerCommand>(1);
workers.insert(key.clone(), PendingWorker { tx: tx.clone() });
workers.get_or_insert_with(key.clone(), || tx.clone()).await;
assert!(remove_worker_if_same_channel(&workers, &key, &tx));
assert!(!workers.contains_key(&key));
assert!(remove_worker_if_same_channel(&workers, &key, &tx).await);
assert!(workers.is_empty().await);
}
#[test]
fn test_remove_worker_if_same_channel_keeps_newer_entry() {
let workers = DashMap::new();
#[tokio::test]
async fn test_remove_worker_if_same_channel_keeps_newer_entry() {
let workers = WorkerRegistry::new();
let key = BatchKey {
catalog: "greptime".to_string(),
schema: "public".to_string(),
@@ -2959,16 +3004,13 @@ mod tests {
let (stale_tx, _stale_rx) = mpsc::channel::<WorkerCommand>(1);
let (fresh_tx, _fresh_rx) = mpsc::channel::<WorkerCommand>(1);
workers.insert(
key.clone(),
PendingWorker {
tx: fresh_tx.clone(),
},
);
workers
.get_or_insert_with(key.clone(), || fresh_tx.clone())
.await;
assert!(!remove_worker_if_same_channel(&workers, &key, &stale_tx));
assert!(workers.contains_key(&key));
assert!(workers.get(&key).unwrap().tx.same_channel(&fresh_tx));
assert!(!remove_worker_if_same_channel(&workers, &key, &stale_tx).await);
assert!(workers.get(&key).await.is_some());
assert!(workers.get(&key).await.unwrap().same_channel(&fresh_tx));
}
#[test]
@@ -2997,7 +3039,7 @@ mod tests {
total_rows,
ctx: session::context::QueryContext::arc(),
response_tx,
_permit: permit,
_permit: Arc::new(permit),
})
.await
.unwrap();
@@ -3039,7 +3081,7 @@ mod tests {
}
#[tokio::test(start_paused = true)]
async fn test_worker_rearms_creation_relative_deadline_after_size_flush() {
async fn test_worker_preserves_first_deadline_and_inline_shutdown() {
let flush_interval = Duration::from_secs(10);
let worker_idle_timeout = Duration::from_secs(30);
let key = BatchKey {
@@ -3048,14 +3090,11 @@ mod tests {
physical_table: "phy".to_string(),
skip_wal: false,
};
let workers = Arc::new(DashMap::new());
let workers = Arc::new(WorkerRegistry::new());
let (worker_tx, worker_rx) = mpsc::channel(1);
workers.insert(
key.clone(),
PendingWorker {
tx: worker_tx.clone(),
},
);
workers
.get_or_insert_with(key.clone(), || worker_tx.clone())
.await;
let backend = Arc::new(MemoryKvBackend::default());
let table_route_cache = Arc::new(new_table_route_cache(
@@ -3077,9 +3116,10 @@ mod tests {
datanodes: Arc::new(HashMap::new()),
});
let catalog_manager = MemoryCatalogManager::with_default_setup();
let (flow_notification_tx, _flow_notification_rx) = mpsc::channel(1);
let (flow_notification_tx, _flow_notification_rx) = Notifier::try_new(1).unwrap();
let (shutdown, _) = broadcast::channel(1);
let flush_limiter = FlushLimiter::try_new(1).unwrap();
start_worker(
key.clone(),
worker_tx.clone(),
@@ -3090,10 +3130,9 @@ mod tests {
node_manager,
catalog_manager,
flow_notification_tx,
flush_interval,
worker_idle_timeout,
2,
Arc::new(Semaphore::new(1)),
TimingFlushPolicy::try_new(flush_interval, 3).unwrap(),
flush_limiter.clone(),
);
// Start the worker, then size-flush a batch halfway to the first
@@ -3101,7 +3140,7 @@ mod tests {
// drains the batch before that deadline is reached.
tokio::task::yield_now().await;
advance(flush_interval / 2).await;
let size_flush_rx = submit_mock_worker_batch(&worker_tx, 2, 1000).await;
let size_flush_rx = submit_mock_worker_batch(&worker_tx, 3, 1000).await;
let size_flush_result =
receive_mock_flush_result(size_flush_rx, "row threshold did not flush the first batch")
.await;
@@ -3112,7 +3151,10 @@ mod tests {
advance(flush_interval / 5).await;
let mut timed_flush_rx = submit_mock_worker_batch(&worker_tx, 1, 2000).await;
advance(flush_interval - Duration::from_millis(1)).await;
let first_submission = tokio::time::Instant::now();
advance(flush_interval / 2).await;
let later_flush_rx = submit_mock_worker_batch(&worker_tx, 1, 3000).await;
advance(flush_interval / 2 - Duration::from_millis(1)).await;
for _ in 0..10 {
tokio::task::yield_now().await;
}
@@ -3129,15 +3171,39 @@ mod tests {
.await;
assert_missing_physical_table(timed_flush_result);
assert_eq!(
first_submission + flush_interval,
tokio::time::Instant::now()
);
assert_missing_physical_table(
receive_mock_flush_result(
later_flush_rx,
"later submission was not included in the timed flush",
)
.await,
);
// Shutdown retains the existing inline path even with no flush permits.
let _held_permit = flush_limiter.acquire().await.unwrap();
let shutdown_flush_rx = submit_mock_worker_batch(&worker_tx, 1, 4000).await;
let shutdown_at = tokio::time::Instant::now();
let _ = shutdown.send(());
assert_missing_physical_table(
receive_mock_flush_result(
shutdown_flush_rx,
"shutdown incorrectly waited for a flush permit",
)
.await,
);
assert_eq!(shutdown_at, tokio::time::Instant::now());
for _ in 0..10 {
if !workers.contains_key(&key) {
if workers.is_empty().await {
break;
}
tokio::task::yield_now().await;
}
assert!(
!workers.contains_key(&key),
workers.is_empty().await,
"worker did not exit after shutdown"
);
}
+135 -8
View File
@@ -42,7 +42,7 @@ use datatypes::error::time_index_not_widening_error;
use datatypes::prelude::ConcreteDataType;
use datatypes::schema::{FulltextOptions, SkippingIndexOptions};
use num_enum::TryFromPrimitive;
use serde::{Deserialize, Serialize};
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use snafu::{OptionExt, ResultExt, ensure};
use strum::{AsRefStr, IntoStaticStr};
@@ -1486,9 +1486,9 @@ impl From<v1::ModifyColumnType> for ModifyColumnType {
/// Region option changes used by ALTER requests.
///
/// This type currently derives serde for request persistence. Keep future changes
/// backward compatible with previously serialized variants.
#[derive(Debug, Eq, PartialEq, Clone, Serialize, Deserialize)]
/// This type is serialized for request persistence. Keep future changes backward
/// compatible with previously serialized variants.
#[derive(Debug, Eq, PartialEq, Clone)]
pub enum SetRegionOption {
WriteBufferSize(Option<ReadableSize>),
Ttl(Option<TimeToLive>),
@@ -1503,10 +1503,92 @@ pub enum SetRegionOption {
// Modifying the max number of rows in a parquet row group.
MaxRowGroupRowCount(Option<usize>),
PreserveRowSequence(bool),
// Stops writing new WAL entries. This operation is irreversible.
// Whether to skip writing new WAL entries.
SkipWal(bool),
}
#[derive(Serialize, Deserialize)]
enum SetRegionOptionSerde {
WriteBufferSize(Option<ReadableSize>),
Ttl(Option<TimeToLive>),
Twsc(String, String),
Format(String),
AppendMode(bool),
AutoFlushInterval(Option<Duration>),
MaxRowGroupRowCount(Option<usize>),
PreserveRowSequence(bool),
SkipWal(bool),
}
impl Serialize for SetRegionOption {
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
where
S: Serializer,
{
// Older binaries deserialize the disable request as a unit variant.
if matches!(self, Self::SkipWal(true)) {
return serializer.serialize_unit_variant("SetRegionOption", 8, "SkipWal");
}
let option = match self {
Self::WriteBufferSize(value) => SetRegionOptionSerde::WriteBufferSize(*value),
Self::Ttl(value) => SetRegionOptionSerde::Ttl(*value),
Self::Twsc(key, value) => SetRegionOptionSerde::Twsc(key.clone(), value.clone()),
Self::Format(value) => SetRegionOptionSerde::Format(value.clone()),
Self::AppendMode(value) => SetRegionOptionSerde::AppendMode(*value),
Self::AutoFlushInterval(value) => SetRegionOptionSerde::AutoFlushInterval(*value),
Self::MaxRowGroupRowCount(value) => SetRegionOptionSerde::MaxRowGroupRowCount(*value),
Self::PreserveRowSequence(value) => SetRegionOptionSerde::PreserveRowSequence(*value),
Self::SkipWal(value) => SetRegionOptionSerde::SkipWal(*value),
};
option.serialize(serializer)
}
}
#[derive(Deserialize)]
enum LegacySetRegionOption {
SkipWal,
}
#[derive(Deserialize)]
#[serde(untagged)]
enum BackwardCompatibleSetRegionOption {
Current(SetRegionOptionSerde),
Legacy(LegacySetRegionOption),
}
impl From<SetRegionOptionSerde> for SetRegionOption {
fn from(option: SetRegionOptionSerde) -> Self {
match option {
SetRegionOptionSerde::WriteBufferSize(value) => Self::WriteBufferSize(value),
SetRegionOptionSerde::Ttl(value) => Self::Ttl(value),
SetRegionOptionSerde::Twsc(key, value) => Self::Twsc(key, value),
SetRegionOptionSerde::Format(value) => Self::Format(value),
SetRegionOptionSerde::AppendMode(value) => Self::AppendMode(value),
SetRegionOptionSerde::AutoFlushInterval(value) => Self::AutoFlushInterval(value),
SetRegionOptionSerde::MaxRowGroupRowCount(value) => Self::MaxRowGroupRowCount(value),
SetRegionOptionSerde::PreserveRowSequence(value) => Self::PreserveRowSequence(value),
SetRegionOptionSerde::SkipWal(value) => Self::SkipWal(value),
}
}
}
impl<'de> Deserialize<'de> for SetRegionOption {
fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
where
D: Deserializer<'de>,
{
Ok(
match BackwardCompatibleSetRegionOption::deserialize(deserializer)? {
BackwardCompatibleSetRegionOption::Current(option) => option.into(),
BackwardCompatibleSetRegionOption::Legacy(LegacySetRegionOption::SkipWal) => {
Self::SkipWal(true)
}
},
)
}
}
impl TryFrom<&PbOption> for SetRegionOption {
type Error = MetadataError;
@@ -1572,7 +1654,12 @@ impl TryFrom<&PbOption> for SetRegionOption {
.map_err(|_| InvalidSetRegionOptionRequestSnafu { key, value }.build())?;
Ok(Self::PreserveRowSequence(preserve))
}
SKIP_WAL_KEY if value == "true" => Ok(Self::SkipWal),
SKIP_WAL_KEY => {
let skip_wal = value
.parse::<bool>()
.map_err(|_| InvalidSetRegionOptionRequestSnafu { key, value }.build())?;
Ok(Self::SkipWal(skip_wal))
}
_ => InvalidSetRegionOptionRequestSnafu { key, value }.fail(),
}
}
@@ -2047,11 +2134,20 @@ mod tests {
value: "true".to_string(),
};
assert_eq!(
SetRegionOption::SkipWal,
SetRegionOption::SkipWal(true),
SetRegionOption::try_from(&pb).unwrap()
);
for value in ["false", "", "invalid"] {
let pb = PbOption {
key: SKIP_WAL_KEY.to_string(),
value: "false".to_string(),
};
assert_eq!(
SetRegionOption::SkipWal(false),
SetRegionOption::try_from(&pb).unwrap()
);
for value in ["", "invalid"] {
let pb = PbOption {
key: SKIP_WAL_KEY.to_string(),
value: value.to_string(),
@@ -2062,6 +2158,37 @@ mod tests {
assert!(UnsetRegionOption::try_from(SKIP_WAL_KEY).is_err());
}
#[test]
fn test_set_region_option_skip_wal_serde_compatibility() {
let legacy = serde_json::from_str::<SetRegionOption>(r#""SkipWal""#).unwrap();
assert_eq!(SetRegionOption::SkipWal(true), legacy);
check_set_region_option_skip_wal_serde_compatibility(false);
check_set_region_option_skip_wal_serde_compatibility(true);
}
fn check_set_region_option_skip_wal_serde_compatibility(skip_wal: bool) {
let option = SetRegionOption::SkipWal(skip_wal);
let serialized = serde_json::to_string(&option).unwrap();
let expected = if skip_wal {
r#""SkipWal""#
} else {
r#"{"SkipWal":false}"#
};
assert_eq!(expected, serialized);
assert_eq!(
option,
serde_json::from_str::<SetRegionOption>(&serialized).unwrap()
);
if skip_wal {
assert!(serde_json::from_str::<LegacySetRegionOption>(&serialized).is_ok());
assert_eq!(
option,
serde_json::from_str::<SetRegionOption>(r#"{"SkipWal":true}"#).unwrap()
);
}
}
#[test]
fn test_set_region_option_max_row_group_row_count_try_from() {
let pb = PbOption {
+23 -4
View File
@@ -431,13 +431,13 @@ impl TableMeta {
new_options.extra_options.remove(PRESERVE_ROW_SEQUENCE);
}
}
SetRegionOption::SkipWal => {
new_options.skip_wal = true;
SetRegionOption::SkipWal(skip_wal) => {
new_options.skip_wal = *skip_wal;
// Keep the explicit table option so it remains distinguishable
// from a value inherited from the schema.
new_options
.extra_options
.insert(SKIP_WAL_KEY.to_string(), true.to_string());
.insert(SKIP_WAL_KEY.to_string(), skip_wal.to_string());
}
}
}
@@ -1926,7 +1926,7 @@ mod tests {
.insert(SKIP_WAL_KEY.to_string(), false.to_string());
let alter_kind = AlterKind::SetTableOptions {
options: vec![SetRegionOption::SkipWal],
options: vec![SetRegionOption::SkipWal(true)],
};
let new_meta = meta
.builder_with_alter_kind("my_table", &alter_kind)
@@ -1943,6 +1943,25 @@ mod tests {
.get(SKIP_WAL_KEY)
.map(String::as_str)
);
let alter_kind = AlterKind::SetTableOptions {
options: vec![SetRegionOption::SkipWal(false)],
};
let new_meta = new_meta
.builder_with_alter_kind("my_table", &alter_kind)
.unwrap()
.build()
.unwrap();
assert!(!new_meta.options.skip_wal);
assert_eq!(
Some("false"),
new_meta
.options
.extra_options
.get(SKIP_WAL_KEY)
.map(String::as_str)
);
}
#[test]
+29 -17
View File
@@ -1,19 +1,31 @@
-----BEGIN CERTIFICATE-----
MIIDFjCCAf6gAwIBAgIUIakjuzbmQLPnVJakOjLtjzjMEI8wDQYJKoZIhvcNAQEL
BQAwGzEZMBcGA1UEAwwQR3JlcHRpbWVEQlJvb3RDQTAeFw0yNTA5MTYxMTM3NTZa
Fw0yNjA5MTYxMTM3NTZaMBUxEzARBgNVBAMMCmdyZXB0aW1lZGIwggEiMA0GCSqG
SIb3DQEBAQUAA4IBDwAwggEKAoIBAQD3WM82qlcPut5bAqFNf3bKSJCeVWaDsVqy
YqdERyyqtXzz9lsOlLlnsOuqiC8uJna9xypbhiFlNGtKmOKnJ34uCzSoXlZy8Vdp
sjlkRxKEWt971IT0KDnC8+du2Y1XvnyNol29wRvq7L4iBtlvL3PObuYIqFPGo2C6
AOiqRCqgekSnd0YjP1EHAIn7ltW5KZIxsZ1beIIvpUaAvKwLroozydy66Dw2gMl0
OuqDDWYVAs+5njReBC1pG8eIuYP6BTHnwQ8g7kw4mle+w77V+gqo0VIqgbivynyn
V0SP0naRAJ31pVhNpg9KjxkmCrHxkfbH47Egvn0I2a8Qlh5rW3AVAgMBAAGjWDBW
MBQGA1UdEQQNMAuCCWxvY2FsaG9zdDAdBgNVHQ4EFgQUjC3lp5jhuLoLJySTIg67
otB0xuAwHwYDVR0jBBgwFoAUcjFqs35tU0JvDB97G0nCHRxZ8rkwDQYJKoZIhvcN
AQELBQADggEBAKBylW71KVI/lbRfBUIZ8eUA5ZkXy5dYbGxC/utL6I0J5GsjnL5x
ShfBdBrIjY9da7b5ep365P56KG6KwG+9XWATtY6pRiCKbHPgpMxCye7ksF0fFFwP
UO7W0ayVnDpl1DqBqLtH+/l70+rfN82JFBmI75Djeof+LsyMeAR8aQB8Ii0ssHq1
t9JJJjeO5vElr7i34gxKoYUuvp5wIGn1Nc06GOYykwGyJyxviXIF6MYDeWrrjYyC
l07YAAsMLt/deAS6WDQHGRE3eu8tDyFT7I4PnlAwzjXuG9lwkByGSPxeZEh4mRa7
fSebkXFmB/NYJVg9mxOM0Ioz0j1pK6I7U7Q=
MIIFRTCCAy2gAwIBAgIUSkDnhXPIILj61plhJa+x1YP8Z6cwDQYJKoZIhvcNAQEL
BQAwGzEZMBcGA1UEAwwQR3JlcHRpbWVEQlJvb3RDQTAeFw0yNjA5MTYxMzAzMzZa
Fw0zNjA5MTMxMzAzMzZaMBUxEzARBgNVBAMMCmdyZXB0aW1lZGIwggIiMA0GCSqG
SIb3DQEBAQUAA4ICDwAwggIKAoICAQDVvnPBkkqibs5OrTNMKzsjea/2IY1NWv60
O7GEGUga+cNt0vECF67hGOtFsO85+EoM8k193/dho+OCb11oza6PE/rentTfMipY
w2VPV/8g6cuvqYlujONvduuLJY0GfHp9KEDt6XO23UcvXpvwFsOKMek1jutUTMNL
jSuOn+vRKsrV//np5ypWSI/2vBBiITTgdzZKfFdkO77p6LKGGPN29m5tZVhb65Z7
Vy6iJp5Bjpyu3j2feIKDtJyHePMCO7GJyJRddF2gpjAnernJI+ObWFdZenDhv3FG
xHyXqVdwsYDDLCsafQbHho4VXQcO3krBA1KN449gjnbv++KUPjP8rlfGKeXI/3Ka
/wKePhjgCI4I7EsF34Bua52AeoczvTv8uPqkrbhsqqcj1cB5h0mIaIKiGqMyNKhz
gDTcQ/4yz4uF6wlNiD3Bs5BxhwaA3u+TKp6VzxwLDy/2MJG0plI5aglFNTBKbuYh
XMDq4OIehg3cuSGyS0RfQp+5akpC9lCbxFecpRTKY81hBBlJBdcRZ2R4htMYM01s
BYa/Qa5KDSe8kKO0DBu1TonTroplqulDxUUV5wc+9Hk8yMKIp0m2OLzcslsZHJMe
0g5xnvBVaTjwXLZLmQeht5YUXB2pYny83omEcQBxdd4qaSi3WYq4L8/d4t4x+ft+
q5tgUZesMwIDAQABo4GGMIGDMAkGA1UdEwQCMAAwCwYDVR0PBAQDAgWgMBMGA1Ud
JQQMMAoGCCsGAQUFBwMCMBQGA1UdEQQNMAuCCWxvY2FsaG9zdDAdBgNVHQ4EFgQU
oi+s++Rc5jGNaHYGSp3+vnhWyUUwHwYDVR0jBBgwFoAUecaZaPw6wTbDM7sipqM6
8MkZhO8wDQYJKoZIhvcNAQELBQADggIBACsjiHkTo3KrwjRkQPjj4RipRg+O+nkN
vy33TDf4Ouj3AO1cMYdbYOvC933arg4wys2QL/cqg8oiqkUKqhdNweFS1yTtzeDv
eyqWxYBD9fUV0iO+pOEX0uuBlBY4g8+SMmz4GQijcQEUiyEttNb2mEwtEeu2k7/k
GtZyRv/+qaeQt/rA/uuUibvE77y/Q8N2RIXkEEAmVXTuSpuN53+wiam0SNJyhxTj
CIId2njhMQcn+X/EnSWWWaaC5HqFNVwd1gx9laN3rMi11RKCn2reSjLclxLGX9vJ
Ri/bwX8IGPkhNRP66BiNnQaWqwxK/pfAWAAw1eHgO/oYzx4InVf0a7LkDy6g5+gU
mM75p5/luVXWhUnw9UzopJM7iaI4TUeGu1fII+M2z8cq45Njbo9DrakdiBnIxZjJ
zp+PcGbeEq0t9oujlNPQYmtMTD9EVxDHxXTcJe6faX3dc0pY0NM5h1fpvG7I/F3K
6TGjikBVzQH8O5xso73NFYvwSXuHj9oXLoFV4Rtvkf/VoxQ9nFcyj4lG0J9uofhL
hUNZ39dXrDKb6+OSKrWaLgWTTFK+X+uEjkak4GguM3BCZeXqCDUCfChI7Av8XsLq
EqfYgMaaBlo4kgglOZqfLl2Rp8Z07en8Xuuo6Tc8d9FJzG39sCPwAuXz5s/zutcp
G8ZBd3h+78oR
-----END CERTIFICATE-----
+50 -26
View File
@@ -1,28 +1,52 @@
-----BEGIN PRIVATE KEY-----
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQD3WM82qlcPut5b
AqFNf3bKSJCeVWaDsVqyYqdERyyqtXzz9lsOlLlnsOuqiC8uJna9xypbhiFlNGtK
mOKnJ34uCzSoXlZy8VdpsjlkRxKEWt971IT0KDnC8+du2Y1XvnyNol29wRvq7L4i
BtlvL3PObuYIqFPGo2C6AOiqRCqgekSnd0YjP1EHAIn7ltW5KZIxsZ1beIIvpUaA
vKwLroozydy66Dw2gMl0OuqDDWYVAs+5njReBC1pG8eIuYP6BTHnwQ8g7kw4mle+
w77V+gqo0VIqgbivynynV0SP0naRAJ31pVhNpg9KjxkmCrHxkfbH47Egvn0I2a8Q
lh5rW3AVAgMBAAECggEAIAUbLhR3GM40IswAXav2kNE8B4M+tiWrfBRRWwFrwt2j
w2FNAGI4bjf1Cip/ERZhoMe7sAV5JUTX7txD2ye9qfBBbNppjo+3pTMxQs4Ak6m/
7KPiYkki0R4KA8ueBJHgReWTBRGOr6beJkGEvGQENeEdXlmMHtbfmrSraf9RFVz9
h3dMJiYmn3Nlf/3mKI7cYr07ed8AH5tjiUkRxQz4RvUDkMNMoXiPBmxqrW0cDmcW
emYOFa+oSIqFQObVnhiEX9tKoGb8n/JmCzJhMcdWKEf8qdNAp28s3iZ6NOfAhcAP
/uEATvqAFlSvy9IeglJ5lw1fW/yMhnsJsSTZfdM0AQKBgQD8BTvKAvgUEVyagsWS
dJCcb5PVIUzTlUq4aYeCqEZVegZHyveHPH0rWqaCg+b5N72CxrLX1GovNFMYRWh0
g6g2tnn5ZPMjxkT/6K194mBFtl5pRY7wYYYxenDXYetGlGR8E2J+rZjrYB8X/nUc
fB8QqHPF4hdNn8JDDRkxS9JMQQKBgQD7QK8BF2XfP+3k/VyalDovCptcl4QX5Lfh
Wg5TU8EewscBgvwCrsiU1j9H9MbJshYzrZ9tkE7cCvAZQMeCdZMZmNixM8kJlIUz
N3ot/0rVvE/CGH0l4tnUP4Hf3F965Q5hZx6LaeTa/T42fNwfKWEos690MhpvpqkZ
VIUsxVp+1QKBgArSNrT/16MU88L2poEZXDQ7Xj+ihGf5WxH8BgOIMiztZmOixWob
IMzBwbCM9VfyGsMIwRkfV8fbGU5LfKa0GH7w6t/Yg9AfbUsHtJtZIwUbRYaDpBWS
Om7ZcQrue2m2aXIiavJBhfVlVTB0jpbAhZzzfrUadiGxIGg8ob5KISeBAoGBAOS9
qdXziFJssFeGdpVgl2wWWwU8CA18tA+c3mfsW2h26ZituGN03rXqK+iFp6ke0emw
ivHDrJfV+Wr0N+7EvexS5AzH2aI4LeBIBJr1zhqye2Z4xw8D7U63lrTIu8B14ta4
e+1uteTNI6OVnw1brdHRbM37g2fcshyWfzwYXGAVAoGAd5NcD+8cGEsEF1we9NVG
jrCCFHaJqga+fsNDPSwtyRBa0gcpyn+JSkmew7UcMC8xsXDWaRJYslz95j6JRIpb
aLO5P9m6yjK+IzwyYHO1YqB4f1JZPZo6I0D74RGWQJrTIWUgf1eWQq3PvzOM+BRp
xL68Sc3xmbloGYxY9m8n8fU=
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
h4gAFxrXO0kT5EIyoB5LTDf17QW1KgE=
-----END PRIVATE KEY-----
+28 -75
View File
@@ -1,77 +1,30 @@
Certificate:
Data:
Version: 3 (0x2)
Serial Number:
24:00:0d:99:42:39:90:76:c8:97:26:b9:54:6a:02:c7:ac:77:05:b7
Signature Algorithm: sha256WithRSAEncryption
Issuer: CN = GreptimeDBRootCA
Validity
Not Before: Sep 16 11:37:56 2025 GMT
Not After : Sep 16 11:37:56 2026 GMT
Subject: CN = GreptimeDBRootCA
Subject Public Key Info:
Public Key Algorithm: rsaEncryption
Public-Key: (2048 bit)
Modulus:
00:d3:38:dc:73:16:e7:4e:54:75:f6:39:37:b1:25:
89:2b:e4:ba:26:2f:2f:b6:d1:3c:7b:c7:f1:54:a1:
74:9d:d6:55:56:1a:e6:b3:ec:e8:d9:db:e7:47:46:
f9:02:48:ee:d8:09:c6:52:7b:5d:49:5b:40:8a:27:
23:49:ee:66:3a:7c:19:38:3a:ae:ac:a8:d8:75:a9:
f5:96:d2:34:9a:ba:50:ff:05:27:ae:3b:3b:19:9c:
30:58:1d:03:9f:a0:10:97:1f:ac:f7:65:d2:cd:ff:
9f:10:72:40:3b:48:02:c7:b7:60:cb:7b:ca:cb:2d:
27:a3:a3:84:7f:86:a4:62:e2:1b:c5:bd:4d:f0:56:
3b:79:f7:fc:79:c9:da:5f:46:f4:36:f6:fe:02:8e:
65:8b:a3:2f:4d:ac:f4:9e:ce:1b:a5:69:aa:ef:6d:
84:58:39:02:b4:29:46:bf:df:22:91:d3:1d:80:22:
2c:22:09:9d:1b:e5:5f:f7:81:14:91:b6:ae:9d:d1:
da:f5:f1:5a:28:a4:f9:78:76:af:0f:cc:f0:2a:9d:
e2:72:e1:ea:15:88:c6:50:c0:59:94:66:de:94:2e:
7d:dd:be:76:8f:5e:7d:19:52:1b:1a:55:9c:77:8f:
38:30:c4:17:6d:fd:d2:bd:51:80:60:0c:33:1c:19:
f9:bf
Exponent: 65537 (0x10001)
X509v3 extensions:
X509v3 Subject Key Identifier:
72:31:6A:B3:7E:6D:53:42:6F:0C:1F:7B:1B:49:C2:1D:1C:59:F2:B9
X509v3 Authority Key Identifier:
72:31:6A:B3:7E:6D:53:42:6F:0C:1F:7B:1B:49:C2:1D:1C:59:F2:B9
X509v3 Basic Constraints: critical
CA:TRUE
Signature Algorithm: sha256WithRSAEncryption
Signature Value:
be:8d:13:94:bd:9c:57:31:9b:80:88:5f:40:5b:fe:60:c3:7f:
a3:b3:e0:7a:41:db:3b:50:9e:63:50:46:f8:14:8a:15:01:b7:
e1:fb:66:93:2a:c5:fc:35:91:cd:30:d2:46:b3:93:f5:6c:25:
da:ee:22:35:be:9c:04:d8:40:3d:40:8d:67:09:dd:d6:fb:52:
4e:07:7f:cf:1a:5a:c0:8b:2c:6f:56:3b:8c:e4:c1:db:1a:e9:
37:93:44:43:18:7e:2d:5f:e2:bc:03:24:7a:c2:d9:6a:14:51:
53:0d:81:c0:1d:1c:04:89:a0:7a:2d:54:23:d8:b3:36:25:b1:
60:f5:71:e2:07:ab:62:49:9e:1a:f4:ef:37:91:1b:95:99:1f:
f9:58:59:54:0d:86:4b:9b:f4:18:fb:34:36:ce:6a:89:54:24:
fa:92:d5:e5:b2:80:c9:8a:b7:aa:86:fc:be:51:49:b1:ce:8f:
a6:be:e9:c7:d8:2e:6d:17:19:7e:e2:7d:a6:d0:38:2a:43:c8:
8d:bf:5c:4c:42:80:1d:36:32:c4:5c:f0:82:8c:89:8b:da:d4:
0d:c2:e7:65:af:ff:3b:d7:f5:e3:c4:3c:34:6d:e4:71:eb:fc:
8f:fc:54:08:f6:d6:4a:06:36:3d:5b:2b:3c:1f:a6:97:88:9d:
e4:b2:25:49
-----BEGIN CERTIFICATE-----
MIIDFzCCAf+gAwIBAgIUJAANmUI5kHbIlya5VGoCx6x3BbcwDQYJKoZIhvcNAQEL
BQAwGzEZMBcGA1UEAwwQR3JlcHRpbWVEQlJvb3RDQTAeFw0yNTA5MTYxMTM3NTZa
Fw0yNjA5MTYxMTM3NTZaMBsxGTAXBgNVBAMMEEdyZXB0aW1lREJSb290Q0EwggEi
MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDTONxzFudOVHX2OTexJYkr5Lom
Ly+20Tx7x/FUoXSd1lVWGuaz7OjZ2+dHRvkCSO7YCcZSe11JW0CKJyNJ7mY6fBk4
Oq6sqNh1qfWW0jSaulD/BSeuOzsZnDBYHQOfoBCXH6z3ZdLN/58QckA7SALHt2DL
e8rLLSejo4R/hqRi4hvFvU3wVjt59/x5ydpfRvQ29v4CjmWLoy9NrPSezhulaarv
bYRYOQK0KUa/3yKR0x2AIiwiCZ0b5V/3gRSRtq6d0dr18VoopPl4dq8PzPAqneJy
4eoViMZQwFmUZt6ULn3dvnaPXn0ZUhsaVZx3jzgwxBdt/dK9UYBgDDMcGfm/AgMB
AAGjUzBRMB0GA1UdDgQWBBRyMWqzfm1TQm8MH3sbScIdHFnyuTAfBgNVHSMEGDAW
gBRyMWqzfm1TQm8MH3sbScIdHFnyuTAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3
DQEBCwUAA4IBAQC+jROUvZxXMZuAiF9AW/5gw3+js+B6Qds7UJ5jUEb4FIoVAbfh
+2aTKsX8NZHNMNJGs5P1bCXa7iI1vpwE2EA9QI1nCd3W+1JOB3/PGlrAiyxvVjuM
5MHbGuk3k0RDGH4tX+K8AyR6wtlqFFFTDYHAHRwEiaB6LVQj2LM2JbFg9XHiB6ti
SZ4a9O83kRuVmR/5WFlUDYZLm/QY+zQ2zmqJVCT6ktXlsoDJireqhvy+UUmxzo+m
vunH2C5tFxl+4n2m0DgqQ8iNv1xMQoAdNjLEXPCCjImL2tQNwudlr/871/XjxDw0
beRx6/yP/FQI9tZKBjY9Wys8H6aXiJ3ksiVJ
MIIFJzCCAw+gAwIBAgIUM8QE5q9TLE1CXDA9Eou/2MgIK7wwDQYJKoZIhvcNAQEL
BQAwGzEZMBcGA1UEAwwQR3JlcHRpbWVEQlJvb3RDQTAeFw0yNjA5MTYxMzAzMzVa
Fw0zNjA5MTMxMzAzMzVaMBsxGTAXBgNVBAMMEEdyZXB0aW1lREJSb290Q0EwggIi
MA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDZamy+GMjnCKpglKq/uTVYzfCg
AohHfNrYg5Io6WJUsXcld4ZfBa9QYQUTha2Sc41HIM5e2BX5N4H7kvJEt+adT0gt
h3gSudlGIEldUETxJYeRlPPWFPp0D/cT2TNJhANDyqu74xnLgoYxCQgXVzOIr2J9
nGYB1fWrZuxZn4rtENvl53Ut/xPi9LuS9rvREmwcP86A4KrnR32sN8fQFxkyhe9u
/IY6e/Imxk445ejyk7n/Nff63nAanHtnP5YziSDj7nQK6ApRzOS6kSBOIynhqNx8
7BegYIEnVd4AmQsIvlTpHeI8A3PyK45vXXFcTnJHuY4yKhzpvi0sd+rlDtEjayyf
JY8nFKrNTAZid+Ip0r5GGZDF07hn+XcwZiQMQdNnvZevLO+hzHpGnNfuk9vNzgxv
TDYet7mtl3Ryx/oSW8hM+ptcUJ/ui9G4T420NJFBDyLyjp/4I0Cg8NdguAnJULTd
ZfMxyXnXRMeV+ekjDOmswR5LfrGkPErBn24ZT0Rj+GzBcnlsosyUvLGD0bBwg+MS
+hQJ5UiXfosmLZ0z4DNmiJNER5G/+TEcsb4lKc9t0tmT+s/SsecSh/e1yEdzuLyI
0a41KdaQvNbsK1KlJUd1deezxnrLSa1L8IrYHDlERqzevSgqFgliM0yostHV12y5
4O8dZ6R3ZxKglgNlLQIDAQABo2MwYTAdBgNVHQ4EFgQUecaZaPw6wTbDM7sipqM6
8MkZhO8wHwYDVR0jBBgwFoAUecaZaPw6wTbDM7sipqM68MkZhO8wDwYDVR0TAQH/
BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwDQYJKoZIhvcNAQELBQADggIBAI9HQGPw
kVAeRTTqHJfeaprxEMX+Me9HP5IOKdQBBKM2YajA8f/rHXBRJIxEW6+NSX6y72eb
fRXNEt3bCiLd7ZKJACcsnGjE5Ot30QbG1TdO6bRp0sXxmcHrYPZAsRoluRs0TMOb
2mkKhkh386KwevAiA8l36BnCvlcq1Re8sNIbG22TNoQtZqa8Da2pOwctZuAGKDPg
ahV9ulIL4eAkoZm8akaPjwwLNWZNB7o4h7H6zMtTHlI71gtQ86lNvu5pKZ9JAhOK
cVP8nFBEayDw7dPK6YgQEV85jPL2mPNTmkLyL2t2olOYrsH2MZxBSYgo86uSRohG
tHj7x8MEo43vJOKSewvrt2RehO6bIq7t4be/Dp5xLBlKp5zCrJFwmAs2n+tyQ3lY
455cbFO0km7IJUvf+6yd+Q5gsCT3aPTdFppGSxLellcc0bA1Qs6vQL48AIFbFJK8
qcGJNOpXlTieXQUA4pSAxRif8EJ0mWEbDDOpHtA7rSQUS+0ElbaRbBagupXnqNVJ
qTUIBFCeJlw7v4q0gbws2Nmu9xPp97fBrlSpBn0MbIrdp3tL2gFH39lOp6f1OLPM
x8Ejb+WfXrxFWcvWKcfDEo0oaISyiGzjAefQY5ukY3S1eY3AdH74qDQiJODLsBJD
n81vhFD2M5+a5Yusw7GFjvd7QnLu8P5zgLNu
-----END CERTIFICATE-----
+50 -26
View File
@@ -1,28 +1,52 @@
-----BEGIN PRIVATE KEY-----
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDTONxzFudOVHX2
OTexJYkr5LomLy+20Tx7x/FUoXSd1lVWGuaz7OjZ2+dHRvkCSO7YCcZSe11JW0CK
JyNJ7mY6fBk4Oq6sqNh1qfWW0jSaulD/BSeuOzsZnDBYHQOfoBCXH6z3ZdLN/58Q
ckA7SALHt2DLe8rLLSejo4R/hqRi4hvFvU3wVjt59/x5ydpfRvQ29v4CjmWLoy9N
rPSezhulaarvbYRYOQK0KUa/3yKR0x2AIiwiCZ0b5V/3gRSRtq6d0dr18VoopPl4
dq8PzPAqneJy4eoViMZQwFmUZt6ULn3dvnaPXn0ZUhsaVZx3jzgwxBdt/dK9UYBg
DDMcGfm/AgMBAAECggEAHji3ihwP0mxRSH6dUlJdkxMx8z6zJARCFC/ifspvJhpB
sYU91yahlnJZYCBuhn4q9mxHnQubqGBPFrVM3konBF03Oo1i6r9p4UQFKKRA9JaN
+j8hk1NUPHDVlfXoy9EaLpVQqUE/bUwS1UaTnCfMhgjd5595v6JhVo+R6LE6Gg+f
aBfvwZT35pZnDG4JNguxl5jCTr1tnyi3QDQkOIjwP0hyjJlO4RZ6KkBZVtphfaiy
+2jEpovAtS831Imph4rCNZ17dmZ2Hn8n7TswUUT8yASqpgR8Eq88y0S9pqOjbK4Z
wbxGGwzLu8amTcRd3HXErBTykbqj0TcKbE+e0CQomQKBgQD1WIWXxwrVQehuiHqr
nMxbU/RwsLglv2oGJ4/yrobvkRMK7O4zscVKDuRciHrAHL7AlYj6wj1jCE9HjJ6n
LJtaE89xTxKEX6SJkFrHBfzdqZYzMfbbK+vASsFYrkCTcxHqjKhGKtepxjdf5KwD
lXHQsKQklLWORnpzktmwlSkF3QKBgQDcZP2eliaHrLV/uTQqJMO4AHU+1+sm2yXL
EockQ7t6zxydhixVsKcfMei7K+f3G/3geehIInEW+CanJyo6CVc8LTHp/w3jN+Pl
bR2Vhc4M9jhl/JucPpCrpNFb0+eb52IgRoU51HZ6at98fPs1pkEVOEeEXv8BHfPO
w7IHWisqSwKBgAVeTH4Hsur7qDRS9CwxIB+XdK/Kjhz1VUKRNwseWoF/cSU6DfQ/
nYuDS+fibJ0rg9j+7KJgAavbELEGGKqbxUJxUIawLj2LQjRoXjQgo2nOPiUcoMQm
79+jQxvHpE4VpHy3Wy893L4wQiHtBP6HFJh2ZQEP5/EXBbLlsbG1cdJZAoGBAKf2
p3rm/7JCo4gVg3Xi0u42GySrtWDFf0YGdsvies0bbrl2ee9MU/FoPe/3hC9FO/SJ
s8bVyM4yjHKzXHEikUMhRnsIkHLsM1i0EenUMbabJ1xCae1nmSXW1F9hXY/phVUH
qZzrGOgu7ueW8HRwogUkAQ0WwMz/IMIhTlpgE6mFAoGAOKktvlw4LpLsM5d18ycI
LPGlXJBMRsbQVkn9F2gBDm1EudFhSz/UWHVrjIjKy1ldNoIXBTP4k/+mZmgL/sHY
9oy1NswlhZqImz6yedudMsudVzu0mRvZqwIK41bqi8ajkp5YTC4fIgKIILysB+YT
FfVzb/lOZCmysmZm3Mfe8Ug=
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
0qPa/EERUvVLwyx/GENnr1Uks2lgnA==
-----END PRIVATE KEY-----
+29 -75
View File
@@ -1,77 +1,31 @@
Certificate:
Data:
Version: 3 (0x2)
Serial Number:
21:a9:23:bb:36:e6:40:b3:e7:54:96:a4:3a:32:ed:8f:38:cc:10:8e
Signature Algorithm: sha256WithRSAEncryption
Issuer: CN = GreptimeDBRootCA
Validity
Not Before: Sep 16 11:37:56 2025 GMT
Not After : Sep 16 11:37:56 2026 GMT
Subject: CN = greptime
Subject Public Key Info:
Public Key Algorithm: rsaEncryption
Public-Key: (2048 bit)
Modulus:
00:b5:df:d8:cd:4a:f2:6f:d0:1f:75:07:f5:ec:2b:
44:af:59:94:c1:47:e1:8e:ad:a9:84:00:50:25:7f:
91:1b:4d:0b:b7:d1:cb:95:7f:f5:83:f5:c1:c2:4c:
83:6a:2b:99:7b:e6:a8:0a:4d:c5:fc:24:24:17:e1:
30:df:5f:31:0a:97:96:9c:1f:0f:10:63:a2:6d:be:
7c:f9:09:df:6e:87:bd:a0:94:6c:ee:71:60:a1:c2:
9a:8c:04:69:eb:7e:0c:46:57:99:30:a3:46:89:d2:
f2:8e:7e:5b:be:07:77:07:22:bd:6b:4a:6b:dd:40:
e5:d2:bd:a7:d7:18:49:4b:d9:6b:73:64:48:b8:29:
05:64:da:88:9b:2c:f4:eb:88:5e:e7:3e:f7:81:28:
55:56:cb:df:07:23:20:49:39:a3:d8:c7:cf:d1:23:
2b:f2:72:e1:95:1d:1c:72:ac:e7:60:09:40:c2:be:
5c:c0:50:d7:79:6c:28:61:5c:55:b2:2a:2c:99:6b:
29:4c:f7:4d:17:bc:d5:ba:44:56:0e:f0:61:24:42:
05:e2:c0:77:c2:19:53:82:10:c0:31:a7:8f:01:88:
71:87:46:d5:2b:00:75:33:06:3d:e3:bf:ff:dc:a9:
6b:63:8f:14:60:88:a7:c6:85:18:4a:33:31:56:fb:
1e:cb
Exponent: 65537 (0x10001)
X509v3 extensions:
X509v3 Subject Alternative Name:
DNS:localhost, IP Address:127.0.0.1
X509v3 Subject Key Identifier:
0D:54:D1:9C:02:61:1C:37:23:8C:E2:1D:4F:F6:00:66:C0:FD:97:54
X509v3 Authority Key Identifier:
72:31:6A:B3:7E:6D:53:42:6F:0C:1F:7B:1B:49:C2:1D:1C:59:F2:B9
Signature Algorithm: sha256WithRSAEncryption
Signature Value:
5d:61:15:7d:9e:60:05:bb:2c:e6:b0:a4:eb:9b:15:91:00:d3:
91:65:b6:87:bd:29:ce:17:93:8b:81:fb:c3:ed:97:6e:c9:93:
1d:5b:0d:35:1e:a8:cd:9d:ca:ae:70:84:cc:af:c6:ff:a9:d3:
b1:1c:cc:da:90:15:7b:85:19:80:3d:c9:dc:94:5d:28:fa:c5:
2d:32:6e:48:d9:f1:74:62:18:d0:f7:9d:3a:80:8b:75:7a:4d:
4a:a7:7a:8a:b4:28:d4:6d:6e:98:d6:96:4d:fc:a0:b1:ee:89:
35:b8:3f:b7:07:29:34:dc:dc:be:c9:eb:6d:fd:fd:86:3d:11:
e1:8c:3e:00:84:91:15:ef:d5:c9:6a:ec:31:73:02:fb:81:ef:
1a:c9:7f:62:a0:7f:8e:d0:d7:f0:b4:59:ba:a3:3f:e8:fb:04:
b3:ad:f9:9a:4e:ee:11:64:ca:a5:5a:4f:4a:36:ad:0e:55:82:
37:6e:8b:6f:b3:a6:89:e0:a9:45:df:f4:1f:d3:7c:76:bd:d8:
a4:9d:44:7d:89:70:76:b4:4f:5f:02:99:13:3c:d5:11:d5:6f:
ca:f8:7a:26:a1:c3:d9:c1:cb:3a:89:7c:97:bf:0d:90:0d:78:
37:ec:cb:6d:0f:13:a8:8f:f1:3e:f6:a9:82:22:1e:97:9c:e4:
0f:8d:6c:3e
-----BEGIN CERTIFICATE-----
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
RKSX3LpRl9tau6O+oV8=
-----END CERTIFICATE-----
+50 -26
View File
@@ -1,28 +1,52 @@
-----BEGIN PRIVATE KEY-----
MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQC139jNSvJv0B91
B/XsK0SvWZTBR+GOramEAFAlf5EbTQu30cuVf/WD9cHCTINqK5l75qgKTcX8JCQX
4TDfXzEKl5acHw8QY6Jtvnz5Cd9uh72glGzucWChwpqMBGnrfgxGV5kwo0aJ0vKO
flu+B3cHIr1rSmvdQOXSvafXGElL2WtzZEi4KQVk2oibLPTriF7nPveBKFVWy98H
IyBJOaPYx8/RIyvycuGVHRxyrOdgCUDCvlzAUNd5bChhXFWyKiyZaylM900XvNW6
RFYO8GEkQgXiwHfCGVOCEMAxp48BiHGHRtUrAHUzBj3jv//cqWtjjxRgiKfGhRhK
MzFW+x7LAgMBAAECggEAGJr2d3JVRuqZkY6zJvFaSH15EjEJrfDcByoNAxW1aZkh
0GDyBwFvE3GTIMM94DDn1zWxtDsott+jgRY2iMcVw8DAX9IVNVAVcH1zO4n/ZV0N
5YSkZsSF9ecgW3hZZW0BzMkbG35raPKrXWXGPvjpkdiRrHXpY98yIzGvXE/tJ/VA
nuXdopFQf8ZRI0qZ29e+zO14xGS0kVD4cPfL5CKboI1dosR2MK1AtH69MY7itZXV
aut0OSY0enHxHGyraZMUOxs4plviGqSTgCsE7K5dBwTYIkoKpbwU7TKob+eyySpW
pO5Xn4gnOkMDj2ePd8mVhg5nuhqc3c2+hRJzayfIGQKBgQD/sR3mWo08DkoI1bpT
K/EkN/WISTIhFDUsAoHtQw6vtDVY0Fmo+AjHqtipNaqvrrOMDmfASC3O4PWZqlJ6
Vwr1cECts6qpQqEw5Grz8jjOPJJsWkRaQRPyMKSGFwcqvdBNZUlArrdeN7fJVvQf
DrKnfz4IBelA2fzN6HiMKjXLJQKBgQC2F/TtT4AnICTGhaev2sgS6CPcIeh9jyAz
ZmgtvgSxn7UKO8+Ycj62gwD2i5vpbrzDnDQ4EbOOFjabPEQeQgRSIfkbYFiNH3TM
5kq6NpHQ53VqavFBg/Ht6cUl3elpVLZip6UxTMHHABerKBBPvDcJqPl2cF0yTgn9
UwYYnKiXLwKBgQDnnKPN9vZ6kSMFD71A+8vql/oEovESv8BOhlL0/co+5IlyUwD0
qwEhBKRP2noru5+OXbojlf5QqLwwnvUpNtGJdE5SSi5sNhvXMwGiyiYkwv+LwNSr
wMl1mBRgFln6oIggo4AQZHNbmDMV4Xsu6txwFzW0hM4+XXe/eTZBGTczNQKBgQCE
isWN4ts+H2iFVA3ts8sD8BMCDXBjzEU9BKJ6ASD7hWtKiqSdqzK4uHx/+LjA3gjq
PQ3JbiELIGIY9nICsDzi57Wh1qw99JKY/luQVV4n+AIANt1qubbkXM9Ss8dNEws6
nhpg2yUFS6zuuVe+IhrRnjwOmNVNY35j9QWv7n/MowKBgQDw0anDxc1QXvfI5lf8
CaYhGxSWaxLDD2XEle5gQRpyBBGIzR1sQ2StZdcyeRbRTUI+5xsrC9FSg9fSqhh7
7hRtt9jIHEQERElWIYgAe/1KdOzvCV+neWBKvjNdv6pinW/z+9dI4sWzrQNbMIo3
ggjGS2/AuAzFB2Pb1QsfoBHA7Q==
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
5cgCHJYsFbwfNHAnfVOPT9SA2EaiOA==
-----END PRIVATE KEY-----
@@ -0,0 +1,348 @@
-- Incremental aggregate state merge over a partitioned source (two regions).
-- A WHERE-filtered flow mixes scalar aggregates (sum, count) with aggregate
-- state columns (hll, stddev_pop_state, uddsketch_state); each later delta in
-- the same time window must merge its state with the state already persisted
-- in the sink. Covers two nonempty deltas across both regions, one round whose
-- delta is empty because the flow's predicate drops the newly inserted row,
-- and a final nonempty delta checked against a direct source aggregation.
CREATE TABLE flow_incr_state_merge_input (
host_id INT,
user_id BIGINT,
n INT,
v DOUBLE,
ts TIMESTAMP TIME INDEX,
PRIMARY KEY(host_id, user_id)
)
PARTITION ON COLUMNS (host_id) (
host_id < 3,
host_id >= 3
)
WITH (
append_mode = 'true'
);
Affected Rows: 0
CREATE FLOW flow_incr_state_merge SINK TO flow_incr_state_merge_sink
WITH (experimental_enable_incremental_read = 'true')
AS
SELECT
sum(n) AS total,
count(n) AS row_count,
hll(user_id) AS user_hll,
stddev_pop_state(v) AS v_stddev_state,
uddsketch_state(128, 0.01, v) AS v_sketch_state,
date_bin(INTERVAL '1 minute', ts, '2024-01-01 00:00:00') AS time_window
FROM
flow_incr_state_merge_input
WHERE
v > 0
GROUP BY
time_window;
Affected Rows: 0
-- ==== Phase 1: initial rows in the same time window, one per region ====
INSERT INTO flow_incr_state_merge_input VALUES
(1, 1, 1, 1.0, '2024-01-01 00:00:00'),
(3, 2, 2, 2.0, '2024-01-01 00:00:10');
Affected Rows: 2
-- SQLNESS REPLACE (ADMIN\sFLUSH_FLOW\('\w+'\)\s+\|\n\+-+\+\n\|\s+)[0-9]+\s+\| $1 FLOW_FLUSHED |
ADMIN FLUSH_FLOW('flow_incr_state_merge');
+-------------------------------------------+
| ADMIN FLUSH_FLOW('flow_incr_state_merge') |
+-------------------------------------------+
| FLOW_FLUSHED |
+-------------------------------------------+
-- The first round seeds the sink with a full-snapshot aggregate state.
SELECT
total,
row_count,
time_window
FROM
flow_incr_state_merge_sink
ORDER BY
time_window;
+-------+-----------+---------------------+
| total | row_count | time_window |
+-------+-----------+---------------------+
| 3 | 2 | 2024-01-01T00:00:00 |
+-------+-----------+---------------------+
-- Move the checkpointed source rows and the seeded sink state into SST, so
-- the next round has to merge a new delta with persisted state.
ADMIN FLUSH_TABLE('flow_incr_state_merge_input');
+--------------------------------------------------+
| ADMIN FLUSH_TABLE('flow_incr_state_merge_input') |
+--------------------------------------------------+
| 0 |
+--------------------------------------------------+
ADMIN FLUSH_TABLE('flow_incr_state_merge_sink');
+-------------------------------------------------+
| ADMIN FLUSH_TABLE('flow_incr_state_merge_sink') |
+-------------------------------------------------+
| 0 |
+-------------------------------------------------+
-- ==== Phase 2: first nonempty delta, same window, both regions ====
INSERT INTO flow_incr_state_merge_input VALUES
(2, 3, 3, 3.0, '2024-01-01 00:00:20'),
(4, 4, 4, 4.0, '2024-01-01 00:00:40');
Affected Rows: 2
-- SQLNESS REPLACE (ADMIN\sFLUSH_FLOW\('\w+'\)\s+\|\n\+-+\+\n\|\s+)[0-9]+\s+\| $1 FLOW_FLUSHED |
ADMIN FLUSH_FLOW('flow_incr_state_merge');
+-------------------------------------------+
| ADMIN FLUSH_FLOW('flow_incr_state_merge') |
+-------------------------------------------+
| FLOW_FLUSHED |
+-------------------------------------------+
-- Scalars accumulate; each state column must merge with the persisted state.
SELECT
total,
row_count,
hll_count(hll_merge(user_hll)) AS users,
ROUND(stddev_pop_calc(stddev_pop_merge(v_stddev_state)), 6) AS v_stddev,
ROUND(uddsketch_calc(0.5, uddsketch_merge(128, 0.01, v_sketch_state)), 4) AS v_p50,
time_window
FROM
flow_incr_state_merge_sink
GROUP BY
total,
row_count,
time_window
ORDER BY
time_window;
+-------+-----------+-------+----------+--------+---------------------+
| total | row_count | users | v_stddev | v_p50 | time_window |
+-------+-----------+-------+----------+--------+---------------------+
| 10 | 4 | 4 | 1.118034 | 2.9742 | 2024-01-01T00:00:00 |
+-------+-----------+-------+----------+--------+---------------------+
ADMIN FLUSH_TABLE('flow_incr_state_merge_input');
+--------------------------------------------------+
| ADMIN FLUSH_TABLE('flow_incr_state_merge_input') |
+--------------------------------------------------+
| 0 |
+--------------------------------------------------+
ADMIN FLUSH_TABLE('flow_incr_state_merge_sink');
+-------------------------------------------------+
| ADMIN FLUSH_TABLE('flow_incr_state_merge_sink') |
+-------------------------------------------------+
| 0 |
+-------------------------------------------------+
-- ==== Phase 3: filtered-out row must run one round with an empty delta ====
-- Record the flow's last execution time before the excluded insert: the
-- comparison below proves the empty delta was actually evaluated.
CREATE TABLE flow_incr_state_merge_last_exec (
marker STRING,
last_exec TIMESTAMP TIME INDEX
) WITH (
append_mode = 'true'
);
Affected Rows: 0
-- The background loop runs on its own interval; keep waiting until it has
-- executed at least once so information_schema.flows.last_execution_time is
-- non-null before snapshotting it.
-- SQLNESS SLEEP 8s
INSERT INTO flow_incr_state_merge_last_exec (marker, last_exec)
SELECT
'before_empty_delta',
last_execution_time
FROM
information_schema.flows
WHERE
flow_name = 'flow_incr_state_merge';
Affected Rows: 1
-- The row's timestamp is inside the existing window, so the source write still
-- dirties that window; the flow's WHERE predicate then drops the row from the
-- delta, leaving the delta empty for the window.
INSERT INTO flow_incr_state_merge_input VALUES
(1, 9, 9, -1.0, '2024-01-01 00:00:30');
Affected Rows: 1
-- SQLNESS SLEEP 6s
-- SQLNESS REPLACE (ADMIN\sFLUSH_FLOW\('\w+'\)\s+\|\n\+-+\+\n\|\s+)[0-9]+\s+\| $1 FLOW_FLUSHED |
ADMIN FLUSH_FLOW('flow_incr_state_merge');
+-------------------------------------------+
| ADMIN FLUSH_FLOW('flow_incr_state_merge') |
+-------------------------------------------+
| FLOW_FLUSHED |
+-------------------------------------------+
-- An empty delta must leave the persisted sink states untouched.
SELECT
total,
row_count,
hll_count(hll_merge(user_hll)) AS users,
ROUND(stddev_pop_calc(stddev_pop_merge(v_stddev_state)), 6) AS v_stddev,
ROUND(uddsketch_calc(0.5, uddsketch_merge(128, 0.01, v_sketch_state)), 4) AS v_p50,
time_window
FROM
flow_incr_state_merge_sink
GROUP BY
total,
row_count,
time_window
ORDER BY
time_window;
+-------+-----------+-------+----------+--------+---------------------+
| total | row_count | users | v_stddev | v_p50 | time_window |
+-------+-----------+-------+----------+--------+---------------------+
| 10 | 4 | 4 | 1.118034 | 2.9742 | 2024-01-01T00:00:00 |
+-------+-----------+-------+----------+--------+---------------------+
-- last_execution_time must have advanced past the mark taken above, proving
-- the dirty window triggered a new round even though its delta was empty. A
-- NULL comparison means the mark never advanced: fail explicitly instead of
-- silently treating it as "unchanged".
INSERT INTO flow_incr_state_merge_last_exec (marker, last_exec)
SELECT
'after_empty_delta',
last_execution_time
FROM
information_schema.flows
WHERE
flow_name = 'flow_incr_state_merge';
Affected Rows: 1
SELECT
(
SELECT
last_exec
FROM
flow_incr_state_merge_last_exec
WHERE
marker = 'after_empty_delta'
) > (
SELECT
last_exec
FROM
flow_incr_state_merge_last_exec
WHERE
marker = 'before_empty_delta'
) AS empty_delta_executed;
+----------------------+
| empty_delta_executed |
+----------------------+
| true |
+----------------------+
-- Flush the input before the final delta so each phase's delta is isolated in
-- its own round; the background loop may have already consumed the last
-- insert, which is fine — the values below only depend on the merge outcome.
ADMIN FLUSH_TABLE('flow_incr_state_merge_input');
+--------------------------------------------------+
| ADMIN FLUSH_TABLE('flow_incr_state_merge_input') |
+--------------------------------------------------+
| 0 |
+--------------------------------------------------+
ADMIN FLUSH_TABLE('flow_incr_state_merge_sink');
+-------------------------------------------------+
| ADMIN FLUSH_TABLE('flow_incr_state_merge_sink') |
+-------------------------------------------------+
| 0 |
+-------------------------------------------------+
-- ==== Phase 4: final nonempty delta, same window, both regions ====
INSERT INTO flow_incr_state_merge_input VALUES
(1, 5, 5, 5.0, '2024-01-01 00:00:05'),
(3, 6, 6, 6.0, '2024-01-01 00:00:55');
Affected Rows: 2
-- SQLNESS REPLACE (ADMIN\sFLUSH_FLOW\('\w+'\)\s+\|\n\+-+\+\n\|\s+)[0-9]+\s+\| $1 FLOW_FLUSHED |
ADMIN FLUSH_FLOW('flow_incr_state_merge');
+-------------------------------------------+
| ADMIN FLUSH_FLOW('flow_incr_state_merge') |
+-------------------------------------------+
| FLOW_FLUSHED |
+-------------------------------------------+
-- Merge the final delta with the persisted states and compare the merged
-- result against a direct aggregation over the accepted source rows.
SELECT
merged.total = direct.total AS total_matches,
merged.row_count = direct.row_count AS count_matches,
merged.users = direct.users AS hll_matches,
abs(merged.v_stddev - direct.v_stddev) < 1e-9 AS stddev_matches,
ROUND(merged.v_p50, 4) = ROUND(direct.v_p50, 4) AS p50_matches
FROM
(
SELECT
total,
row_count,
hll_count(hll_merge(user_hll)) AS users,
stddev_pop_calc(stddev_pop_merge(v_stddev_state)) AS v_stddev,
uddsketch_calc(0.5, uddsketch_merge(128, 0.01, v_sketch_state)) AS v_p50
FROM
flow_incr_state_merge_sink
GROUP BY
total,
row_count
) AS merged,
(
SELECT
sum(n) AS total,
count(n) AS row_count,
hll_count(hll(user_id)) AS users,
stddev_pop(v) AS v_stddev,
uddsketch_calc(0.5, uddsketch_state(128, 0.01, v)) AS v_p50
FROM
flow_incr_state_merge_input
WHERE
v > 0
) AS direct;
+---------------+---------------+-------------+----------------+-------------+
| total_matches | count_matches | hll_matches | stddev_matches | p50_matches |
+---------------+---------------+-------------+----------------+-------------+
| true | true | true | true | true |
+---------------+---------------+-------------+----------------+-------------+
-- Clean up
DROP FLOW flow_incr_state_merge;
Affected Rows: 0
DROP TABLE flow_incr_state_merge_input;
Affected Rows: 0
DROP TABLE flow_incr_state_merge_sink;
Affected Rows: 0
DROP TABLE flow_incr_state_merge_last_exec;
Affected Rows: 0
@@ -0,0 +1,232 @@
-- Incremental aggregate state merge over a partitioned source (two regions).
-- A WHERE-filtered flow mixes scalar aggregates (sum, count) with aggregate
-- state columns (hll, stddev_pop_state, uddsketch_state); each later delta in
-- the same time window must merge its state with the state already persisted
-- in the sink. Covers two nonempty deltas across both regions, one round whose
-- delta is empty because the flow's predicate drops the newly inserted row,
-- and a final nonempty delta checked against a direct source aggregation.
CREATE TABLE flow_incr_state_merge_input (
host_id INT,
user_id BIGINT,
n INT,
v DOUBLE,
ts TIMESTAMP TIME INDEX,
PRIMARY KEY(host_id, user_id)
)
PARTITION ON COLUMNS (host_id) (
host_id < 3,
host_id >= 3
)
WITH (
append_mode = 'true'
);
CREATE FLOW flow_incr_state_merge SINK TO flow_incr_state_merge_sink
WITH (experimental_enable_incremental_read = 'true')
AS
SELECT
sum(n) AS total,
count(n) AS row_count,
hll(user_id) AS user_hll,
stddev_pop_state(v) AS v_stddev_state,
uddsketch_state(128, 0.01, v) AS v_sketch_state,
date_bin(INTERVAL '1 minute', ts, '2024-01-01 00:00:00') AS time_window
FROM
flow_incr_state_merge_input
WHERE
v > 0
GROUP BY
time_window;
-- ==== Phase 1: initial rows in the same time window, one per region ====
INSERT INTO flow_incr_state_merge_input VALUES
(1, 1, 1, 1.0, '2024-01-01 00:00:00'),
(3, 2, 2, 2.0, '2024-01-01 00:00:10');
-- SQLNESS REPLACE (ADMIN\sFLUSH_FLOW\('\w+'\)\s+\|\n\+-+\+\n\|\s+)[0-9]+\s+\| $1 FLOW_FLUSHED |
ADMIN FLUSH_FLOW('flow_incr_state_merge');
-- The first round seeds the sink with a full-snapshot aggregate state.
SELECT
total,
row_count,
time_window
FROM
flow_incr_state_merge_sink
ORDER BY
time_window;
-- Move the checkpointed source rows and the seeded sink state into SST, so
-- the next round has to merge a new delta with persisted state.
ADMIN FLUSH_TABLE('flow_incr_state_merge_input');
ADMIN FLUSH_TABLE('flow_incr_state_merge_sink');
-- ==== Phase 2: first nonempty delta, same window, both regions ====
INSERT INTO flow_incr_state_merge_input VALUES
(2, 3, 3, 3.0, '2024-01-01 00:00:20'),
(4, 4, 4, 4.0, '2024-01-01 00:00:40');
-- SQLNESS REPLACE (ADMIN\sFLUSH_FLOW\('\w+'\)\s+\|\n\+-+\+\n\|\s+)[0-9]+\s+\| $1 FLOW_FLUSHED |
ADMIN FLUSH_FLOW('flow_incr_state_merge');
-- Scalars accumulate; each state column must merge with the persisted state.
SELECT
total,
row_count,
hll_count(hll_merge(user_hll)) AS users,
ROUND(stddev_pop_calc(stddev_pop_merge(v_stddev_state)), 6) AS v_stddev,
ROUND(uddsketch_calc(0.5, uddsketch_merge(128, 0.01, v_sketch_state)), 4) AS v_p50,
time_window
FROM
flow_incr_state_merge_sink
GROUP BY
total,
row_count,
time_window
ORDER BY
time_window;
ADMIN FLUSH_TABLE('flow_incr_state_merge_input');
ADMIN FLUSH_TABLE('flow_incr_state_merge_sink');
-- ==== Phase 3: filtered-out row must run one round with an empty delta ====
-- Record the flow's last execution time before the excluded insert: the
-- comparison below proves the empty delta was actually evaluated.
CREATE TABLE flow_incr_state_merge_last_exec (
marker STRING,
last_exec TIMESTAMP TIME INDEX
) WITH (
append_mode = 'true'
);
-- The background loop runs on its own interval; keep waiting until it has
-- executed at least once so information_schema.flows.last_execution_time is
-- non-null before snapshotting it.
-- SQLNESS SLEEP 8s
INSERT INTO flow_incr_state_merge_last_exec (marker, last_exec)
SELECT
'before_empty_delta',
last_execution_time
FROM
information_schema.flows
WHERE
flow_name = 'flow_incr_state_merge';
-- The row's timestamp is inside the existing window, so the source write still
-- dirties that window; the flow's WHERE predicate then drops the row from the
-- delta, leaving the delta empty for the window.
INSERT INTO flow_incr_state_merge_input VALUES
(1, 9, 9, -1.0, '2024-01-01 00:00:30');
-- SQLNESS SLEEP 6s
-- SQLNESS REPLACE (ADMIN\sFLUSH_FLOW\('\w+'\)\s+\|\n\+-+\+\n\|\s+)[0-9]+\s+\| $1 FLOW_FLUSHED |
ADMIN FLUSH_FLOW('flow_incr_state_merge');
-- An empty delta must leave the persisted sink states untouched.
SELECT
total,
row_count,
hll_count(hll_merge(user_hll)) AS users,
ROUND(stddev_pop_calc(stddev_pop_merge(v_stddev_state)), 6) AS v_stddev,
ROUND(uddsketch_calc(0.5, uddsketch_merge(128, 0.01, v_sketch_state)), 4) AS v_p50,
time_window
FROM
flow_incr_state_merge_sink
GROUP BY
total,
row_count,
time_window
ORDER BY
time_window;
-- last_execution_time must have advanced past the mark taken above, proving
-- the dirty window triggered a new round even though its delta was empty. A
-- NULL comparison means the mark never advanced: fail explicitly instead of
-- silently treating it as "unchanged".
INSERT INTO flow_incr_state_merge_last_exec (marker, last_exec)
SELECT
'after_empty_delta',
last_execution_time
FROM
information_schema.flows
WHERE
flow_name = 'flow_incr_state_merge';
SELECT
(
SELECT
last_exec
FROM
flow_incr_state_merge_last_exec
WHERE
marker = 'after_empty_delta'
) > (
SELECT
last_exec
FROM
flow_incr_state_merge_last_exec
WHERE
marker = 'before_empty_delta'
) AS empty_delta_executed;
-- Flush the input before the final delta so each phase's delta is isolated in
-- its own round; the background loop may have already consumed the last
-- insert, which is fine — the values below only depend on the merge outcome.
ADMIN FLUSH_TABLE('flow_incr_state_merge_input');
ADMIN FLUSH_TABLE('flow_incr_state_merge_sink');
-- ==== Phase 4: final nonempty delta, same window, both regions ====
INSERT INTO flow_incr_state_merge_input VALUES
(1, 5, 5, 5.0, '2024-01-01 00:00:05'),
(3, 6, 6, 6.0, '2024-01-01 00:00:55');
-- SQLNESS REPLACE (ADMIN\sFLUSH_FLOW\('\w+'\)\s+\|\n\+-+\+\n\|\s+)[0-9]+\s+\| $1 FLOW_FLUSHED |
ADMIN FLUSH_FLOW('flow_incr_state_merge');
-- Merge the final delta with the persisted states and compare the merged
-- result against a direct aggregation over the accepted source rows.
SELECT
merged.total = direct.total AS total_matches,
merged.row_count = direct.row_count AS count_matches,
merged.users = direct.users AS hll_matches,
abs(merged.v_stddev - direct.v_stddev) < 1e-9 AS stddev_matches,
ROUND(merged.v_p50, 4) = ROUND(direct.v_p50, 4) AS p50_matches
FROM
(
SELECT
total,
row_count,
hll_count(hll_merge(user_hll)) AS users,
stddev_pop_calc(stddev_pop_merge(v_stddev_state)) AS v_stddev,
uddsketch_calc(0.5, uddsketch_merge(128, 0.01, v_sketch_state)) AS v_p50
FROM
flow_incr_state_merge_sink
GROUP BY
total,
row_count
) AS merged,
(
SELECT
sum(n) AS total,
count(n) AS row_count,
hll_count(hll(user_id)) AS users,
stddev_pop(v) AS v_stddev,
uddsketch_calc(0.5, uddsketch_state(128, 0.01, v)) AS v_p50
FROM
flow_incr_state_merge_input
WHERE
v > 0
) AS direct;
-- Clean up
DROP FLOW flow_incr_state_merge;
DROP TABLE flow_incr_state_merge_input;
DROP TABLE flow_incr_state_merge_sink;
DROP TABLE flow_incr_state_merge_last_exec;
+41 -4
View File
@@ -54,6 +54,11 @@ SELECT * FROM system_metrics;
| host2 | idc_a | 80.0 | 70.3 | 90.0 | 2022-11-03T03:39:57.450 |
+-------+-------+----------+-------------+-----------+-------------------------+
-- A table created without a real WAL provider cannot enable WAL later.
ALTER TABLE system_metrics SET 'skip_wal' = 'false';
Error: 1001(Unsupported), Unsupported operation setting skip_wal = 'false' without an existing WAL provider
DROP TABLE system_metrics;
Affected Rows: 0
@@ -95,15 +100,45 @@ SHOW CREATE TABLE alter_skip_wal;
| | ) |
+----------------+-----------------------------------------------+
-- This row is intentionally not written to WAL.
INSERT INTO alter_skip_wal VALUES ('host2', 2, 2000);
Affected Rows: 1
ALTER TABLE alter_skip_wal SET 'skip_wal' = 'false';
Error: 1004(InvalidArguments), Invalid set table option request: Invalid set region option request, key: skip_wal, value: false
Affected Rows: 0
SHOW CREATE TABLE alter_skip_wal;
+----------------+-----------------------------------------------+
| Table | Create Table |
+----------------+-----------------------------------------------+
| alter_skip_wal | CREATE TABLE IF NOT EXISTS "alter_skip_wal" ( |
| | "host" STRING NULL, |
| | "val" DOUBLE NULL, |
| | "ts" TIMESTAMP(3) NOT NULL, |
| | TIME INDEX ("ts"), |
| | PRIMARY KEY ("host") |
| | ) |
| | |
| | ENGINE=mito |
| | WITH( |
| | skip_wal = 'false' |
| | ) |
+----------------+-----------------------------------------------+
-- Repeating the transition is idempotent.
ALTER TABLE alter_skip_wal SET 'skip_wal' = 'false';
Affected Rows: 0
ALTER TABLE alter_skip_wal UNSET 'skip_wal';
Error: 1004(InvalidArguments), Invalid unset table option request: Invalid set region option request, key: skip_wal
INSERT INTO alter_skip_wal VALUES ('host2', 2, 2000);
-- This row uses the restored WAL provider.
INSERT INTO alter_skip_wal VALUES ('host3', 3, 3000);
Affected Rows: 1
@@ -114,9 +149,10 @@ SELECT * FROM alter_skip_wal ORDER BY ts;
+-------+-----+---------------------+
| host1 | 1.0 | 1970-01-01T00:00:01 |
| host2 | 2.0 | 1970-01-01T00:00:02 |
| host3 | 3.0 | 1970-01-01T00:00:03 |
+-------+-----+---------------------+
-- The post-ALTER row can be lost because restart does not flush skip-WAL memtables.
-- Re-enabling WAL does not retroactively write the skipped row to WAL.
-- SQLNESS ARG restart=true
SHOW CREATE TABLE alter_skip_wal;
@@ -133,7 +169,7 @@ SHOW CREATE TABLE alter_skip_wal;
| | |
| | ENGINE=mito |
| | WITH( |
| | skip_wal = 'true' |
| | skip_wal = 'false' |
| | ) |
+----------------+-----------------------------------------------+
@@ -143,6 +179,7 @@ SELECT * FROM alter_skip_wal ORDER BY ts;
| host | val | ts |
+-------+-----+---------------------+
| host1 | 1.0 | 1970-01-01T00:00:01 |
| host3 | 3.0 | 1970-01-01T00:00:03 |
+-------+-----+---------------------+
DROP TABLE alter_skip_wal;
+14 -2
View File
@@ -29,6 +29,9 @@ ADMIN flush_table('system_metrics');
-- SQLNESS ARG restart=true
SELECT * FROM system_metrics;
-- A table created without a real WAL provider cannot enable WAL later.
ALTER TABLE system_metrics SET 'skip_wal' = 'false';
DROP TABLE system_metrics;
CREATE TABLE alter_skip_wal (
@@ -45,15 +48,24 @@ ALTER TABLE alter_skip_wal SET 'skip_wal' = 'true';
SHOW CREATE TABLE alter_skip_wal;
-- This row is intentionally not written to WAL.
INSERT INTO alter_skip_wal VALUES ('host2', 2, 2000);
ALTER TABLE alter_skip_wal SET 'skip_wal' = 'false';
SHOW CREATE TABLE alter_skip_wal;
-- Repeating the transition is idempotent.
ALTER TABLE alter_skip_wal SET 'skip_wal' = 'false';
ALTER TABLE alter_skip_wal UNSET 'skip_wal';
INSERT INTO alter_skip_wal VALUES ('host2', 2, 2000);
-- This row uses the restored WAL provider.
INSERT INTO alter_skip_wal VALUES ('host3', 3, 3000);
SELECT * FROM alter_skip_wal ORDER BY ts;
-- The post-ALTER row can be lost because restart does not flush skip-WAL memtables.
-- Re-enabling WAL does not retroactively write the skipped row to WAL.
-- SQLNESS ARG restart=true
SHOW CREATE TABLE alter_skip_wal;
+1 -1
View File
@@ -6,7 +6,7 @@
# exercised by nightly runs (--nightly-window); they are not retained in the PR
# window. Run .github/scripts/update-compat-versions.py --update --published-only
# after release tags land.
from_versions = ["v1.1.4", "v1.2.0"]
from_versions = ["v1.1.4", "v1.2.1"]
# Recent releases that must be able to reopen tables written by the PR build.
downgrade_to_versions = ["v1.1.4"]