Files
greptimedb/tests-integration/tests/event_recorder_test_util.rs
T
Weny Xu ed4271af40 feat(procedure): record event actor (#8849)
* feat(event): record procedure actor

Signed-off-by: WenyXu <wenymedia@gmail.com>

* test: handle streamed region migration output

Signed-off-by: WenyXu <wenymedia@gmail.com>

* test: cover procedure actors across SQL protocols

Signed-off-by: WenyXu <wenymedia@gmail.com>

---------

Signed-off-by: WenyXu <wenymedia@gmail.com>
2026-08-14 09:36:01 +00:00

215 lines
6.7 KiB
Rust

// Copyright 2023 Greptime Team
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use std::future::Future;
use std::sync::Arc;
use std::time::Duration;
use client::OutputData;
use common_recordbatch::RecordBatches;
use datatypes::arrow::array::AsArray;
use datatypes::arrow::datatypes::{DataType as ArrowDataType, UInt32Type};
use frontend::instance::Instance;
use servers::query_handler::sql::SqlQueryHandler;
use session::context::QueryContext;
const MAX_ATTEMPTS: usize = 60;
const POLL_INTERVAL: Duration = Duration::from_millis(250);
/// Asserts through the supplied SQL executor that a table procedure records
/// the expected actor for both submission and completion events.
pub(crate) async fn assert_procedure_actor_by_table<F, Fut>(
event_type: &str,
table_name: &str,
actor: &str,
mut execute: F,
) where
F: FnMut(String) -> Fut,
Fut: Future<Output = bool>,
{
let query = format!(
"SELECT count(DISTINCT json_get_string(procedure_trigger, 'type')) = 2 \
AND count(*) = count(CASE WHEN actor = '{actor}' THEN 1 END) AS actor_matches \
FROM greptime_private.events \
WHERE type = '{event_type}' AND table_name = '{table_name}' \
AND json_get_string(procedure_trigger, 'type') IN ('Submitted', 'Succeeded')"
);
for _ in 0..MAX_ATTEMPTS {
if execute(query.clone()).await {
return;
}
tokio::time::sleep(POLL_INTERVAL).await;
}
panic!("timed out waiting for procedure actor: {query}");
}
/// Asserts that submission and completion events record the expected actor.
pub(crate) async fn assert_procedure_actor(
instance: &Arc<Instance>,
procedure_id: &str,
actor: Option<&str>,
) {
let actor_predicate = actor.map_or_else(
|| "count(actor) = 0".to_string(),
|actor| format!("count(*) = count(CASE WHEN actor = '{actor}' THEN 1 END)"),
);
for trigger in ["Submitted", "Succeeded"] {
assert_eventually_eq(
instance,
&format!(
"SELECT count(*) > 0 AND {actor_predicate} AS actor_matches FROM greptime_private.events WHERE procedure_id = '{procedure_id}' AND json_get_string(procedure_trigger, 'type') = '{trigger}'"
),
"\
+---------------+
| actor_matches |
+---------------+
| true |
+---------------+",
)
.await;
}
}
/// Asserts that the query eventually returns exactly one event.
pub(crate) async fn assert_single_event(instance: &Arc<Instance>, query: &str) {
for _ in 0..MAX_ATTEMPTS {
if event_count_is_one(instance, query).await {
return;
}
tokio::time::sleep(POLL_INTERVAL).await;
}
panic!("timed out waiting for event: {query}");
}
/// Returns the first non-null string from `column` once the query produces one.
pub(crate) async fn find_eventually_string(
instance: &Arc<Instance>,
query: &str,
column: &str,
) -> String {
for _ in 0..MAX_ATTEMPTS {
if let Some(value) = query_first_string(instance, query, column).await {
return value;
}
tokio::time::sleep(POLL_INTERVAL).await;
}
panic!("timed out waiting for event query: {query}");
}
/// Returns the first non-null u32 from `column` once the query produces one.
pub(crate) async fn find_eventually_u32(
instance: &Arc<Instance>,
query: &str,
column: &str,
) -> u32 {
for _ in 0..MAX_ATTEMPTS {
if let Some(value) = query_first_u32(instance, query, column).await {
return value;
}
tokio::time::sleep(POLL_INTERVAL).await;
}
panic!("timed out waiting for event query: {query}");
}
/// Asserts that the query eventually returns the expected pretty-printed rows.
pub(crate) async fn assert_eventually_eq(instance: &Arc<Instance>, query: &str, expected: &str) {
let mut last_actual = None;
for _ in 0..MAX_ATTEMPTS {
last_actual = query_pretty_print(instance, query).await;
if last_actual.as_deref() == Some(expected) {
return;
}
tokio::time::sleep(POLL_INTERVAL).await;
}
panic!(
"timed out waiting for event query: {query}\nexpected:\n{expected}\nactual:\n{}",
last_actual.as_deref().unwrap_or("<query failed>")
);
}
async fn event_count_is_one(instance: &Arc<Instance>, query: &str) -> bool {
query_pretty_print(instance, query).await.as_deref()
== Some(
"\
+-------------+
| event_count |
+-------------+
| 1 |
+-------------+",
)
}
async fn query_first_string(instance: &Arc<Instance>, query: &str, column: &str) -> Option<String> {
let batches = query_record_batches(instance, query).await?;
let batch = batches.take().into_iter().next()?;
let column = batch.column_by_name(column)?;
match column.data_type() {
ArrowDataType::Utf8 => column
.as_string::<i32>()
.iter()
.next()
.flatten()
.map(ToString::to_string),
ArrowDataType::LargeUtf8 => column
.as_string::<i64>()
.iter()
.next()
.flatten()
.map(ToString::to_string),
ArrowDataType::Utf8View => column
.as_string_view()
.iter()
.next()
.flatten()
.map(ToString::to_string),
data_type => panic!("expected a string column, got {data_type:?}"),
}
}
async fn query_first_u32(instance: &Arc<Instance>, query: &str, column: &str) -> Option<u32> {
let batches = query_record_batches(instance, query).await?;
let batch = batches.take().into_iter().next()?;
batch
.column_by_name(column)?
.as_primitive::<UInt32Type>()
.iter()
.next()
.flatten()
}
async fn query_pretty_print(instance: &Arc<Instance>, query: &str) -> Option<String> {
query_record_batches(instance, query)
.await?
.pretty_print()
.ok()
}
async fn query_record_batches(instance: &Arc<Instance>, query: &str) -> Option<RecordBatches> {
let output = instance
.do_query(query, QueryContext::arc())
.await
.remove(0)
.ok()?;
let OutputData::Stream(stream) = output.data else {
return None;
};
RecordBatches::try_collect(stream).await.ok()
}