mirror of
https://github.com/GreptimeTeam/greptimedb.git
synced 2026-10-12 15:02:32 +00:00
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:
@@ -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
@@ -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",
|
||||
|
||||
@@ -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" }
|
||||
|
||||
@@ -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"] }
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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]);
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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<()> {
|
||||
|
||||
@@ -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(®ion_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,
|
||||
|
||||
@@ -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 ®ion_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 ®ion_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);
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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()))
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}),
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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,
|
||||
®ion_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(|®ion_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
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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, ®ion.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, ®ion.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, ®ion.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, ®ion.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) {
|
||||
|
||||
@@ -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(®ion, 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!(®ion.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();
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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=
|
||||
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
|
||||
G8ZBd3h+78oR
|
||||
-----END CERTIFICATE-----
|
||||
|
||||
@@ -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-----
|
||||
|
||||
@@ -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-----
|
||||
|
||||
@@ -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-----
|
||||
|
||||
@@ -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-----
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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"]
|
||||
|
||||
Reference in New Issue
Block a user