Files
greptimedb/tests-integration/tests/region_migration.rs
T
+1 c60311973d fix: backport v1.2.0 correctness and liveness fixes (#9023)
* fix(mito2): fence checkpoints during region transitions (#8847)

* fix: fence checkpoints during region transitions

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

* test(datanode): fix transient downgrade setup

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

* test(mito2): fix checkpoint lifecycle test setup

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

* test(mito2): cover cancelled downgrade waiter retry

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

* fix(mito2): fence direct follower transitions

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

* test: trim checkpoint transition coverage

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

* refactor(mito2): clarify checkpoint task lifecycle

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

---------

Signed-off-by: WenyXu <wenymedia@gmail.com>
(cherry picked from commit a502dfdefd)

* fix(mito2): split SSTs at primary key series boundaries (#8888)

* fix(mito2): split SSTs at series boundaries

Signed-off-by: Lei, HUANG <ratuthomm@gmail.com>

* test(mito2): cover SST splitting without primary key

Also document the sortedness precondition and the series boundary
split semantics on write_all_flat/write_all_flat_as_primary_key and
the new split helpers.

Signed-off-by: Lei, HUANG <ratuthomm@gmail.com>

* test(mito2): avoid per-row Vec allocation for empty primary key

Signed-off-by: Lei, HUANG <ratuthomm@gmail.com>

* Update src/mito2/src/sst/parquet/writer.rs

Co-authored-by: Yingwen <realevenyag@gmail.com>

---------

Signed-off-by: Lei, HUANG <ratuthomm@gmail.com>
Co-authored-by: Yingwen <realevenyag@gmail.com>
(cherry picked from commit 4ac3423261)

* fix(meta): allow manual migration from offline datanodes (#8934)

* fix(meta): allow migration from offline datanodes

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

* test: fix offline migration event actor

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

* test: read migration routes from metadata

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

---------

Signed-off-by: WenyXu <wenymedia@gmail.com>
(cherry picked from commit 8a473c5bf0)

* fix(flight): bound DoGet response wait (#8943)

* fix(flight): defer datanode query initialization

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

* fix(client): retain Flight stream peer context

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

* fix(client): improve Flight stream diagnostics

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

---------

Signed-off-by: WenyXu <wenymedia@gmail.com>
(cherry picked from commit 28398138ec)

* fix(query): keep INSERT timestamp conversion out of the source query (#8911)

* fix(query): keep INSERT timestamp conversion out of the source query

Interpreting an INSERT's string timestamps used to work by pushing the
conversion down into the source query, which changed what that query
means. Two consequences:

- Pushing through a UNION's DISTINCT moved the dedup key from the raw
  strings to parsed instants, so rows spelling the same instant
  differently collapsed into one. On an append-only table that is a
  silently dropped row.
- A UNION branch that needed no conversion (a NULL, or an explicit cast)
  made the whole column give up, leaving sibling branches on UTC while
  the rest of the row used the session timezone.

Convert at the assignment instead, by routing its cast through a
timezone-carrying timestamp type and back. Arrow applies the timezone
when a cast target carries one, and stripping it afterwards preserves
the value. The source query is no longer touched, so both cases go away
and the tree-walking rewrite (roughly 160 lines) is deleted.

The rewrite reads source types, so it now runs TypeCoercion first: a
UNION still carries its loose per-branch schema before coercion, and
retargeting a cast whose input later becomes a timestamp would shift the
value rather than reinterpret it.

Signed-off-by: Dennis Zhuang <killme2008@gmail.com>

* fix(query): address review on INSERT assignment rewrite

- Clone the input `Arc` instead of the whole subtree, and only rebuild it
  when a `Values` row actually changes.
- Defer cloning the cast source until the literal-folding path has been
  ruled out.
- Move the UTC check onto `Timezone::is_utc`, replacing a bare string
  compare.
- Cover a prepared `INSERT ... VALUES (?)`: an untyped placeholder types
  as `Null`, so the assignment cast is left for parameter substitution.

Signed-off-by: Dennis Zhuang <killme2008@gmail.com>

---------

Signed-off-by: Dennis Zhuang <killme2008@gmail.com>
(cherry picked from commit 1851f6bf4d)

* test: renew etcd TLS certificates (#8956)

Signed-off-by: WenyXu <wenymedia@gmail.com>
(cherry picked from commit 144f83528d)

* fix: update tokio-postgres and correct explain/fetch cursor output schema (#8955)

* chore(deps): update tokio-postgres

* fix: describing fetch cursor and analyze

(cherry picked from commit b31f05eb59)
Signed-off-by: discord9 <55937128+discord9@users.noreply.github.com>

* fix(flow): avoid insert select HTTP/2 stalls

Signed-off-by: discord9 <55937128+discord9@users.noreply.github.com>
(cherry picked from commit d4a3d88cd7)

* fix(query): share record batch forwarding for DML

Signed-off-by: discord9 <55937128+discord9@users.noreply.github.com>
(cherry picked from commit c34bec197c)

* fix(mysql): interpret prepared statement datetime params in session timezone (#8923)

* fix(mysql): interpret prepared statement datetime params in session timezone

Binary DATETIME parameters of server-side prepared statements were
converted as if UTC, ignoring the session timezone set via SET time_zone.
Convert them with the session timezone and add an integration test
covering prepared inserts and predicates under Asia/Shanghai.

Signed-off-by: wy471x <wy471x@gmail.com>

* refactor: share naive datetime timezone policy via common-time

Address review feedback on the prepared-statement timezone fix:

- Expose Timestamp::from_naive_datetime in common-time so the DST policy
  (gap -> error, ambiguous -> earlier instant) lives in one place, shared
  by the text protocol (Timestamp::from_str) and the MySQL binary protocol.
- Route the MySQL prepared-statement datetime conversion through it.
- Match the target type before converting datetime params so
  PreparedStmtTypeMismatch fails fast without wasted conversion.
- Use the short Timezone import form for consistency with the rest of servers.

Signed-off-by: wy471x <wy471x@gmail.com>

---------

Signed-off-by: wy471x <wy471x@gmail.com>
Co-authored-by: Ning Sun <sunng@protonmail.com>
(cherry picked from commit aaa843104b)

* fix(mito2): use target sequence for foreign SSTs (#8946)

* fix(mito2): use target sequence for foreign SSTs

Signed-off-by: discord9 <55937128+discord9@users.noreply.github.com>

* chore(mito2): address foreign SST review feedback

Signed-off-by: discord9 <55937128+discord9@users.noreply.github.com>

---------

Signed-off-by: discord9 <55937128+discord9@users.noreply.github.com>
(cherry picked from commit bd7d2c1dfa)

* fix: postgres describe for more statements (#8974)

* fix: postgres describe for more statements

Signed-off-by: Ning Sun <sunning@greptime.com>

* fix: cover more show statements

Signed-off-by: Ning Sun <sunning@greptime.com>

* fix: address review comments

- add missing `clippy::too_many_arguments` allow on
  `query_from_information_schema_dataframe` (CI clippy failure)
- take `&ShowKind` in the information-schema dataframe helper so `kind`
  is no longer cloned at every call site; only the WHERE arm (which needs
  an owned expression for `sql_to_expr`) clones internally
- document why re-applying TQL explain formats never overwrites an
  existing value (per-query context state)

Signed-off-by: Ning Sun <sunning@greptime.com>

* chore: trim comments to essentials

Signed-off-by: Ning Sun <sunning@greptime.com>

---------

Signed-off-by: Ning Sun <sunning@greptime.com>
(cherry picked from commit d32cd77505)

* fix(promql): resolve derived labels in aggregation arithmetic (#8994)

Signed-off-by: shuiyisong <xixing.sys@gmail.com>
(cherry picked from commit c4dafb5826)

* fix(deps): narrow postgres lockfile update

Signed-off-by: discord9 <55937128+discord9@users.noreply.github.com>

* fix(deps): normalize remaining x509 lock reference

Signed-off-by: discord9 <55937128+discord9@users.noreply.github.com>

* fix(client): isolate query and control transports

Signed-off-by: discord9 <55937128+discord9@users.noreply.github.com>

* docs(frontend): clarify adaptive window purpose

Signed-off-by: discord9 <55937128+discord9@users.noreply.github.com>

* test(grpc): satisfy clippy in transport isolation coverage

Signed-off-by: discord9 <55937128+discord9@users.noreply.github.com>

* fix(pipeline): coalesce concurrent pipeline cache misses (#9022)

Signed-off-by: discord9 <55937128+discord9@users.noreply.github.com>

* Revert "test(grpc): satisfy clippy in transport isolation coverage"

This reverts commit 888bcedc96.

(cherry picked from commit e0216b4074485bcffaae53348a17091532cb3600)
Signed-off-by: discord9 <55937128+discord9@users.noreply.github.com>

* Revert "docs(frontend): clarify adaptive window purpose"

This reverts commit 84cf69d099.

(cherry picked from commit da30e605ad7ebfb2a3fa86ea44b67405e5afde98)
Signed-off-by: discord9 <55937128+discord9@users.noreply.github.com>

* Revert "fix(client): isolate query and control transports"

This reverts commit a1d2dbb050.

(cherry picked from commit 03f5c31f544d3d3852beddfa87cff34d04a646ae)
Signed-off-by: discord9 <55937128+discord9@users.noreply.github.com>

* refactor(json2): add bounded auto-expansion to the JSON2 vector builder (#8909)

* refactor(json2): add bounded auto-expansion to the JSON2 vector builder

Signed-off-by: luofucong <luofc@foxmail.com>

* resolve PR comments

Signed-off-by: luofucong <luofc@foxmail.com>

* fix ci

Signed-off-by: luofucong <luofc@foxmail.com>

---------

Signed-off-by: luofucong <luofc@foxmail.com>
(cherry picked from commit 76f08d2b3f)
(cherry picked from commit 640fa63bd8ee098fac8e8d62d6fb7ca5af4ddcd0)

* refactor(json2): optimize JSON2 building without auto-expanded paths (#8928)

* refactor(json2): optimize JSON2 building without auto-expanded paths

Signed-off-by: luofucong <luofc@foxmail.com>

* resolve PR comments

Signed-off-by: luofucong <luofc@foxmail.com>

* avoid panicking memtable write

Signed-off-by: luofucong <luofc@foxmail.com>

---------

Signed-off-by: luofucong <luofc@foxmail.com>
(cherry picked from commit 7fd0a7bb98)
(cherry picked from commit 63c68ee60101e2a09e14420051f8f889b41a2a3b)

* refactor(json2): support querying v2 storage layout (#8940)

* feat(json2): support querying v2 storage layout

- route missing JSON2 paths to the v2 remainder field
- reconstruct complete values from explicit fields and remainder data
- preserve root JSON2 columns across projections and filters
- support nested JSON values in json_get string results
- add and reorganize JSON2 sqlness coverage

Signed-off-by: luofucong <luofc@foxmail.com>

* resolve PR comments

Signed-off-by: luofucong <luofc@foxmail.com>

---------

Signed-off-by: luofucong <luofc@foxmail.com>
(cherry picked from commit 932f87f7a8)
(cherry picked from commit 5af1394a1f319bb99cafa9474b8a5a075be1e805)

* feat(pipeline): support table-aware JSON2 transforms (#8964)

* feat(pipeline): support table-aware JSON2 transforms

Signed-off-by: shuiyisong <xixing.sys@gmail.com>

* feat(pipeline): support JSON2 type hints in transforms

Signed-off-by: shuiyisong <xixing.sys@gmail.com>

* fix(pipeline): default failed JSON2 transforms to null

Signed-off-by: shuiyisong <xixing.sys@gmail.com>

* refactor(json2): distinguish invalid settings from layout errors

Signed-off-by: shuiyisong <xixing.sys@gmail.com>

---------

Signed-off-by: shuiyisong <xixing.sys@gmail.com>
(cherry picked from commit fb86f6573e)
(cherry picked from commit 18a21a9ce017b6887c857a3f3bbb9a83aa7f968b)

* refactor: json2 v2 storage layout (#8979)

* refactor: json2 v2 storage layout

Signed-off-by: luofucong <luofc@foxmail.com>

* resolve PR comments

Signed-off-by: luofucong <luofc@foxmail.com>

* fix ci

Signed-off-by: luofucong <luofc@foxmail.com>

* rethinking when "needs_remainder"

Signed-off-by: luofucong <luofc@foxmail.com>

* restore "ReadColumns"

Signed-off-by: luofucong <luofc@foxmail.com>

* resolve PR comments

Signed-off-by: luofucong <luofc@foxmail.com>

* fix ci

Signed-off-by: luofucong <luofc@foxmail.com>

---------

Signed-off-by: luofucong <luofc@foxmail.com>
(cherry picked from commit 529f046110)
(cherry picked from commit 7628d795197c786872730c3a5ef36c5c662d4322)

* feat(json2): support JSON2 paths in SQL functions (#9007)

feat(query): support JSON2 paths in SQL functions

Update the DataFusion fork to expose scalar function planning hooks.

Infer JSON2 path output types from scalar, aggregate, and window function signatures, while preserving the default Utf8View behavior for functions that accept arbitrary inputs.

Add unit and sqlness coverage for type conflicts, mixed typed and untyped JSON paths, filters, aggregates, and window functions.

Signed-off-by: luofucong <luofc@foxmail.com>
(cherry picked from commit 15317a131b)
(cherry picked from commit a945cbc7a4398e6bee74d4d2c67528772faf398d)

* feat(json2): support empty and null JSON2 value (#9010)

* feat(json2): support empty and null JSON2 value

Signed-off-by: fys <fengys1996@gmail.com>

* test(json2): cover explicit NULL and omitted-column inserts

Signed-off-by: fys <fengys1996@gmail.com>

* fix: cargo fmt

Signed-off-by: fys <fengys1996@gmail.com>

* fix: infer empty JSON object as object type

Signed-off-by: fys <fengys1996@gmail.com>

---------

Signed-off-by: fys <fengys1996@gmail.com>
(cherry picked from commit 05c65f54a8)
(cherry picked from commit 05c27452ea5e958fab85e0ba4b82f5df205a5d08)

* feat(json2): support list indexing for JSON2 columns (#9013)

feat(query): support list indexing for JSON2 columns

Extend JSON2 paths through DataFusion field-access planning, including nested list indexes and object fields following an index.

Preserve Variant reads for bracket JSONPath expressions and normalize dot accesses after subscripts to work around the current DataFusion planner limitation.

Add unit and sqlness coverage for nested indexes, type conflicts, missing paths, flushes, and compacted SSTs.

Signed-off-by: luofucong <luofc@foxmail.com>
(cherry picked from commit d62a5a990a)
(cherry picked from commit c11d8647feb30ecdd5ba1bc8ed4df78999801672)

* refactor(json2): concretize JSON2 schemas at merge scan boundaries (#9016)

* refactor(json2): concretize JSON2 schemas at merge scan boundaries

Infer concrete JSON2 output types from remote plans and expose them on MergeScanLogicalPlan before physical planning. Recompute affected local schemas and remove the JSON2-specific rewrite from MergeScanExec.

Add SQLness coverage for whole JSON2 columns in windows and joins.

Signed-off-by: luofucong <luofc@foxmail.com>

* fix ci

Signed-off-by: luofucong <luofc@foxmail.com>

---------

Signed-off-by: luofucong <luofc@foxmail.com>
(cherry picked from commit 84bd993131)
(cherry picked from commit 51c98a7e31a929fc33ff5133714bb47108f7539b)

* fix(json2): keep empty structs in remainder (#9027)

Signed-off-by: luofucong <luofc@foxmail.com>
(cherry picked from commit d67d3501a9)
(cherry picked from commit b3f2c1cc5bf0cfddb7c0670ac8f3522eb67c35a0)

* fix(mito2): remove stale scan test imports

Signed-off-by: discord9 <55937128+discord9@users.noreply.github.com>
(cherry picked from commit d63bbe6aef298f5b8e960bb02eed08743a6684ee)

* test(query): expect bounded merge scan partitions

Signed-off-by: discord9 <55937128+discord9@users.noreply.github.com>
(cherry picked from commit 7bcef46b1c010d2e73ec49be7581c216e6b5b1b6)

* style(mito2): remove trailing whitespace

Signed-off-by: discord9 <55937128+discord9@users.noreply.github.com>

* test(sqlness): refresh bounded partition plans

Signed-off-by: discord9 <55937128+discord9@users.noreply.github.com>

* test(sqlness): normalize CPU-dependent partition plans

Signed-off-by: discord9 <55937128+discord9@users.noreply.github.com>

---------

Signed-off-by: WenyXu <wenymedia@gmail.com>
Signed-off-by: Lei, HUANG <ratuthomm@gmail.com>
Signed-off-by: Dennis Zhuang <killme2008@gmail.com>
Signed-off-by: discord9 <55937128+discord9@users.noreply.github.com>
Signed-off-by: wy471x <wy471x@gmail.com>
Signed-off-by: Ning Sun <sunning@greptime.com>
Signed-off-by: shuiyisong <xixing.sys@gmail.com>
Signed-off-by: luofucong <luofc@foxmail.com>
Signed-off-by: fys <fengys1996@gmail.com>
Co-authored-by: WenyXu <wenymedia@gmail.com>
Co-authored-by: Lei, HUANG <ratuthomm@gmail.com>
Co-authored-by: Yingwen <realevenyag@gmail.com>
Co-authored-by: dennis zhuang <killme2008@gmail.com>
Co-authored-by: Ning Sun <sunng@protonmail.com>
Co-authored-by: wy471x <wy471x@gmail.com>
Co-authored-by: Ning Sun <sunning@greptime.com>
Co-authored-by: shuiyisong <xixing.sys@gmail.com>
Co-authored-by: luofucong <luofc@foxmail.com>
Co-authored-by: fys <fengys1996@gmail.com>
2026-09-07 19:29:54 +08:00

1614 lines
55 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::sync::Arc;
use std::time::Duration;
use client::{DEFAULT_CATALOG_NAME, DEFAULT_SCHEMA_NAME, Database, OutputData};
use common_catalog::consts::DEFAULT_PRIVATE_SCHEMA_NAME;
use common_event_recorder::event_table::{
ACTOR_COLUMN, REGION_ID_COLUMN, REGION_MIGRATION_DST_NODE_ID_COLUMN,
REGION_MIGRATION_SRC_NODE_ID_COLUMN, REGION_MIGRATION_TRIGGER_REASON_COLUMN,
};
use common_event_recorder::{
DEFAULT_EVENTS_TABLE_NAME, DEFAULT_FLUSH_INTERVAL_SECONDS, EVENTS_TABLE_TIMESTAMP_COLUMN_NAME,
EVENTS_TABLE_TYPE_COLUMN_NAME, PersistentEventContext, TriggerReason,
};
use common_meta::distributed_time_constants::default_distributed_time_constants;
use common_meta::key::{RegionDistribution, RegionRoleSet, TableMetadataManagerRef};
use common_meta::peer::Peer;
use common_meta::rpc::store::BatchDeleteRequest;
use common_procedure::ProcedureContext;
use common_procedure::event::{
EVENTS_TABLE_PROCEDURE_ID_COLUMN_NAME, EVENTS_TABLE_PROCEDURE_STATE_COLUMN_NAME,
};
use common_query::Output;
use common_recordbatch::RecordBatches;
use common_telemetry::info;
use common_test_util::recordbatch::check_output_stream;
use common_test_util::temp_dir::create_temp_dir;
use common_wal::config::kafka::common::{KafkaConnectionConfig, KafkaTopicConfig};
use common_wal::config::kafka::{DatanodeKafkaConfig, MetasrvKafkaConfig};
use common_wal::config::{DatanodeWalConfig, MetasrvWalConfig};
use datatypes::arrow::array::AsArray;
use datatypes::arrow::datatypes::UInt64Type;
use frontend::instance::Instance;
use futures::future::BoxFuture;
use meta_srv::error;
use meta_srv::error::Result as MetaResult;
use meta_srv::event::region_migration::REGION_MIGRATION_EVENT_TYPE;
use meta_srv::key::DatanodeLeaseKey;
use meta_srv::metasrv::SelectorContext;
use meta_srv::procedure::region_migration::{
RegionMigrationProcedureTask, RegionMigrationTriggerReason,
};
use meta_srv::selector::{Selector, SelectorOptions};
use sea_query::{Alias, Expr, Iden, Order, PostgresQueryBuilder, Query};
use servers::error::Result as ServerResult;
use servers::query_handler::sql::SqlQueryHandler;
use session::context::{QueryContext, QueryContextRef};
use store_api::storage::RegionId;
use table::metadata::TableId;
use tests_integration::cluster::{GreptimeDbCluster, GreptimeDbClusterBuilder};
use tests_integration::test_util::{
PEER_PLACEHOLDER_ADDR, StorageType, get_test_store_config, setup_authenticated_grpc_database,
};
use uuid::Uuid;
const TEST_TABLE_NAME: &str = "migration_target";
const PROCEDURE_ACTOR: &str = "procedure_actor";
const PROCEDURE_ACTOR_PASSWORD: &str = "procedure_actor_pwd";
#[macro_export]
macro_rules! region_migration_test {
($service:ident, $($(#[$meta:meta])* $test:ident),*,) => {
paste::item! {
mod [<integration_region_migration_ $service:lower _test>] {
$(
#[tokio::test(flavor = "multi_thread")]
$(
#[$meta]
)*
async fn [< $test >]() {
let store_type = tests_integration::test_util::StorageType::$service;
if store_type.test_on() {
common_telemetry::init_default_ut_logging();
common_wal::maybe_skip_kafka_integration_test!();
let endpoints = common_wal::test_util::get_kafka_endpoints();
$crate::region_migration::$test(store_type, endpoints).await
}
}
)*
}
}
};
}
#[macro_export]
macro_rules! region_migration_tests {
($($service:ident),*) => {
$(
region_migration_test!(
$service,
test_region_migration,
test_region_migration_by_sql,
test_region_migration_with_offline_source_by_sql,
test_region_migration_multiple_regions,
test_region_migration_all_regions,
test_region_migration_incorrect_from_peer,
test_region_migration_incorrect_region_id,
test_metric_table_region_migration_by_sql,
);
)*
};
}
/// A naive region migration test.
pub async fn test_region_migration(store_type: StorageType, endpoints: Vec<String>) {
let cluster_name = "test_region_migration";
let peer_factory = |id| Peer {
id,
addr: PEER_PLACEHOLDER_ADDR.to_string(),
};
// Prepares test cluster.
let (store_config, _guard) = get_test_store_config(&store_type);
let home_dir = create_temp_dir("test_migration_data_home");
let datanodes = 5u64;
let builder = GreptimeDbClusterBuilder::new(cluster_name).await;
let const_selector = Arc::new(ConstNodeSelector::new(vec![
peer_factory(1),
peer_factory(2),
peer_factory(3),
]));
let cluster = builder
.with_datanodes(datanodes as u32)
.with_store_config(store_config)
.with_datanode_wal_config(DatanodeWalConfig::Kafka(DatanodeKafkaConfig {
connection: KafkaConnectionConfig {
broker_endpoints: endpoints.clone(),
..Default::default()
},
..Default::default()
}))
.with_metasrv_wal_config(MetasrvWalConfig::Kafka(MetasrvKafkaConfig {
connection: KafkaConnectionConfig {
broker_endpoints: endpoints,
..Default::default()
},
kafka_topic: KafkaTopicConfig {
num_topics: 3,
topic_name_prefix: Uuid::new_v4().to_string(),
..Default::default()
},
..Default::default()
}))
.with_shared_home_dir(Arc::new(home_dir))
.with_meta_selector(const_selector.clone())
.build(true)
.await;
let mut logical_timer = 1685508715000;
let table_metadata_manager = cluster.metasrv.table_metadata_manager().clone();
// Prepares test table.
let table_id = prepare_testing_table(&cluster).await;
// Inserts data
let results = insert_values(cluster.fe_instance(), logical_timer).await;
logical_timer += 1000;
for result in results {
assert!(matches!(result.unwrap().data, OutputData::AffectedRows(1)));
}
// The region distribution
let mut distribution = find_region_distribution(&table_metadata_manager, table_id).await;
// Selecting target of region migration.
let region_migration_manager = cluster.metasrv.region_migration_manager();
let (from_peer_id, from_regions) = distribution.pop_first().unwrap();
info!(
"Selecting from peer: {from_peer_id}, and regions: {:?}",
from_regions
);
let (to_peer_id, mut to_regions) = distribution.pop_first().unwrap();
info!(
"Selecting to peer: {to_peer_id}, and regions: {:?}",
to_regions
);
let region_id = RegionId::new(table_id, from_regions.leader_regions[0]);
// Trigger region migration.
let procedure = region_migration_manager
.submit_procedure(
ProcedureContext::from_event_context(PersistentEventContext::new(
TriggerReason::Manual,
)),
RegionMigrationProcedureTask::new(
region_id,
peer_factory(from_peer_id),
peer_factory(to_peer_id),
Duration::from_millis(1000),
RegionMigrationTriggerReason::Manual,
),
)
.await
.unwrap();
info!("Started region procedure: {}!", procedure.unwrap());
// Prepares expected region distribution.
to_regions
.leader_regions
.extend(from_regions.leader_regions);
to_regions
.follower_regions
.extend(from_regions.follower_regions);
// Keeps asc order.
to_regions.sort();
distribution.insert(to_peer_id, to_regions);
// Waits condition
wait_condition(
Duration::from_secs(10),
Box::pin(async move {
loop {
let region_migration =
find_region_distribution(&table_metadata_manager, table_id).await;
if region_migration == distribution {
info!("Found new distribution: {region_migration:?}");
break;
} else {
info!("Found the unexpected distribution: {region_migration:?}, expected: {distribution:?}");
tokio::time::sleep(Duration::from_millis(200)).await;
}
}
}),
)
.await;
// Inserts more table.
let results = insert_values(cluster.fe_instance(), logical_timer).await;
for result in results {
assert!(matches!(result.unwrap().data, OutputData::AffectedRows(1)));
}
// Asserts the writes.
assert_values(cluster.fe_instance()).await;
// Triggers again.
let err = region_migration_manager
.submit_procedure(
ProcedureContext::from_event_context(PersistentEventContext::new(
TriggerReason::Manual,
)),
RegionMigrationProcedureTask::new(
region_id,
peer_factory(from_peer_id),
peer_factory(to_peer_id),
Duration::from_millis(1000),
RegionMigrationTriggerReason::Manual,
),
)
.await
.unwrap_err();
assert!(matches!(err, error::Error::RegionMigrated { .. }));
check_region_migration_events_system_table(
cluster.fe_instance(),
&procedure.unwrap().to_string(),
region_id.as_u64(),
from_peer_id,
to_peer_id,
None,
)
.await;
}
/// A naive metric table region migration test by SQL function
pub async fn test_metric_table_region_migration_by_sql(
store_type: StorageType,
endpoints: Vec<String>,
) {
let cluster_name = "test_region_migration";
let peer_factory = |id| Peer {
id,
addr: PEER_PLACEHOLDER_ADDR.to_string(),
};
// Prepares test cluster.
let (store_config, _guard) = get_test_store_config(&store_type);
let home_dir = create_temp_dir("test_migration_data_home");
let datanodes = 5u64;
let builder = GreptimeDbClusterBuilder::new(cluster_name).await;
let const_selector = Arc::new(ConstNodeSelector::new(vec![
peer_factory(1),
peer_factory(2),
peer_factory(3),
]));
let cluster = builder
.with_datanodes(datanodes as u32)
.with_store_config(store_config)
.with_datanode_wal_config(DatanodeWalConfig::Kafka(DatanodeKafkaConfig {
connection: KafkaConnectionConfig {
broker_endpoints: endpoints.clone(),
..Default::default()
},
..Default::default()
}))
.with_metasrv_wal_config(MetasrvWalConfig::Kafka(MetasrvKafkaConfig {
connection: KafkaConnectionConfig {
broker_endpoints: endpoints,
..Default::default()
},
kafka_topic: KafkaTopicConfig {
num_topics: 3,
topic_name_prefix: Uuid::new_v4().to_string(),
..Default::default()
},
..Default::default()
}))
.with_shared_home_dir(Arc::new(home_dir))
.with_meta_selector(const_selector.clone())
.build(true)
.await;
// Prepares test metric tables.
let table_id = prepare_testing_metric_table(&cluster).await;
let query_ctx = QueryContext::arc();
// Inserts values
run_sql(
cluster.fe_instance(),
r#"INSERT INTO t1 VALUES ('host1',0, 0), ('host2', 1, 1);"#,
query_ctx.clone(),
)
.await
.unwrap();
run_sql(
cluster.fe_instance(),
r#"INSERT INTO t2 VALUES ('job1', 0, 0), ('job2', 1, 1);"#,
query_ctx.clone(),
)
.await
.unwrap();
// The region distribution
let mut distribution = find_region_distribution_by_sql(&cluster, "phy").await;
// Selecting target of region migration.
let (from_peer_id, from_regions) = distribution.pop_first().unwrap();
info!(
"Selecting from peer: {from_peer_id}, and regions: {:?}",
from_regions.leader_regions[0]
);
let to_peer_id = (from_peer_id + 1) % 3;
let region_id = RegionId::new(table_id, from_regions.leader_regions[0]);
// Trigger region migration.
let procedure_id =
trigger_migration_by_sql(&cluster, region_id.as_u64(), from_peer_id, to_peer_id).await;
info!("Started region procedure: {}!", procedure_id);
// Waits condition by checking procedure state
let frontend = cluster.fe_instance().clone();
let procedure_id_for_closure = procedure_id.clone();
wait_condition(
Duration::from_secs(10),
Box::pin(async move {
loop {
let state = query_procedure_by_sql(&frontend, &procedure_id_for_closure).await;
if state == "{\"status\":\"Done\"}" {
info!("Migration done: {state}");
break;
} else {
info!("Migration not finished: {state}");
tokio::time::sleep(Duration::from_millis(200)).await;
}
}
}),
)
.await;
let result = cluster
.frontend
.instance
.do_query("select * from t1 order by host desc", query_ctx.clone())
.await
.remove(0);
let expected = "\
+-------+-------------------------+-----+
| host | ts | val |
+-------+-------------------------+-----+
| host2 | 1970-01-01T00:00:00.001 | 1.0 |
| host1 | 1970-01-01T00:00:00 | 0.0 |
+-------+-------------------------+-----+";
check_output_stream(result.unwrap().data, expected).await;
let result = cluster
.frontend
.instance
.do_query("select * from t2 order by job desc", query_ctx)
.await
.remove(0);
let expected = "\
+------+-------------------------+-----+
| job | ts | val |
+------+-------------------------+-----+
| job2 | 1970-01-01T00:00:00.001 | 1.0 |
| job1 | 1970-01-01T00:00:00 | 0.0 |
+------+-------------------------+-----+";
check_output_stream(result.unwrap().data, expected).await;
check_region_migration_events_system_table(
cluster.fe_instance(),
&procedure_id,
region_id.as_u64(),
from_peer_id,
to_peer_id,
Some("greptime"),
)
.await;
}
/// A naive region migration test by SQL function.
pub async fn test_region_migration_by_sql(store_type: StorageType, endpoints: Vec<String>) {
test_region_migration_by_sql_inner(store_type, endpoints, false).await;
}
/// A region migration test by SQL function with an offline source datanode.
pub async fn test_region_migration_with_offline_source_by_sql(
store_type: StorageType,
endpoints: Vec<String>,
) {
test_region_migration_by_sql_inner(store_type, endpoints, true).await;
}
async fn test_region_migration_by_sql_inner(
store_type: StorageType,
endpoints: Vec<String>,
simulate_offline_source: bool,
) {
let cluster_name = "test_region_migration";
let peer_factory = |id| Peer {
id,
addr: PEER_PLACEHOLDER_ADDR.to_string(),
};
// Prepares test cluster.
let (store_config, _guard) = get_test_store_config(&store_type);
let home_dir = create_temp_dir("test_migration_data_home");
let datanodes = 5u64;
let builder = GreptimeDbClusterBuilder::new(cluster_name).await;
let const_selector = Arc::new(ConstNodeSelector::new(vec![
peer_factory(1),
peer_factory(2),
peer_factory(3),
]));
let mut cluster = builder
.with_datanodes(datanodes as u32)
.with_store_config(store_config)
.with_datanode_wal_config(DatanodeWalConfig::Kafka(DatanodeKafkaConfig {
connection: KafkaConnectionConfig {
broker_endpoints: endpoints.clone(),
..Default::default()
},
..Default::default()
}))
.with_metasrv_wal_config(MetasrvWalConfig::Kafka(MetasrvKafkaConfig {
connection: KafkaConnectionConfig {
broker_endpoints: endpoints,
..Default::default()
},
kafka_topic: KafkaTopicConfig {
num_topics: 3,
topic_name_prefix: Uuid::new_v4().to_string(),
..Default::default()
},
..Default::default()
}))
.with_shared_home_dir(Arc::new(home_dir))
.with_meta_selector(const_selector.clone())
.build(true)
.await;
let table_metadata_manager = cluster.metasrv.table_metadata_manager().clone();
let (actor_db, _actor_grpc_server) = setup_authenticated_grpc_database(
cluster.fe_instance().clone(),
PROCEDURE_ACTOR,
PROCEDURE_ACTOR_PASSWORD,
)
.await;
let mut logical_timer = 1685508715000;
// Prepares test table.
let table_id = prepare_testing_table(&cluster).await;
// Inserts data
let results = insert_values(cluster.fe_instance(), logical_timer).await;
logical_timer += 1000;
for result in results {
assert!(matches!(result.unwrap().data, OutputData::AffectedRows(1)));
}
// The region distribution
let mut distribution = find_region_distribution_by_sql(&cluster, TEST_TABLE_NAME).await;
let old_distribution = distribution.clone();
// Selecting target of region migration.
let region_migration_manager = cluster.metasrv.region_migration_manager().clone();
let (from_peer_id, from_regions) = distribution.pop_first().unwrap();
info!(
"Selecting from peer: {from_peer_id}, and regions: {:?}",
from_regions
);
let (to_peer_id, to_regions) = distribution.pop_first().unwrap();
info!(
"Selecting to peer: {to_peer_id}, and regions: {:?}",
to_regions
);
let region_id = RegionId::new(table_id, from_regions.leader_regions[0]);
// Trigger region migration.
let procedure_id =
trigger_migration_by_grpc(&actor_db, region_id.as_u64(), from_peer_id, to_peer_id).await;
info!("Started region procedure: {}!", procedure_id);
// Waits condition by checking procedure state
let frontend = cluster.fe_instance().clone();
let procedure_id_for_closure = procedure_id.clone();
wait_condition(
Duration::from_secs(10),
Box::pin(async move {
loop {
let state = query_procedure_by_sql(&frontend, &procedure_id_for_closure).await;
if state == "{\"status\":\"Done\"}" {
info!("Migration done: {state}");
break;
} else {
info!("Migration not finished: {state}");
tokio::time::sleep(Duration::from_millis(200)).await;
}
}
}),
)
.await;
check_region_migration_events_system_table(
cluster.fe_instance(),
&procedure_id,
region_id.as_u64(),
from_peer_id,
to_peer_id,
Some(PROCEDURE_ACTOR),
)
.await;
// Inserts more table.
let results = insert_values(cluster.fe_instance(), logical_timer).await;
for result in results {
assert!(matches!(result.unwrap().data, OutputData::AffectedRows(1)));
}
// Asserts the writes.
assert_values(cluster.fe_instance()).await;
if simulate_offline_source {
let mut expected_distribution =
find_region_distribution(&table_metadata_manager, table_id).await;
let (offline_from_peer_id, offline_region_number) = expected_distribution
.iter()
.find(|(peer_id, regions)| {
**peer_id != from_peer_id
&& **peer_id != to_peer_id
&& !regions.leader_regions.is_empty()
})
.map(|(peer_id, regions)| (*peer_id, regions.leader_regions[0]))
.unwrap();
let offline_region_id = RegionId::new(table_id, offline_region_number);
// Simulates scale-in: the source datanode stops and its lease disappears.
cluster
.datanode_instances
.get_mut(&offline_from_peer_id)
.unwrap()
.shutdown()
.await
.unwrap();
let source_lease_key: Vec<u8> = DatanodeLeaseKey {
node_id: offline_from_peer_id,
}
.try_into()
.unwrap();
cluster
.metasrv
.in_memory()
.batch_delete(BatchDeleteRequest {
keys: vec![source_lease_key],
prev_kv: false,
})
.await
.unwrap();
let offline_procedure_id = trigger_migration_by_sql(
&cluster,
offline_region_id.as_u64(),
offline_from_peer_id,
from_peer_id,
)
.await;
let frontend = cluster.fe_instance().clone();
let procedure_id_for_closure = offline_procedure_id.clone();
wait_condition(
default_distributed_time_constants().region_lease + Duration::from_secs(10),
Box::pin(async move {
loop {
let state = query_procedure_by_sql(&frontend, &procedure_id_for_closure).await;
if state == "{\"status\":\"Done\"}" {
info!("Offline-source migration done: {state}");
break;
}
info!("Offline-source migration not finished: {state}");
tokio::time::sleep(Duration::from_millis(200)).await;
}
}),
)
.await;
check_region_migration_events_system_table(
cluster.fe_instance(),
&offline_procedure_id,
offline_region_id.as_u64(),
offline_from_peer_id,
from_peer_id,
Some("greptime"),
)
.await;
let remove_offline_source = {
let source_regions = expected_distribution
.get_mut(&offline_from_peer_id)
.unwrap();
source_regions
.leader_regions
.retain(|region_number| *region_number != offline_region_number);
source_regions.leader_regions.is_empty() && source_regions.follower_regions.is_empty()
};
if remove_offline_source {
expected_distribution.remove(&offline_from_peer_id);
}
let target_regions = expected_distribution.entry(from_peer_id).or_default();
target_regions.add_leader_region(offline_region_number);
target_regions.sort();
let table_metadata_manager = table_metadata_manager.clone();
wait_condition(
Duration::from_secs(10),
Box::pin(async move {
loop {
let distribution =
find_region_distribution(&table_metadata_manager, table_id).await;
if distribution == expected_distribution {
break;
}
info!("Offline-source migration has unexpected distribution: {distribution:?}");
tokio::time::sleep(Duration::from_millis(200)).await;
}
}),
)
.await;
assert_values(cluster.fe_instance()).await;
}
// Triggers again.
let err = region_migration_manager
.submit_procedure(
ProcedureContext::from_event_context(PersistentEventContext::new(
TriggerReason::Manual,
)),
RegionMigrationProcedureTask::new(
region_id,
peer_factory(from_peer_id),
peer_factory(to_peer_id),
Duration::from_millis(1000),
RegionMigrationTriggerReason::Manual,
),
)
.await
.unwrap_err();
assert!(matches!(err, error::Error::RegionMigrated { .. }));
let new_distribution = find_region_distribution_by_sql(&cluster, TEST_TABLE_NAME).await;
assert_ne!(old_distribution, new_distribution);
}
/// A region migration test for a region server contains multiple regions of the table.
pub async fn test_region_migration_multiple_regions(
store_type: StorageType,
endpoints: Vec<String>,
) {
let cluster_name = "test_region_migration_multiple_regions";
let peer_factory = |id| Peer {
id,
addr: PEER_PLACEHOLDER_ADDR.to_string(),
};
// Prepares test cluster.
let (store_config, _guard) = get_test_store_config(&store_type);
let home_dir = create_temp_dir("test_region_migration_multiple_regions_data_home");
let datanodes = 5u64;
let builder = GreptimeDbClusterBuilder::new(cluster_name).await;
let const_selector = Arc::new(ConstNodeSelector::new(vec![
peer_factory(1),
peer_factory(2),
peer_factory(2),
]));
let cluster = builder
.with_datanodes(datanodes as u32)
.with_store_config(store_config)
.with_datanode_wal_config(DatanodeWalConfig::Kafka(DatanodeKafkaConfig {
connection: KafkaConnectionConfig {
broker_endpoints: endpoints.clone(),
..Default::default()
},
..Default::default()
}))
.with_metasrv_wal_config(MetasrvWalConfig::Kafka(MetasrvKafkaConfig {
connection: KafkaConnectionConfig {
broker_endpoints: endpoints,
..Default::default()
},
kafka_topic: KafkaTopicConfig {
num_topics: 3,
topic_name_prefix: Uuid::new_v4().to_string(),
..Default::default()
},
..Default::default()
}))
.with_shared_home_dir(Arc::new(home_dir))
.with_meta_selector(const_selector.clone())
.build(true)
.await;
let mut logical_timer = 1685508715000;
let table_metadata_manager = cluster.metasrv.table_metadata_manager().clone();
// Prepares test table.
let table_id = prepare_testing_table(&cluster).await;
// Inserts data
let results = insert_values(cluster.fe_instance(), logical_timer).await;
logical_timer += 1000;
for result in results {
assert!(matches!(result.unwrap().data, OutputData::AffectedRows(1)));
}
// The region distribution
let mut distribution = find_region_distribution(&table_metadata_manager, table_id).await;
assert_eq!(distribution.len(), 2);
// Selecting target of region migration.
let region_migration_manager = cluster.metasrv.region_migration_manager();
let (peer_1, peer_1_regions) = distribution.pop_first().unwrap();
let (peer_2, peer_2_regions) = distribution.pop_first().unwrap();
// Picks the peer only contains as from peer.
let ((from_peer_id, from_regions), (to_peer_id, mut to_regions)) =
if peer_1_regions.leader_regions.len() == 1 {
((peer_1, peer_1_regions), (peer_2, peer_2_regions))
} else {
((peer_2, peer_2_regions), (peer_1, peer_1_regions))
};
info!(
"Selecting from peer: {from_peer_id}, and regions: {:?}",
from_regions
);
info!(
"Selecting to peer: {to_peer_id}, and regions: {:?}",
to_regions
);
let region_id = RegionId::new(table_id, from_regions.leader_regions[0]);
// Trigger region migration.
let procedure = region_migration_manager
.submit_procedure(
ProcedureContext::from_event_context(PersistentEventContext::new(
TriggerReason::Manual,
)),
RegionMigrationProcedureTask::new(
region_id,
peer_factory(from_peer_id),
peer_factory(to_peer_id),
Duration::from_millis(1000),
RegionMigrationTriggerReason::Manual,
),
)
.await
.unwrap();
info!("Started region procedure: {}!", procedure.unwrap());
// Prepares expected region distribution.
to_regions
.leader_regions
.extend(from_regions.leader_regions);
to_regions
.follower_regions
.extend(from_regions.follower_regions);
// Keeps asc order.
to_regions.sort();
distribution.insert(to_peer_id, to_regions);
// Waits condition
wait_condition(
Duration::from_secs(10),
Box::pin(async move {
loop {
let region_migration =
find_region_distribution(&table_metadata_manager, table_id).await;
if region_migration == distribution {
info!("Found new distribution: {region_migration:?}");
break;
} else {
info!("Found the unexpected distribution: {region_migration:?}, expected: {distribution:?}");
tokio::time::sleep(Duration::from_millis(200)).await;
}
}
}),
)
.await;
// Inserts more table.
let results = insert_values(cluster.fe_instance(), logical_timer).await;
for result in results {
assert!(matches!(result.unwrap().data, OutputData::AffectedRows(1)));
}
check_region_migration_events_system_table(
cluster.fe_instance(),
&procedure.unwrap().to_string(),
region_id.as_u64(),
from_peer_id,
to_peer_id,
None,
)
.await;
// Asserts the writes.
assert_values(cluster.fe_instance()).await;
// Triggers again.
let err = region_migration_manager
.submit_procedure(
ProcedureContext::from_event_context(PersistentEventContext::new(
TriggerReason::Manual,
)),
RegionMigrationProcedureTask::new(
region_id,
peer_factory(from_peer_id),
peer_factory(to_peer_id),
Duration::from_millis(1000),
RegionMigrationTriggerReason::Manual,
),
)
.await
.unwrap_err();
assert!(matches!(err, error::Error::RegionMigrated { .. }));
}
/// A region migration test for a region server contains all regions of the table.
pub async fn test_region_migration_all_regions(store_type: StorageType, endpoints: Vec<String>) {
let cluster_name = "test_region_migration_all_regions";
let peer_factory = |id| Peer {
id,
addr: PEER_PLACEHOLDER_ADDR.to_string(),
};
// Prepares test cluster.
let (store_config, _guard) = get_test_store_config(&store_type);
let home_dir = create_temp_dir("test_region_migration_all_regions_data_home");
let datanodes = 5u64;
let builder = GreptimeDbClusterBuilder::new(cluster_name).await;
let const_selector = Arc::new(ConstNodeSelector::new(vec![
peer_factory(2),
peer_factory(2),
peer_factory(2),
]));
let cluster = builder
.with_datanodes(datanodes as u32)
.with_store_config(store_config)
.with_datanode_wal_config(DatanodeWalConfig::Kafka(DatanodeKafkaConfig {
connection: KafkaConnectionConfig {
broker_endpoints: endpoints.clone(),
..Default::default()
},
..Default::default()
}))
.with_metasrv_wal_config(MetasrvWalConfig::Kafka(MetasrvKafkaConfig {
connection: KafkaConnectionConfig {
broker_endpoints: endpoints,
..Default::default()
},
kafka_topic: KafkaTopicConfig {
num_topics: 3,
topic_name_prefix: Uuid::new_v4().to_string(),
..Default::default()
},
..Default::default()
}))
.with_shared_home_dir(Arc::new(home_dir))
.with_meta_selector(const_selector.clone())
.build(true)
.await;
let mut logical_timer = 1685508715000;
let table_metadata_manager = cluster.metasrv.table_metadata_manager().clone();
// Prepares test table.
let table_id = prepare_testing_table(&cluster).await;
// Inserts data
let results = insert_values(cluster.fe_instance(), logical_timer).await;
logical_timer += 1000;
for result in results {
assert!(matches!(result.unwrap().data, OutputData::AffectedRows(1)));
}
// The region distribution
let mut distribution = find_region_distribution(&table_metadata_manager, table_id).await;
assert_eq!(distribution.len(), 1);
// Selecting target of region migration.
let region_migration_manager = cluster.metasrv.region_migration_manager();
let (from_peer_id, mut from_regions) = distribution.pop_first().unwrap();
let to_peer_id = 1;
let mut to_regions = RegionRoleSet::default();
info!(
"Selecting from peer: {from_peer_id}, and regions: {:?}",
from_regions
);
info!(
"Selecting to peer: {to_peer_id}, and regions: {:?}",
to_regions
);
let region_id = RegionId::new(table_id, from_regions.leader_regions[0]);
// Trigger region migration.
let procedure = region_migration_manager
.submit_procedure(
ProcedureContext::from_event_context(PersistentEventContext::new(
TriggerReason::Manual,
)),
RegionMigrationProcedureTask::new(
region_id,
peer_factory(from_peer_id),
peer_factory(to_peer_id),
Duration::from_millis(1000),
RegionMigrationTriggerReason::Manual,
),
)
.await
.unwrap();
info!("Started region procedure: {}!", procedure.unwrap());
// Prepares expected region distribution.
to_regions
.leader_regions
.push(from_regions.leader_regions.remove(0));
// Keeps asc order.
to_regions.sort();
distribution.insert(to_peer_id, to_regions);
distribution.insert(from_peer_id, from_regions);
// Waits condition
wait_condition(
Duration::from_secs(10),
Box::pin(async move {
loop {
let region_migration =
find_region_distribution(&table_metadata_manager, table_id).await;
if region_migration == distribution {
info!("Found new distribution: {region_migration:?}");
break;
} else {
info!("Found the unexpected distribution: {region_migration:?}, expected: {distribution:?}");
tokio::time::sleep(Duration::from_millis(200)).await;
}
}
}),
)
.await;
check_region_migration_events_system_table(
cluster.fe_instance(),
&procedure.unwrap().to_string(),
region_id.as_u64(),
from_peer_id,
to_peer_id,
None,
)
.await;
// Inserts more table.
let results = insert_values(cluster.fe_instance(), logical_timer).await;
for result in results {
assert!(matches!(result.unwrap().data, OutputData::AffectedRows(1)));
}
// Asserts the writes.
assert_values(cluster.fe_instance()).await;
// Triggers again.
let err = region_migration_manager
.submit_procedure(
ProcedureContext::from_event_context(PersistentEventContext::new(
TriggerReason::Manual,
)),
RegionMigrationProcedureTask::new(
region_id,
peer_factory(from_peer_id),
peer_factory(to_peer_id),
Duration::from_millis(1000),
RegionMigrationTriggerReason::Manual,
),
)
.await
.unwrap_err();
assert!(matches!(err, error::Error::RegionMigrated { .. }));
}
pub async fn test_region_migration_incorrect_from_peer(
store_type: StorageType,
endpoints: Vec<String>,
) {
let cluster_name = "test_region_migration_incorrect_from_peer";
let peer_factory = |id| Peer {
id,
addr: PEER_PLACEHOLDER_ADDR.to_string(),
};
// Prepares test cluster.
let (store_config, _guard) = get_test_store_config(&store_type);
let home_dir = create_temp_dir("test_region_migration_incorrect_from_peer_data_home");
let datanodes = 5u64;
let builder = GreptimeDbClusterBuilder::new(cluster_name).await;
let const_selector = Arc::new(ConstNodeSelector::new(vec![
peer_factory(1),
peer_factory(2),
peer_factory(3),
]));
let cluster = builder
.with_datanodes(datanodes as u32)
.with_store_config(store_config)
.with_datanode_wal_config(DatanodeWalConfig::Kafka(DatanodeKafkaConfig {
connection: KafkaConnectionConfig {
broker_endpoints: endpoints.clone(),
..Default::default()
},
..Default::default()
}))
.with_metasrv_wal_config(MetasrvWalConfig::Kafka(MetasrvKafkaConfig {
connection: KafkaConnectionConfig {
broker_endpoints: endpoints,
..Default::default()
},
kafka_topic: KafkaTopicConfig {
num_topics: 3,
topic_name_prefix: Uuid::new_v4().to_string(),
..Default::default()
},
..Default::default()
}))
.with_shared_home_dir(Arc::new(home_dir))
.with_meta_selector(const_selector.clone())
.build(true)
.await;
let logical_timer = 1685508715000;
let table_metadata_manager = cluster.metasrv.table_metadata_manager().clone();
// Prepares test table.
let table_id = prepare_testing_table(&cluster).await;
// Inserts data
let results = insert_values(cluster.fe_instance(), logical_timer).await;
for result in results {
assert!(matches!(result.unwrap().data, OutputData::AffectedRows(1)));
}
// The region distribution
let distribution = find_region_distribution(&table_metadata_manager, table_id).await;
assert_eq!(distribution.len(), 3);
let region_migration_manager = cluster.metasrv.region_migration_manager();
let region_id = RegionId::new(table_id, 1);
// Trigger region migration.
let err = region_migration_manager
.submit_procedure(
ProcedureContext::from_event_context(PersistentEventContext::new(
TriggerReason::Manual,
)),
RegionMigrationProcedureTask::new(
region_id,
peer_factory(5),
peer_factory(1),
Duration::from_millis(1000),
RegionMigrationTriggerReason::Manual,
),
)
.await
.unwrap_err();
assert!(matches!(
err,
meta_srv::error::Error::LeaderPeerChanged { .. }
));
}
pub async fn test_region_migration_incorrect_region_id(
store_type: StorageType,
endpoints: Vec<String>,
) {
let cluster_name = "test_region_migration_incorrect_region_id";
let peer_factory = |id| Peer {
id,
addr: PEER_PLACEHOLDER_ADDR.to_string(),
};
// Prepares test cluster.
let (store_config, _guard) = get_test_store_config(&store_type);
let home_dir = create_temp_dir("test_region_migration_incorrect_region_id_data_home");
let datanodes = 5u64;
let builder = GreptimeDbClusterBuilder::new(cluster_name).await;
let const_selector = Arc::new(ConstNodeSelector::new(vec![
peer_factory(1),
peer_factory(2),
peer_factory(3),
]));
let cluster = builder
.with_datanodes(datanodes as u32)
.with_store_config(store_config)
.with_datanode_wal_config(DatanodeWalConfig::Kafka(DatanodeKafkaConfig {
connection: KafkaConnectionConfig {
broker_endpoints: endpoints.clone(),
..Default::default()
},
..Default::default()
}))
.with_metasrv_wal_config(MetasrvWalConfig::Kafka(MetasrvKafkaConfig {
connection: KafkaConnectionConfig {
broker_endpoints: endpoints,
..Default::default()
},
kafka_topic: KafkaTopicConfig {
num_topics: 3,
topic_name_prefix: Uuid::new_v4().to_string(),
..Default::default()
},
..Default::default()
}))
.with_shared_home_dir(Arc::new(home_dir))
.with_meta_selector(const_selector.clone())
.build(true)
.await;
let logical_timer = 1685508715000;
let table_metadata_manager = cluster.metasrv.table_metadata_manager().clone();
// Prepares test table.
let table_id = prepare_testing_table(&cluster).await;
// Inserts data
let results = insert_values(cluster.fe_instance(), logical_timer).await;
for result in results {
assert!(matches!(result.unwrap().data, OutputData::AffectedRows(1)));
}
// The region distribution
let distribution = find_region_distribution(&table_metadata_manager, table_id).await;
assert_eq!(distribution.len(), 3);
let region_migration_manager = cluster.metasrv.region_migration_manager();
let region_id = RegionId::new(table_id, 5);
// Trigger region migration.
let err = region_migration_manager
.submit_procedure(
ProcedureContext::from_event_context(PersistentEventContext::new(
TriggerReason::Manual,
)),
RegionMigrationProcedureTask::new(
region_id,
peer_factory(2),
peer_factory(1),
Duration::from_millis(1000),
RegionMigrationTriggerReason::Manual,
),
)
.await
.unwrap_err();
assert!(matches!(
err,
meta_srv::error::Error::RegionRouteNotFound { .. }
));
}
struct ConstNodeSelector {
peers: Vec<Peer>,
}
impl ConstNodeSelector {
fn new(peers: Vec<Peer>) -> Self {
Self { peers }
}
}
#[async_trait::async_trait]
impl Selector for ConstNodeSelector {
type Context = SelectorContext;
type Output = Vec<Peer>;
async fn select(
&self,
_ctx: &Self::Context,
_opts: SelectorOptions,
) -> MetaResult<Self::Output> {
Ok(self.peers.clone())
}
}
async fn wait_condition(timeout: Duration, condition: BoxFuture<'static, ()>) {
tokio::time::timeout(timeout, condition).await.unwrap();
}
async fn assert_values(instance: &Arc<Instance>) {
let query_ctx = QueryContext::arc();
let result = instance
.do_query(
&format!("select * from {TEST_TABLE_NAME} order by i, ts"),
query_ctx,
)
.await
.remove(0);
let expected = "\
+----+---------------------+
| i | ts |
+----+---------------------+
| 5 | 2023-05-31T04:51:55 |
| 5 | 2023-05-31T04:51:56 |
| 15 | 2023-05-31T04:51:55 |
| 15 | 2023-05-31T04:51:56 |
| 55 | 2023-05-31T04:51:55 |
| 55 | 2023-05-31T04:51:56 |
+----+---------------------+";
check_output_stream(result.unwrap().data, expected).await;
}
async fn prepare_testing_metric_table(cluster: &GreptimeDbCluster) -> TableId {
let sql = r#"CREATE TABLE phy (ts timestamp time index, val double) engine=metric with ("physical_metric_table" = "");"#;
let mut result = cluster
.frontend
.instance
.do_query(sql, QueryContext::arc())
.await;
let output = result.remove(0).unwrap();
assert!(matches!(output.data, OutputData::AffectedRows(0)));
let sql = r#"CREATE TABLE t1 (ts timestamp time index, val double, host string primary key) engine = metric with ("on_physical_table" = "phy");"#;
let mut result = cluster
.frontend
.instance
.do_query(sql, QueryContext::arc())
.await;
let output = result.remove(0).unwrap();
assert!(matches!(output.data, OutputData::AffectedRows(0)));
let sql = r#"CREATE TABLE t2 (ts timestamp time index, job string primary key, val double) engine = metric with ("on_physical_table" = "phy");"#;
let mut result = cluster
.frontend
.instance
.do_query(sql, QueryContext::arc())
.await;
let output = result.remove(0).unwrap();
assert!(matches!(output.data, OutputData::AffectedRows(0)));
let table = cluster
.frontend
.instance
.catalog_manager()
.table(DEFAULT_CATALOG_NAME, DEFAULT_SCHEMA_NAME, "phy", None)
.await
.unwrap()
.unwrap();
table.table_info().table_id()
}
async fn prepare_testing_table(cluster: &GreptimeDbCluster) -> TableId {
let sql = format!(
r"
CREATE TABLE {TEST_TABLE_NAME} (
i INT PRIMARY KEY,
ts TIMESTAMP TIME INDEX,
) PARTITION ON COLUMNS (i) (
i <= 10,
i > 10 AND i <= 50,
i > 50
)"
);
let mut result = cluster
.frontend
.instance
.do_query(&sql, QueryContext::arc())
.await;
let output = result.remove(0).unwrap();
assert!(matches!(output.data, OutputData::AffectedRows(0)));
let table = cluster
.frontend
.instance
.catalog_manager()
.table(
DEFAULT_CATALOG_NAME,
DEFAULT_SCHEMA_NAME,
TEST_TABLE_NAME,
None,
)
.await
.unwrap()
.unwrap();
table.table_info().table_id()
}
async fn find_region_distribution(
table_metadata_manager: &TableMetadataManagerRef,
table_id: TableId,
) -> RegionDistribution {
table_metadata_manager
.table_route_manager()
.get_region_distribution(table_id)
.await
.unwrap()
.unwrap()
}
/// Find region distribution by SQL query
async fn find_region_distribution_by_sql(
cluster: &GreptimeDbCluster,
table: &str,
) -> RegionDistribution {
let query_ctx = QueryContext::arc();
let OutputData::Stream(stream) = run_sql(
cluster.fe_instance(),
&format!(
r#"select b.peer_id as datanode_id,
a.greptime_partition_id as region_id
from information_schema.partitions a left join information_schema.region_peers b
on a.greptime_partition_id = b.region_id
where a.table_name='{table}' order by datanode_id asc"#
),
query_ctx.clone(),
)
.await
.unwrap()
.data
else {
unreachable!();
};
let recordbatches = RecordBatches::try_collect(stream).await.unwrap();
info!("SQL result:\n {}", recordbatches.pretty_print().unwrap());
let mut distribution = RegionDistribution::new();
for batch in recordbatches.take() {
let column = batch.column_by_name("datanode_id").unwrap();
let datanode_ids = column.as_primitive::<UInt64Type>();
let column = batch.column_by_name("region_id").unwrap();
let region_ids = column.as_primitive::<UInt64Type>();
for (datanode_id, region_id) in datanode_ids.iter().zip(region_ids.iter()) {
let (Some(datanode_id), Some(region_id)) = (datanode_id, region_id) else {
unreachable!();
};
let region_id = RegionId::from_u64(region_id);
distribution
.entry(datanode_id)
.or_default()
.add_leader_region(region_id.region_number());
}
}
distribution
}
/// Trigger the region migration by SQL, returns the procedure id if success.
async fn trigger_migration_by_sql(
cluster: &GreptimeDbCluster,
region_id: u64,
from_peer_id: u64,
to_peer_id: u64,
) -> String {
let OutputData::RecordBatches(recordbatches) = run_sql(
cluster.fe_instance(),
&format!("admin migrate_region({region_id}, {from_peer_id}, {to_peer_id})"),
QueryContext::arc(),
)
.await
.unwrap()
.data
else {
unreachable!();
};
info!("SQL result:\n {}", recordbatches.pretty_print().unwrap());
let record_batch = &recordbatches.take()[0];
let column = record_batch.column(0);
let column = column.as_string::<i32>();
column.value(0).to_string()
}
/// Triggers region migration through the authenticated public gRPC SQL endpoint.
async fn trigger_migration_by_grpc(
database: &Database,
region_id: u64,
from_peer_id: u64,
to_peer_id: u64,
) -> String {
let output = database
.sql(format!(
"admin migrate_region({region_id}, {from_peer_id}, {to_peer_id})"
))
.await
.unwrap();
let recordbatches = match output.data {
OutputData::RecordBatches(recordbatches) => recordbatches,
OutputData::Stream(stream) => RecordBatches::try_collect(stream).await.unwrap(),
OutputData::AffectedRows(_) => unreachable!(),
};
let record_batch = &recordbatches.take()[0];
record_batch
.column(0)
.as_string::<i32>()
.value(0)
.to_string()
}
/// Query procedure state by SQL.
async fn query_procedure_by_sql(instance: &Arc<Instance>, pid: &str) -> String {
let OutputData::RecordBatches(recordbatches) = run_sql(
instance,
&format!("admin procedure_state('{pid}')"),
QueryContext::arc(),
)
.await
.unwrap()
.data
else {
unreachable!();
};
info!("SQL result:\n {}", recordbatches.pretty_print().unwrap());
let record_batch = &recordbatches.take()[0];
let column = record_batch.column(0);
let column = column.as_string::<i32>();
column.value(0).to_string()
}
async fn insert_values(instance: &Arc<Instance>, ts: u64) -> Vec<ServerResult<Output>> {
let query_ctx = QueryContext::arc();
let mut results = Vec::new();
for range in [5, 15, 55] {
let result = run_sql(
instance,
&format!("INSERT INTO {TEST_TABLE_NAME} VALUES ({},{})", range, ts),
query_ctx.clone(),
)
.await;
results.push(result);
}
results
}
async fn run_sql(
instance: &Arc<Instance>,
sql: &str,
query_ctx: QueryContextRef,
) -> ServerResult<Output> {
info!("Run SQL: {sql}");
instance.do_query(sql, query_ctx).await.remove(0)
}
enum RegionMigrationEvents {
Actor,
ProcedureId,
Timestamp,
ProcedureState,
Schema,
Table,
EventType,
RegionMigrationTriggerReason,
RegionId,
SrcNodeId,
DstNodeId,
}
impl Iden for RegionMigrationEvents {
fn unquoted(&self, s: &mut dyn std::fmt::Write) {
write!(
s,
"{}",
match self {
Self::Actor => ACTOR_COLUMN.name(),
Self::ProcedureId => EVENTS_TABLE_PROCEDURE_ID_COLUMN_NAME,
Self::Timestamp => EVENTS_TABLE_TIMESTAMP_COLUMN_NAME,
Self::ProcedureState => EVENTS_TABLE_PROCEDURE_STATE_COLUMN_NAME,
Self::Schema => DEFAULT_PRIVATE_SCHEMA_NAME,
Self::Table => DEFAULT_EVENTS_TABLE_NAME,
Self::EventType => EVENTS_TABLE_TYPE_COLUMN_NAME,
Self::RegionMigrationTriggerReason => REGION_MIGRATION_TRIGGER_REASON_COLUMN.name(),
Self::RegionId => REGION_ID_COLUMN.name(),
Self::SrcNodeId => REGION_MIGRATION_SRC_NODE_ID_COLUMN.name(),
Self::DstNodeId => REGION_MIGRATION_DST_NODE_ID_COLUMN.name(),
}
)
.unwrap();
}
}
async fn check_region_migration_events_system_table(
fe_instance: &Arc<Instance>,
procedure_id: &str,
region_id: u64,
from_peer_id: u64,
to_peer_id: u64,
actor: Option<&str>,
) {
// Sleep enough time to ensure the event is recorded.
tokio::time::sleep(DEFAULT_FLUSH_INTERVAL_SECONDS * 2).await;
// The query is equivalent to the following SQL:
// SELECT region_migration_trigger_reason, procedure_state,
// json_get_string(procedure_trigger, 'type') AS procedure_trigger
// FROM greptime_private.events WHERE
// type = 'region_migration' AND
// procedure_id = '${procedure_id}' AND
// table_id = ${table_id} AND
// region_id = ${region_id} AND
// region_migration_src_node_id = ${from_peer_id} AND
// region_migration_dst_node_id = ${to_peer_id}
// ORDER BY timestamp ASC
let mut query = Query::select();
query
.column(RegionMigrationEvents::RegionMigrationTriggerReason)
.column(RegionMigrationEvents::ProcedureState)
.expr_as(
Expr::cust("json_get_string(procedure_trigger, 'type')"),
Alias::new("procedure_trigger"),
)
.expr_as(
Expr::cust("json_to_string(event_context)"),
Alias::new("event_context"),
)
.from((RegionMigrationEvents::Schema, RegionMigrationEvents::Table))
.and_where(Expr::col(RegionMigrationEvents::EventType).eq(REGION_MIGRATION_EVENT_TYPE))
.and_where(Expr::col(RegionMigrationEvents::ProcedureId).eq(procedure_id))
.and_where(Expr::col(RegionMigrationEvents::RegionId).eq(region_id))
.and_where(Expr::col(RegionMigrationEvents::SrcNodeId).eq(from_peer_id))
.and_where(Expr::col(RegionMigrationEvents::DstNodeId).eq(to_peer_id));
if let Some(actor) = actor {
query.and_where(Expr::col(RegionMigrationEvents::Actor).eq(actor));
} else {
query.and_where(Expr::col(RegionMigrationEvents::Actor).is_null());
}
let query = query
.order_by(RegionMigrationEvents::Timestamp, Order::Asc)
.to_string(PostgresQueryBuilder);
let result = fe_instance
.do_query(&query, QueryContext::arc())
.await
.remove(0);
let expected = if actor == Some(PROCEDURE_ACTOR) {
"\
+---------------------------------+-----------------+-------------------+---------------------------------------+
| region_migration_trigger_reason | procedure_state | procedure_trigger | event_context |
+---------------------------------+-----------------+-------------------+---------------------------------------+
| Manual | Running | Submitted | {\"protocol\":\"grpc\",\"reason\":\"manual\"} |
| Manual | Done | Succeeded | |
+---------------------------------+-----------------+-------------------+---------------------------------------+"
} else {
"\
+---------------------------------+-----------------+-------------------+---------------------+
| region_migration_trigger_reason | procedure_state | procedure_trigger | event_context |
+---------------------------------+-----------------+-------------------+---------------------+
| Manual | Running | Submitted | {\"reason\":\"manual\"} |
| Manual | Done | Succeeded | |
+---------------------------------+-----------------+-------------------+---------------------+"
};
check_output_stream(result.unwrap().data, expected).await;
}