This is an automated email from the ASF dual-hosted git repository.
yiguolei pushed a commit to branch branch-4.1
in repository https://gitbox.apache.org/repos/asf/doris.git
The following commit(s) were added to refs/heads/branch-4.1 by this push:
new 22d7dd9e493 branch-4.1: [enhance](cloud) proactively sync tablet meta
after alter #61585 (#66798)
22d7dd9e493 is described below
commit 22d7dd9e4934d8b7a9a5abb3b5f18cae87d503c0
Author: github-actions[bot]
<41898282+github-actions[bot]@users.noreply.github.com>
AuthorDate: Thu Nov 5 09:20:42 2026 +0800
branch-4.1: [enhance](cloud) proactively sync tablet meta after alter
#61585 (#66798)
Cherry-picked from #61585
---------
Co-authored-by: Luwei <[email protected]>
---
be/src/cloud/cloud_internal_service.cpp | 77 ++++
be/src/cloud/cloud_internal_service.h | 8 +
be/src/cloud/cloud_tablet.cpp | 95 ++--
be/src/cloud/cloud_tablet_mgr.cpp | 4 +
be/src/cloud/cloud_tablet_mgr.h | 2 +
be/src/service/internal_service.cpp | 9 +
be/src/service/internal_service.h | 4 +
be/test/cloud/cloud_internal_service_test.cpp | 180 ++++++++
be/test/cloud/cloud_tablet_mgr_test.cpp | 32 ++
be/test/cloud/cloud_tablet_test.cpp | 63 ++-
.../cloud/alter/CloudSchemaChangeHandler.java | 83 ++++
.../org/apache/doris/rpc/BackendServiceClient.java | 5 +
.../org/apache/doris/rpc/BackendServiceProxy.java | 12 +
.../cloud/alter/CloudSchemaChangeHandlerTest.java | 478 +++++++++++++++++++++
.../apache/doris/rpc/BackendServiceClientTest.java | 30 ++
.../apache/doris/rpc/BackendServiceProxyTest.java | 54 +++
gensrc/proto/internal_service.proto | 12 +
.../test_cloud_sync_tablet_meta_after_alter.groovy | 284 ++++++++++++
18 files changed, 1384 insertions(+), 48 deletions(-)
diff --git a/be/src/cloud/cloud_internal_service.cpp
b/be/src/cloud/cloud_internal_service.cpp
index 6ad86dabfaf..ab5c51d84d6 100644
--- a/be/src/cloud/cloud_internal_service.cpp
+++ b/be/src/cloud/cloud_internal_service.cpp
@@ -103,6 +103,67 @@ bvar::Adder<int64_t>
g_peer_server_fill_rejected("peer_server_fill_rejected");
bvar::LatencyRecorder g_peer_server_fill_latency("peer_server_fill_latency");
bvar::LatencyRecorder
g_cloud_internal_service_get_file_cache_meta_by_tablet_id_latency(
"cloud_internal_service_get_file_cache_meta_by_tablet_id_latency");
+bvar::Adder<int64_t> g_cloud_sync_tablet_meta_requests_total(
+ "cloud_sync_tablet_meta_requests_total");
+bvar::Adder<int64_t>
g_cloud_sync_tablet_meta_synced_total("cloud_sync_tablet_meta_synced_total");
+bvar::Adder<int64_t>
g_cloud_sync_tablet_meta_skipped_total("cloud_sync_tablet_meta_skipped_total");
+bvar::Adder<int64_t>
g_cloud_sync_tablet_meta_failed_total("cloud_sync_tablet_meta_failed_total");
+
+namespace {
+
+void submit_sync_tablet_meta(CloudStorageEngine& engine, FifoThreadPool&
work_pool,
+ const PSyncTabletMetaRequest* request,
+ PSyncTabletMetaResponse* response,
google::protobuf::Closure* done) {
+ auto start_time = std::chrono::steady_clock::now();
+ bool ret = work_pool.try_offer([engine = &engine, request, response, done,
start_time]() {
+ brpc::ClosureGuard closure_guard(done);
+ LOG(INFO) << "begin to sync tablet meta, request=" <<
request->ShortDebugString();
+ int64_t synced = 0;
+ int64_t skipped = 0;
+ int64_t failed = 0;
+ g_cloud_sync_tablet_meta_requests_total << 1;
+ for (const auto tablet_id : request->tablet_ids()) {
+ auto tablet = engine->tablet_mgr().get_tablet_if_cached(tablet_id);
+ if (!tablet) {
+ ++skipped;
+ continue;
+ }
+ auto st = tablet->sync_meta();
+ if (!st.ok()) {
+ ++failed;
+ LOG(WARNING) << "failed to sync tablet meta from cloud meta
service, tablet="
+ << tablet_id << ", err=" << st;
+ continue;
+ }
+ ++synced;
+ }
+ g_cloud_sync_tablet_meta_synced_total << synced;
+ g_cloud_sync_tablet_meta_skipped_total << skipped;
+ g_cloud_sync_tablet_meta_failed_total << failed;
+ response->set_synced_tablets(synced);
+ response->set_skipped_tablets(skipped);
+ response->set_failed_tablets(failed);
+ Status::OK().to_protobuf(response->mutable_status());
+ auto cost_ms = std::chrono::duration_cast<std::chrono::milliseconds>(
+ std::chrono::steady_clock::now() - start_time)
+ .count();
+ LOG(INFO) << "finish to sync tablet meta, request=" <<
request->ShortDebugString()
+ << ", response=" << response->ShortDebugString() << ",
cost_ms=" << cost_ms;
+ });
+ if (!ret) {
+ brpc::ClosureGuard closure_guard(done);
+ Status::InternalError("failed to offer sync_tablet_meta request to
work pool")
+ .to_protobuf(response->mutable_status());
+ auto cost_ms = std::chrono::duration_cast<std::chrono::milliseconds>(
+ std::chrono::steady_clock::now() - start_time)
+ .count();
+ LOG(WARNING) << "failed to offer sync_tablet_meta request to work
pool, request="
+ << request->ShortDebugString() << ", response=" <<
response->ShortDebugString()
+ << ", cost_ms=" << cost_ms;
+ }
+}
+
+} // namespace
// Concurrency guard for server-side S3 pull-through fills.
static std::atomic<int32_t> g_active_server_fills {0};
@@ -115,6 +176,22 @@
CloudInternalServiceImpl::CloudInternalServiceImpl(CloudStorageEngine& engine, E
CloudInternalServiceImpl::~CloudInternalServiceImpl() = default;
+void
CloudInternalServiceImpl::sync_tablet_meta(google::protobuf::RpcController*
controller,
+ const PSyncTabletMetaRequest*
request,
+ PSyncTabletMetaResponse*
response,
+ google::protobuf::Closure*
done) {
+ submit_sync_tablet_meta(_engine, _light_work_pool, request, response,
done);
+}
+
+#ifdef BE_TEST
+void test_submit_sync_tablet_meta(CloudStorageEngine& engine, FifoThreadPool&
work_pool,
+ const PSyncTabletMetaRequest* request,
+ PSyncTabletMetaResponse* response,
+ google::protobuf::Closure* done) {
+ submit_sync_tablet_meta(engine, work_pool, request, response, done);
+}
+#endif
+
void
CloudInternalServiceImpl::alter_vault_sync(google::protobuf::RpcController*
controller,
const
doris::PAlterVaultSyncRequest* request,
PAlterVaultSyncResponse*
response,
diff --git a/be/src/cloud/cloud_internal_service.h
b/be/src/cloud/cloud_internal_service.h
index 75fad0ba0ce..524ce14d275 100644
--- a/be/src/cloud/cloud_internal_service.h
+++ b/be/src/cloud/cloud_internal_service.h
@@ -35,6 +35,10 @@ Status test_handle_peer_file_cache_block_request(const
PFetchPeerDataRequest* re
brpc::Controller* cntl);
bool test_try_reject_if_queue_timed_out(std::chrono::steady_clock::time_point
enqueue_ts,
PFetchPeerDataResponse* response);
+void test_submit_sync_tablet_meta(CloudStorageEngine& engine, FifoThreadPool&
work_pool,
+ const PSyncTabletMetaRequest* request,
+ PSyncTabletMetaResponse* response,
+ google::protobuf::Closure* done);
#endif
class CloudInternalServiceImpl final : public PInternalService {
@@ -48,6 +52,10 @@ public:
PAlterVaultSyncResponse* response,
google::protobuf::Closure* done) override;
+ void sync_tablet_meta(google::protobuf::RpcController* controller,
+ const PSyncTabletMetaRequest* request,
PSyncTabletMetaResponse* response,
+ google::protobuf::Closure* done) override;
+
// Get messages (filename, offset, size) about the tablet data in cache
void get_file_cache_meta_by_tablet_id(google::protobuf::RpcController*
controller,
const PGetFileCacheMetaRequest*
request,
diff --git a/be/src/cloud/cloud_tablet.cpp b/be/src/cloud/cloud_tablet.cpp
index ec67ac5bc18..52010e5d92c 100644
--- a/be/src/cloud/cloud_tablet.cpp
+++ b/be/src/cloud/cloud_tablet.cpp
@@ -959,6 +959,12 @@ void CloudTablet::get_compaction_status(std::string*
json_result) {
// get snapshot version path json_doc
_timestamped_version_tracker.get_stale_version_path_json_doc(path_arr);
+ rapidjson::Value compaction_policy_value;
+ auto compaction_policy = _tablet_meta->compaction_policy();
+ compaction_policy_value.SetString(compaction_policy.c_str(),
+
cast_set<uint32_t>(compaction_policy.length()),
+ root.GetAllocator());
+ root.AddMember("compaction policy", compaction_policy_value,
root.GetAllocator());
root.AddMember("cumulative point", _cumulative_point.load(),
root.GetAllocator());
rapidjson::Value cumu_value;
std::string format_str =
ToStringFromUnixMillis(_last_cumu_compaction_failure_millis.load());
@@ -1490,9 +1496,9 @@ void CloudTablet::agg_delete_bitmap_for_compaction(
}
Status CloudTablet::sync_meta() {
- if (!config::enable_file_cache) {
- return Status::OK();
- }
+ // Keep tablet metadata refresh serialized with rowset sync and apply
local metadata updates
+ // under _meta_lock. The lock order must stay _sync_meta_lock ->
_meta_lock.
+ std::unique_lock lock(_sync_meta_lock);
TabletMetaSharedPtr tablet_meta;
auto st = _engine.meta_mgr().get_tablet_meta(tablet_id(), &tablet_meta);
@@ -1504,57 +1510,64 @@ Status CloudTablet::sync_meta() {
}
auto new_compaction_policy = tablet_meta->compaction_policy();
- if (_tablet_meta->compaction_policy() != new_compaction_policy) {
- _tablet_meta->set_compaction_policy(new_compaction_policy);
- }
+ auto new_ttl_seconds = tablet_meta->ttl_seconds();
auto new_time_series_compaction_goal_size_mbytes =
tablet_meta->time_series_compaction_goal_size_mbytes();
- if (_tablet_meta->time_series_compaction_goal_size_mbytes() !=
- new_time_series_compaction_goal_size_mbytes) {
- _tablet_meta->set_time_series_compaction_goal_size_mbytes(
- new_time_series_compaction_goal_size_mbytes);
- }
auto new_time_series_compaction_file_count_threshold =
tablet_meta->time_series_compaction_file_count_threshold();
- if (_tablet_meta->time_series_compaction_file_count_threshold() !=
- new_time_series_compaction_file_count_threshold) {
- _tablet_meta->set_time_series_compaction_file_count_threshold(
- new_time_series_compaction_file_count_threshold);
- }
auto new_time_series_compaction_time_threshold_seconds =
tablet_meta->time_series_compaction_time_threshold_seconds();
- if (_tablet_meta->time_series_compaction_time_threshold_seconds() !=
- new_time_series_compaction_time_threshold_seconds) {
- _tablet_meta->set_time_series_compaction_time_threshold_seconds(
- new_time_series_compaction_time_threshold_seconds);
- }
auto new_time_series_compaction_empty_rowsets_threshold =
tablet_meta->time_series_compaction_empty_rowsets_threshold();
- if (_tablet_meta->time_series_compaction_empty_rowsets_threshold() !=
- new_time_series_compaction_empty_rowsets_threshold) {
- _tablet_meta->set_time_series_compaction_empty_rowsets_threshold(
- new_time_series_compaction_empty_rowsets_threshold);
- }
auto new_time_series_compaction_level_threshold =
tablet_meta->time_series_compaction_level_threshold();
- if (_tablet_meta->time_series_compaction_level_threshold() !=
- new_time_series_compaction_level_threshold) {
- _tablet_meta->set_time_series_compaction_level_threshold(
- new_time_series_compaction_level_threshold);
- }
- // Sync disable_auto_compaction (stored in tablet_schema)
auto new_disable_auto_compaction =
tablet_meta->tablet_schema()->disable_auto_compaction();
- if (_tablet_meta->tablet_schema()->disable_auto_compaction() !=
new_disable_auto_compaction) {
- _tablet_meta->mutable_tablet_schema()->set_disable_auto_compaction(
- new_disable_auto_compaction);
- }
- // Sync vertical_compaction_num_columns_per_group
auto new_vertical_compaction_num_columns_per_group =
tablet_meta->vertical_compaction_num_columns_per_group();
- if (_tablet_meta->vertical_compaction_num_columns_per_group() !=
- new_vertical_compaction_num_columns_per_group) {
- _tablet_meta->set_vertical_compaction_num_columns_per_group(
- new_vertical_compaction_num_columns_per_group);
+
+ {
+ std::unique_lock wlock(_meta_lock);
+ if (_tablet_meta->compaction_policy() != new_compaction_policy) {
+ _tablet_meta->set_compaction_policy(new_compaction_policy);
+ }
+ if (_tablet_meta->ttl_seconds() != new_ttl_seconds) {
+ _tablet_meta->set_ttl_seconds(new_ttl_seconds);
+ }
+ if (_tablet_meta->time_series_compaction_goal_size_mbytes() !=
+ new_time_series_compaction_goal_size_mbytes) {
+ _tablet_meta->set_time_series_compaction_goal_size_mbytes(
+ new_time_series_compaction_goal_size_mbytes);
+ }
+ if (_tablet_meta->time_series_compaction_file_count_threshold() !=
+ new_time_series_compaction_file_count_threshold) {
+ _tablet_meta->set_time_series_compaction_file_count_threshold(
+ new_time_series_compaction_file_count_threshold);
+ }
+ if (_tablet_meta->time_series_compaction_time_threshold_seconds() !=
+ new_time_series_compaction_time_threshold_seconds) {
+ _tablet_meta->set_time_series_compaction_time_threshold_seconds(
+ new_time_series_compaction_time_threshold_seconds);
+ }
+ if (_tablet_meta->time_series_compaction_empty_rowsets_threshold() !=
+ new_time_series_compaction_empty_rowsets_threshold) {
+ _tablet_meta->set_time_series_compaction_empty_rowsets_threshold(
+ new_time_series_compaction_empty_rowsets_threshold);
+ }
+ if (_tablet_meta->time_series_compaction_level_threshold() !=
+ new_time_series_compaction_level_threshold) {
+ _tablet_meta->set_time_series_compaction_level_threshold(
+ new_time_series_compaction_level_threshold);
+ }
+ if (_tablet_meta->tablet_schema()->disable_auto_compaction() !=
+ new_disable_auto_compaction) {
+ _tablet_meta->mutable_tablet_schema()->set_disable_auto_compaction(
+ new_disable_auto_compaction);
+ }
+ if (_tablet_meta->vertical_compaction_num_columns_per_group() !=
+ new_vertical_compaction_num_columns_per_group) {
+ _tablet_meta->set_vertical_compaction_num_columns_per_group(
+ new_vertical_compaction_num_columns_per_group);
+ }
}
last_sync_tablet_meta_time_s = ::time(nullptr);
diff --git a/be/src/cloud/cloud_tablet_mgr.cpp
b/be/src/cloud/cloud_tablet_mgr.cpp
index 1e10cb243fd..3e5c7d0b764 100644
--- a/be/src/cloud/cloud_tablet_mgr.cpp
+++ b/be/src/cloud/cloud_tablet_mgr.cpp
@@ -293,6 +293,10 @@ bool CloudTabletMgr::peek_tablet_meta(int64_t tablet_id,
TabletMetaSharedPtr* ta
return true;
}
+std::shared_ptr<CloudTablet> CloudTabletMgr::get_tablet_if_cached(int64_t
tablet_id) {
+ return _tablet_map->get(tablet_id);
+}
+
void CloudTabletMgr::erase_tablet(int64_t tablet_id) {
auto tablet_id_str = std::to_string(tablet_id);
CacheKey key(tablet_id_str.data(), tablet_id_str.size());
diff --git a/be/src/cloud/cloud_tablet_mgr.h b/be/src/cloud/cloud_tablet_mgr.h
index c44f4f36f8c..5513468809b 100644
--- a/be/src/cloud/cloud_tablet_mgr.h
+++ b/be/src/cloud/cloud_tablet_mgr.h
@@ -62,6 +62,8 @@ public:
// Return true if cached tablet meta is found (without triggering RPC) and
filled.
bool peek_tablet_meta(int64_t tablet_id, TabletMetaSharedPtr* tablet_meta);
+ std::shared_ptr<CloudTablet> get_tablet_if_cached(int64_t tablet_id);
+
void erase_tablet(int64_t tablet_id);
void vacuum_stale_rowsets(const CountDownLatch& stop_latch);
diff --git a/be/src/service/internal_service.cpp
b/be/src/service/internal_service.cpp
index 887140a97aa..033a1694c6e 100644
--- a/be/src/service/internal_service.cpp
+++ b/be/src/service/internal_service.cpp
@@ -2565,5 +2565,14 @@ void
PInternalService::request_cdc_client(google::protobuf::RpcController* contr
}
}
+void PInternalService::sync_tablet_meta(google::protobuf::RpcController*
controller,
+ const PSyncTabletMetaRequest* request,
+ PSyncTabletMetaResponse* response,
+ google::protobuf::Closure* done) {
+ brpc::ClosureGuard closure_guard(done);
+ Status::NotSupported("sync_tablet_meta only supports cloud mode")
+ .to_protobuf(response->mutable_status());
+}
+
#include "common/compile_check_avoid_end.h"
} // namespace doris
diff --git a/be/src/service/internal_service.h
b/be/src/service/internal_service.h
index 4098c82120a..5fdc9fa3db0 100644
--- a/be/src/service/internal_service.h
+++ b/be/src/service/internal_service.h
@@ -223,6 +223,10 @@ public:
const PGetBeResourceRequest* request,
PGetBeResourceResponse* response,
google::protobuf::Closure* done) override;
+ void sync_tablet_meta(google::protobuf::RpcController* controller,
+ const PSyncTabletMetaRequest* request,
PSyncTabletMetaResponse* response,
+ google::protobuf::Closure* done) override;
+
void delete_dictionary(google::protobuf::RpcController* controller,
const PDeleteDictionaryRequest* request,
PDeleteDictionaryResponse* response,
diff --git a/be/test/cloud/cloud_internal_service_test.cpp
b/be/test/cloud/cloud_internal_service_test.cpp
new file mode 100644
index 00000000000..7bcb0f90a42
--- /dev/null
+++ b/be/test/cloud/cloud_internal_service_test.cpp
@@ -0,0 +1,180 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you 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.
+
+#include "cloud/cloud_internal_service.h"
+
+#include <chrono>
+#include <condition_variable>
+#include <functional>
+#include <mutex>
+#include <unordered_map>
+
+#include "cloud/cloud_storage_engine.h"
+#include "cloud/cloud_tablet_mgr.h"
+#include "cpp/sync_point.h"
+#include "gen_cpp/Status_types.h"
+#include "storage/tablet/tablet_meta.h"
+#include "util/uid_util.h"
+#include "util/work_thread_pool.hpp"
+
+namespace doris {
+
+namespace {
+
+class TestClosure final : public google::protobuf::Closure {
+public:
+ explicit TestClosure(std::function<void()> callback) :
_callback(std::move(callback)) {}
+
+ void Run() override {
+ if (_callback) {
+ _callback();
+ }
+ }
+
+private:
+ std::function<void()> _callback;
+};
+
+} // namespace
+
+class CloudInternalServiceTest : public testing::Test {
+public:
+ CloudInternalServiceTest() : _engine(CloudStorageEngine(EngineOptions()))
{}
+
+protected:
+ static constexpr int64_t kSyncedTabletId = 90001;
+ static constexpr int64_t kFailedTabletId = 90002;
+ static constexpr int64_t kUncachedTabletId = 90003;
+
+ void SetUp() override {
+ auto sp = SyncPoint::get_instance();
+ sp->clear_all_call_backs();
+ sp->enable_processing();
+
+ sp->set_call_back("CloudMetaMgr::sync_tablet_rowsets",
+ [](auto&& args) {
try_any_cast_ret<Status>(args)->second = true; });
+ sp->set_call_back("CloudMetaMgr::get_tablet_meta", [this](auto&& args)
{
+ const auto tablet_id = try_any_cast<int64_t>(args[0]);
+ auto* tablet_meta_ptr =
try_any_cast<TabletMetaSharedPtr*>(args[1]);
+ auto* ret = try_any_cast_ret<Status>(args);
+
+ auto& call_count = _tablet_meta_call_count[tablet_id];
+ ++call_count;
+ if (tablet_id == kFailedTabletId && call_count >= 2) {
+ ret->first = Status::InternalError<false>("injected sync_meta
failure");
+ ret->second = true;
+ return;
+ }
+
+ *tablet_meta_ptr =
+ create_tablet_meta(tablet_id, call_count >= 2 ?
"time_series" : "size_based");
+ ret->second = true;
+ });
+ }
+
+ void TearDown() override {
+ auto sp = SyncPoint::get_instance();
+ sp->disable_processing();
+ sp->clear_all_call_backs();
+ }
+
+ TabletMetaSharedPtr create_tablet_meta(int64_t tablet_id, std::string
compaction_policy) {
+ TTabletSchema tablet_schema;
+ TColumn column;
+ column.__set_column_name("c1");
+ column.__set_column_type(TColumnType());
+ column.column_type.__set_type(TPrimitiveType::INT);
+ column.__set_is_key(true);
+ column.__set_aggregation_type(TAggregationType::NONE);
+ column.__set_col_unique_id(0);
+ tablet_schema.__set_columns({column});
+ tablet_schema.__set_keys_type(TKeysType::DUP_KEYS);
+
+ std::unordered_map<uint32_t, uint32_t> col_ordinal_to_unique_id = {{0,
0}};
+ auto tablet_meta = std::make_shared<TabletMeta>(
+ 1, 2, tablet_id, 15674, 4, 5, tablet_schema, 1,
col_ordinal_to_unique_id,
+ UniqueId(9, 10), TTabletType::TABLET_TYPE_DISK,
TCompressionType::LZ4F);
+ tablet_meta->set_compaction_policy(std::move(compaction_policy));
+ return tablet_meta;
+ }
+
+ CloudStorageEngine _engine;
+ std::unordered_map<int64_t, int> _tablet_meta_call_count;
+};
+
+TEST_F(CloudInternalServiceTest,
TestSyncTabletMetaCountsSyncedSkippedAndFailedTablets) {
+ auto synced_res = _engine.tablet_mgr().get_tablet(kSyncedTabletId);
+ ASSERT_TRUE(synced_res.has_value()) << synced_res.error();
+ auto failed_res = _engine.tablet_mgr().get_tablet(kFailedTabletId);
+ ASSERT_TRUE(failed_res.has_value()) << failed_res.error();
+
+ auto cached_synced_tablet = synced_res.value();
+ auto cached_failed_tablet = failed_res.value();
+ ASSERT_EQ("size_based",
cached_synced_tablet->tablet_meta()->compaction_policy());
+ ASSERT_EQ("size_based",
cached_failed_tablet->tablet_meta()->compaction_policy());
+
+ FifoThreadPool work_pool(1, 1, "sync_tablet_meta_test");
+ PSyncTabletMetaRequest request;
+ request.add_tablet_ids(kSyncedTabletId);
+ request.add_tablet_ids(kUncachedTabletId);
+ request.add_tablet_ids(kFailedTabletId);
+ PSyncTabletMetaResponse response;
+
+ std::mutex mutex;
+ std::condition_variable cv;
+ bool done = false;
+ TestClosure closure([&]() {
+ std::lock_guard lock(mutex);
+ done = true;
+ cv.notify_all();
+ });
+
+ test_submit_sync_tablet_meta(_engine, work_pool, &request, &response,
&closure);
+
+ std::unique_lock lock(mutex);
+ ASSERT_TRUE(cv.wait_for(lock, std::chrono::seconds(5), [&] { return done;
}));
+
+ ASSERT_TRUE(response.has_status());
+ EXPECT_EQ(TStatusCode::OK, response.status().status_code()) <<
response.status().DebugString();
+ EXPECT_EQ(1, response.synced_tablets());
+ EXPECT_EQ(1, response.skipped_tablets());
+ EXPECT_EQ(1, response.failed_tablets());
+ EXPECT_EQ("time_series",
cached_synced_tablet->tablet_meta()->compaction_policy());
+ EXPECT_EQ("size_based",
cached_failed_tablet->tablet_meta()->compaction_policy());
+ EXPECT_EQ(nullptr,
_engine.tablet_mgr().get_tablet_if_cached(kUncachedTabletId));
+}
+
+TEST_F(CloudInternalServiceTest, TestSyncTabletMetaOfferFailure) {
+ FifoThreadPool work_pool(1, 0, "sync_tablet_meta_offer_failure_test");
+ PSyncTabletMetaRequest request;
+ request.add_tablet_ids(kSyncedTabletId);
+ PSyncTabletMetaResponse response;
+
+ bool done = false;
+ TestClosure closure([&]() { done = true; });
+
+ test_submit_sync_tablet_meta(_engine, work_pool, &request, &response,
&closure);
+
+ EXPECT_TRUE(done);
+ ASSERT_TRUE(response.has_status());
+ EXPECT_EQ(TStatusCode::INTERNAL_ERROR, response.status().status_code());
+ EXPECT_TRUE(response.status().error_msgs(0).find(
+ "failed to offer sync_tablet_meta request to work
pool") !=
+ std::string::npos);
+}
+
+} // namespace doris
diff --git a/be/test/cloud/cloud_tablet_mgr_test.cpp
b/be/test/cloud/cloud_tablet_mgr_test.cpp
index 1838cc9c544..2521cbb9e74 100644
--- a/be/test/cloud/cloud_tablet_mgr_test.cpp
+++ b/be/test/cloud/cloud_tablet_mgr_test.cpp
@@ -363,4 +363,36 @@ TEST_F(CloudTabletMgrTest,
SyncTabletsRefreshesMetaOfContinuouslyIngestedTablet)
sp->clear_all_call_backs();
}
+TEST_F(CloudTabletMgrTest, TestGetTabletIfCachedOnlyReturnsCachedTablet) {
+ auto sp = SyncPoint::get_instance();
+ sp->clear_all_call_backs();
+ sp->enable_processing();
+
+ sp->set_call_back("CloudMetaMgr::get_tablet_meta", [this](auto&& args) {
+ auto* tablet_meta_ptr = try_any_cast<TabletMetaSharedPtr*>(args[1]);
+ *tablet_meta_ptr = _tablet_meta;
+ try_any_cast_ret<Status>(args)->second = true;
+ });
+ sp->set_call_back("CloudMetaMgr::sync_tablet_rowsets",
+ [](auto&& args) { try_any_cast_ret<Status>(args)->second
= true; });
+
+ CloudTabletMgr mgr(_engine);
+ const int64_t cached_tablet_id = 99999;
+ const int64_t uncached_tablet_id = 100000;
+
+ EXPECT_EQ(nullptr, mgr.get_tablet_if_cached(cached_tablet_id));
+
+ auto res = mgr.get_tablet(cached_tablet_id);
+ ASSERT_TRUE(res.has_value()) << res.error();
+
+ auto cached_tablet = mgr.get_tablet_if_cached(cached_tablet_id);
+ ASSERT_NE(nullptr, cached_tablet);
+ EXPECT_EQ(cached_tablet_id, cached_tablet->tablet_id());
+
+ EXPECT_EQ(nullptr, mgr.get_tablet_if_cached(uncached_tablet_id));
+
+ sp->disable_processing();
+ sp->clear_all_call_backs();
+}
+
} // namespace doris
diff --git a/be/test/cloud/cloud_tablet_test.cpp
b/be/test/cloud/cloud_tablet_test.cpp
index 533e39ca1b4..7d92cc35dc0 100644
--- a/be/test/cloud/cloud_tablet_test.cpp
+++ b/be/test/cloud/cloud_tablet_test.cpp
@@ -1094,6 +1094,7 @@ protected:
const std::string&
compaction_policy = "size_based") {
TTabletSchema new_schema;
new_schema.__set_disable_auto_compaction(disable_auto_compaction);
+ new_schema.__set_is_in_memory(false);
TColumn col;
col.__set_column_name("test_col_" +
std::to_string(_current_tablet_id));
col.__set_column_type(TColumnType());
@@ -1251,8 +1252,8 @@ TEST_F(CloudTabletSyncMetaTest,
TestSyncMetaDisableAutoCompactionUnchanged) {
sp->clear_all_call_backs();
}
-// Test sync_meta is skipped when enable_file_cache is false
-TEST_F(CloudTabletSyncMetaTest, TestSyncMetaSkippedWhenFileCacheDisabled) {
+// sync_meta should keep refreshing tablet meta even when file cache is
disabled.
+TEST_F(CloudTabletSyncMetaTest, TestSyncMetaWhenFileCacheDisabled) {
// Disable file cache
config::enable_file_cache = false;
@@ -1263,17 +1264,22 @@ TEST_F(CloudTabletSyncMetaTest,
TestSyncMetaSkippedWhenFileCacheDisabled) {
sp->clear_all_call_backs();
sp->enable_processing();
+ auto mock_tablet_meta = createMockTabletMeta(true, "time_series");
bool callback_called = false;
sp->set_call_back("CloudMetaMgr::get_tablet_meta",
- [&callback_called](auto&& args) { callback_called =
true; });
+ [mock_tablet_meta, &callback_called](auto&& args) {
+ callback_called = true;
+ auto* tablet_meta_ptr =
try_any_cast<TabletMetaSharedPtr*>(args[1]);
+ *tablet_meta_ptr = mock_tablet_meta;
+ try_any_cast_ret<Status>(args)->second = true;
+ });
- // Call sync_meta - should return early without calling get_tablet_meta
Status st = _tablet->sync_meta();
EXPECT_TRUE(st.ok());
- EXPECT_FALSE(callback_called);
+ EXPECT_TRUE(callback_called);
- // Verify disable_auto_compaction is not changed
-
EXPECT_FALSE(_tablet->tablet_meta()->tablet_schema()->disable_auto_compaction());
+
EXPECT_TRUE(_tablet->tablet_meta()->tablet_schema()->disable_auto_compaction());
+ EXPECT_EQ(_tablet->tablet_meta()->compaction_policy(), "time_series");
sp->disable_processing();
sp->clear_all_call_backs();
@@ -1292,6 +1298,12 @@ TEST_F(CloudTabletSyncMetaTest,
TestSyncMetaMultipleProperties) {
// Create mock tablet meta with updated properties
auto mock_tablet_meta = createMockTabletMeta(true, "time_series");
+ mock_tablet_meta->set_time_series_compaction_goal_size_mbytes(1234);
+ mock_tablet_meta->set_time_series_compaction_file_count_threshold(17);
+ mock_tablet_meta->set_time_series_compaction_time_threshold_seconds(3601);
+ mock_tablet_meta->set_time_series_compaction_empty_rowsets_threshold(9);
+ mock_tablet_meta->set_time_series_compaction_level_threshold(7);
+ mock_tablet_meta->set_vertical_compaction_num_columns_per_group(13);
// Mock get_tablet_meta to return tablet_meta with updated properties
sp->set_call_back("CloudMetaMgr::get_tablet_meta",
[mock_tablet_meta](auto&& args) {
@@ -1309,10 +1321,47 @@ TEST_F(CloudTabletSyncMetaTest,
TestSyncMetaMultipleProperties) {
// Verify both properties are synced
EXPECT_TRUE(_tablet->tablet_meta()->tablet_schema()->disable_auto_compaction());
EXPECT_EQ(_tablet->tablet_meta()->compaction_policy(), "time_series");
+
EXPECT_EQ(_tablet->tablet_meta()->time_series_compaction_goal_size_mbytes(),
1234);
+
EXPECT_EQ(_tablet->tablet_meta()->time_series_compaction_file_count_threshold(),
17);
+
EXPECT_EQ(_tablet->tablet_meta()->time_series_compaction_time_threshold_seconds(),
3601);
+
EXPECT_EQ(_tablet->tablet_meta()->time_series_compaction_empty_rowsets_threshold(),
9);
+
EXPECT_EQ(_tablet->tablet_meta()->time_series_compaction_level_threshold(), 7);
+
EXPECT_EQ(_tablet->tablet_meta()->vertical_compaction_num_columns_per_group(),
13);
+
+ sp->disable_processing();
+ sp->clear_all_call_backs();
+}
+
+TEST_F(CloudTabletSyncMetaTest, TestSyncMetaSyncsTtlWithoutChangingInMemory) {
+ EXPECT_EQ(0, _tablet->tablet_meta()->ttl_seconds());
+ EXPECT_FALSE(_tablet->tablet_meta()->tablet_schema()->is_in_memory());
+
+ auto sp = SyncPoint::get_instance();
+ sp->clear_all_call_backs();
+ sp->enable_processing();
+
+ auto mock_tablet_meta = createMockTabletMeta(true, "time_series");
+ mock_tablet_meta->set_ttl_seconds(3600);
+ mock_tablet_meta->mutable_tablet_schema()->set_is_in_memory(true);
+
+ sp->set_call_back("CloudMetaMgr::get_tablet_meta",
[mock_tablet_meta](auto&& args) {
+ auto* tablet_meta_ptr = try_any_cast<TabletMetaSharedPtr*>(args[1]);
+ *tablet_meta_ptr = mock_tablet_meta;
+ try_any_cast_ret<Status>(args)->second = true;
+ });
+
+ Status st = _tablet->sync_meta();
+ EXPECT_TRUE(st.ok());
+
+ EXPECT_EQ(3600, _tablet->tablet_meta()->ttl_seconds());
+ EXPECT_FALSE(_tablet->tablet_meta()->tablet_schema()->is_in_memory());
+
EXPECT_TRUE(_tablet->tablet_meta()->tablet_schema()->disable_auto_compaction());
+ EXPECT_EQ(_tablet->tablet_meta()->compaction_policy(), "time_series");
sp->disable_processing();
sp->clear_all_call_backs();
}
+
class CloudTabletApplyVisiblePendingTest : public testing::Test {
public:
CloudTabletApplyVisiblePendingTest() :
_engine(CloudStorageEngine(EngineOptions {})) {}
diff --git
a/fe/fe-core/src/main/java/org/apache/doris/cloud/alter/CloudSchemaChangeHandler.java
b/fe/fe-core/src/main/java/org/apache/doris/cloud/alter/CloudSchemaChangeHandler.java
index 87a5f3f5f4a..afbd99f5512 100644
---
a/fe/fe-core/src/main/java/org/apache/doris/cloud/alter/CloudSchemaChangeHandler.java
+++
b/fe/fe-core/src/main/java/org/apache/doris/cloud/alter/CloudSchemaChangeHandler.java
@@ -34,12 +34,21 @@ import org.apache.doris.common.Config;
import org.apache.doris.common.DdlException;
import org.apache.doris.common.MetaNotFoundException;
import org.apache.doris.common.UserException;
+import org.apache.doris.common.util.DebugPointUtil;
import org.apache.doris.common.util.DynamicPartitionUtil;
import org.apache.doris.common.util.PropertyAnalyzer;
+import org.apache.doris.proto.InternalService;
+import org.apache.doris.rpc.BackendServiceProxy;
import org.apache.doris.service.FrontendOptions;
+import org.apache.doris.system.Backend;
+import org.apache.doris.thrift.TStatusCode;
import com.google.common.base.Preconditions;
import com.google.common.collect.Lists;
+import com.google.common.util.concurrent.FutureCallback;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.common.util.concurrent.MoreExecutors;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
@@ -538,6 +547,80 @@ public class CloudSchemaChangeHandler extends
SchemaChangeHandler {
if (response.getStatus().getCode() != Cloud.MetaServiceCode.OK) {
throw new UserException(response.getStatus().getMsg());
}
+
+ notifyBackendsToSyncTabletMeta(tableName,
updateTabletReq.getTabletMetaInfosList().stream()
+ .map(Cloud.TabletMetaInfoPB::getTabletId)
+ .collect(Collectors.toList()));
+ }
+ }
+
+ void notifyBackendsToSyncTabletMeta(String tableName, List<Long>
tabletIds) {
+ if (tabletIds.isEmpty()) {
+ return;
+ }
+ if
(DebugPointUtil.isEnable("CloudSchemaChangeHandler.notifyBackendsToSyncTabletMeta.skip"))
{
+ LOG.info("skip sync tablet meta rpc dispatch by debug point,
tableName={}, tabletIds={}",
+ tableName, tabletIds);
+ return;
+ }
+ List<Backend> backends;
+ try {
+ backends =
Env.getCurrentSystemInfo().getAllBackendsByAllCluster().values().asList();
+ } catch (UserException e) {
+ LOG.warn("failed to get alive backends for sync tablet meta,
tableName={}, tabletIds={}",
+ tableName, tabletIds, e);
+ return;
+ }
+
+ InternalService.PSyncTabletMetaRequest request =
InternalService.PSyncTabletMetaRequest.newBuilder()
+ .addAllTabletIds(tabletIds)
+ .build();
+ long dispatchStartTimeMs = System.currentTimeMillis();
+ int sentBackends = 0;
+ int skippedBackends = 0;
+ LOG.info("start to dispatch sync tablet meta rpc, tableName={},
tabletIds={}, request={}",
+ tableName, tabletIds, request);
+ for (Backend backend : backends) {
+ if (!backend.isAlive() || backend.getBrpcPort() <= 0) {
+ skippedBackends++;
+ continue;
+ }
+ try {
+ long rpcStartTimeMs = System.currentTimeMillis();
+ ListenableFuture<InternalService.PSyncTabletMetaResponse>
future =
+
BackendServiceProxy.getInstance().syncTabletMeta(backend.getBrpcAddress(),
request);
+ sentBackends++;
+ Futures.addCallback(future, new
FutureCallback<InternalService.PSyncTabletMetaResponse>() {
+ @Override
+ public void
onSuccess(InternalService.PSyncTabletMetaResponse response) {
+ long costMs = System.currentTimeMillis() -
rpcStartTimeMs;
+ if (response == null || !response.hasStatus()
+ || response.getStatus().getStatusCode() !=
TStatusCode.OK.getValue()) {
+ LOG.warn("sync tablet meta rpc returned non-ok
response, backendId={}, tabletIds={},"
+ + " tableName={}, response={},
costMs={}",
+ backend.getId(), tabletIds, tableName,
response, costMs);
+ return;
+ }
+ LOG.info("sync tablet meta rpc finished, backendId={},
tableName={}, tabletIds={},"
+ + " response={}, costMs={}",
+ backend.getId(), tableName, tabletIds,
response, costMs);
+ }
+
+ @Override
+ public void onFailure(Throwable t) {
+ LOG.warn("sync tablet meta rpc failed, backendId={},
tableName={}, tabletIds={}, costMs={}",
+ backend.getId(), tableName, tabletIds,
+ System.currentTimeMillis() - rpcStartTimeMs,
t);
+ }
+ }, MoreExecutors.directExecutor());
+ } catch (Exception e) {
+ LOG.warn("failed to dispatch sync tablet meta rpc,
backendId={}, tableName={}, tabletIds={}",
+ backend.getId(), tableName, tabletIds, e);
+ }
}
+ LOG.info("finish dispatching sync tablet meta rpc, tableName={},
tabletIds={}, backendNum={},"
+ + " sentBackends={}, skippedBackends={}, costMs={}",
+ tableName, tabletIds, backends.size(), sentBackends,
skippedBackends,
+ System.currentTimeMillis() - dispatchStartTimeMs);
}
}
diff --git
a/fe/fe-core/src/main/java/org/apache/doris/rpc/BackendServiceClient.java
b/fe/fe-core/src/main/java/org/apache/doris/rpc/BackendServiceClient.java
index 13489e1894f..d61c1c3e00d 100644
--- a/fe/fe-core/src/main/java/org/apache/doris/rpc/BackendServiceClient.java
+++ b/fe/fe-core/src/main/java/org/apache/doris/rpc/BackendServiceClient.java
@@ -189,6 +189,11 @@ public class BackendServiceClient {
return stub.alterVaultSync(request);
}
+ public ListenableFuture<InternalService.PSyncTabletMetaResponse>
syncTabletMeta(
+ InternalService.PSyncTabletMetaRequest request) {
+ return stub.syncTabletMeta(request);
+ }
+
public Future<InternalService.PGetBeResourceResponse>
getBeResource(InternalService.PGetBeResourceRequest request,
int timeoutSec) {
return stub.withDeadlineAfter(timeoutSec,
TimeUnit.SECONDS).getBeResource(request);
diff --git
a/fe/fe-core/src/main/java/org/apache/doris/rpc/BackendServiceProxy.java
b/fe/fe-core/src/main/java/org/apache/doris/rpc/BackendServiceProxy.java
index 15f35d37117..70e96650622 100644
--- a/fe/fe-core/src/main/java/org/apache/doris/rpc/BackendServiceProxy.java
+++ b/fe/fe-core/src/main/java/org/apache/doris/rpc/BackendServiceProxy.java
@@ -548,6 +548,18 @@ public class BackendServiceProxy {
}
}
+ public ListenableFuture<InternalService.PSyncTabletMetaResponse>
syncTabletMeta(TNetworkAddress address,
+ InternalService.PSyncTabletMetaRequest request) throws
RpcException {
+ try {
+ final BackendServiceClient client = getProxy(address);
+ return client.syncTabletMeta(request);
+ } catch (Throwable e) {
+ LOG.warn("failed to sync tablet meta from address={}:{}",
address.getHostname(),
+ address.getPort(), e);
+ throw new RpcException(address.getHostname(), e.getMessage());
+ }
+ }
+
public Future<InternalService.PFetchRemoteSchemaResponse>
fetchRemoteTabletSchemaAsync(
TNetworkAddress address, InternalService.PFetchRemoteSchemaRequest
request) throws RpcException {
try {
diff --git
a/fe/fe-core/src/test/java/org/apache/doris/cloud/alter/CloudSchemaChangeHandlerTest.java
b/fe/fe-core/src/test/java/org/apache/doris/cloud/alter/CloudSchemaChangeHandlerTest.java
new file mode 100644
index 00000000000..467b7ae40b2
--- /dev/null
+++
b/fe/fe-core/src/test/java/org/apache/doris/cloud/alter/CloudSchemaChangeHandlerTest.java
@@ -0,0 +1,478 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you 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.
+
+package org.apache.doris.cloud.alter;
+
+import org.apache.doris.catalog.Database;
+import org.apache.doris.catalog.Env;
+import org.apache.doris.catalog.KeysType;
+import org.apache.doris.catalog.MaterializedIndex;
+import org.apache.doris.catalog.OlapTable;
+import org.apache.doris.catalog.Partition;
+import org.apache.doris.catalog.Table;
+import org.apache.doris.cloud.proto.Cloud;
+import org.apache.doris.cloud.rpc.MetaServiceProxy;
+import org.apache.doris.common.Config;
+import org.apache.doris.common.UserException;
+import org.apache.doris.common.util.DebugPointUtil;
+import org.apache.doris.proto.InternalService;
+import org.apache.doris.rpc.BackendServiceProxy;
+import org.apache.doris.system.Backend;
+import org.apache.doris.system.SystemInfoService;
+import org.apache.doris.thrift.TNetworkAddress;
+import org.apache.doris.thrift.TStatusCode;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.common.util.concurrent.Futures;
+import org.junit.After;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.ArgumentCaptor;
+import org.mockito.MockedStatic;
+import org.mockito.Mockito;
+
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+public class CloudSchemaChangeHandlerTest {
+ private int originalCloudTxnTabletBatchSize;
+ private String originalCloudUniqueId;
+ private String originalMetaServiceEndpoint;
+ private boolean originalEnableDebugPoints;
+
+ @Before
+ public void setUp() {
+ originalCloudTxnTabletBatchSize = Config.cloud_txn_tablet_batch_size;
+ originalCloudUniqueId = Config.cloud_unique_id;
+ originalMetaServiceEndpoint = Config.meta_service_endpoint;
+ originalEnableDebugPoints = Config.enable_debug_points;
+ Config.cloud_txn_tablet_batch_size = 2;
+ Config.cloud_unique_id = "cloud-test";
+ Config.meta_service_endpoint = "127.0.0.1:20121";
+ Config.enable_debug_points = false;
+ DebugPointUtil.clearDebugPoints();
+ }
+
+ @After
+ public void tearDown() {
+ Config.cloud_txn_tablet_batch_size = originalCloudTxnTabletBatchSize;
+ Config.cloud_unique_id = originalCloudUniqueId;
+ Config.meta_service_endpoint = originalMetaServiceEndpoint;
+ Config.enable_debug_points = originalEnableDebugPoints;
+ DebugPointUtil.clearDebugPoints();
+ }
+
+ @Test
+ public void testUpdateTablePropertiesNotifiesAllAliveBackendsInBatches()
throws Exception {
+ CloudSchemaChangeHandler handler = new CloudSchemaChangeHandler();
+ Database db = createMockDatabaseWithThreeTablets();
+
+ Backend aliveBackend1 = new Backend(1L, "be-1", 9050);
+ aliveBackend1.setAlive(true);
+ aliveBackend1.setBrpcPort(8060);
+ Backend aliveBackend2 = new Backend(2L, "be-2", 9050);
+ aliveBackend2.setAlive(true);
+ aliveBackend2.setBrpcPort(8061);
+ Backend deadBackend = new Backend(3L, "be-dead", 9050);
+ deadBackend.setAlive(false);
+ deadBackend.setBrpcPort(8062);
+
+ SystemInfoService systemInfoService =
Mockito.mock(SystemInfoService.class);
+
Mockito.when(systemInfoService.getAllBackendsByAllCluster()).thenReturn(ImmutableMap.of(
+ aliveBackend1.getId(), aliveBackend1,
+ aliveBackend2.getId(), aliveBackend2,
+ deadBackend.getId(), deadBackend));
+
+ Env env = Mockito.mock(Env.class);
+
+ MetaServiceProxy metaServiceProxy =
Mockito.mock(MetaServiceProxy.class);
+
Mockito.when(metaServiceProxy.updateTablet(Mockito.any())).thenReturn(okUpdateTabletResponse());
+
+ BackendServiceProxy backendServiceProxy =
Mockito.mock(BackendServiceProxy.class);
+ Mockito.when(backendServiceProxy.syncTabletMeta(Mockito.any(),
Mockito.any()))
+
.thenReturn(Futures.immediateFuture(okSyncTabletMetaResponse()));
+
+ try (MockedStatic<Env> envMock = Mockito.mockStatic(Env.class);
+ MockedStatic<MetaServiceProxy> metaProxyMock =
Mockito.mockStatic(MetaServiceProxy.class);
+ MockedStatic<BackendServiceProxy> backendProxyMock =
Mockito.mockStatic(BackendServiceProxy.class)) {
+ envMock.when(Env::getCurrentEnv).thenReturn(env);
+
envMock.when(Env::getCurrentSystemInfo).thenReturn(systemInfoService);
+
metaProxyMock.when(MetaServiceProxy::getInstance).thenReturn(metaServiceProxy);
+
backendProxyMock.when(BackendServiceProxy::getInstance).thenReturn(backendServiceProxy);
+
+ Map<String, String> properties = new HashMap<>();
+ properties.put("compaction_policy", "time_series");
+ handler.updateTableProperties(db, "tbl", properties);
+ }
+
+ ArgumentCaptor<Cloud.UpdateTabletRequest> updateCaptor =
+ ArgumentCaptor.forClass(Cloud.UpdateTabletRequest.class);
+ Mockito.verify(metaServiceProxy,
Mockito.times(2)).updateTablet(updateCaptor.capture());
+ List<Cloud.UpdateTabletRequest> updateRequests =
updateCaptor.getAllValues();
+ Assert.assertEquals(Arrays.asList(101L, 102L),
+ updateRequests.get(0).getTabletMetaInfosList().stream()
+
.map(Cloud.TabletMetaInfoPB::getTabletId).collect(Collectors.toList()));
+ Assert.assertEquals(Arrays.asList(103L),
+ updateRequests.get(1).getTabletMetaInfosList().stream()
+
.map(Cloud.TabletMetaInfoPB::getTabletId).collect(Collectors.toList()));
+
+ ArgumentCaptor<TNetworkAddress> addressCaptor =
ArgumentCaptor.forClass(TNetworkAddress.class);
+ ArgumentCaptor<InternalService.PSyncTabletMetaRequest> syncCaptor =
+
ArgumentCaptor.forClass(InternalService.PSyncTabletMetaRequest.class);
+ Mockito.verify(backendServiceProxy, Mockito.times(4))
+ .syncTabletMeta(addressCaptor.capture(), syncCaptor.capture());
+
+ List<TNetworkAddress> addresses = addressCaptor.getAllValues();
+ Assert.assertFalse(addresses.stream().anyMatch(addr ->
"be-dead".equals(addr.getHostname())));
+ Assert.assertEquals(2L, addresses.stream().filter(addr ->
"be-1".equals(addr.getHostname())).count());
+ Assert.assertEquals(2L, addresses.stream().filter(addr ->
"be-2".equals(addr.getHostname())).count());
+
+ List<InternalService.PSyncTabletMetaRequest> syncRequests =
syncCaptor.getAllValues();
+ Assert.assertEquals(Arrays.asList(101L, 102L),
syncRequests.get(0).getTabletIdsList());
+ Assert.assertEquals(Arrays.asList(101L, 102L),
syncRequests.get(1).getTabletIdsList());
+ Assert.assertEquals(Arrays.asList(103L),
syncRequests.get(2).getTabletIdsList());
+ Assert.assertEquals(Arrays.asList(103L),
syncRequests.get(3).getTabletIdsList());
+ }
+
+ @Test
+ public void testUpdateTablePropertiesThrowsWhenUpdateTabletFails() throws
Exception {
+ CloudSchemaChangeHandler handler = new CloudSchemaChangeHandler();
+ Database db = createMockDatabaseWithThreeTablets();
+
+ MetaServiceProxy metaServiceProxy =
Mockito.mock(MetaServiceProxy.class);
+ Mockito.when(metaServiceProxy.updateTablet(Mockito.any()))
+ .thenThrow(new RuntimeException("update failed"));
+
+ try (MockedStatic<MetaServiceProxy> metaProxyMock =
Mockito.mockStatic(MetaServiceProxy.class)) {
+
metaProxyMock.when(MetaServiceProxy::getInstance).thenReturn(metaServiceProxy);
+
+ Map<String, String> properties = new HashMap<>();
+ properties.put("compaction_policy", "time_series");
+ UserException exception = Assert.assertThrows(UserException.class,
+ () -> handler.updateTableProperties(db, "tbl",
properties));
+ Assert.assertTrue(exception.getMessage().contains("update
failed"));
+ }
+ }
+
+ @Test
+ public void testUpdateTablePropertiesThrowsWhenUpdateTabletResponseNotOk()
throws Exception {
+ CloudSchemaChangeHandler handler = new CloudSchemaChangeHandler();
+ Database db = createMockDatabaseWithThreeTablets();
+
+ MetaServiceProxy metaServiceProxy =
Mockito.mock(MetaServiceProxy.class);
+ Mockito.when(metaServiceProxy.updateTablet(Mockito.any()))
+ .thenReturn(Cloud.UpdateTabletResponse.newBuilder()
+ .setStatus(Cloud.MetaServiceResponseStatus.newBuilder()
+
.setCode(Cloud.MetaServiceCode.INVALID_ARGUMENT)
+ .setMsg("meta service rejected"))
+ .build());
+
+ try (MockedStatic<MetaServiceProxy> metaProxyMock =
Mockito.mockStatic(MetaServiceProxy.class)) {
+
metaProxyMock.when(MetaServiceProxy::getInstance).thenReturn(metaServiceProxy);
+
+ Map<String, String> properties = new HashMap<>();
+ properties.put("compaction_policy", "time_series");
+ UserException exception = Assert.assertThrows(UserException.class,
+ () -> handler.updateTableProperties(db, "tbl",
properties));
+ Assert.assertTrue(exception.getMessage().contains("meta service
rejected"));
+ }
+ }
+
+ @Test
+ public void testNotifyBackendsToSyncTabletMetaSwallowsDispatchFailure()
throws Exception {
+ CloudSchemaChangeHandler handler = new CloudSchemaChangeHandler();
+ Backend aliveBackend1 = new Backend(1L, "be-1", 9050);
+ aliveBackend1.setAlive(true);
+ aliveBackend1.setBrpcPort(8060);
+ Backend aliveBackend2 = new Backend(2L, "be-2", 9050);
+ aliveBackend2.setAlive(true);
+ aliveBackend2.setBrpcPort(8061);
+
+ SystemInfoService systemInfoService =
Mockito.mock(SystemInfoService.class);
+
Mockito.when(systemInfoService.getAllBackendsByAllCluster()).thenReturn(ImmutableMap.of(
+ aliveBackend1.getId(), aliveBackend1,
+ aliveBackend2.getId(), aliveBackend2));
+
+ BackendServiceProxy backendServiceProxy =
Mockito.mock(BackendServiceProxy.class);
+ Mockito.when(backendServiceProxy.syncTabletMeta(Mockito.argThat(
+ addr -> addr != null &&
"be-1".equals(addr.getHostname())), Mockito.any()))
+
.thenReturn(Futures.immediateFuture(okSyncTabletMetaResponse()));
+ Mockito.when(backendServiceProxy.syncTabletMeta(Mockito.argThat(
+ addr -> addr != null &&
"be-2".equals(addr.getHostname())), Mockito.any()))
+ .thenThrow(new org.apache.doris.rpc.RpcException("be-2",
"dispatch failed"));
+
+ try (MockedStatic<Env> envMock = Mockito.mockStatic(Env.class);
+ MockedStatic<BackendServiceProxy> backendProxyMock =
Mockito.mockStatic(BackendServiceProxy.class)) {
+
envMock.when(Env::getCurrentSystemInfo).thenReturn(systemInfoService);
+
backendProxyMock.when(BackendServiceProxy::getInstance).thenReturn(backendServiceProxy);
+
+ handler.notifyBackendsToSyncTabletMeta("test_table",
Arrays.asList(101L, 102L));
+ }
+
+ Mockito.verify(backendServiceProxy, Mockito.times(2))
+ .syncTabletMeta(Mockito.any(), Mockito.any());
+ }
+
+ @Test
+ public void testNotifyBackendsToSyncTabletMetaReturnsForEmptyTabletList() {
+ CloudSchemaChangeHandler handler = new CloudSchemaChangeHandler();
+ BackendServiceProxy backendServiceProxy =
Mockito.mock(BackendServiceProxy.class);
+
+ try (MockedStatic<BackendServiceProxy> backendProxyMock =
Mockito.mockStatic(BackendServiceProxy.class)) {
+
backendProxyMock.when(BackendServiceProxy::getInstance).thenReturn(backendServiceProxy);
+
+ handler.notifyBackendsToSyncTabletMeta("test_table",
Arrays.asList());
+ }
+
+ Mockito.verifyNoInteractions(backendServiceProxy);
+ }
+
+ @Test
+ public void
testNotifyBackendsToSyncTabletMetaReturnsWhenDebugPointEnabled() {
+ CloudSchemaChangeHandler handler = new CloudSchemaChangeHandler();
+ BackendServiceProxy backendServiceProxy =
Mockito.mock(BackendServiceProxy.class);
+ Config.enable_debug_points = true;
+
DebugPointUtil.addDebugPoint("CloudSchemaChangeHandler.notifyBackendsToSyncTabletMeta.skip");
+
+ try (MockedStatic<BackendServiceProxy> backendProxyMock =
Mockito.mockStatic(BackendServiceProxy.class)) {
+
backendProxyMock.when(BackendServiceProxy::getInstance).thenReturn(backendServiceProxy);
+
+ handler.notifyBackendsToSyncTabletMeta("test_table",
Arrays.asList(101L, 102L));
+ }
+
+ Mockito.verifyNoInteractions(backendServiceProxy);
+ }
+
+ @Test
+ public void testNotifyBackendsToSyncTabletMetaSkipsUnavailableBackends()
throws Exception {
+ CloudSchemaChangeHandler handler = new CloudSchemaChangeHandler();
+ Backend aliveBackend = new Backend(1L, "be-1", 9050);
+ aliveBackend.setAlive(true);
+ aliveBackend.setBrpcPort(8060);
+ Backend deadBackend = new Backend(2L, "be-dead", 9050);
+ deadBackend.setAlive(false);
+ deadBackend.setBrpcPort(8061);
+ Backend noBrpcBackend = new Backend(3L, "be-no-brpc", 9050);
+ noBrpcBackend.setAlive(true);
+ noBrpcBackend.setBrpcPort(0);
+
+ SystemInfoService systemInfoService =
Mockito.mock(SystemInfoService.class);
+
Mockito.when(systemInfoService.getAllBackendsByAllCluster()).thenReturn(ImmutableMap.of(
+ aliveBackend.getId(), aliveBackend,
+ deadBackend.getId(), deadBackend,
+ noBrpcBackend.getId(), noBrpcBackend));
+
+ BackendServiceProxy backendServiceProxy =
Mockito.mock(BackendServiceProxy.class);
+ Mockito.when(backendServiceProxy.syncTabletMeta(Mockito.any(),
Mockito.any()))
+
.thenReturn(Futures.immediateFuture(okSyncTabletMetaResponse()));
+
+ try (MockedStatic<Env> envMock = Mockito.mockStatic(Env.class);
+ MockedStatic<BackendServiceProxy> backendProxyMock =
Mockito.mockStatic(BackendServiceProxy.class)) {
+
envMock.when(Env::getCurrentSystemInfo).thenReturn(systemInfoService);
+
backendProxyMock.when(BackendServiceProxy::getInstance).thenReturn(backendServiceProxy);
+
+ handler.notifyBackendsToSyncTabletMeta("test_table",
Arrays.asList(101L, 102L));
+ }
+
+ ArgumentCaptor<InternalService.PSyncTabletMetaRequest> syncCaptor =
+
ArgumentCaptor.forClass(InternalService.PSyncTabletMetaRequest.class);
+ Mockito.verify(backendServiceProxy, Mockito.times(1))
+ .syncTabletMeta(Mockito.argThat(addr ->
"be-1".equals(addr.getHostname())), syncCaptor.capture());
+ Assert.assertEquals(Arrays.asList(101L, 102L),
syncCaptor.getValue().getTabletIdsList());
+ }
+
+ @Test
+ public void testNotifyBackendsToSyncTabletMetaSwallowsAsyncFailure()
throws Exception {
+ CloudSchemaChangeHandler handler = new CloudSchemaChangeHandler();
+ Backend aliveBackend = new Backend(1L, "be-1", 9050);
+ aliveBackend.setAlive(true);
+ aliveBackend.setBrpcPort(8060);
+
+ SystemInfoService systemInfoService =
Mockito.mock(SystemInfoService.class);
+
Mockito.when(systemInfoService.getAllBackendsByAllCluster()).thenReturn(ImmutableMap.of(
+ aliveBackend.getId(), aliveBackend));
+
+ BackendServiceProxy backendServiceProxy =
Mockito.mock(BackendServiceProxy.class);
+ Mockito.when(backendServiceProxy.syncTabletMeta(Mockito.any(),
Mockito.any()))
+ .thenReturn(Futures.immediateFailedFuture(new
RuntimeException("async failed")));
+
+ try (MockedStatic<Env> envMock = Mockito.mockStatic(Env.class);
+ MockedStatic<BackendServiceProxy> backendProxyMock =
Mockito.mockStatic(BackendServiceProxy.class)) {
+
envMock.when(Env::getCurrentSystemInfo).thenReturn(systemInfoService);
+
backendProxyMock.when(BackendServiceProxy::getInstance).thenReturn(backendServiceProxy);
+
+ handler.notifyBackendsToSyncTabletMeta("test_table",
Arrays.asList(101L, 102L));
+ }
+
+ Mockito.verify(backendServiceProxy, Mockito.times(1))
+ .syncTabletMeta(Mockito.any(), Mockito.any());
+ }
+
+ @Test
+ public void testNotifyBackendsToSyncTabletMetaSwallowsNullResponse()
throws Exception {
+ CloudSchemaChangeHandler handler = new CloudSchemaChangeHandler();
+ Backend aliveBackend = new Backend(1L, "be-1", 9050);
+ aliveBackend.setAlive(true);
+ aliveBackend.setBrpcPort(8060);
+
+ SystemInfoService systemInfoService =
Mockito.mock(SystemInfoService.class);
+
Mockito.when(systemInfoService.getAllBackendsByAllCluster()).thenReturn(ImmutableMap.of(
+ aliveBackend.getId(), aliveBackend));
+
+ BackendServiceProxy backendServiceProxy =
Mockito.mock(BackendServiceProxy.class);
+ Mockito.when(backendServiceProxy.syncTabletMeta(Mockito.any(),
Mockito.any()))
+ .thenReturn(Futures.immediateFuture(null));
+
+ try (MockedStatic<Env> envMock = Mockito.mockStatic(Env.class);
+ MockedStatic<BackendServiceProxy> backendProxyMock =
Mockito.mockStatic(BackendServiceProxy.class)) {
+
envMock.when(Env::getCurrentSystemInfo).thenReturn(systemInfoService);
+
backendProxyMock.when(BackendServiceProxy::getInstance).thenReturn(backendServiceProxy);
+
+ handler.notifyBackendsToSyncTabletMeta("test_table",
Arrays.asList(101L, 102L));
+ }
+
+ Mockito.verify(backendServiceProxy, Mockito.times(1))
+ .syncTabletMeta(Mockito.any(), Mockito.any());
+ }
+
+ @Test
+ public void
testNotifyBackendsToSyncTabletMetaSwallowsResponseWithoutStatus() throws
Exception {
+ CloudSchemaChangeHandler handler = new CloudSchemaChangeHandler();
+ Backend aliveBackend = new Backend(1L, "be-1", 9050);
+ aliveBackend.setAlive(true);
+ aliveBackend.setBrpcPort(8060);
+
+ SystemInfoService systemInfoService =
Mockito.mock(SystemInfoService.class);
+
Mockito.when(systemInfoService.getAllBackendsByAllCluster()).thenReturn(ImmutableMap.of(
+ aliveBackend.getId(), aliveBackend));
+
+ BackendServiceProxy backendServiceProxy =
Mockito.mock(BackendServiceProxy.class);
+ Mockito.when(backendServiceProxy.syncTabletMeta(Mockito.any(),
Mockito.any()))
+
.thenReturn(Futures.immediateFuture(InternalService.PSyncTabletMetaResponse.newBuilder()
+ .setSyncedTablets(2)
+ .build()));
+
+ try (MockedStatic<Env> envMock = Mockito.mockStatic(Env.class);
+ MockedStatic<BackendServiceProxy> backendProxyMock =
Mockito.mockStatic(BackendServiceProxy.class)) {
+
envMock.when(Env::getCurrentSystemInfo).thenReturn(systemInfoService);
+
backendProxyMock.when(BackendServiceProxy::getInstance).thenReturn(backendServiceProxy);
+
+ handler.notifyBackendsToSyncTabletMeta("test_table",
Arrays.asList(101L, 102L));
+ }
+
+ Mockito.verify(backendServiceProxy, Mockito.times(1))
+ .syncTabletMeta(Mockito.any(), Mockito.any());
+ }
+
+ @Test
+ public void testNotifyBackendsToSyncTabletMetaSwallowsNonOkResponse()
throws Exception {
+ CloudSchemaChangeHandler handler = new CloudSchemaChangeHandler();
+ Backend aliveBackend1 = new Backend(1L, "be-1", 9050);
+ aliveBackend1.setAlive(true);
+ aliveBackend1.setBrpcPort(8060);
+ Backend aliveBackend2 = new Backend(2L, "be-2", 9050);
+ aliveBackend2.setAlive(true);
+ aliveBackend2.setBrpcPort(8061);
+
+ SystemInfoService systemInfoService =
Mockito.mock(SystemInfoService.class);
+
Mockito.when(systemInfoService.getAllBackendsByAllCluster()).thenReturn(ImmutableMap.of(
+ aliveBackend1.getId(), aliveBackend1,
+ aliveBackend2.getId(), aliveBackend2));
+
+ BackendServiceProxy backendServiceProxy =
Mockito.mock(BackendServiceProxy.class);
+ Mockito.when(backendServiceProxy.syncTabletMeta(Mockito.any(),
Mockito.any()))
+
.thenReturn(Futures.immediateFuture(InternalService.PSyncTabletMetaResponse.newBuilder()
+
.setStatus(org.apache.doris.proto.Types.PStatus.newBuilder()
+
.setStatusCode(TStatusCode.CANCELLED.getValue()))
+ .setFailedTablets(2)
+ .build()));
+
+ try (MockedStatic<Env> envMock = Mockito.mockStatic(Env.class);
+ MockedStatic<BackendServiceProxy> backendProxyMock =
Mockito.mockStatic(BackendServiceProxy.class)) {
+
envMock.when(Env::getCurrentSystemInfo).thenReturn(systemInfoService);
+
backendProxyMock.when(BackendServiceProxy::getInstance).thenReturn(backendServiceProxy);
+
+ handler.notifyBackendsToSyncTabletMeta("test_table",
Arrays.asList(101L, 102L));
+ }
+
+ Mockito.verify(backendServiceProxy, Mockito.times(2))
+ .syncTabletMeta(Mockito.any(), Mockito.any());
+ }
+
+ @Test
+ public void
testNotifyBackendsToSyncTabletMetaReturnsWhenBackendDiscoveryFails() throws
Exception {
+ CloudSchemaChangeHandler handler = new CloudSchemaChangeHandler();
+ SystemInfoService systemInfoService =
Mockito.mock(SystemInfoService.class);
+ Mockito.when(systemInfoService.getAllBackendsByAllCluster())
+ .thenThrow(new
org.apache.doris.common.AnalysisException("backend discovery failed"));
+
+ BackendServiceProxy backendServiceProxy =
Mockito.mock(BackendServiceProxy.class);
+
+ try (MockedStatic<Env> envMock = Mockito.mockStatic(Env.class);
+ MockedStatic<BackendServiceProxy> backendProxyMock =
Mockito.mockStatic(BackendServiceProxy.class)) {
+
envMock.when(Env::getCurrentSystemInfo).thenReturn(systemInfoService);
+
backendProxyMock.when(BackendServiceProxy::getInstance).thenReturn(backendServiceProxy);
+
+ handler.notifyBackendsToSyncTabletMeta("test_table",
Arrays.asList(101L, 102L));
+ }
+
+ Mockito.verifyNoInteractions(backendServiceProxy);
+ }
+
+ private Cloud.UpdateTabletResponse okUpdateTabletResponse() {
+ return Cloud.UpdateTabletResponse.newBuilder()
+
.setStatus(Cloud.MetaServiceResponseStatus.newBuilder().setCode(Cloud.MetaServiceCode.OK))
+ .build();
+ }
+
+ private Database createMockDatabaseWithThreeTablets() throws Exception {
+ Database db = Mockito.mock(Database.class);
+ OlapTable table = Mockito.mock(OlapTable.class);
+ Partition partition = Mockito.mock(Partition.class);
+ MaterializedIndex index = Mockito.mock(MaterializedIndex.class);
+ org.apache.doris.catalog.Tablet tablet1 =
Mockito.mock(org.apache.doris.catalog.Tablet.class);
+ org.apache.doris.catalog.Tablet tablet2 =
Mockito.mock(org.apache.doris.catalog.Tablet.class);
+ org.apache.doris.catalog.Tablet tablet3 =
Mockito.mock(org.apache.doris.catalog.Tablet.class);
+
+ Mockito.when(db.getTableOrMetaException("tbl",
Table.TableType.OLAP)).thenReturn(table);
+ Mockito.when(table.getName()).thenReturn("tbl");
+ Mockito.when(table.getCompactionPolicy()).thenReturn("size_based");
+ Mockito.when(table.getKeysType()).thenReturn(KeysType.DUP_KEYS);
+
Mockito.when(table.getPartitions()).thenReturn(Arrays.asList(partition));
+ Mockito.when(table.getPartition("p1")).thenReturn(partition);
+ Mockito.when(partition.getName()).thenReturn("p1");
+
Mockito.when(partition.getMaterializedIndices(MaterializedIndex.IndexExtState.VISIBLE))
+ .thenReturn(Arrays.asList(index));
+ Mockito.when(index.getTablets()).thenReturn(Arrays.asList(tablet1,
tablet2, tablet3));
+ Mockito.when(tablet1.getId()).thenReturn(101L);
+ Mockito.when(tablet2.getId()).thenReturn(102L);
+ Mockito.when(tablet3.getId()).thenReturn(103L);
+ return db;
+ }
+
+ private InternalService.PSyncTabletMetaResponse okSyncTabletMetaResponse()
{
+ return InternalService.PSyncTabletMetaResponse.newBuilder()
+ .setStatus(org.apache.doris.proto.Types.PStatus.newBuilder()
+ .setStatusCode(TStatusCode.OK.getValue()))
+ .setSyncedTablets(1)
+ .build();
+ }
+}
diff --git
a/fe/fe-core/src/test/java/org/apache/doris/rpc/BackendServiceClientTest.java
b/fe/fe-core/src/test/java/org/apache/doris/rpc/BackendServiceClientTest.java
index 68aa36eee74..7992106cffd 100644
---
a/fe/fe-core/src/test/java/org/apache/doris/rpc/BackendServiceClientTest.java
+++
b/fe/fe-core/src/test/java/org/apache/doris/rpc/BackendServiceClientTest.java
@@ -19,13 +19,18 @@ package org.apache.doris.rpc;
import org.apache.doris.common.Config;
import org.apache.doris.common.jmockit.Deencapsulation;
+import org.apache.doris.proto.InternalService;
+import org.apache.doris.proto.PBackendServiceGrpc;
import org.apache.doris.thrift.TNetworkAddress;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
import io.grpc.ManagedChannel;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
+import org.mockito.Mockito;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
@@ -224,4 +229,29 @@ public class BackendServiceClientTest {
client1.shutdown();
client2.shutdown();
}
+
+ @Test
+ public void testSyncTabletMeta() {
+ TNetworkAddress address = new TNetworkAddress("localhost", 9060);
+ BackendServiceClient client = new BackendServiceClient(address,
"127.0.0.1", executor);
+
+ PBackendServiceGrpc.PBackendServiceFutureStub stub =
+
Mockito.mock(PBackendServiceGrpc.PBackendServiceFutureStub.class);
+ Deencapsulation.setField(client, "stub", stub);
+
+ InternalService.PSyncTabletMetaRequest request =
InternalService.PSyncTabletMetaRequest.newBuilder()
+ .addTabletIds(10001L)
+ .build();
+ ListenableFuture<InternalService.PSyncTabletMetaResponse>
expectedFuture = Futures.immediateFuture(
+ InternalService.PSyncTabletMetaResponse.newBuilder()
+ .setSyncedTablets(1)
+ .build());
+ Mockito.when(stub.syncTabletMeta(request)).thenReturn(expectedFuture);
+
+ ListenableFuture<InternalService.PSyncTabletMetaResponse> actualFuture
= client.syncTabletMeta(request);
+
+ Assert.assertSame(expectedFuture, actualFuture);
+ Mockito.verify(stub).syncTabletMeta(request);
+ client.shutdown();
+ }
}
diff --git
a/fe/fe-core/src/test/java/org/apache/doris/rpc/BackendServiceProxyTest.java
b/fe/fe-core/src/test/java/org/apache/doris/rpc/BackendServiceProxyTest.java
index 2a897f0e400..427ff457034 100644
--- a/fe/fe-core/src/test/java/org/apache/doris/rpc/BackendServiceProxyTest.java
+++ b/fe/fe-core/src/test/java/org/apache/doris/rpc/BackendServiceProxyTest.java
@@ -21,8 +21,11 @@ import org.apache.doris.catalog.Env;
import org.apache.doris.common.Config;
import org.apache.doris.common.DNSCache;
import org.apache.doris.common.jmockit.Deencapsulation;
+import org.apache.doris.proto.InternalService;
import org.apache.doris.thrift.TNetworkAddress;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
@@ -30,6 +33,7 @@ import org.junit.Test;
import org.mockito.MockedStatic;
import org.mockito.Mockito;
+import java.lang.reflect.Constructor;
import java.net.UnknownHostException;
import java.util.Map;
@@ -300,4 +304,54 @@ public class BackendServiceProxyTest {
Assert.assertTrue(serviceMap.containsKey(address1));
Assert.assertTrue(serviceMap.containsKey(address2));
}
+
+ @Test
+ public void testSyncTabletMeta() throws Exception {
+ String hostname = "backend-host.example.com";
+ String resolvedIp = "10.0.0.1";
+ int port = 9060;
+ TNetworkAddress address = new TNetworkAddress(hostname, port);
+ InternalService.PSyncTabletMetaRequest request =
InternalService.PSyncTabletMetaRequest.newBuilder()
+ .addTabletIds(10001L)
+ .build();
+
+ BackendServiceClient client = Mockito.mock(BackendServiceClient.class);
+ Mockito.when(client.isNormalState()).thenReturn(true);
+ Mockito.when(client.isUsingLatestChannelConfig()).thenReturn(true);
+ ListenableFuture<InternalService.PSyncTabletMetaResponse>
expectedFuture = Futures.immediateFuture(
+ InternalService.PSyncTabletMetaResponse.newBuilder()
+ .setSyncedTablets(1)
+ .build());
+
Mockito.when(client.syncTabletMeta(request)).thenReturn(expectedFuture);
+ Mockito.when(mockDnsCache.get(hostname)).thenReturn(resolvedIp);
+
+ Map<TNetworkAddress, Object> serviceMap =
Deencapsulation.getField(proxy, "serviceMap");
+ serviceMap.put(address, newBackendServiceClientExtIp(resolvedIp,
client));
+
+ ListenableFuture<InternalService.PSyncTabletMetaResponse> actualFuture
= proxy.syncTabletMeta(address, request);
+
+ Assert.assertSame(expectedFuture, actualFuture);
+ Mockito.verify(client).syncTabletMeta(request);
+ }
+
+ @Test
+ public void testSyncTabletMetaThrowsRpcException() {
+ String hostname = "non-existent-host.example.com";
+ TNetworkAddress address = new TNetworkAddress(hostname, 9060);
+ InternalService.PSyncTabletMetaRequest request =
InternalService.PSyncTabletMetaRequest.newBuilder()
+ .addTabletIds(10001L)
+ .build();
+ Mockito.when(mockDnsCache.get(hostname)).thenReturn("");
+
+ RpcException exception = Assert.assertThrows(RpcException.class, () ->
proxy.syncTabletMeta(address, request));
+ Assert.assertTrue(exception.getMessage().contains(hostname));
+ }
+
+ private Object newBackendServiceClientExtIp(String realIp,
BackendServiceClient client) throws Exception {
+ Class<?> clazz =
Class.forName("org.apache.doris.rpc.BackendServiceProxy$BackendServiceClientExtIp");
+ Constructor<?> constructor =
clazz.getDeclaredConstructor(BackendServiceProxy.class,
+ String.class, BackendServiceClient.class);
+ constructor.setAccessible(true);
+ return constructor.newInstance(proxy, realIp, client);
+ }
}
diff --git a/gensrc/proto/internal_service.proto
b/gensrc/proto/internal_service.proto
index aa8fbaa1478..ab666bda634 100644
--- a/gensrc/proto/internal_service.proto
+++ b/gensrc/proto/internal_service.proto
@@ -1118,6 +1118,17 @@ message PAlterVaultSyncRequest {
message PAlterVaultSyncResponse {
}
+message PSyncTabletMetaRequest {
+ repeated int64 tablet_ids = 1;
+}
+
+message PSyncTabletMetaResponse {
+ optional PStatus status = 1;
+ optional int64 synced_tablets = 2;
+ optional int64 skipped_tablets = 3;
+ optional int64 failed_tablets = 4;
+}
+
message PGetBeResourceRequest {
}
@@ -1283,6 +1294,7 @@ service PBackendService {
rpc fetch_remote_tablet_schema(PFetchRemoteSchemaRequest) returns
(PFetchRemoteSchemaResponse);
rpc test_jdbc_connection(PJdbcTestConnectionRequest) returns
(PJdbcTestConnectionResult);
rpc alter_vault_sync(PAlterVaultSyncRequest) returns
(PAlterVaultSyncResponse);
+ rpc sync_tablet_meta(PSyncTabletMetaRequest) returns
(PSyncTabletMetaResponse);
rpc get_be_resource(PGetBeResourceRequest) returns
(PGetBeResourceResponse);
rpc delete_dictionary(PDeleteDictionaryRequest) returns
(PDeleteDictionaryResponse);
rpc commit_refresh_dictionary(PCommitRefreshDictionaryRequest) returns
(PCommitRefreshDictionaryResponse);
diff --git
a/regression-test/suites/fault_injection_p0/cloud/test_cloud_sync_tablet_meta_after_alter.groovy
b/regression-test/suites/fault_injection_p0/cloud/test_cloud_sync_tablet_meta_after_alter.groovy
new file mode 100644
index 00000000000..17bdeae2d12
--- /dev/null
+++
b/regression-test/suites/fault_injection_p0/cloud/test_cloud_sync_tablet_meta_after_alter.groovy
@@ -0,0 +1,284 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you 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.
+
+suite("test_cloud_sync_tablet_meta_after_alter", "nonConcurrent") {
+ if (!isCloudMode()) {
+ return
+ }
+ if (!getFeConfig("enable_debug_points").equalsIgnoreCase("true")) {
+ logger.info("enable_debug_points=false, skip")
+ return
+ }
+ if (context.config.multiClusterBes == null ||
context.config.multiClusterBes.trim().isEmpty()) {
+ logger.info("multiClusterBes is empty, skip")
+ return
+ }
+
+ GetDebugPoint().clearDebugPointsForAllFEs()
+ GetDebugPoint().clearDebugPointsForAllBEs()
+
+ def cluster0 = "sync_meta_cluster0"
+ def cluster1 = "sync_meta_cluster1"
+ def clusterId0 = "sync_meta_cluster_id0"
+ def clusterId1 = "sync_meta_cluster_id1"
+ def dbName = "reg_sync_tablet_meta_db"
+ def tableName = "test_cloud_sync_tablet_meta_after_alter"
+ def versionLimitTableName =
"test_cloud_sync_tablet_meta_version_limit_after_alter"
+
+ List<String> ipList = []
+ List<String> hbPortList = []
+ List<String> beUniqueIdList = []
+
+ String[] bes = context.config.multiClusterBes.split(',')
+ for (String values : bes) {
+ if (ipList.size() == 2) {
+ break
+ }
+ String[] beInfo = values.split(':')
+ ipList.add(beInfo[0])
+ hbPortList.add(beInfo[1])
+ beUniqueIdList.add(beInfo[3])
+ }
+ assertTrue(ipList.size() >= 2, "need at least two backends in
multiClusterBes")
+
+ def getBackendForCluster = { String clusterName ->
+ def backend = sql_return_maparray("show backends").find {
it.Tag.contains(clusterName) }
+ assert backend != null: "backend for cluster ${clusterName} not found"
+ return backend
+ }
+
+ def getBvar = { backend, String name ->
+ def url = "http://${backend.Host}:${backend.BrpcPort}/vars/${name}"
+ def (code, out, err) = curl("GET", url)
+ assertEquals(0, code)
+ def matcher = (out =~
/(?m)^${java.util.regex.Pattern.quote(name)}\\s*:\\s*(\\d+)$/)
+ assertTrue(matcher.find(), "failed to parse bvar ${name} from ${url},
out=${out}, err=${err}")
+ return matcher.group(1).toLong()
+ }
+
+ def waitForBvarIncrease = { backend, String name, long baseline, long
delta ->
+ for (int retry = 0; retry < 20; retry++) {
+ def current = getBvar(backend, name)
+ logger.info("wait bvar ${name} on
${backend.Host}:${backend.BrpcPort}, baseline=${baseline}, current=${current}")
+ if (current >= baseline + delta) {
+ return current
+ }
+ sleep(500)
+ }
+ return getBvar(backend, name)
+ }
+
+ def getCompactionPolicy = { String currentTableName ->
+ def tablets = sql_return_maparray("""show tablets from
${currentTableName};""")
+ assertEquals(1, tablets.size())
+ def (code, out, err) = curl("GET", tablets[0].CompactionStatus)
+ assertEquals(0, code)
+ def tabletJson = parseJson(out.trim())
+ return tabletJson["compaction policy"]
+ }
+
+ def useCluster = { String clusterName ->
+ sql """use @${clusterName}"""
+ sql """use ${dbName}"""
+ }
+
+ def cleanupClusters = {
+ for (uniqueId in beUniqueIdList) {
+ def resp = get_cluster.call(uniqueId)
+ for (cluster in resp) {
+ if (cluster.type == "COMPUTE") {
+ drop_cluster.call(cluster.cluster_name, cluster.cluster_id)
+ }
+ }
+ }
+ wait_cluster_change()
+ }
+
+ setBeConfigTemporary([
+ tablet_sync_interval_s: 18000,
+ schedule_sync_tablets_interval_s: 18000
+ ]) {
+ try {
+ cleanupClusters.call()
+ add_cluster.call(beUniqueIdList[0], ipList[0], hbPortList[0],
cluster0, clusterId0)
+ add_cluster.call(beUniqueIdList[1], ipList[1], hbPortList[1],
cluster1, clusterId1)
+ wait_cluster_change()
+
+ sql """use @${cluster0}"""
+ sql """create database if not exists ${dbName}"""
+ sql """use ${dbName}"""
+ sql """drop table if exists ${tableName} force"""
+ sql """drop table if exists ${versionLimitTableName} force"""
+ sql """
+ CREATE TABLE ${tableName} (
+ id INT NOT NULL,
+ value INT
+ ) ENGINE=OLAP
+ DUPLICATE KEY(id)
+ DISTRIBUTED BY HASH(id) BUCKETS 1
+ PROPERTIES (
+ "replication_num" = "1",
+ "disable_auto_compaction" = "true"
+ )
+ """
+ sql """insert into ${tableName} values (1, 10), (2, 20), (3, 30)"""
+
+ def be0 = getBackendForCluster(cluster0)
+ def be1 = getBackendForCluster(cluster1)
+
+ // Case 1: warm only cluster0, alter property, and verify cluster1
is notified but skipped.
+ useCluster.call(cluster0)
+ qt_case1_cluster0_warmup """select count(*) from ${tableName}"""
+ assertEquals("size_based", getCompactionPolicy(tableName))
+
+ def case1Synced0 = getBvar(be0,
"cloud_sync_tablet_meta_synced_total")
+ def case1Skipped1 = getBvar(be1,
"cloud_sync_tablet_meta_skipped_total")
+
+ sql """alter table ${tableName} set ("compaction_policy" =
"time_series")"""
+
+ assertTrue(waitForBvarIncrease(be0,
"cloud_sync_tablet_meta_synced_total", case1Synced0, 1) >= case1Synced0 + 1)
+ assertTrue(waitForBvarIncrease(be1,
"cloud_sync_tablet_meta_skipped_total", case1Skipped1, 1) >= case1Skipped1 + 1)
+
+ useCluster.call(cluster0)
+ qt_case1_cluster0_after_alter """select count(*) from
${tableName}"""
+ assertEquals("time_series", getCompactionPolicy(tableName))
+
+ useCluster.call(cluster1)
+ qt_case1_cluster1_first_access """select count(*) from
${tableName}"""
+ assertEquals("time_series", getCompactionPolicy(tableName))
+
+ // Case 2: warm both clusters, alter property again, and verify
both clusters sync immediately.
+ useCluster.call(cluster0)
+ qt_case2_cluster0_warmup """select count(*) from ${tableName}"""
+ useCluster.call(cluster1)
+ qt_case2_cluster1_warmup """select count(*) from ${tableName}"""
+
+ def case2Synced0 = getBvar(be0,
"cloud_sync_tablet_meta_synced_total")
+ def case2Synced1 = getBvar(be1,
"cloud_sync_tablet_meta_synced_total")
+
+ useCluster.call(cluster0)
+ sql """alter table ${tableName} set ("compaction_policy" =
"size_based")"""
+
+ assertTrue(waitForBvarIncrease(be0,
"cloud_sync_tablet_meta_synced_total", case2Synced0, 1) >= case2Synced0 + 1)
+ assertTrue(waitForBvarIncrease(be1,
"cloud_sync_tablet_meta_synced_total", case2Synced1, 1) >= case2Synced1 + 1)
+
+ useCluster.call(cluster0)
+ assertEquals("size_based", getCompactionPolicy(tableName))
+ useCluster.call(cluster1)
+ assertEquals("size_based", getCompactionPolicy(tableName))
+
+ // Case 3: hit the size_based version limit first, then alter to
time_series and verify writes succeed immediately.
+ useCluster.call(cluster0)
+ sql """drop table if exists ${versionLimitTableName} force"""
+ sql """
+ CREATE TABLE ${versionLimitTableName} (
+ id BIGINT NOT NULL,
+ value BIGINT
+ ) ENGINE=OLAP
+ DUPLICATE KEY(id)
+ DISTRIBUTED BY HASH(id) BUCKETS 1
+ PROPERTIES (
+ "replication_num" = "1",
+ "disable_auto_compaction" = "true"
+ )
+ """
+ qt_case3_version_limit_warmup """select count(*) from
${versionLimitTableName}"""
+ assertEquals("size_based",
getCompactionPolicy(versionLimitTableName))
+
+ int successfulBeforeAlter = 0
+ String versionLimitFailure = null
+ for (int i = 0; i < 2400; i++) {
+ try {
+ sql """insert into ${versionLimitTableName} values (${i},
${i})"""
+ successfulBeforeAlter++
+ if ((i + 1) % 200 == 0) {
+ logger.info("inserted {} versions into {}", i + 1,
versionLimitTableName)
+ }
+ } catch (Exception e) {
+ versionLimitFailure = e.getMessage()
+ logger.info("hit version limit after {} successful
inserts, msg={}",
+ successfulBeforeAlter, versionLimitFailure)
+ break
+ }
+ }
+ assertTrue(versionLimitFailure != null, "expected version limit
failure before alter")
+ assertTrue(versionLimitFailure.contains("-235")
+ || versionLimitFailure.toLowerCase().contains("too many
versions")
+ || versionLimitFailure.toLowerCase().contains("version
count")
+ || versionLimitFailure.toLowerCase().contains("exceed
limit"),
+ "unexpected failure message: ${versionLimitFailure}")
+
+ def versionLimitTablets = sql_return_maparray("""show tablets from
${versionLimitTableName};""")
+ assertEquals(1, versionLimitTablets.size())
+ long versionCountBeforeAlter =
versionLimitTablets[0].VersionCount.toLong()
+ assertTrue(versionCountBeforeAlter >= 2000,
+ "expected version count near max limit,
actual=${versionCountBeforeAlter}")
+
+ sql """alter table ${versionLimitTableName} set
("compaction_policy" = "time_series")"""
+ assertEquals("time_series",
getCompactionPolicy(versionLimitTableName))
+
+ int successfulAfterAlter = 0
+ for (int i = 0; i < 50; i++) {
+ long rowId = 100000L + i
+ sql """insert into ${versionLimitTableName} values (${rowId},
${rowId})"""
+ successfulAfterAlter++
+ }
+ assertEquals(50, successfulAfterAlter)
+ def versionLimitTabletsAfterAlter = sql_return_maparray("""show
tablets from ${versionLimitTableName};""")
+ assertEquals(1, versionLimitTabletsAfterAlter.size())
+ assertTrue(versionLimitTabletsAfterAlter[0].VersionCount.toLong()
> versionCountBeforeAlter,
+ "expected version count to keep increasing immediately
after alter")
+ qt_case3_version_limit_after_alter """select count(*) from
${versionLimitTableName}"""
+
+ // Case 4: disable FE proactive notification and verify cached
tablets remain stale.
+
GetDebugPoint().enableDebugPointForAllFEs("CloudSchemaChangeHandler.notifyBackendsToSyncTabletMeta.skip")
+ def case4Request0 = getBvar(be0,
"cloud_sync_tablet_meta_requests_total")
+ def case4Request1 = getBvar(be1,
"cloud_sync_tablet_meta_requests_total")
+ def case4Synced0 = getBvar(be0,
"cloud_sync_tablet_meta_synced_total")
+ def case4Synced1 = getBvar(be1,
"cloud_sync_tablet_meta_synced_total")
+ def case4Skipped0 = getBvar(be0,
"cloud_sync_tablet_meta_skipped_total")
+ def case4Skipped1 = getBvar(be1,
"cloud_sync_tablet_meta_skipped_total")
+
+ useCluster.call(cluster0)
+ sql """alter table ${tableName} set ("compaction_policy" =
"time_series")"""
+ sleep(2000)
+
+ assertEquals(case4Request0, getBvar(be0,
"cloud_sync_tablet_meta_requests_total"))
+ assertEquals(case4Request1, getBvar(be1,
"cloud_sync_tablet_meta_requests_total"))
+ assertEquals(case4Synced0, getBvar(be0,
"cloud_sync_tablet_meta_synced_total"))
+ assertEquals(case4Synced1, getBvar(be1,
"cloud_sync_tablet_meta_synced_total"))
+ assertEquals(case4Skipped0, getBvar(be0,
"cloud_sync_tablet_meta_skipped_total"))
+ assertEquals(case4Skipped1, getBvar(be1,
"cloud_sync_tablet_meta_skipped_total"))
+
+ useCluster.call(cluster0)
+ assertEquals("size_based", getCompactionPolicy(tableName))
+ useCluster.call(cluster1)
+ assertEquals("size_based", getCompactionPolicy(tableName))
+ } finally {
+ GetDebugPoint().clearDebugPointsForAllFEs()
+ GetDebugPoint().clearDebugPointsForAllBEs()
+ try {
+ sql """use @${cluster0}"""
+ sql """drop table if exists ${dbName}.${tableName} force"""
+ sql """drop table if exists ${dbName}.${versionLimitTableName}
force"""
+ } catch (Exception e) {
+ logger.info("drop table in finally failed: ${e.getMessage()}")
+ }
+ cleanupClusters.call()
+ }
+ }
+}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]