| // 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 <brpc/controller.h> |
| #include <fmt/format.h> |
| #include <gen_cpp/cloud.pb.h> |
| #include <gen_cpp/olap_file.pb.h> |
| #include <gtest/gtest.h> |
| |
| #include <cstdint> |
| #include <functional> |
| #include <memory> |
| #include <string> |
| |
| #include "common/defer.h" |
| #include "common/util.h" |
| #include "cpp/sync_point.h" |
| #include "meta-service/meta_service.h" |
| #include "meta-store/document_message.h" |
| #include "meta-store/keys.h" |
| #include "meta-store/meta_reader.h" |
| #include "meta-store/txn_kv.h" |
| #include "meta-store/txn_kv_error.h" |
| #include "meta-store/versioned_value.h" |
| |
| namespace doris::cloud { |
| // External functions from meta_service_test.cpp |
| extern std::unique_ptr<MetaServiceProxy> get_meta_service(); |
| extern std::unique_ptr<MetaServiceProxy> get_meta_service(bool mock_resource_mgr); |
| extern void create_tablet(MetaServiceProxy* meta_service, int64_t table_id, int64_t index_id, |
| int64_t partition_id, int64_t tablet_id); |
| extern doris::RowsetMetaCloudPB create_rowset(int64_t txn_id, int64_t tablet_id, int partition_id, |
| int64_t version, int num_rows); |
| extern void commit_rowset(MetaServiceProxy* meta_service, const doris::RowsetMetaCloudPB& rowset, |
| CreateRowsetResponse& res); |
| extern void add_tablet(CreateTabletsRequest& req, int64_t table_id, int64_t index_id, |
| int64_t partition_id, int64_t tablet_id); |
| |
| // Convert a string to a hex-escaped string. |
| // A non-displayed character is represented as \xHH where HH is the hexadecimal value of the character. |
| // A displayed character is represented as itself. |
| static std::string escape_hex(std::string_view data) { |
| std::string result; |
| for (char c : data) { |
| if (isprint(c)) { |
| result += c; |
| } else { |
| result += fmt::format("\\x{:02x}", static_cast<unsigned char>(c)); |
| } |
| } |
| return result; |
| } |
| |
| static size_t count_range(TxnKv* txn_kv, std::string_view begin = "", |
| std::string_view end = "\xFF") { |
| std::unique_ptr<Transaction> txn; |
| EXPECT_EQ(txn_kv->create_txn(&txn), TxnErrorCode::TXN_OK); |
| if (!txn) { |
| return 0; // Failed to create transaction |
| } |
| |
| FullRangeGetOptions opts; |
| opts.txn = txn.get(); |
| auto iter = txn_kv->full_range_get(std::string(begin), std::string(end), std::move(opts)); |
| size_t total = 0; |
| for (auto&& kvp = iter->next(); kvp.has_value(); kvp = iter->next()) { |
| total += 1; |
| } |
| |
| EXPECT_TRUE(iter->is_valid()); // The iterator should still be valid after the next call. |
| return total; |
| } |
| |
| static std::string dump_range(TxnKv* txn_kv, std::string_view begin = "", |
| std::string_view end = "\xFF") { |
| std::unique_ptr<Transaction> txn; |
| if (txn_kv->create_txn(&txn) != TxnErrorCode::TXN_OK) { |
| return "Failed to create dump range transaction"; |
| } |
| FullRangeGetOptions opts; |
| opts.txn = txn.get(); |
| auto iter = txn_kv->full_range_get(std::string(begin), std::string(end), std::move(opts)); |
| std::string buffer; |
| for (auto&& kv = iter->next(); kv.has_value(); kv = iter->next()) { |
| buffer += |
| fmt::format("Key: {}, Value: {}\n", escape_hex(kv->first), escape_hex(kv->second)); |
| } |
| EXPECT_TRUE(iter->is_valid()); // The iterator should still be valid after the next call. |
| return buffer; |
| } |
| |
| TEST(MetaServiceOperationLogTest, CommitPartitionLog) { |
| auto meta_service = get_meta_service(false); |
| std::string instance_id = "commit_partition_log"; |
| auto* sp = SyncPoint::get_instance(); |
| DORIS_CLOUD_DEFER { |
| SyncPoint::get_instance()->clear_all_call_backs(); |
| }; |
| sp->set_call_back("get_instance_id", [&](auto&& args) { |
| auto* ret = try_any_cast_ret<std::string>(args); |
| ret->first = instance_id; |
| ret->second = true; |
| }); |
| sp->enable_processing(); |
| |
| constexpr int64_t db_id = 123; |
| constexpr int64_t table_id = 10001; |
| constexpr int64_t index_id = 10002; |
| constexpr int64_t partition_id = 10003; |
| |
| { |
| // write instance |
| InstanceInfoPB instance_info; |
| instance_info.set_instance_id(instance_id); |
| instance_info.set_multi_version_status(MULTI_VERSION_WRITE_ONLY); |
| std::unique_ptr<Transaction> txn; |
| ASSERT_EQ(meta_service->txn_kv()->create_txn(&txn), TxnErrorCode::TXN_OK); |
| txn->put(instance_key(instance_id), instance_info.SerializeAsString()); |
| ASSERT_EQ(txn->commit(), TxnErrorCode::TXN_OK); |
| |
| meta_service->resource_mgr()->refresh_instance(instance_id); |
| ASSERT_TRUE(meta_service->resource_mgr()->is_version_write_enabled(instance_id)); |
| } |
| |
| { |
| // Prepare partition |
| brpc::Controller ctrl; |
| PartitionRequest req; |
| PartitionResponse res; |
| req.set_db_id(db_id); |
| req.set_table_id(table_id); |
| req.add_index_ids(index_id); |
| req.add_partition_ids(partition_id); |
| meta_service->prepare_partition(&ctrl, &req, &res, nullptr); |
| ASSERT_EQ(res.status().code(), MetaServiceCode::OK) << res.status().DebugString(); |
| } |
| |
| { |
| // Commit partition |
| brpc::Controller ctrl; |
| PartitionRequest req; |
| PartitionResponse res; |
| req.set_db_id(db_id); |
| req.set_table_id(table_id); |
| req.add_index_ids(index_id); |
| req.add_partition_ids(partition_id); |
| meta_service->commit_partition(&ctrl, &req, &res, nullptr); |
| ASSERT_EQ(res.status().code(), MetaServiceCode::OK) << res.status().DebugString(); |
| } |
| |
| auto txn_kv = meta_service->txn_kv(); |
| Versionstamp version1; |
| { |
| // Verify partition meta/index/inverted indexes are exists |
| std::string partition_meta_key = versioned::meta_partition_key({instance_id, partition_id}); |
| std::unique_ptr<Transaction> txn; |
| ASSERT_EQ(txn_kv->create_txn(&txn), TxnErrorCode::TXN_OK); |
| std::string value; |
| ASSERT_EQ(versioned_get(txn.get(), partition_meta_key, &version1, &value), |
| TxnErrorCode::TXN_OK); |
| |
| std::string partition_inverted_index_key = versioned::partition_inverted_index_key( |
| {instance_id, db_id, table_id, partition_id}); |
| ASSERT_EQ(txn->get(partition_inverted_index_key, &value), TxnErrorCode::TXN_OK); |
| |
| std::string partition_index_key = |
| versioned::partition_index_key({instance_id, partition_id}); |
| ASSERT_EQ(txn->get(partition_index_key, &value), TxnErrorCode::TXN_OK); |
| PartitionIndexPB partition_index; |
| ASSERT_TRUE(partition_index.ParseFromString(value)); |
| ASSERT_EQ(partition_index.db_id(), db_id); |
| ASSERT_EQ(partition_index.table_id(), table_id); |
| } |
| |
| Versionstamp version2; |
| { |
| // Verify table version exists |
| MetaReader meta_reader(instance_id, txn_kv.get()); |
| ASSERT_EQ(meta_reader.get_table_version(table_id, &version2), TxnErrorCode::TXN_OK); |
| } |
| |
| ASSERT_EQ(version1, version2); |
| |
| { |
| // verify commit partition log |
| std::unique_ptr<Transaction> txn; |
| ASSERT_EQ(txn_kv->create_txn(&txn), TxnErrorCode::TXN_OK); |
| std::string log_key = versioned::log_key({instance_id}); |
| std::string value; |
| ASSERT_EQ(versioned_get(txn.get(), log_key, &version2, &value), TxnErrorCode::TXN_OK); |
| OperationLogPB operation_log; |
| ASSERT_TRUE(operation_log.ParseFromString(value)); |
| ASSERT_TRUE(operation_log.has_commit_partition()); |
| } |
| |
| ASSERT_EQ(version1, version2); |
| } |
| |
| TEST(MetaServiceOperationLogTest, DropPartitionLog) { |
| auto meta_service = get_meta_service(false); |
| std::string instance_id = "commit_partition_log"; |
| auto* sp = SyncPoint::get_instance(); |
| DORIS_CLOUD_DEFER { |
| SyncPoint::get_instance()->clear_all_call_backs(); |
| }; |
| sp->set_call_back("get_instance_id", [&](auto&& args) { |
| auto* ret = try_any_cast_ret<std::string>(args); |
| ret->first = instance_id; |
| ret->second = true; |
| }); |
| sp->enable_processing(); |
| |
| constexpr int64_t db_id = 123; |
| constexpr int64_t table_id = 10001; |
| constexpr int64_t index_id = 10002; |
| constexpr int64_t partition_id = 10003; |
| |
| { |
| // write instance |
| InstanceInfoPB instance_info; |
| instance_info.set_instance_id(instance_id); |
| instance_info.set_multi_version_status(MULTI_VERSION_WRITE_ONLY); |
| std::unique_ptr<Transaction> txn; |
| ASSERT_EQ(meta_service->txn_kv()->create_txn(&txn), TxnErrorCode::TXN_OK); |
| txn->put(instance_key(instance_id), instance_info.SerializeAsString()); |
| ASSERT_EQ(txn->commit(), TxnErrorCode::TXN_OK); |
| |
| meta_service->resource_mgr()->refresh_instance(instance_id); |
| ASSERT_TRUE(meta_service->resource_mgr()->is_version_write_enabled(instance_id)); |
| } |
| |
| { |
| // Prepare partition 0,1,2,3 |
| brpc::Controller ctrl; |
| PartitionRequest req; |
| PartitionResponse res; |
| req.set_db_id(db_id); |
| req.set_table_id(table_id); |
| req.add_index_ids(index_id); |
| req.add_partition_ids(partition_id); |
| req.add_partition_ids(partition_id + 1); |
| req.add_partition_ids(partition_id + 2); |
| req.add_partition_ids(partition_id + 3); |
| meta_service->prepare_partition(&ctrl, &req, &res, nullptr); |
| ASSERT_EQ(res.status().code(), MetaServiceCode::OK) << res.status().DebugString(); |
| } |
| |
| { |
| // Commit partition 2,3 |
| brpc::Controller ctrl; |
| PartitionRequest req; |
| PartitionResponse res; |
| req.set_db_id(db_id); |
| req.set_table_id(table_id); |
| req.add_index_ids(index_id); |
| req.add_partition_ids(partition_id + 2); |
| req.add_partition_ids(partition_id + 3); |
| meta_service->commit_partition(&ctrl, &req, &res, nullptr); |
| ASSERT_EQ(res.status().code(), MetaServiceCode::OK) << res.status().DebugString(); |
| } |
| |
| auto txn_kv = meta_service->txn_kv(); |
| size_t num_logs = count_range(txn_kv.get(), versioned::log_key(instance_id), |
| versioned::log_key(instance_id) + "\xFF"); |
| |
| { |
| // Drop partition 0 |
| brpc::Controller ctrl; |
| PartitionRequest req; |
| PartitionResponse res; |
| req.set_db_id(db_id); |
| req.set_table_id(table_id); |
| req.add_index_ids(index_id); |
| req.add_partition_ids(partition_id); |
| meta_service->drop_partition(&ctrl, &req, &res, nullptr); |
| ASSERT_EQ(res.status().code(), MetaServiceCode::OK) << res.status().DebugString(); |
| |
| // No operation log are generated for drop partition 0 (it is not committed). |
| size_t new_num_logs = count_range(txn_kv.get(), versioned::log_key(instance_id), |
| versioned::log_key(instance_id) + "\xFF"); |
| ASSERT_EQ(new_num_logs, num_logs) |
| << "Expected no new operation logs for drop partition 0, but found " |
| << new_num_logs - num_logs << dump_range(txn_kv.get()); |
| |
| // The recycle partition key must exists. |
| std::string recycle_key = recycle_partition_key({instance_id, partition_id}); |
| std::unique_ptr<Transaction> txn; |
| ASSERT_EQ(txn_kv->create_txn(&txn), TxnErrorCode::TXN_OK); |
| std::string value; |
| TxnErrorCode err = txn->get(recycle_key, &value); |
| ASSERT_EQ(err, TxnErrorCode::TXN_OK); |
| } |
| |
| { |
| // Drop partition 1,2, it should generate operation logs. |
| brpc::Controller ctrl; |
| PartitionRequest req; |
| PartitionResponse res; |
| req.set_db_id(db_id); |
| req.set_table_id(table_id); |
| req.add_index_ids(index_id); |
| req.add_partition_ids(partition_id + 1); |
| req.add_partition_ids(partition_id + 2); |
| req.set_need_update_table_version(true); |
| meta_service->drop_partition(&ctrl, &req, &res, nullptr); |
| ASSERT_EQ(res.status().code(), MetaServiceCode::OK) << res.status().DebugString(); |
| // Check that new operation logs are generated. |
| size_t new_num_logs = count_range(txn_kv.get(), versioned::log_key(instance_id), |
| versioned::log_key(instance_id) + "\xFF"); |
| ASSERT_GT(new_num_logs, num_logs) |
| << "Expected new operation logs for drop partition 1,2, but found " |
| << new_num_logs - num_logs << dump_range(txn_kv.get()); |
| num_logs = new_num_logs; |
| } |
| |
| { |
| // Drop partition 3, it should generate operation logs. |
| brpc::Controller ctrl; |
| PartitionRequest req; |
| PartitionResponse res; |
| req.set_db_id(db_id); |
| req.set_table_id(table_id); |
| req.add_index_ids(index_id); |
| req.add_partition_ids(partition_id + 3); |
| req.set_need_update_table_version(true); |
| meta_service->drop_partition(&ctrl, &req, &res, nullptr); |
| ASSERT_EQ(res.status().code(), MetaServiceCode::OK) << res.status().DebugString(); |
| // Check that new operation logs are generated. |
| size_t new_num_logs = count_range(txn_kv.get(), versioned::log_key(instance_id), |
| versioned::log_key(instance_id) + "\xFF"); |
| ASSERT_GT(new_num_logs, num_logs) |
| << "Expected new operation logs for drop partition 3, but found " |
| << new_num_logs - num_logs << dump_range(txn_kv.get()); |
| num_logs = new_num_logs; |
| |
| // The recycle partition key should not be exists. |
| std::string recycle_key = recycle_partition_key({instance_id, partition_id + 3}); |
| std::unique_ptr<Transaction> txn; |
| ASSERT_EQ(txn_kv->create_txn(&txn), TxnErrorCode::TXN_OK); |
| std::string value; |
| TxnErrorCode err = txn->get(recycle_key, &value); |
| ASSERT_EQ(err, TxnErrorCode::TXN_KEY_NOT_FOUND) |
| << "Expected recycle partition key to not exist, but found it: " << hex(recycle_key) |
| << " with value: " << escape_hex(value); |
| } |
| |
| Versionstamp version1; |
| Versionstamp version2; |
| { |
| // Verify table version exists |
| MetaReader meta_reader(instance_id, txn_kv.get()); |
| ASSERT_EQ(meta_reader.get_table_version(table_id, &version1), TxnErrorCode::TXN_OK); |
| } |
| |
| { |
| // verify last drop partition log |
| std::unique_ptr<Transaction> txn; |
| ASSERT_EQ(txn_kv->create_txn(&txn), TxnErrorCode::TXN_OK); |
| std::string log_key = versioned::log_key({instance_id}); |
| std::string value; |
| ASSERT_EQ(versioned_get(txn.get(), log_key, &version2, &value), TxnErrorCode::TXN_OK); |
| OperationLogPB operation_log; |
| ASSERT_TRUE(operation_log.ParseFromString(value)); |
| ASSERT_TRUE(operation_log.has_drop_partition()); |
| ASSERT_EQ(operation_log.drop_partition().partition_ids_size(), 1); |
| ASSERT_EQ(operation_log.drop_partition().partition_ids(0), partition_id + 3); |
| } |
| |
| ASSERT_EQ(version1, version2); |
| } |
| |
| TEST(MetaServiceOperationLogTest, CommitIndexLog) { |
| auto meta_service = get_meta_service(false); |
| std::string instance_id = "commit_index_log"; |
| auto* sp = SyncPoint::get_instance(); |
| DORIS_CLOUD_DEFER { |
| SyncPoint::get_instance()->clear_all_call_backs(); |
| }; |
| sp->set_call_back("get_instance_id", [&](auto&& args) { |
| auto* ret = try_any_cast_ret<std::string>(args); |
| ret->first = instance_id; |
| ret->second = true; |
| }); |
| sp->enable_processing(); |
| |
| constexpr int64_t db_id = 123; |
| constexpr int64_t table_id = 10001; |
| constexpr int64_t index_id = 10002; |
| |
| { |
| // write instance |
| InstanceInfoPB instance_info; |
| instance_info.set_instance_id(instance_id); |
| instance_info.set_multi_version_status(MULTI_VERSION_WRITE_ONLY); |
| std::unique_ptr<Transaction> txn; |
| ASSERT_EQ(meta_service->txn_kv()->create_txn(&txn), TxnErrorCode::TXN_OK); |
| txn->put(instance_key(instance_id), instance_info.SerializeAsString()); |
| ASSERT_EQ(txn->commit(), TxnErrorCode::TXN_OK); |
| |
| meta_service->resource_mgr()->refresh_instance(instance_id); |
| ASSERT_TRUE(meta_service->resource_mgr()->is_version_write_enabled(instance_id)); |
| } |
| |
| { |
| // Prepare index |
| brpc::Controller ctrl; |
| IndexRequest req; |
| IndexResponse res; |
| req.set_db_id(db_id); |
| req.set_table_id(table_id); |
| req.add_index_ids(index_id); |
| meta_service->prepare_index(&ctrl, &req, &res, nullptr); |
| ASSERT_EQ(res.status().code(), MetaServiceCode::OK) << res.status().DebugString(); |
| } |
| |
| { |
| // Commit index |
| brpc::Controller ctrl; |
| IndexRequest req; |
| IndexResponse res; |
| req.set_db_id(db_id); |
| req.set_table_id(table_id); |
| req.add_index_ids(index_id); |
| req.set_is_new_table(true); |
| meta_service->commit_index(&ctrl, &req, &res, nullptr); |
| ASSERT_EQ(res.status().code(), MetaServiceCode::OK) << res.status().DebugString(); |
| } |
| |
| auto txn_kv = meta_service->txn_kv(); |
| Versionstamp version1; |
| { |
| // Verify index meta/index/inverted indexes are exists |
| std::string index_meta_key = versioned::meta_index_key({instance_id, index_id}); |
| std::unique_ptr<Transaction> txn; |
| ASSERT_EQ(txn_kv->create_txn(&txn), TxnErrorCode::TXN_OK); |
| std::string value; |
| ASSERT_EQ(versioned_get(txn.get(), index_meta_key, &version1, &value), |
| TxnErrorCode::TXN_OK); |
| |
| std::string index_inverted_key = |
| versioned::index_inverted_key({instance_id, db_id, table_id, index_id}); |
| ASSERT_EQ(txn->get(index_inverted_key, &value), TxnErrorCode::TXN_OK); |
| |
| std::string index_index_key = versioned::index_index_key({instance_id, index_id}); |
| ASSERT_EQ(txn->get(index_index_key, &value), TxnErrorCode::TXN_OK); |
| IndexIndexPB index_index; |
| ASSERT_TRUE(index_index.ParseFromString(value)); |
| ASSERT_EQ(index_index.db_id(), db_id); |
| ASSERT_EQ(index_index.table_id(), table_id); |
| } |
| |
| Versionstamp version2; |
| { |
| // Verify table version exists |
| MetaReader meta_reader(instance_id, txn_kv.get()); |
| ASSERT_EQ(meta_reader.get_table_version(table_id, &version2), TxnErrorCode::TXN_OK); |
| } |
| |
| ASSERT_EQ(version1, version2); |
| |
| { |
| // verify commit index log |
| std::unique_ptr<Transaction> txn; |
| ASSERT_EQ(txn_kv->create_txn(&txn), TxnErrorCode::TXN_OK); |
| std::string log_key = versioned::log_key({instance_id}); |
| std::string value; |
| ASSERT_EQ(versioned_get(txn.get(), log_key, &version2, &value), TxnErrorCode::TXN_OK); |
| OperationLogPB operation_log; |
| ASSERT_TRUE(operation_log.ParseFromString(value)); |
| ASSERT_TRUE(operation_log.has_commit_index()); |
| } |
| |
| ASSERT_EQ(version1, version2); |
| |
| { |
| // Prepare index2 |
| brpc::Controller ctrl; |
| IndexRequest req; |
| IndexResponse res; |
| req.set_db_id(db_id); |
| req.set_table_id(table_id); |
| req.add_index_ids(index_id + 1); |
| meta_service->prepare_index(&ctrl, &req, &res, nullptr); |
| ASSERT_EQ(res.status().code(), MetaServiceCode::OK) << res.status().DebugString(); |
| } |
| |
| { |
| // Commit index2 without set_is_new_table(true) |
| brpc::Controller ctrl; |
| IndexRequest req; |
| IndexResponse res; |
| req.set_db_id(db_id); |
| req.set_table_id(table_id); |
| req.add_index_ids(index_id + 1); |
| meta_service->commit_index(&ctrl, &req, &res, nullptr); |
| ASSERT_EQ(res.status().code(), MetaServiceCode::OK) << res.status().DebugString(); |
| } |
| |
| { |
| // Verify index1 meta/index/inverted indexes are exists |
| std::string index_meta_key = versioned::meta_index_key({instance_id, index_id + 1}); |
| std::unique_ptr<Transaction> txn; |
| ASSERT_EQ(txn_kv->create_txn(&txn), TxnErrorCode::TXN_OK); |
| std::string value; |
| ASSERT_EQ(versioned_get(txn.get(), index_meta_key, &version1, &value), |
| TxnErrorCode::TXN_OK); |
| |
| std::string index_inverted_key = |
| versioned::index_inverted_key({instance_id, db_id, table_id, index_id + 1}); |
| ASSERT_EQ(txn->get(index_inverted_key, &value), TxnErrorCode::TXN_OK); |
| |
| std::string index_index_key = versioned::index_index_key({instance_id, index_id + 1}); |
| ASSERT_EQ(txn->get(index_index_key, &value), TxnErrorCode::TXN_OK); |
| IndexIndexPB index_index; |
| ASSERT_TRUE(index_index.ParseFromString(value)); |
| ASSERT_EQ(index_index.db_id(), db_id); |
| ASSERT_EQ(index_index.table_id(), table_id); |
| } |
| |
| Versionstamp version3; |
| { |
| // Verify table version exists and does not update |
| MetaReader meta_reader(instance_id, txn_kv.get()); |
| ASSERT_EQ(meta_reader.get_table_version(table_id, &version3), TxnErrorCode::TXN_OK); |
| } |
| |
| ASSERT_EQ(version3, version2); |
| ASSERT_NE(version3, version1); |
| } |
| |
| TEST(MetaServiceOperationLogTest, DropIndexLog) { |
| auto meta_service = get_meta_service(false); |
| std::string instance_id = "drop_index_log"; |
| auto* sp = SyncPoint::get_instance(); |
| DORIS_CLOUD_DEFER { |
| SyncPoint::get_instance()->clear_all_call_backs(); |
| }; |
| sp->set_call_back("get_instance_id", [&](auto&& args) { |
| auto* ret = try_any_cast_ret<std::string>(args); |
| ret->first = instance_id; |
| ret->second = true; |
| }); |
| sp->enable_processing(); |
| |
| constexpr int64_t db_id = 123; |
| constexpr int64_t table_id = 10001; |
| constexpr int64_t index_id = 10002; |
| |
| { |
| // write instance |
| InstanceInfoPB instance_info; |
| instance_info.set_instance_id(instance_id); |
| instance_info.set_multi_version_status(MULTI_VERSION_WRITE_ONLY); |
| std::unique_ptr<Transaction> txn; |
| ASSERT_EQ(meta_service->txn_kv()->create_txn(&txn), TxnErrorCode::TXN_OK); |
| txn->put(instance_key(instance_id), instance_info.SerializeAsString()); |
| ASSERT_EQ(txn->commit(), TxnErrorCode::TXN_OK); |
| |
| meta_service->resource_mgr()->refresh_instance(instance_id); |
| ASSERT_TRUE(meta_service->resource_mgr()->is_version_write_enabled(instance_id)); |
| } |
| |
| { |
| // Prepare index 0,1,2,3 |
| brpc::Controller ctrl; |
| IndexRequest req; |
| IndexResponse res; |
| req.set_db_id(db_id); |
| req.set_table_id(table_id); |
| req.add_index_ids(index_id); |
| req.add_index_ids(index_id + 1); |
| req.add_index_ids(index_id + 2); |
| req.add_index_ids(index_id + 3); |
| meta_service->prepare_index(&ctrl, &req, &res, nullptr); |
| ASSERT_EQ(res.status().code(), MetaServiceCode::OK) << res.status().DebugString(); |
| } |
| |
| { |
| // Commit index 2,3 |
| brpc::Controller ctrl; |
| IndexRequest req; |
| IndexResponse res; |
| req.set_db_id(db_id); |
| req.set_table_id(table_id); |
| req.add_index_ids(index_id + 2); |
| req.add_index_ids(index_id + 3); |
| meta_service->commit_index(&ctrl, &req, &res, nullptr); |
| ASSERT_EQ(res.status().code(), MetaServiceCode::OK) << res.status().DebugString(); |
| } |
| |
| auto txn_kv = meta_service->txn_kv(); |
| size_t num_logs = count_range(txn_kv.get(), versioned::log_key(instance_id), |
| versioned::log_key(instance_id) + "\xFF"); |
| |
| { |
| // Drop index 0 |
| brpc::Controller ctrl; |
| IndexRequest req; |
| IndexResponse res; |
| req.set_db_id(db_id); |
| req.set_table_id(table_id); |
| req.add_index_ids(index_id); |
| meta_service->drop_index(&ctrl, &req, &res, nullptr); |
| ASSERT_EQ(res.status().code(), MetaServiceCode::OK) << res.status().DebugString(); |
| |
| // No operation log are generated for drop index 0 (it is not committed). |
| size_t new_num_logs = count_range(txn_kv.get(), versioned::log_key(instance_id), |
| versioned::log_key(instance_id) + "\xFF"); |
| ASSERT_EQ(new_num_logs, num_logs) |
| << "Expected no new operation logs for drop index 0, but found " |
| << new_num_logs - num_logs << dump_range(txn_kv.get()); |
| |
| // The recycle index key must exists. |
| std::string recycle_key = recycle_index_key({instance_id, index_id}); |
| std::unique_ptr<Transaction> txn; |
| ASSERT_EQ(txn_kv->create_txn(&txn), TxnErrorCode::TXN_OK); |
| std::string value; |
| TxnErrorCode err = txn->get(recycle_key, &value); |
| ASSERT_EQ(err, TxnErrorCode::TXN_OK); |
| } |
| |
| { |
| // Drop index 1,2, it should generate operation logs. |
| brpc::Controller ctrl; |
| IndexRequest req; |
| IndexResponse res; |
| req.set_db_id(db_id); |
| req.set_table_id(table_id); |
| req.add_index_ids(index_id + 1); |
| req.add_index_ids(index_id + 2); |
| meta_service->drop_index(&ctrl, &req, &res, nullptr); |
| ASSERT_EQ(res.status().code(), MetaServiceCode::OK) << res.status().DebugString(); |
| // Check that new operation logs are generated. |
| size_t new_num_logs = count_range(txn_kv.get(), versioned::log_key(instance_id), |
| versioned::log_key(instance_id) + "\xFF"); |
| ASSERT_GT(new_num_logs, num_logs) |
| << "Expected new operation logs for drop index 1,2, but found " |
| << new_num_logs - num_logs << dump_range(txn_kv.get()); |
| num_logs = new_num_logs; |
| |
| // The recycle index 1 key must exists. |
| std::string recycle_key = recycle_index_key({instance_id, index_id + 1}); |
| std::unique_ptr<Transaction> txn; |
| ASSERT_EQ(txn_kv->create_txn(&txn), TxnErrorCode::TXN_OK); |
| std::string value; |
| TxnErrorCode err = txn->get(recycle_key, &value); |
| ASSERT_EQ(err, TxnErrorCode::TXN_OK); |
| |
| // The recycle index 2 key should not be exists. |
| recycle_key = recycle_index_key({instance_id, index_id + 2}); |
| ASSERT_EQ(txn_kv->create_txn(&txn), TxnErrorCode::TXN_OK); |
| err = txn->get(recycle_key, &value); |
| ASSERT_EQ(err, TxnErrorCode::TXN_KEY_NOT_FOUND) |
| << "Expected recycle index key to not exist, but found it: " << hex(recycle_key) |
| << " with value: " << escape_hex(value); |
| } |
| |
| { |
| // Drop index 3, it should generate operation logs. |
| brpc::Controller ctrl; |
| IndexRequest req; |
| IndexResponse res; |
| req.set_db_id(db_id); |
| req.set_table_id(table_id); |
| req.add_index_ids(index_id + 3); |
| meta_service->drop_index(&ctrl, &req, &res, nullptr); |
| ASSERT_EQ(res.status().code(), MetaServiceCode::OK) << res.status().DebugString(); |
| // Check that new operation logs are generated. |
| size_t new_num_logs = count_range(txn_kv.get(), versioned::log_key(instance_id), |
| versioned::log_key(instance_id) + "\xFF"); |
| ASSERT_GT(new_num_logs, num_logs) |
| << "Expected new operation logs for drop index 3, but found " |
| << new_num_logs - num_logs << dump_range(txn_kv.get()); |
| num_logs = new_num_logs; |
| |
| // The recycle index key should not be exists. |
| std::string recycle_key = recycle_index_key({instance_id, index_id + 3}); |
| std::unique_ptr<Transaction> txn; |
| ASSERT_EQ(txn_kv->create_txn(&txn), TxnErrorCode::TXN_OK); |
| std::string value; |
| TxnErrorCode err = txn->get(recycle_key, &value); |
| ASSERT_EQ(err, TxnErrorCode::TXN_KEY_NOT_FOUND) |
| << "Expected recycle index key to not exist, but found it: " << hex(recycle_key) |
| << " with value: " << escape_hex(value); |
| } |
| |
| { |
| // Verify table version not exists |
| Versionstamp version; |
| MetaReader meta_reader(instance_id, txn_kv.get()); |
| ASSERT_EQ(meta_reader.get_table_version(table_id, &version), |
| TxnErrorCode::TXN_KEY_NOT_FOUND); |
| } |
| |
| { |
| // verify last drop index log |
| Versionstamp version; |
| std::unique_ptr<Transaction> txn; |
| ASSERT_EQ(txn_kv->create_txn(&txn), TxnErrorCode::TXN_OK); |
| std::string log_key = versioned::log_key({instance_id}); |
| std::string value; |
| ASSERT_EQ(versioned_get(txn.get(), log_key, &version, &value), TxnErrorCode::TXN_OK); |
| OperationLogPB operation_log; |
| ASSERT_TRUE(operation_log.ParseFromString(value)); |
| ASSERT_TRUE(operation_log.has_drop_index()); |
| ASSERT_EQ(operation_log.drop_index().index_ids_size(), 1); |
| ASSERT_EQ(operation_log.drop_index().index_ids(0), index_id + 3); |
| } |
| } |
| |
| TEST(MetaServiceOperationLogTest, CommitTxn) { |
| auto meta_service = get_meta_service(false); |
| std::string instance_id = "commit_txn_versioned_write"; |
| auto* sp = SyncPoint::get_instance(); |
| DORIS_CLOUD_DEFER { |
| SyncPoint::get_instance()->clear_all_call_backs(); |
| }; |
| sp->set_call_back("get_instance_id", [&](auto&& args) { |
| auto* ret = try_any_cast_ret<std::string>(args); |
| ret->first = instance_id; |
| ret->second = true; |
| }); |
| sp->enable_processing(); |
| |
| constexpr int64_t db_id = 123; |
| constexpr int64_t table_id = 10001; |
| constexpr int64_t partition_id = 10003; |
| constexpr int64_t tablet_id_base = 8113; |
| |
| { |
| // write instance with versioned write enabled |
| InstanceInfoPB instance_info; |
| instance_info.set_instance_id(instance_id); |
| instance_info.set_multi_version_status(MULTI_VERSION_WRITE_ONLY); |
| std::unique_ptr<Transaction> txn; |
| ASSERT_EQ(meta_service->txn_kv()->create_txn(&txn), TxnErrorCode::TXN_OK); |
| txn->put(instance_key(instance_id), instance_info.SerializeAsString()); |
| ASSERT_EQ(txn->commit(), TxnErrorCode::TXN_OK); |
| |
| meta_service->resource_mgr()->refresh_instance(instance_id); |
| ASSERT_TRUE(meta_service->resource_mgr()->is_version_write_enabled(instance_id)); |
| } |
| |
| int64_t txn_id = -1; |
| const std::string& label = "test_versioned_write_commit"; |
| |
| // begin txn |
| { |
| brpc::Controller cntl; |
| BeginTxnRequest req; |
| req.set_cloud_unique_id("test_cloud_unique_id"); |
| TxnInfoPB txn_info_pb; |
| txn_info_pb.set_db_id(db_id); |
| txn_info_pb.set_label(label); |
| txn_info_pb.add_table_ids(table_id); |
| txn_info_pb.set_timeout_ms(36000); |
| req.mutable_txn_info()->CopyFrom(txn_info_pb); |
| BeginTxnResponse res; |
| meta_service->begin_txn(reinterpret_cast<::google::protobuf::RpcController*>(&cntl), &req, |
| &res, nullptr); |
| ASSERT_EQ(res.status().code(), MetaServiceCode::OK); |
| txn_id = res.txn_id(); |
| } |
| |
| // create tablets and rowsets |
| for (int i = 0; i < 3; ++i) { |
| create_tablet(meta_service.get(), table_id, 1235, partition_id, tablet_id_base + i); |
| auto tmp_rowset = create_rowset(txn_id, tablet_id_base + i, partition_id, 2, 100); |
| CreateRowsetResponse res; |
| LOG(INFO) << "Creating rowset for tablet_id=" << (tablet_id_base + i) |
| << ", partition_id=" << partition_id << ", txn_id=" << txn_id |
| << ", rowset=" << tmp_rowset.ShortDebugString(); |
| |
| commit_rowset(meta_service.get(), tmp_rowset, res); |
| ASSERT_EQ(res.status().code(), MetaServiceCode::OK); |
| } |
| |
| // commit txn with versioned write |
| Versionstamp commit_version; |
| { |
| brpc::Controller cntl; |
| CommitTxnRequest req; |
| req.set_cloud_unique_id("test_cloud_unique_id"); |
| req.set_db_id(db_id); |
| req.set_txn_id(txn_id); |
| CommitTxnResponse res; |
| meta_service->commit_txn(reinterpret_cast<::google::protobuf::RpcController*>(&cntl), &req, |
| &res, nullptr); |
| ASSERT_EQ(res.status().code(), MetaServiceCode::OK); |
| } |
| |
| auto txn_kv = meta_service->txn_kv(); |
| Versionstamp version; |
| |
| { |
| // Verify txn_info has versioned_write flag set |
| std::unique_ptr<Transaction> txn; |
| ASSERT_EQ(txn_kv->create_txn(&txn), TxnErrorCode::TXN_OK); |
| std::string txn_key = txn_info_key({instance_id, db_id, txn_id}); |
| std::string txn_val; |
| ASSERT_EQ(txn->get(txn_key, &txn_val), TxnErrorCode::TXN_OK); |
| |
| TxnInfoPB txn_info; |
| ASSERT_TRUE(txn_info.ParseFromString(txn_val)); |
| ASSERT_TRUE(txn_info.has_versioned_write()); |
| ASSERT_TRUE(txn_info.versioned_write()); |
| } |
| |
| { |
| // Verify no recycle txn key is written immediately for versioned write |
| std::unique_ptr<Transaction> txn; |
| ASSERT_EQ(txn_kv->create_txn(&txn), TxnErrorCode::TXN_OK); |
| std::string recycle_key = recycle_txn_key({instance_id, db_id, txn_id}); |
| std::string recycle_val; |
| ASSERT_EQ(txn->get(recycle_key, &recycle_val), TxnErrorCode::TXN_KEY_NOT_FOUND); |
| } |
| |
| { |
| // Verify table version exists |
| MetaReader meta_reader(instance_id, txn_kv.get()); |
| ASSERT_EQ(meta_reader.get_table_version(table_id, &version), TxnErrorCode::TXN_OK); |
| commit_version = version; |
| } |
| |
| { |
| // Verify versioned tablet stats are written |
| std::unique_ptr<Transaction> txn; |
| ASSERT_EQ(txn_kv->create_txn(&txn), TxnErrorCode::TXN_OK); |
| |
| for (int i = 0; i < 3; ++i) { |
| int64_t tablet_id = tablet_id_base + i; |
| std::string versioned_stats_key = |
| versioned::tablet_load_stats_key({instance_id, tablet_id}); |
| std::string versioned_stats_val; |
| ASSERT_EQ(versioned_get(txn.get(), versioned_stats_key, &version, &versioned_stats_val), |
| TxnErrorCode::TXN_OK); |
| |
| TabletStatsPB versioned_stats; |
| ASSERT_TRUE(versioned_stats.ParseFromString(versioned_stats_val)); |
| ASSERT_GT(versioned_stats.num_rows(), 0); |
| ASSERT_GT(versioned_stats.data_size(), 0); |
| ASSERT_GT(versioned_stats.num_rowsets(), 0); |
| } |
| } |
| |
| { |
| // Verify versioned rowset meta are written |
| std::unique_ptr<Transaction> txn; |
| ASSERT_EQ(txn_kv->create_txn(&txn), TxnErrorCode::TXN_OK); |
| |
| for (int i = 0; i < 3; ++i) { |
| int64_t tablet_id = tablet_id_base + i; |
| // Construct versioned rowset meta key for version 2 |
| std::string versioned_rowset_key = |
| versioned::meta_rowset_load_key({instance_id, tablet_id, 2}); |
| |
| // Try to get the versioned rowset meta using document_get |
| RowsetMetaCloudPB versioned_rowset_meta; |
| auto ret = versioned::document_get(txn.get(), versioned_rowset_key, |
| &versioned_rowset_meta, &version); |
| ASSERT_EQ(ret, TxnErrorCode::TXN_OK) |
| << "Failed to get versioned rowset meta for tablet " << tablet_id; |
| |
| ASSERT_EQ(versioned_rowset_meta.tablet_id(), tablet_id); |
| ASSERT_EQ(versioned_rowset_meta.partition_id(), partition_id); |
| ASSERT_GT(versioned_rowset_meta.num_rows(), 0); |
| } |
| } |
| |
| { |
| // verify versioned partition version key |
| std::unique_ptr<Transaction> txn; |
| ASSERT_EQ(txn_kv->create_txn(&txn), TxnErrorCode::TXN_OK); |
| std::string key = versioned::partition_version_key({instance_id, partition_id}); |
| std::string partition_version_val; |
| Versionstamp version; |
| ASSERT_EQ(versioned_get(txn.get(), key, &version, &partition_version_val), |
| TxnErrorCode::TXN_OK); |
| VersionPB versioned_partition_version; |
| ASSERT_TRUE(versioned_partition_version.ParseFromString(partition_version_val)); |
| ASSERT_EQ(version, commit_version); |
| |
| key = partition_version_key({instance_id, db_id, table_id, partition_id}); |
| ASSERT_EQ(txn->get(key, &partition_version_val), TxnErrorCode::TXN_OK); |
| VersionPB partition_version; |
| ASSERT_TRUE(partition_version.ParseFromString(partition_version_val)); |
| ASSERT_EQ(versioned_partition_version.version(), partition_version.version()); |
| } |
| |
| { |
| // verify commit txn operation log |
| std::unique_ptr<Transaction> txn; |
| ASSERT_EQ(txn_kv->create_txn(&txn), TxnErrorCode::TXN_OK); |
| std::string log_key = versioned::log_key({instance_id}); |
| std::string value; |
| ASSERT_EQ(versioned_get(txn.get(), log_key, &version, &value), TxnErrorCode::TXN_OK); |
| OperationLogPB operation_log; |
| ASSERT_TRUE(operation_log.ParseFromString(value)); |
| ASSERT_TRUE(operation_log.has_commit_txn()); |
| |
| const auto& commit_log = operation_log.commit_txn(); |
| ASSERT_EQ(commit_log.txn_id(), txn_id); |
| ASSERT_TRUE(commit_log.has_recycle_txn()); |
| ASSERT_EQ(commit_log.recycle_txn().label(), label); |
| ASSERT_GT(commit_log.tablet_to_partition_map_size(), 0); |
| ASSERT_GT(commit_log.partition_version_map_size(), 0); |
| ASSERT_EQ(commit_log.db_id(), db_id); |
| |
| // Verify tablet to partition mapping |
| for (const auto& [tablet_id, partition_id_in_map] : commit_log.tablet_to_partition_map()) { |
| ASSERT_EQ(partition_id_in_map, partition_id); |
| } |
| |
| // Verify partition version mapping |
| ASSERT_EQ(commit_log.partition_version_map_size(), 1); |
| auto it = commit_log.partition_version_map().find(partition_id); |
| ASSERT_NE(it, commit_log.partition_version_map().end()); |
| ASSERT_GT(it->second, 0); |
| } |
| |
| ASSERT_EQ(version, commit_version); |
| } |
| |
| TEST(MetaServiceOperationLogTest, CommitTxnEventually) { |
| auto meta_service = get_meta_service(false); |
| std::string instance_id = "commit_txn_eventually_versioned_write"; |
| auto* sp = SyncPoint::get_instance(); |
| DORIS_CLOUD_DEFER { |
| SyncPoint::get_instance()->clear_all_call_backs(); |
| }; |
| sp->set_call_back("get_instance_id", [&](auto&& args) { |
| auto* ret = try_any_cast_ret<std::string>(args); |
| ret->first = instance_id; |
| ret->second = true; |
| }); |
| sp->enable_processing(); |
| |
| constexpr int64_t db_id = 125; |
| constexpr int64_t table_id = 10003; |
| constexpr int64_t partition_id = 10005; |
| constexpr int64_t tablet_id_base = 8115; |
| |
| { |
| // write instance with versioned write enabled |
| InstanceInfoPB instance_info; |
| instance_info.set_instance_id(instance_id); |
| instance_info.set_multi_version_status(MULTI_VERSION_WRITE_ONLY); |
| std::unique_ptr<Transaction> txn; |
| ASSERT_EQ(meta_service->txn_kv()->create_txn(&txn), TxnErrorCode::TXN_OK); |
| txn->put(instance_key(instance_id), instance_info.SerializeAsString()); |
| ASSERT_EQ(txn->commit(), TxnErrorCode::TXN_OK); |
| |
| meta_service->resource_mgr()->refresh_instance(instance_id); |
| ASSERT_TRUE(meta_service->resource_mgr()->is_version_write_enabled(instance_id)); |
| } |
| |
| int64_t txn_id = -1; |
| const std::string& label = "test_eventually_versioned_write_commit"; |
| |
| // begin txn |
| { |
| brpc::Controller cntl; |
| BeginTxnRequest req; |
| req.set_cloud_unique_id("test_cloud_unique_id"); |
| TxnInfoPB txn_info_pb; |
| txn_info_pb.set_db_id(db_id); |
| txn_info_pb.set_label(label); |
| txn_info_pb.add_table_ids(table_id); |
| txn_info_pb.set_timeout_ms(36000); |
| req.mutable_txn_info()->CopyFrom(txn_info_pb); |
| BeginTxnResponse res; |
| meta_service->begin_txn(reinterpret_cast<::google::protobuf::RpcController*>(&cntl), &req, |
| &res, nullptr); |
| ASSERT_EQ(res.status().code(), MetaServiceCode::OK); |
| txn_id = res.txn_id(); |
| } |
| |
| create_tablet(meta_service.get(), table_id, 1237, partition_id, tablet_id_base); |
| auto tmp_rowset = create_rowset(txn_id, tablet_id_base, partition_id, 1, 100); |
| CreateRowsetResponse res; |
| commit_rowset(meta_service.get(), tmp_rowset, res); |
| ASSERT_EQ(res.status().code(), MetaServiceCode::OK); |
| |
| // Force eventual commit path by setting max_txn_bytes to a small value |
| int64_t old_max_txn_bytes = config::max_txn_commit_byte; |
| config::max_txn_commit_byte = 100; |
| |
| // commit txn with versioned write (should use eventual commit) |
| { |
| brpc::Controller cntl; |
| CommitTxnRequest req; |
| req.set_cloud_unique_id("test_cloud_unique_id"); |
| req.set_db_id(db_id); |
| req.set_txn_id(txn_id); |
| CommitTxnResponse res; |
| meta_service->commit_txn(reinterpret_cast<::google::protobuf::RpcController*>(&cntl), &req, |
| &res, nullptr); |
| ASSERT_EQ(res.status().code(), MetaServiceCode::OK); |
| } |
| |
| // Restore config |
| config::max_txn_commit_byte = old_max_txn_bytes; |
| |
| auto txn_kv = meta_service->txn_kv(); |
| Versionstamp version; |
| |
| { |
| // Verify txn is marked as versioned_write |
| std::unique_ptr<Transaction> txn; |
| ASSERT_EQ(txn_kv->create_txn(&txn), TxnErrorCode::TXN_OK); |
| std::string txn_key = txn_info_key({instance_id, db_id, txn_id}); |
| std::string txn_val; |
| ASSERT_EQ(txn->get(txn_key, &txn_val), TxnErrorCode::TXN_OK); |
| |
| TxnInfoPB txn_info; |
| ASSERT_TRUE(txn_info.ParseFromString(txn_val)); |
| ASSERT_TRUE(txn_info.has_versioned_write()); |
| ASSERT_TRUE(txn_info.versioned_write()); |
| } |
| |
| { |
| // In lazy commit, recycle txn should not be written when versioned_write is true |
| std::unique_ptr<Transaction> txn; |
| ASSERT_EQ(txn_kv->create_txn(&txn), TxnErrorCode::TXN_OK); |
| std::string recycle_key = recycle_txn_key({instance_id, db_id, txn_id}); |
| std::string recycle_val; |
| ASSERT_EQ(txn->get(recycle_key, &recycle_val), TxnErrorCode::TXN_KEY_NOT_FOUND); |
| } |
| |
| { |
| // Verify table version exists |
| MetaReader meta_reader(instance_id, txn_kv.get()); |
| ASSERT_EQ(meta_reader.get_table_version(table_id, &version), TxnErrorCode::TXN_OK); |
| } |
| |
| // Store the commit versionstamp to verify consistency later |
| Versionstamp commit_versionstamp; |
| |
| { |
| // verify commit txn operation log is written in eventual commit |
| std::unique_ptr<Transaction> txn; |
| ASSERT_EQ(txn_kv->create_txn(&txn), TxnErrorCode::TXN_OK); |
| std::string log_key = versioned::log_key({instance_id}); |
| std::string value; |
| ASSERT_EQ(versioned_get(txn.get(), log_key, &commit_versionstamp, &value), |
| TxnErrorCode::TXN_OK); |
| OperationLogPB operation_log; |
| ASSERT_TRUE(operation_log.ParseFromString(value)); |
| ASSERT_TRUE(operation_log.has_commit_txn()); |
| |
| const auto& commit_log = operation_log.commit_txn(); |
| ASSERT_EQ(commit_log.txn_id(), txn_id); |
| ASSERT_TRUE(commit_log.has_recycle_txn()); |
| ASSERT_EQ(commit_log.recycle_txn().label(), label); |
| ASSERT_GT(commit_log.tablet_to_partition_map_size(), 0); |
| ASSERT_GT(commit_log.partition_version_map_size(), 0); |
| ASSERT_EQ(commit_log.db_id(), db_id); |
| } |
| |
| { |
| // Verify versioned tablet stats are written with consistent versionstamp |
| std::unique_ptr<Transaction> txn; |
| ASSERT_EQ(txn_kv->create_txn(&txn), TxnErrorCode::TXN_OK); |
| |
| std::string versioned_stats_key = |
| versioned::tablet_load_stats_key({instance_id, tablet_id_base}); |
| std::string versioned_stats_val; |
| Versionstamp stats_versionstamp; |
| ASSERT_EQ(versioned_get(txn.get(), versioned_stats_key, &stats_versionstamp, |
| &versioned_stats_val), |
| TxnErrorCode::TXN_OK); |
| |
| TabletStatsPB versioned_stats; |
| ASSERT_TRUE(versioned_stats.ParseFromString(versioned_stats_val)); |
| ASSERT_GT(versioned_stats.num_rows(), 0); |
| ASSERT_GT(versioned_stats.data_size(), 0); |
| ASSERT_GT(versioned_stats.num_rowsets(), 0); |
| |
| // Verify versionstamp consistency with commit log |
| ASSERT_EQ(stats_versionstamp, commit_versionstamp) |
| << "Versioned tablet stats versionstamp should match commit log versionstamp"; |
| } |
| |
| { |
| // verify versioned partition version key |
| std::unique_ptr<Transaction> txn; |
| ASSERT_EQ(txn_kv->create_txn(&txn), TxnErrorCode::TXN_OK); |
| std::string key = versioned::partition_version_key({instance_id, partition_id}); |
| std::string partition_version_val; |
| Versionstamp version; |
| ASSERT_EQ(versioned_get(txn.get(), key, &version, &partition_version_val), |
| TxnErrorCode::TXN_OK); |
| VersionPB versioned_partition_version; |
| ASSERT_TRUE(versioned_partition_version.ParseFromString(partition_version_val)); |
| ASSERT_EQ(version, commit_versionstamp); |
| |
| key = partition_version_key({instance_id, db_id, table_id, partition_id}); |
| ASSERT_EQ(txn->get(key, &partition_version_val), TxnErrorCode::TXN_OK); |
| VersionPB partition_version; |
| ASSERT_TRUE(partition_version.ParseFromString(partition_version_val)); |
| ASSERT_EQ(versioned_partition_version.version(), partition_version.version()); |
| } |
| |
| { |
| // Verify versioned rowset meta are written with consistent versionstamp |
| std::unique_ptr<Transaction> txn; |
| ASSERT_EQ(txn_kv->create_txn(&txn), TxnErrorCode::TXN_OK); |
| |
| // Construct versioned rowset meta key for version 2 (first committed version after initial) |
| std::string versioned_rowset_key = |
| versioned::meta_rowset_load_key({instance_id, tablet_id_base, 2}); |
| |
| // Try to get the versioned rowset meta using document_get |
| RowsetMetaCloudPB versioned_rowset_meta; |
| Versionstamp rowset_versionstamp; |
| auto ret = versioned::document_get(txn.get(), versioned_rowset_key, &versioned_rowset_meta, |
| &rowset_versionstamp); |
| ASSERT_EQ(ret, TxnErrorCode::TXN_OK) |
| << "Failed to get versioned rowset meta for tablet " << tablet_id_base; |
| |
| ASSERT_EQ(versioned_rowset_meta.tablet_id(), tablet_id_base); |
| ASSERT_EQ(versioned_rowset_meta.partition_id(), partition_id); |
| ASSERT_GT(versioned_rowset_meta.num_rows(), 0); |
| |
| // Verify versionstamp consistency with commit log |
| ASSERT_EQ(rowset_versionstamp, commit_versionstamp) |
| << "Versioned rowset meta versionstamp should match commit log versionstamp"; |
| } |
| } |
| |
| TEST(MetaServiceOperationLogTest, CommitTxnWithSubTxn) { |
| auto meta_service = get_meta_service(false); |
| std::string instance_id = "commit_txn_with_sub_txn_versioned_write"; |
| auto* sp = SyncPoint::get_instance(); |
| DORIS_CLOUD_DEFER { |
| SyncPoint::get_instance()->clear_all_call_backs(); |
| }; |
| sp->set_call_back("get_instance_id", [&](auto&& args) { |
| auto* ret = try_any_cast_ret<std::string>(args); |
| ret->first = instance_id; |
| ret->second = true; |
| }); |
| sp->enable_processing(); |
| |
| constexpr int64_t db_id = 126; |
| constexpr int64_t table_id = 10004; |
| constexpr int64_t partition_id = 10006; |
| constexpr int64_t tablet_id_base = 8116; |
| |
| { |
| // write instance with versioned write enabled |
| InstanceInfoPB instance_info; |
| instance_info.set_instance_id(instance_id); |
| instance_info.set_multi_version_status(MULTI_VERSION_WRITE_ONLY); |
| std::unique_ptr<Transaction> txn; |
| ASSERT_EQ(meta_service->txn_kv()->create_txn(&txn), TxnErrorCode::TXN_OK); |
| txn->put(instance_key(instance_id), instance_info.SerializeAsString()); |
| ASSERT_EQ(txn->commit(), TxnErrorCode::TXN_OK); |
| |
| meta_service->resource_mgr()->refresh_instance(instance_id); |
| ASSERT_TRUE(meta_service->resource_mgr()->is_version_write_enabled(instance_id)); |
| } |
| |
| int64_t txn_id = -1; |
| const std::string& label = "test_sub_txn_versioned_write_commit"; |
| |
| // begin txn |
| { |
| brpc::Controller cntl; |
| BeginTxnRequest req; |
| req.set_cloud_unique_id("test_cloud_unique_id"); |
| TxnInfoPB txn_info_pb; |
| txn_info_pb.set_db_id(db_id); |
| txn_info_pb.set_label(label); |
| txn_info_pb.add_table_ids(table_id); |
| txn_info_pb.set_timeout_ms(36000); |
| req.mutable_txn_info()->CopyFrom(txn_info_pb); |
| BeginTxnResponse res; |
| meta_service->begin_txn(reinterpret_cast<::google::protobuf::RpcController*>(&cntl), &req, |
| &res, nullptr); |
| ASSERT_EQ(res.status().code(), MetaServiceCode::OK); |
| txn_id = res.txn_id(); |
| } |
| |
| // begin sub txn |
| int64_t sub_txn_id = -1; |
| { |
| brpc::Controller cntl; |
| BeginSubTxnRequest req; |
| req.set_cloud_unique_id("test_cloud_unique_id"); |
| req.set_txn_id(txn_id); |
| req.set_db_id(db_id); |
| req.set_label(label); |
| req.set_sub_txn_num(0); |
| req.add_table_ids(table_id); |
| BeginSubTxnResponse res; |
| meta_service->begin_sub_txn(reinterpret_cast<::google::protobuf::RpcController*>(&cntl), |
| &req, &res, nullptr); |
| ASSERT_EQ(res.status().code(), MetaServiceCode::OK); |
| sub_txn_id = res.sub_txn_id(); |
| } |
| |
| // create tablets and rowsets for sub txn |
| for (int i = 0; i < 2; ++i) { |
| create_tablet(meta_service.get(), table_id, 1238, partition_id, tablet_id_base + i); |
| auto tmp_rowset = create_rowset(sub_txn_id, tablet_id_base + i, partition_id, 1, 100); |
| CreateRowsetResponse res; |
| commit_rowset(meta_service.get(), tmp_rowset, res); |
| ASSERT_EQ(res.status().code(), MetaServiceCode::OK); |
| } |
| |
| // commit txn with sub txn and versioned write |
| CommitTxnResponse commit_res; |
| { |
| brpc::Controller cntl; |
| CommitTxnRequest req; |
| req.set_cloud_unique_id("test_cloud_unique_id"); |
| req.set_db_id(db_id); |
| req.set_txn_id(txn_id); |
| req.set_is_txn_load(true); |
| // Add sub txn info |
| SubTxnInfo* sub_txn_info = req.add_sub_txn_infos(); |
| sub_txn_info->set_sub_txn_id(sub_txn_id); |
| meta_service->commit_txn(reinterpret_cast<::google::protobuf::RpcController*>(&cntl), &req, |
| &commit_res, nullptr); |
| ASSERT_EQ(commit_res.status().code(), MetaServiceCode::OK); |
| } |
| |
| auto txn_kv = meta_service->txn_kv(); |
| Versionstamp commit_versionstamp; |
| |
| { |
| // Verify txn_info has versioned_write flag set |
| std::unique_ptr<Transaction> txn; |
| ASSERT_EQ(txn_kv->create_txn(&txn), TxnErrorCode::TXN_OK); |
| std::string txn_key = txn_info_key({instance_id, db_id, txn_id}); |
| std::string txn_val; |
| ASSERT_EQ(txn->get(txn_key, &txn_val), TxnErrorCode::TXN_OK); |
| |
| TxnInfoPB txn_info; |
| ASSERT_TRUE(txn_info.ParseFromString(txn_val)); |
| ASSERT_TRUE(txn_info.has_versioned_write()); |
| ASSERT_TRUE(txn_info.versioned_write()); |
| } |
| |
| { |
| // Verify no recycle txn key is written immediately for versioned write |
| std::unique_ptr<Transaction> txn; |
| ASSERT_EQ(txn_kv->create_txn(&txn), TxnErrorCode::TXN_OK); |
| std::string recycle_key = recycle_txn_key({instance_id, db_id, txn_id}); |
| std::string recycle_val; |
| ASSERT_EQ(txn->get(recycle_key, &recycle_val), TxnErrorCode::TXN_KEY_NOT_FOUND); |
| } |
| |
| { |
| // Verify table version exists |
| MetaReader meta_reader(instance_id, txn_kv.get()); |
| ASSERT_EQ(meta_reader.get_table_version(table_id, &commit_versionstamp), |
| TxnErrorCode::TXN_OK); |
| } |
| |
| { |
| // Verify versioned tablet stats are written |
| std::unique_ptr<Transaction> txn; |
| ASSERT_EQ(txn_kv->create_txn(&txn), TxnErrorCode::TXN_OK); |
| |
| for (int i = 0; i < 2; ++i) { |
| int64_t tablet_id = tablet_id_base + i; |
| std::string versioned_stats_key = |
| versioned::tablet_load_stats_key({instance_id, tablet_id}); |
| std::string versioned_stats_val; |
| ASSERT_EQ(versioned_get(txn.get(), versioned_stats_key, &commit_versionstamp, |
| &versioned_stats_val), |
| TxnErrorCode::TXN_OK); |
| |
| TabletStatsPB versioned_stats; |
| ASSERT_TRUE(versioned_stats.ParseFromString(versioned_stats_val)); |
| ASSERT_GT(versioned_stats.num_rows(), 0); |
| ASSERT_GT(versioned_stats.data_size(), 0); |
| ASSERT_GT(versioned_stats.num_rowsets(), 0); |
| } |
| } |
| |
| { |
| // Verify versioned rowset meta are written |
| std::unique_ptr<Transaction> txn; |
| ASSERT_EQ(txn_kv->create_txn(&txn), TxnErrorCode::TXN_OK); |
| |
| // Get the actual version from the commit response |
| ASSERT_GT(commit_res.versions_size(), 0); |
| int64_t actual_version = commit_res.versions(0); |
| |
| for (int i = 0; i < 2; ++i) { |
| int64_t tablet_id = tablet_id_base + i; |
| // Construct versioned rowset meta key using the actual version |
| std::string versioned_rowset_key = |
| versioned::meta_rowset_load_key({instance_id, tablet_id, actual_version}); |
| |
| // Try to get the versioned rowset meta using document_get |
| RowsetMetaCloudPB versioned_rowset_meta; |
| auto ret = versioned::document_get(txn.get(), versioned_rowset_key, |
| &versioned_rowset_meta, &commit_versionstamp); |
| ASSERT_EQ(ret, TxnErrorCode::TXN_OK) |
| << "Failed to get versioned rowset meta for tablet " << tablet_id |
| << " with version " << actual_version; |
| |
| ASSERT_EQ(versioned_rowset_meta.tablet_id(), tablet_id); |
| ASSERT_EQ(versioned_rowset_meta.partition_id(), partition_id); |
| ASSERT_GT(versioned_rowset_meta.num_rows(), 0); |
| } |
| } |
| |
| { |
| // verify versioned partition version key |
| std::unique_ptr<Transaction> txn; |
| ASSERT_EQ(txn_kv->create_txn(&txn), TxnErrorCode::TXN_OK); |
| std::string key = versioned::partition_version_key({instance_id, partition_id}); |
| std::string partition_version_val; |
| Versionstamp versionstamp; |
| ASSERT_EQ(versioned_get(txn.get(), key, &versionstamp, &partition_version_val), |
| TxnErrorCode::TXN_OK); |
| VersionPB versioned_partition_version; |
| ASSERT_TRUE(versioned_partition_version.ParseFromString(partition_version_val)); |
| ASSERT_EQ(versionstamp, commit_versionstamp); |
| |
| key = partition_version_key({instance_id, db_id, table_id, partition_id}); |
| ASSERT_EQ(txn->get(key, &partition_version_val), TxnErrorCode::TXN_OK); |
| VersionPB partition_version; |
| ASSERT_TRUE(partition_version.ParseFromString(partition_version_val)); |
| ASSERT_EQ(versioned_partition_version.version(), partition_version.version()); |
| } |
| |
| { |
| // verify commit txn operation log for sub txn |
| std::unique_ptr<Transaction> txn; |
| ASSERT_EQ(txn_kv->create_txn(&txn), TxnErrorCode::TXN_OK); |
| std::string log_key = versioned::log_key({instance_id}); |
| std::string value; |
| ASSERT_EQ(versioned_get(txn.get(), log_key, &commit_versionstamp, &value), |
| TxnErrorCode::TXN_OK); |
| OperationLogPB operation_log; |
| ASSERT_TRUE(operation_log.ParseFromString(value)); |
| ASSERT_TRUE(operation_log.has_commit_txn()); |
| |
| const auto& commit_log = operation_log.commit_txn(); |
| ASSERT_EQ(commit_log.txn_id(), txn_id); |
| ASSERT_TRUE(commit_log.has_recycle_txn()); |
| ASSERT_EQ(commit_log.recycle_txn().label(), label); |
| ASSERT_GT(commit_log.tablet_to_partition_map_size(), 0); |
| ASSERT_GT(commit_log.partition_version_map_size(), 0); |
| ASSERT_EQ(commit_log.db_id(), db_id); |
| |
| // Verify tablet to partition mapping |
| for (const auto& [tablet_id, partition_id_in_map] : commit_log.tablet_to_partition_map()) { |
| ASSERT_EQ(partition_id_in_map, partition_id); |
| } |
| |
| // Verify partition version mapping |
| ASSERT_EQ(commit_log.partition_version_map_size(), 1); |
| auto it = commit_log.partition_version_map().find(partition_id); |
| ASSERT_NE(it, commit_log.partition_version_map().end()); |
| ASSERT_GT(it->second, 0); |
| } |
| } |
| |
| TEST(MetaServiceOperationLogTest, UpdateVersionedTabletMeta) { |
| auto meta_service = get_meta_service(false); |
| std::string instance_id = "commit_partition_log"; |
| std::string cloud_unique_id = "1:" + instance_id + ":1"; |
| |
| auto* sp = SyncPoint::get_instance(); |
| DORIS_CLOUD_DEFER { |
| SyncPoint::get_instance()->clear_all_call_backs(); |
| }; |
| sp->set_call_back("get_instance_id", [&](auto&& args) { |
| auto* ret = try_any_cast_ret<std::string>(args); |
| ret->first = instance_id; |
| ret->second = true; |
| }); |
| sp->enable_processing(); |
| |
| constexpr int64_t table_id = 10001; |
| constexpr int64_t index_id = 10002; |
| constexpr int64_t partition_id = 10003; |
| constexpr int64_t tablet_id1 = 10004; |
| constexpr int64_t tablet_id2 = 10005; |
| |
| { |
| // write instance |
| InstanceInfoPB instance_info; |
| instance_info.set_instance_id(instance_id); |
| instance_info.set_multi_version_status(MULTI_VERSION_WRITE_ONLY); |
| std::unique_ptr<Transaction> txn; |
| ASSERT_EQ(meta_service->txn_kv()->create_txn(&txn), TxnErrorCode::TXN_OK); |
| txn->put(instance_key(instance_id), instance_info.SerializeAsString()); |
| ASSERT_EQ(txn->commit(), TxnErrorCode::TXN_OK); |
| |
| meta_service->resource_mgr()->refresh_instance(instance_id); |
| ASSERT_TRUE(meta_service->resource_mgr()->is_version_write_enabled(instance_id)); |
| } |
| |
| { |
| // Create tablets |
| create_tablet(meta_service.get(), table_id, index_id, partition_id, tablet_id1); |
| create_tablet(meta_service.get(), table_id, index_id, partition_id, tablet_id2); |
| } |
| |
| // Update tablets |
| { |
| brpc::Controller cntl; |
| UpdateTabletRequest req; |
| UpdateTabletResponse resp; |
| req.set_cloud_unique_id(cloud_unique_id); |
| TabletMetaInfoPB* tablet_meta_info = req.add_tablet_meta_infos(); |
| tablet_meta_info->set_tablet_id(tablet_id1); |
| tablet_meta_info->set_ttl_seconds(300); |
| tablet_meta_info = req.add_tablet_meta_infos(); |
| tablet_meta_info->set_tablet_id(tablet_id2); |
| tablet_meta_info->set_ttl_seconds(3000); |
| meta_service->update_tablet(&cntl, &req, &resp, nullptr); |
| ASSERT_EQ(resp.status().code(), MetaServiceCode::OK); |
| } |
| |
| // Verify versioned tablet meta keys exist and have same commit_versionstamp |
| Versionstamp versionstamp1; |
| { |
| std::unique_ptr<Transaction> txn; |
| ASSERT_EQ(meta_service->txn_kv()->create_txn(&txn), TxnErrorCode::TXN_OK); |
| |
| // Check versioned tablet meta for tablet_id1 |
| std::string tablet_meta_key1 = versioned::meta_tablet_key({instance_id, tablet_id1}); |
| doris::TabletMetaCloudPB tablet_meta1; |
| TxnErrorCode err = |
| versioned::document_get(txn.get(), tablet_meta_key1, &tablet_meta1, &versionstamp1); |
| ASSERT_EQ(err, TxnErrorCode::TXN_OK); |
| EXPECT_EQ(tablet_meta1.ttl_seconds(), 300); |
| } |
| |
| // Check versioned tablet meta for tablet_id2 |
| Versionstamp versionstamp2; |
| { |
| std::unique_ptr<Transaction> txn; |
| ASSERT_EQ(meta_service->txn_kv()->create_txn(&txn), TxnErrorCode::TXN_OK); |
| |
| std::string tablet_meta_key2 = versioned::meta_tablet_key({instance_id, tablet_id2}); |
| doris::TabletMetaCloudPB tablet_meta2; |
| TxnErrorCode err = |
| versioned::document_get(txn.get(), tablet_meta_key2, &tablet_meta2, &versionstamp2); |
| ASSERT_EQ(err, TxnErrorCode::TXN_OK); |
| EXPECT_EQ(tablet_meta2.ttl_seconds(), 3000); |
| } |
| |
| // Check operation log exists |
| Versionstamp log_versionstamp; |
| { |
| std::unique_ptr<Transaction> txn; |
| ASSERT_EQ(meta_service->txn_kv()->create_txn(&txn), TxnErrorCode::TXN_OK); |
| |
| std::string log_key = versioned::log_key(instance_id); |
| OperationLogPB operation_log; |
| TxnErrorCode err = |
| versioned::document_get(txn.get(), log_key, &operation_log, &log_versionstamp); |
| ASSERT_EQ(err, TxnErrorCode::TXN_OK); |
| ASSERT_TRUE(operation_log.has_update_tablet()); |
| EXPECT_EQ(operation_log.update_tablet().tablet_ids_size(), 2); |
| EXPECT_TRUE(std::find(operation_log.update_tablet().tablet_ids().begin(), |
| operation_log.update_tablet().tablet_ids().end(), |
| tablet_id1) != operation_log.update_tablet().tablet_ids().end()); |
| EXPECT_TRUE(std::find(operation_log.update_tablet().tablet_ids().begin(), |
| operation_log.update_tablet().tablet_ids().end(), |
| tablet_id2) != operation_log.update_tablet().tablet_ids().end()); |
| } |
| |
| // Verify all versioned keys have the same commit_versionstamp |
| EXPECT_EQ(versionstamp1, versionstamp2); |
| EXPECT_EQ(versionstamp1, log_versionstamp); |
| EXPECT_EQ(versionstamp2, log_versionstamp); |
| } |
| |
| TEST(MetaServiceOperationLogTest, CompactionLog) { |
| auto meta_service = get_meta_service(false); |
| std::string instance_id = "compaction_log_test"; |
| auto* sp = SyncPoint::get_instance(); |
| DORIS_CLOUD_DEFER { |
| SyncPoint::get_instance()->clear_all_call_backs(); |
| }; |
| sp->set_call_back("get_instance_id", [&](auto&& args) { |
| auto* ret = try_any_cast_ret<std::string>(args); |
| ret->first = instance_id; |
| ret->second = true; |
| }); |
| sp->enable_processing(); |
| |
| constexpr int64_t table_id = 20001; |
| constexpr int64_t index_id = 20002; |
| constexpr int64_t partition_id = 20003; |
| constexpr int64_t tablet_id = 20004; |
| |
| { |
| // write instance |
| InstanceInfoPB instance_info; |
| instance_info.set_instance_id(instance_id); |
| instance_info.set_multi_version_status(MULTI_VERSION_WRITE_ONLY); |
| std::unique_ptr<Transaction> txn; |
| ASSERT_EQ(meta_service->txn_kv()->create_txn(&txn), TxnErrorCode::TXN_OK); |
| txn->put(instance_key(instance_id), instance_info.SerializeAsString()); |
| ASSERT_EQ(txn->commit(), TxnErrorCode::TXN_OK); |
| |
| meta_service->resource_mgr()->refresh_instance(instance_id); |
| ASSERT_TRUE(meta_service->resource_mgr()->is_version_write_enabled(instance_id)); |
| } |
| |
| { |
| // Create tablet first |
| create_tablet(meta_service.get(), table_id, index_id, partition_id, tablet_id); |
| } |
| |
| // Create input rowsets for compaction (versions 2-4) |
| std::vector<doris::RowsetMetaCloudPB> input_rowsets; |
| auto txn_kv = meta_service->txn_kv(); |
| { |
| std::unique_ptr<Transaction> txn; |
| ASSERT_EQ(txn_kv->create_txn(&txn), TxnErrorCode::TXN_OK); |
| |
| for (int i = 0; i < 3; ++i) { |
| auto rowset = create_rowset(i + 100, tablet_id, partition_id, i + 2, 50 * (i + 1)); |
| input_rowsets.push_back(rowset); |
| |
| // Put rowset directly to meta storage |
| auto rowset_key = meta_rowset_key({instance_id, tablet_id, rowset.end_version()}); |
| auto rowset_val = rowset.SerializeAsString(); |
| txn->put(rowset_key, rowset_val); |
| } |
| ASSERT_EQ(txn->commit(), TxnErrorCode::TXN_OK); |
| } |
| |
| // Create output rowset as tmp rowset |
| constexpr int64_t txn_id = 30001; |
| constexpr int64_t output_start_version = 2; |
| constexpr int64_t output_end_version = 4; |
| auto output_rowset = create_rowset(200, tablet_id, partition_id, output_start_version, 100); |
| output_rowset.set_end_version(output_end_version); // Set end version to create 2-4 range |
| output_rowset.set_txn_id(txn_id); |
| |
| { |
| std::unique_ptr<Transaction> txn; |
| ASSERT_EQ(txn_kv->create_txn(&txn), TxnErrorCode::TXN_OK); |
| |
| // Put tmp rowset |
| auto tmp_rowset_key = meta_rowset_tmp_key({instance_id, txn_id, tablet_id}); |
| auto tmp_rowset_val = output_rowset.SerializeAsString(); |
| txn->put(tmp_rowset_key, tmp_rowset_val); |
| |
| // Create initial tablet stats |
| TabletStatsPB initial_stats; |
| initial_stats.set_num_rows(150); |
| initial_stats.set_data_size(150 * 50); |
| initial_stats.set_num_rowsets(3); |
| initial_stats.set_num_segments(3); |
| initial_stats.set_index_size(100); |
| initial_stats.set_segment_size(200); |
| initial_stats.set_cumulative_point(1); |
| |
| auto stats_key = |
| stats_tablet_key({instance_id, table_id, index_id, partition_id, tablet_id}); |
| auto stats_val = initial_stats.SerializeAsString(); |
| txn->put(stats_key, stats_val); |
| |
| // Create tablet compact stats for versioned storage |
| initial_stats.set_num_rows(-150); |
| initial_stats.set_data_size(-150 * 50); |
| initial_stats.set_num_rowsets(-3); |
| initial_stats.set_num_segments(-3); |
| initial_stats.set_index_size(-100); |
| initial_stats.set_segment_size(-200); |
| initial_stats.set_cumulative_point(1); |
| auto tablet_compact_stats_key = |
| versioned::tablet_compact_stats_key({instance_id, tablet_id}); |
| auto tablet_compact_stats_val = initial_stats.SerializeAsString(); |
| versioned_put(txn.get(), tablet_compact_stats_key, tablet_compact_stats_val); |
| |
| ASSERT_EQ(txn->commit(), TxnErrorCode::TXN_OK); |
| } |
| |
| // Start compaction job first |
| const std::string job_id = "test_compaction_job"; |
| const std::string initiator = "test_be"; |
| |
| { |
| brpc::Controller cntl; |
| StartTabletJobRequest req; |
| StartTabletJobResponse res; |
| req.mutable_job()->mutable_idx()->set_tablet_id(tablet_id); |
| auto compaction = req.mutable_job()->add_compaction(); |
| compaction->set_id(job_id); |
| compaction->set_initiator(initiator); |
| compaction->set_type(TabletCompactionJobPB::CUMULATIVE); |
| compaction->set_base_compaction_cnt(0); |
| compaction->set_cumulative_compaction_cnt(0); |
| compaction->add_input_versions(2); |
| compaction->add_input_versions(4); |
| long now = time(nullptr); |
| compaction->set_expiration(now + 12); |
| compaction->set_lease(now + 3); |
| meta_service->start_tablet_job(&cntl, &req, &res, nullptr); |
| ASSERT_EQ(res.status().code(), MetaServiceCode::OK); |
| } |
| |
| size_t num_logs_before = count_range(txn_kv.get(), versioned::log_key(instance_id), |
| versioned::log_key(instance_id) + "\xFF"); |
| |
| // Now use real finish_tablet_job to trigger process_compaction_job |
| { |
| brpc::Controller cntl; |
| FinishTabletJobRequest req; |
| FinishTabletJobResponse res; |
| |
| req.set_action(FinishTabletJobRequest::COMMIT); |
| req.mutable_job()->mutable_idx()->set_table_id(table_id); |
| req.mutable_job()->mutable_idx()->set_index_id(index_id); |
| req.mutable_job()->mutable_idx()->set_partition_id(partition_id); |
| req.mutable_job()->mutable_idx()->set_tablet_id(tablet_id); |
| |
| auto compaction = req.mutable_job()->add_compaction(); |
| compaction->set_id(job_id); |
| compaction->set_initiator(initiator); |
| compaction->set_type(TabletCompactionJobPB::CUMULATIVE); |
| |
| // Input versions and rowsets |
| compaction->add_input_versions(2); |
| compaction->add_input_versions(4); |
| |
| // Output information |
| compaction->add_txn_id(txn_id); |
| compaction->add_output_versions(output_end_version); |
| compaction->add_output_rowset_ids(output_rowset.rowset_id_v2()); |
| compaction->set_output_cumulative_point(5); |
| |
| // Compaction stats for updating tablet stats |
| compaction->set_size_input_rowsets(150 * 50); // Size of input rowsets |
| compaction->set_index_size_input_rowsets(100); |
| compaction->set_segment_size_input_rowsets(200); |
| compaction->set_num_input_rows(150); |
| compaction->set_num_input_rowsets(3); |
| compaction->set_num_input_segments(3); |
| |
| compaction->set_size_output_rowsets(100 * 50); // Size of output rowset |
| compaction->set_index_size_output_rowsets(50); |
| compaction->set_segment_size_output_rowsets(100); |
| compaction->set_num_output_rows(100); |
| compaction->set_num_output_rowsets(1); |
| compaction->set_num_output_segments(1); |
| |
| // This will trigger process_compaction_job internally |
| meta_service->finish_tablet_job(&cntl, &req, &res, nullptr); |
| ASSERT_EQ(res.status().code(), MetaServiceCode::OK); |
| } |
| |
| // Verify that operation log was created |
| size_t num_logs_after = count_range(txn_kv.get(), versioned::log_key(instance_id), |
| versioned::log_key(instance_id) + "\xFF"); |
| ASSERT_GT(num_logs_after, num_logs_before) |
| << "Expected new compaction operation log, but found no new logs"; |
| |
| Versionstamp log_version; |
| { |
| // Verify compaction operation log content |
| std::unique_ptr<Transaction> txn; |
| ASSERT_EQ(txn_kv->create_txn(&txn), TxnErrorCode::TXN_OK); |
| std::string log_key = versioned::log_key({instance_id}); |
| std::string value; |
| ASSERT_EQ(versioned_get(txn.get(), log_key, &log_version, &value), TxnErrorCode::TXN_OK); |
| |
| OperationLogPB operation_log; |
| ASSERT_TRUE(operation_log.ParseFromString(value)); |
| ASSERT_TRUE(operation_log.has_compaction()); |
| |
| const auto& compaction_log = operation_log.compaction(); |
| ASSERT_EQ(compaction_log.tablet_id(), tablet_id); |
| ASSERT_EQ(compaction_log.start_version(), 2); |
| ASSERT_EQ(compaction_log.end_version(), 4); |
| ASSERT_EQ(compaction_log.recycle_rowsets_size(), 3); |
| |
| // Verify recycle rowsets content - these should be the input rowsets marked for recycling |
| for (int i = 0; i < compaction_log.recycle_rowsets_size(); ++i) { |
| const auto& recycle_rowset = compaction_log.recycle_rowsets(i); |
| ASSERT_EQ(recycle_rowset.type(), RecycleRowsetPB::COMPACT); |
| ASSERT_EQ(recycle_rowset.rowset_meta().tablet_id(), tablet_id); |
| ASSERT_EQ(recycle_rowset.rowset_meta().partition_id(), partition_id); |
| ASSERT_GT(recycle_rowset.creation_time(), 0); |
| |
| // Verify this matches one of our input rowsets |
| bool found_matching_rowset = false; |
| for (const auto& input_rowset : input_rowsets) { |
| if (recycle_rowset.rowset_meta().rowset_id_v2() == input_rowset.rowset_id_v2()) { |
| found_matching_rowset = true; |
| ASSERT_EQ(recycle_rowset.rowset_meta().start_version(), |
| input_rowset.start_version()); |
| ASSERT_EQ(recycle_rowset.rowset_meta().end_version(), |
| input_rowset.end_version()); |
| break; |
| } |
| } |
| ASSERT_TRUE(found_matching_rowset) << "Recycle rowset should match an input rowset"; |
| } |
| } |
| |
| // Verify tablet compact stats were updated |
| { |
| std::unique_ptr<Transaction> txn; |
| ASSERT_EQ(txn_kv->create_txn(&txn), TxnErrorCode::TXN_OK); |
| |
| std::string tablet_compact_stats_key = |
| versioned::tablet_compact_stats_key({instance_id, tablet_id}); |
| std::string tablet_compact_stats_value; |
| Versionstamp* versionstamp = nullptr; |
| ASSERT_EQ(versioned_get(txn.get(), tablet_compact_stats_key, versionstamp, |
| &tablet_compact_stats_value), |
| TxnErrorCode::TXN_OK); |
| |
| TabletStatsPB compact_stats; |
| ASSERT_TRUE(compact_stats.ParseFromString(tablet_compact_stats_value)); |
| ASSERT_EQ(compact_stats.cumulative_compaction_cnt(), 1); |
| ASSERT_EQ(compact_stats.cumulative_point(), 5); |
| ASSERT_GT(compact_stats.last_cumu_compaction_time_ms(), 0); |
| // Check that rowset count decreased: 3 input -> 1 output = -2 |
| ASSERT_EQ(compact_stats.num_rowsets(), -5); // - 3 - 3 + 1 = -5 |
| ASSERT_EQ(compact_stats.data_size(), -10000); // - 7500 - 7500 + 5000 = -10000 |
| ASSERT_EQ(compact_stats.num_rows(), -200); // - 150 - 150 + 100 = -200 |
| ASSERT_EQ(compact_stats.num_segments(), -5); // - 3 - 3 + 1 = -5 |
| ASSERT_EQ(compact_stats.index_size(), -150); // - 200 - 200 + 50 = -150 |
| ASSERT_EQ(compact_stats.segment_size(), -300); // - 200 - 200 + 100 = -300 |
| } |
| |
| // Verify input rowsets were removed and output rowset was created correctly |
| { |
| std::unique_ptr<Transaction> txn; |
| ASSERT_EQ(txn_kv->create_txn(&txn), TxnErrorCode::TXN_OK); |
| |
| // All original input rowsets (versions 2, 3, 4) should be deleted |
| for (const auto& rowset : input_rowsets) { |
| if (rowset.end_version() < 4) { |
| auto rowset_key = meta_rowset_key({instance_id, tablet_id, rowset.end_version()}); |
| std::string rowset_value; |
| auto result = txn->get(rowset_key, &rowset_value); |
| EXPECT_EQ(result, TxnErrorCode::TXN_KEY_NOT_FOUND) |
| << "Input rowset version " << rowset.end_version() |
| << " should have been removed after compaction"; |
| } else { |
| // The new output rowset (2-4) should exist at version 4 (end_version) |
| auto output_rowset_key = |
| meta_rowset_key({instance_id, tablet_id, output_end_version}); |
| std::string output_rowset_value; |
| ASSERT_EQ(txn->get(output_rowset_key, &output_rowset_value), TxnErrorCode::TXN_OK) |
| << "Output rowset should exist at version " << output_end_version; |
| // Verify this is the correct output rowset (2-4 range) |
| doris::RowsetMetaCloudPB output_meta; |
| ASSERT_TRUE(output_meta.ParseFromString(output_rowset_value)); |
| ASSERT_EQ(output_meta.rowset_id_v2(), output_rowset.rowset_id_v2()) |
| << "Output rowset should have the correct rowset ID"; |
| ASSERT_EQ(output_meta.tablet_id(), tablet_id); |
| ASSERT_EQ(output_meta.start_version(), output_start_version); |
| ASSERT_EQ(output_meta.end_version(), output_end_version); |
| } |
| } |
| |
| // Verify versioned compact rowset exists for output version |
| auto meta_rowset_compact_key = |
| versioned::meta_rowset_compact_key({instance_id, tablet_id, output_end_version}); |
| std::string compact_rowset_value; |
| Versionstamp* versionstamp = nullptr; |
| ASSERT_EQ(versioned_get(txn.get(), meta_rowset_compact_key, versionstamp, |
| &compact_rowset_value), |
| TxnErrorCode::TXN_OK); |
| |
| // Check meta rowset compact value is not empty and valid |
| ASSERT_FALSE(compact_rowset_value.empty()) |
| << "Meta rowset compact value should not be empty"; |
| |
| // Parse and verify the compact rowset content |
| doris::RowsetMetaCloudPB compact_meta; |
| ASSERT_TRUE(compact_meta.ParseFromString(compact_rowset_value)) |
| << "Meta rowset compact value should be valid protobuf"; |
| ASSERT_EQ(compact_meta.tablet_id(), tablet_id) |
| << "Meta rowset compact value tablet_id should match"; |
| ASSERT_EQ(compact_meta.end_version(), output_end_version) |
| << "Meta rowset compact value version should match output version"; |
| } |
| |
| // Verify tmp rowset was removed |
| { |
| std::unique_ptr<Transaction> txn; |
| ASSERT_EQ(txn_kv->create_txn(&txn), TxnErrorCode::TXN_OK); |
| |
| auto tmp_rowset_key = meta_rowset_tmp_key({instance_id, txn_id, tablet_id}); |
| std::string tmp_rowset_value; |
| ASSERT_EQ(txn->get(tmp_rowset_key, &tmp_rowset_value), TxnErrorCode::TXN_KEY_NOT_FOUND) |
| << "Tmp rowset should have been removed after compaction"; |
| } |
| } |
| |
| TEST(MetaServiceOperationLogTest, CompactionLogFirstTimeTabletStats) { |
| auto meta_service = get_meta_service(false); |
| std::string instance_id = "first_time_tablet_stats_test"; |
| auto* sp = SyncPoint::get_instance(); |
| DORIS_CLOUD_DEFER { |
| SyncPoint::get_instance()->clear_all_call_backs(); |
| }; |
| sp->set_call_back("get_instance_id", [&](auto&& args) { |
| auto* ret = try_any_cast_ret<std::string>(args); |
| ret->first = instance_id; |
| ret->second = true; |
| }); |
| sp->enable_processing(); |
| |
| constexpr int64_t table_id = 60001; |
| constexpr int64_t index_id = 60002; |
| constexpr int64_t partition_id = 60003; |
| constexpr int64_t tablet_id = 60004; |
| |
| { |
| // Create instance without multi-version settings first |
| InstanceInfoPB instance_info; |
| instance_info.set_instance_id(instance_id); |
| std::unique_ptr<Transaction> txn; |
| ASSERT_EQ(meta_service->txn_kv()->create_txn(&txn), TxnErrorCode::TXN_OK); |
| txn->put(instance_key(instance_id), instance_info.SerializeAsString()); |
| ASSERT_EQ(txn->commit(), TxnErrorCode::TXN_OK); |
| meta_service->resource_mgr()->refresh_instance(instance_id); |
| } |
| |
| { |
| // Create tablet first |
| create_tablet(meta_service.get(), table_id, index_id, partition_id, tablet_id); |
| } |
| |
| { |
| // Now enable versioned write after tablet creation |
| InstanceInfoPB instance_info; |
| instance_info.set_instance_id(instance_id); |
| instance_info.set_multi_version_status(MULTI_VERSION_WRITE_ONLY); |
| std::unique_ptr<Transaction> txn; |
| ASSERT_EQ(meta_service->txn_kv()->create_txn(&txn), TxnErrorCode::TXN_OK); |
| txn->put(instance_key(instance_id), instance_info.SerializeAsString()); |
| ASSERT_EQ(txn->commit(), TxnErrorCode::TXN_OK); |
| |
| meta_service->resource_mgr()->refresh_instance(instance_id); |
| ASSERT_TRUE(meta_service->resource_mgr()->is_version_write_enabled(instance_id)); |
| } |
| |
| // Create input rowset for compaction (version 2) |
| auto txn_kv = meta_service->txn_kv(); |
| constexpr int64_t input_version = 2; |
| auto input_rowset = create_rowset(100, tablet_id, partition_id, input_version, 100); |
| { |
| std::unique_ptr<Transaction> txn; |
| ASSERT_EQ(txn_kv->create_txn(&txn), TxnErrorCode::TXN_OK); |
| |
| // Put input rowset |
| auto rowset_key = meta_rowset_key({instance_id, tablet_id, input_version}); |
| auto rowset_val = input_rowset.SerializeAsString(); |
| txn->put(rowset_key, rowset_val); |
| |
| // Create initial tablet stats with some existing compaction data |
| TabletStatsPB initial_stats; |
| initial_stats.set_num_rows(100); |
| initial_stats.set_data_size(5000); |
| initial_stats.set_num_rowsets(2); |
| initial_stats.set_num_segments(2); |
| initial_stats.set_index_size(100); |
| initial_stats.set_segment_size(200); |
| initial_stats.set_cumulative_point(2); |
| // Set some existing compaction stats to verify they are copied |
| initial_stats.set_base_compaction_cnt(2); |
| initial_stats.set_cumulative_compaction_cnt(3); |
| initial_stats.set_full_compaction_cnt(1); |
| initial_stats.set_last_base_compaction_time_ms(1000); |
| initial_stats.set_last_cumu_compaction_time_ms(2000); |
| initial_stats.set_last_full_compaction_time_ms(3000); |
| |
| auto stats_key = |
| stats_tablet_key({instance_id, table_id, index_id, partition_id, tablet_id}); |
| auto stats_val = initial_stats.SerializeAsString(); |
| txn->put(stats_key, stats_val); |
| |
| // DO NOT create versioned tablet_compact_stats - this will trigger TXN_KEY_NOT_FOUND |
| |
| ASSERT_EQ(txn->commit(), TxnErrorCode::TXN_OK); |
| } |
| |
| // Create output rowset as tmp rowset |
| constexpr int64_t txn_id = 60001; |
| auto output_rowset = create_rowset(200, tablet_id, partition_id, input_version, 80); |
| output_rowset.set_txn_id(txn_id); |
| |
| { |
| std::unique_ptr<Transaction> txn; |
| ASSERT_EQ(txn_kv->create_txn(&txn), TxnErrorCode::TXN_OK); |
| |
| // Put tmp rowset |
| auto tmp_rowset_key = meta_rowset_tmp_key({instance_id, txn_id, tablet_id}); |
| auto tmp_rowset_val = output_rowset.SerializeAsString(); |
| txn->put(tmp_rowset_key, tmp_rowset_val); |
| |
| ASSERT_EQ(txn->commit(), TxnErrorCode::TXN_OK); |
| } |
| |
| // Start compaction job |
| const std::string job_id = "first_time_stats_compaction_job"; |
| const std::string initiator = "test_be"; |
| |
| { |
| brpc::Controller cntl; |
| StartTabletJobRequest req; |
| StartTabletJobResponse res; |
| req.mutable_job()->mutable_idx()->set_tablet_id(tablet_id); |
| auto compaction = req.mutable_job()->add_compaction(); |
| compaction->set_id(job_id); |
| compaction->set_initiator(initiator); |
| compaction->set_type(TabletCompactionJobPB::CUMULATIVE); |
| compaction->set_base_compaction_cnt(3); |
| compaction->set_cumulative_compaction_cnt(4); |
| compaction->add_input_versions(input_version); |
| compaction->add_input_versions(input_version); |
| long now = time(nullptr); |
| compaction->set_expiration(now + 12); |
| compaction->set_lease(now + 3); |
| meta_service->start_tablet_job(&cntl, &req, &res, nullptr); |
| ASSERT_EQ(res.status().code(), MetaServiceCode::OK); |
| } |
| |
| // Finish compaction job - this should handle first-time tablet stats creation |
| { |
| brpc::Controller cntl; |
| FinishTabletJobRequest req; |
| FinishTabletJobResponse res; |
| |
| req.set_action(FinishTabletJobRequest::COMMIT); |
| req.mutable_job()->mutable_idx()->set_table_id(table_id); |
| req.mutable_job()->mutable_idx()->set_index_id(index_id); |
| req.mutable_job()->mutable_idx()->set_partition_id(partition_id); |
| req.mutable_job()->mutable_idx()->set_tablet_id(tablet_id); |
| |
| auto compaction = req.mutable_job()->add_compaction(); |
| compaction->set_id(job_id); |
| compaction->set_initiator(initiator); |
| compaction->set_type(TabletCompactionJobPB::CUMULATIVE); |
| compaction->add_input_versions(input_version); |
| compaction->add_input_versions(input_version); |
| compaction->add_txn_id(txn_id); |
| compaction->add_output_versions(input_version); |
| compaction->add_output_rowset_ids(output_rowset.rowset_id_v2()); |
| compaction->set_output_cumulative_point(3); |
| |
| // Compaction stats |
| compaction->set_size_input_rowsets(5000); |
| compaction->set_index_size_input_rowsets(100); |
| compaction->set_segment_size_input_rowsets(200); |
| compaction->set_num_input_rows(100); |
| compaction->set_num_input_rowsets(2); |
| compaction->set_num_input_segments(2); |
| |
| compaction->set_size_output_rowsets(4000); |
| compaction->set_index_size_output_rowsets(80); |
| compaction->set_segment_size_output_rowsets(160); |
| compaction->set_num_output_rows(80); |
| compaction->set_num_output_rowsets(1); |
| compaction->set_num_output_segments(1); |
| |
| meta_service->finish_tablet_job(&cntl, &req, &res, nullptr); |
| ASSERT_EQ(res.status().code(), MetaServiceCode::OK); |
| } |
| |
| // Check that tablet compact stats were created with proper zero values for first-time write |
| { |
| std::unique_ptr<Transaction> txn; |
| ASSERT_EQ(txn_kv->create_txn(&txn), TxnErrorCode::TXN_OK); |
| |
| std::string tablet_compact_stats_key = |
| versioned::tablet_compact_stats_key({instance_id, tablet_id}); |
| std::string tablet_compact_stats_value; |
| Versionstamp* versionstamp = nullptr; |
| ASSERT_EQ(versioned_get(txn.get(), tablet_compact_stats_key, versionstamp, |
| &tablet_compact_stats_value), |
| TxnErrorCode::TXN_OK) |
| << "Tablet compact stats should be created by process_compaction_job"; |
| |
| TabletStatsPB compact_stats; |
| ASSERT_TRUE(compact_stats.ParseFromString(tablet_compact_stats_value)); |
| |
| // Check TXN_KEY_NOT_FOUND logic: size fields should be cleared to zero for versioned stats |
| ASSERT_EQ(compact_stats.num_rows(), -20) // 80 - 100 = -20 |
| << "num_rows should be -20 in TXN_KEY_NOT_FOUND logic"; |
| ASSERT_EQ(compact_stats.data_size(), -1000) // 4000 - 5000 = -1000 |
| << "data_size should be -1000 in TXN_KEY_NOT_FOUND logic"; |
| ASSERT_EQ(compact_stats.num_rowsets(), -1) // 1 - 2 = -1 |
| << "num_rowsets should be -1 in TXN_KEY_NOT_FOUND logic"; |
| ASSERT_EQ(compact_stats.num_segments(), -1) // 1 - 2 = -1 |
| << "num_segments should be -1 in TXN_KEY_NOT_FOUND logic"; |
| ASSERT_EQ(compact_stats.index_size(), -20) // 80 - 100 = -20 |
| << "index_size should be -20 in TXN_KEY_NOT_FOUND logic"; |
| ASSERT_EQ(compact_stats.segment_size(), -40) // 160 - 200 = -40 |
| << "segment_size should be -40 in TXN_KEY_NOT_FOUND logic"; |
| |
| // Check that compaction count fields are copied from initial tablet stats |
| ASSERT_EQ(compact_stats.base_compaction_cnt(), 2) |
| << "Base compaction count should be copied from initial tablet stats"; |
| ASSERT_EQ(compact_stats.cumulative_compaction_cnt(), 4) |
| << "Cumulative compaction count should be incremented from 3 to 4"; |
| ASSERT_EQ(compact_stats.full_compaction_cnt(), 1) |
| << "Full compaction count should be copied from initial tablet stats"; |
| |
| // Check that time fields are copied from initial tablet stats |
| ASSERT_EQ(compact_stats.last_base_compaction_time_ms(), 1000) |
| << "Last base compaction time should be copied from initial tablet stats"; |
| ASSERT_GT(compact_stats.last_cumu_compaction_time_ms(), 2000) |
| << "Last cumulative compaction time should be updated after compaction"; |
| ASSERT_EQ(compact_stats.last_full_compaction_time_ms(), 3000) |
| << "Last full compaction time should be copied from initial tablet stats"; |
| |
| // Check that cumulative_point is properly set |
| ASSERT_EQ(compact_stats.cumulative_point(), 3); |
| } |
| } |
| |
| TEST(MetaServiceOperationLogTest, SchemaChangeLog) { |
| // ========================================================================= |
| // Schema Change Operation Log Test Flow |
| // ========================================================================= |
| // This test simulates the complete schema change process and verifies that: |
| // 1. Schema change operation log is correctly generated |
| // 2. Multi-version double write works for new tablet (meta tablet key, |
| // tablet load stats, meta rowset compact key) |
| // 3. Rowset management is handled correctly during schema change |
| // |
| // Test Scenario: |
| // - Old tablet starts with versions [2-2, 3-3] (20+30=50 rows) |
| // - During schema change, both tablets receive versions [4-4, 5-5] (alter version = 5, 40+50=90 rows) |
| // - After schema change starts, both tablets receive versions [6-6, 7-7] (60+70=130 rows) |
| // - When finishing schema change: |
| // * Old tablet's [2-2, 3-3, 4-4, 5-5] data is converted to tmp rowsets in new tablet |
| // * New tablet deletes its own [4-4, 5-5] rowsets and converts tmp rowsets to permanent |
| // * New tablet keeps [6-6, 7-7] unchanged |
| // * Schema change net effect: add [2-2, 3-3] data to new tablet's tablet_load_stats |
| // * Multi-version keys are properly created for new tablet |
| // ========================================================================= |
| |
| // Step 1: Initialize test environment and create old tablet with versions [2-2, 3-3] |
| auto meta_service = get_meta_service(false); |
| std::string instance_id = "schema_change_log_test"; |
| auto* sp = SyncPoint::get_instance(); |
| DORIS_CLOUD_DEFER { |
| SyncPoint::get_instance()->clear_all_call_backs(); |
| }; |
| sp->set_call_back("get_instance_id", [&](auto&& args) { |
| auto* ret = try_any_cast_ret<std::string>(args); |
| ret->first = instance_id; |
| ret->second = true; |
| }); |
| sp->enable_processing(); |
| |
| constexpr int64_t table_id = 30001; |
| constexpr int64_t index_id = 30002; |
| constexpr int64_t partition_id = 30003; |
| constexpr int64_t old_tablet_id = 30004; |
| constexpr int64_t new_tablet_id = 30005; |
| |
| // Create instance with multi-version support |
| { |
| InstanceInfoPB instance_info; |
| instance_info.set_instance_id(instance_id); |
| instance_info.set_multi_version_status(MULTI_VERSION_WRITE_ONLY); |
| std::unique_ptr<Transaction> txn; |
| ASSERT_EQ(meta_service->txn_kv()->create_txn(&txn), TxnErrorCode::TXN_OK); |
| txn->put(instance_key(instance_id), instance_info.SerializeAsString()); |
| ASSERT_EQ(txn->commit(), TxnErrorCode::TXN_OK); |
| |
| meta_service->resource_mgr()->refresh_instance(instance_id); |
| ASSERT_TRUE(meta_service->resource_mgr()->is_version_write_enabled(instance_id)); |
| } |
| |
| // Create old tablet |
| { create_tablet(meta_service.get(), table_id, index_id, partition_id, old_tablet_id); } |
| |
| auto txn_kv = meta_service->txn_kv(); |
| |
| // Create initial rowsets for old tablet: versions 2, 3 |
| std::vector<doris::RowsetMetaCloudPB> initial_rowsets; |
| { |
| std::unique_ptr<Transaction> txn; |
| ASSERT_EQ(txn_kv->create_txn(&txn), TxnErrorCode::TXN_OK); |
| |
| for (int version = 2; version <= 3; ++version) { |
| auto rowset = create_rowset(100 + version, old_tablet_id, partition_id, version, |
| version * 10); |
| initial_rowsets.push_back(rowset); |
| |
| auto rowset_key = meta_rowset_key({instance_id, old_tablet_id, version}); |
| auto rowset_val = rowset.SerializeAsString(); |
| txn->put(rowset_key, rowset_val); |
| } |
| ASSERT_EQ(txn->commit(), TxnErrorCode::TXN_OK); |
| } |
| |
| // Step 2: Create new tablet in NOTREADY state (empty, waiting for schema change) |
| { |
| brpc::Controller cntl; |
| CreateTabletsRequest req; |
| CreateTabletsResponse res; |
| req.set_cloud_unique_id("test_cloud_unique_id"); |
| add_tablet(req, table_id, index_id, partition_id, new_tablet_id); |
| |
| // Set new tablet state to NOTREADY (for schema change) |
| auto tablet_meta = req.mutable_tablet_metas(0); |
| tablet_meta->set_tablet_state(doris::TabletStatePB::PB_NOTREADY); |
| |
| meta_service->create_tablets(&cntl, &req, &res, nullptr); |
| ASSERT_EQ(res.status().code(), MetaServiceCode::OK); |
| } |
| |
| // Step 3: Add alter versions [4-4, 5-5] and start schema change job (alter_version = 5) |
| const std::string job_id = "test_schema_change_job"; |
| const std::string initiator = "test_be"; |
| constexpr int64_t alter_version = 5; // Determined when starting the job |
| |
| // Add versions 4-5 to old tablet (alter versions) |
| std::vector<doris::RowsetMetaCloudPB> alter_rowsets_old; |
| { |
| std::unique_ptr<Transaction> txn; |
| ASSERT_EQ(txn_kv->create_txn(&txn), TxnErrorCode::TXN_OK); |
| |
| for (int version = 4; version <= 5; ++version) { |
| auto rowset = create_rowset(200 + version, old_tablet_id, partition_id, version, |
| version * 10); |
| alter_rowsets_old.push_back(rowset); |
| |
| auto rowset_key = meta_rowset_key({instance_id, old_tablet_id, version}); |
| auto rowset_val = rowset.SerializeAsString(); |
| txn->put(rowset_key, rowset_val); |
| } |
| ASSERT_EQ(txn->commit(), TxnErrorCode::TXN_OK); |
| } |
| |
| // Add versions 4-5 to new tablet (alter versions) |
| std::vector<doris::RowsetMetaCloudPB> alter_rowsets_new; |
| { |
| std::unique_ptr<Transaction> txn; |
| ASSERT_EQ(txn_kv->create_txn(&txn), TxnErrorCode::TXN_OK); |
| |
| for (int version = 4; version <= 5; ++version) { |
| auto rowset = create_rowset(300 + version, new_tablet_id, partition_id, version, |
| version * 10); |
| alter_rowsets_new.push_back(rowset); |
| |
| auto rowset_key = meta_rowset_key({instance_id, new_tablet_id, version}); |
| auto rowset_val = rowset.SerializeAsString(); |
| txn->put(rowset_key, rowset_val); |
| } |
| ASSERT_EQ(txn->commit(), TxnErrorCode::TXN_OK); |
| } |
| |
| // Start schema change job |
| { |
| brpc::Controller cntl; |
| StartTabletJobRequest req; |
| StartTabletJobResponse res; |
| req.mutable_job()->mutable_idx()->set_tablet_id(old_tablet_id); |
| auto* schema_change = req.mutable_job()->mutable_schema_change(); |
| schema_change->set_id(job_id); |
| schema_change->set_initiator(initiator); |
| schema_change->mutable_new_tablet_idx()->set_table_id(table_id); |
| schema_change->mutable_new_tablet_idx()->set_index_id(index_id); |
| schema_change->mutable_new_tablet_idx()->set_partition_id(partition_id); |
| schema_change->mutable_new_tablet_idx()->set_tablet_id(new_tablet_id); |
| schema_change->set_alter_version(alter_version); |
| |
| long now = time(nullptr); |
| schema_change->set_expiration(now + 12); |
| |
| meta_service->start_tablet_job(&cntl, &req, &res, nullptr); |
| ASSERT_EQ(res.status().code(), MetaServiceCode::OK); |
| } |
| |
| // Step 4: Simulate ongoing data ingestion - add post-alter versions [6-6, 7-7] |
| std::vector<doris::RowsetMetaCloudPB> post_alter_rowsets_old; |
| std::vector<doris::RowsetMetaCloudPB> post_alter_rowsets_new; |
| |
| // Add versions 6-7 to old tablet |
| { |
| std::unique_ptr<Transaction> txn; |
| ASSERT_EQ(txn_kv->create_txn(&txn), TxnErrorCode::TXN_OK); |
| |
| for (int version = 6; version <= 7; ++version) { |
| auto rowset = create_rowset(400 + version, old_tablet_id, partition_id, version, |
| version * 10); |
| post_alter_rowsets_old.push_back(rowset); |
| |
| auto rowset_key = meta_rowset_key({instance_id, old_tablet_id, version}); |
| auto rowset_val = rowset.SerializeAsString(); |
| txn->put(rowset_key, rowset_val); |
| } |
| ASSERT_EQ(txn->commit(), TxnErrorCode::TXN_OK); |
| } |
| |
| // Add versions 6-7 to new tablet |
| { |
| std::unique_ptr<Transaction> txn; |
| ASSERT_EQ(txn_kv->create_txn(&txn), TxnErrorCode::TXN_OK); |
| |
| for (int version = 6; version <= 7; ++version) { |
| auto rowset = create_rowset(500 + version, new_tablet_id, partition_id, version, |
| version * 10); |
| post_alter_rowsets_new.push_back(rowset); |
| |
| auto rowset_key = meta_rowset_key({instance_id, new_tablet_id, version}); |
| auto rowset_val = rowset.SerializeAsString(); |
| txn->put(rowset_key, rowset_val); |
| } |
| ASSERT_EQ(txn->commit(), TxnErrorCode::TXN_OK); |
| } |
| |
| // Step 5: Complete schema change - convert old tablet data via tmp rowsets |
| // Create tmp rowsets representing old tablet's [2-2, 3-3, 4-4, 5-5] data that will be converted to new tablet |
| std::vector<int64_t> tmp_txn_ids; |
| { |
| std::unique_ptr<Transaction> txn; |
| ASSERT_EQ(txn_kv->create_txn(&txn), TxnErrorCode::TXN_OK); |
| |
| // Create tmp rowsets for versions 2-2, 3-3, 4-4, 5-5 (simulating old tablet data conversion) |
| for (int version = 2; version <= 5; ++version) { |
| int64_t tmp_txn_id = 40000 + version; |
| tmp_txn_ids.push_back(tmp_txn_id); |
| |
| // Create tmp rowset with data from old tablet |
| auto tmp_rowset = create_rowset(600 + version, new_tablet_id, partition_id, version, |
| version * 10); |
| tmp_rowset.set_txn_id(tmp_txn_id); |
| |
| auto tmp_rowset_key = meta_rowset_tmp_key({instance_id, tmp_txn_id, new_tablet_id}); |
| auto tmp_rowset_val = tmp_rowset.SerializeAsString(); |
| txn->put(tmp_rowset_key, tmp_rowset_val); |
| } |
| |
| // Create initial tablet stats for both tablets |
| TabletStatsPB old_tablet_stats; |
| old_tablet_stats.set_num_rows(270); // sum of versions 2-7: 20+30+40+50+60+70 |
| old_tablet_stats.set_data_size(270 * 100); |
| old_tablet_stats.set_num_rowsets(6); |
| old_tablet_stats.set_num_segments(6); |
| old_tablet_stats.set_index_size(300); |
| old_tablet_stats.set_segment_size(600); |
| old_tablet_stats.set_cumulative_point(3); |
| |
| auto old_stats_key = |
| stats_tablet_key({instance_id, table_id, index_id, partition_id, old_tablet_id}); |
| txn->put(old_stats_key, old_tablet_stats.SerializeAsString()); |
| |
| // Create tablet load stats for old tablet (versions 2-2, 3-3, 4-4, 5-5, 6-6, 7-7: all data) |
| TabletStatsPB old_tablet_load_stats; |
| old_tablet_load_stats.set_num_rows(270); // all versions 2-7: 20+30+40+50+60+70 |
| old_tablet_load_stats.set_data_size(270 * 100); |
| old_tablet_load_stats.set_num_rowsets(6); |
| old_tablet_load_stats.set_num_segments(6); |
| old_tablet_load_stats.set_index_size(300); |
| old_tablet_load_stats.set_segment_size(600); |
| old_tablet_load_stats.set_cumulative_point(3); |
| |
| auto old_tablet_load_stats_key = |
| versioned::tablet_load_stats_key({instance_id, old_tablet_id}); |
| versioned_put(txn.get(), old_tablet_load_stats_key, |
| old_tablet_load_stats.SerializeAsString()); |
| |
| TabletStatsPB new_tablet_stats; |
| new_tablet_stats.set_num_rows(220); // versions 4-7: 40+50+60+70 |
| new_tablet_stats.set_data_size(220 * 100); |
| new_tablet_stats.set_num_rowsets(4); |
| new_tablet_stats.set_num_segments(4); |
| new_tablet_stats.set_index_size(200); |
| new_tablet_stats.set_segment_size(400); |
| new_tablet_stats.set_cumulative_point(3); |
| |
| auto new_stats_key = |
| stats_tablet_key({instance_id, table_id, index_id, partition_id, new_tablet_id}); |
| txn->put(new_stats_key, new_tablet_stats.SerializeAsString()); |
| |
| // Create initial tablet load stats for new tablet (versions 4-4, 5-5, 6-6, 7-7) |
| TabletStatsPB new_tablet_load_stats; |
| new_tablet_load_stats.set_num_rows(220); // versions 4-7: 40+50+60+70 |
| new_tablet_load_stats.set_data_size(220 * 100); |
| new_tablet_load_stats.set_num_rowsets(4); |
| new_tablet_load_stats.set_num_segments(4); |
| new_tablet_load_stats.set_index_size(200); |
| new_tablet_load_stats.set_segment_size(400); |
| new_tablet_load_stats.set_cumulative_point(6); |
| |
| auto new_tablet_load_stats_key = |
| versioned::tablet_load_stats_key({instance_id, new_tablet_id}); |
| versioned_put(txn.get(), new_tablet_load_stats_key, |
| new_tablet_load_stats.SerializeAsString()); |
| |
| ASSERT_EQ(txn->commit(), TxnErrorCode::TXN_OK); |
| } |
| |
| size_t num_logs_before = count_range(txn_kv.get(), versioned::log_key(instance_id), |
| versioned::log_key(instance_id) + "\xFF"); |
| |
| // Call finish_tablet_job to complete schema change (triggers process_schema_change_job) |
| { |
| brpc::Controller cntl; |
| FinishTabletJobRequest req; |
| FinishTabletJobResponse res; |
| |
| req.set_action(FinishTabletJobRequest::COMMIT); |
| req.mutable_job()->mutable_idx()->set_table_id(table_id); |
| req.mutable_job()->mutable_idx()->set_index_id(index_id); |
| req.mutable_job()->mutable_idx()->set_partition_id(partition_id); |
| req.mutable_job()->mutable_idx()->set_tablet_id(old_tablet_id); |
| |
| auto* schema_change = req.mutable_job()->mutable_schema_change(); |
| schema_change->set_id(job_id); |
| schema_change->set_initiator(initiator); |
| schema_change->mutable_new_tablet_idx()->set_table_id(table_id); |
| schema_change->mutable_new_tablet_idx()->set_index_id(index_id); |
| schema_change->mutable_new_tablet_idx()->set_partition_id(partition_id); |
| schema_change->mutable_new_tablet_idx()->set_tablet_id(new_tablet_id); |
| schema_change->set_alter_version(alter_version); |
| |
| // Specify tmp txn ids containing converted data from old tablet [2-2, 3-3, 4-4, 5-5] |
| for (int64_t tmp_txn_id : tmp_txn_ids) { |
| schema_change->add_txn_ids(tmp_txn_id); |
| } |
| |
| // Add output versions corresponding to tmp rowsets [2-2, 3-3, 4-4, 5-5] |
| for (int version = 2; version <= 5; ++version) { |
| schema_change->add_output_versions(version); |
| } |
| |
| // Add schema change statistics |
| // Output rowsets [2-2, 3-3, 4-4, 5-5]: 20+30+40+50 = 140 |
| schema_change->set_num_output_rows(140); |
| schema_change->set_num_output_rowsets(4); |
| schema_change->set_num_output_segments(4); |
| schema_change->set_size_output_rowsets(140 * 100); |
| schema_change->set_index_size_output_rowsets(200); |
| schema_change->set_segment_size_output_rowsets(400); |
| schema_change->set_output_cumulative_point(5); |
| |
| meta_service->finish_tablet_job(&cntl, &req, &res, nullptr); |
| ASSERT_EQ(res.status().code(), MetaServiceCode::OK); |
| } |
| |
| // Verification Step 1: Check schema change operation log was generated |
| size_t num_logs_after = count_range(txn_kv.get(), versioned::log_key(instance_id), |
| versioned::log_key(instance_id) + "\xFF"); |
| ASSERT_GT(num_logs_after, num_logs_before) |
| << "Expected new schema change operation log, but found no new logs"; |
| |
| // Verification Step 2: Validate schema change operation log content |
| { |
| std::unique_ptr<Transaction> txn; |
| ASSERT_EQ(txn_kv->create_txn(&txn), TxnErrorCode::TXN_OK); |
| std::string log_key = versioned::log_key({instance_id}); |
| std::string value; |
| Versionstamp log_version; |
| ASSERT_EQ(versioned_get(txn.get(), log_key, &log_version, &value), TxnErrorCode::TXN_OK); |
| |
| OperationLogPB operation_log; |
| ASSERT_TRUE(operation_log.ParseFromString(value)); |
| ASSERT_TRUE(operation_log.has_schema_change()); |
| |
| const auto& schema_change_log = operation_log.schema_change(); |
| ASSERT_EQ(schema_change_log.old_tablet_id(), old_tablet_id); |
| ASSERT_EQ(schema_change_log.new_tablet_id(), new_tablet_id); |
| |
| // Verify tablet load stats were updated for versions [1-3] only |
| // (versions [4-6][7-9] should already be in tablet_load_stats) |
| } |
| |
| // Verification Step 3: Check new tablet state changed to RUNNING |
| { |
| std::unique_ptr<Transaction> txn; |
| ASSERT_EQ(txn_kv->create_txn(&txn), TxnErrorCode::TXN_OK); |
| |
| auto new_tablet_key = |
| meta_tablet_key({instance_id, table_id, index_id, partition_id, new_tablet_id}); |
| std::string new_tablet_val; |
| ASSERT_EQ(txn->get(new_tablet_key, &new_tablet_val), TxnErrorCode::TXN_OK); |
| |
| doris::TabletMetaCloudPB new_tablet_meta; |
| ASSERT_TRUE(new_tablet_meta.ParseFromString(new_tablet_val)); |
| ASSERT_EQ(new_tablet_meta.tablet_state(), doris::TabletStatePB::PB_RUNNING); |
| } |
| |
| // Verification Step 4: Check rowset conversion and cleanup |
| { |
| std::unique_ptr<Transaction> txn; |
| ASSERT_EQ(txn_kv->create_txn(&txn), TxnErrorCode::TXN_OK); |
| |
| // Check that tmp rowsets were removed |
| for (int64_t tmp_txn_id : tmp_txn_ids) { |
| auto tmp_rowset_key = meta_rowset_tmp_key({instance_id, tmp_txn_id, new_tablet_id}); |
| std::string tmp_rowset_value; |
| ASSERT_EQ(txn->get(tmp_rowset_key, &tmp_rowset_value), TxnErrorCode::TXN_KEY_NOT_FOUND) |
| << "Tmp rowset should have been removed after schema change"; |
| } |
| |
| // Verify that new tablet's [4-4, 5-5] rowsets were deleted (multi-version doesn't physically delete) |
| // and [2-2, 3-3, 4-4, 5-5] rowsets from tmp conversion exist |
| for (int version = 2; version <= 5; ++version) { |
| auto rowset_key = meta_rowset_key({instance_id, new_tablet_id, version}); |
| std::string rowset_value; |
| ASSERT_EQ(txn->get(rowset_key, &rowset_value), TxnErrorCode::TXN_OK) |
| << "Converted rowset should exist at version " << version; |
| } |
| |
| // Verify [6-6, 7-7] rowsets remain unchanged |
| for (int version = 6; version <= 7; ++version) { |
| auto rowset_key = meta_rowset_key({instance_id, new_tablet_id, version}); |
| std::string rowset_value; |
| ASSERT_EQ(txn->get(rowset_key, &rowset_value), TxnErrorCode::TXN_OK) |
| << "Post-alter rowset should remain at version " << version; |
| } |
| } |
| |
| // Verification Step 5: Validate multi-version double writes for new tablet |
| { |
| std::unique_ptr<Transaction> txn; |
| ASSERT_EQ(txn_kv->create_txn(&txn), TxnErrorCode::TXN_OK); |
| |
| // Check 1: Versioned meta tablet key (multi-version double write) |
| auto versioned_tablet_key = versioned::meta_tablet_key({instance_id, new_tablet_id}); |
| std::string versioned_tablet_value; |
| Versionstamp* versionstamp = nullptr; |
| ASSERT_EQ(versioned_get(txn.get(), versioned_tablet_key, versionstamp, |
| &versioned_tablet_value), |
| TxnErrorCode::TXN_OK) |
| << "Versioned meta tablet key should exist for new tablet"; |
| |
| // Parse and verify the versioned tablet meta |
| doris::TabletMetaCloudPB versioned_tablet_meta; |
| ASSERT_TRUE(versioned_tablet_meta.ParseFromString(versioned_tablet_value)) |
| << "Versioned tablet meta should be valid protobuf"; |
| ASSERT_EQ(versioned_tablet_meta.tablet_state(), doris::TabletStatePB::PB_RUNNING) |
| << "Versioned tablet should be in RUNNING state after schema change"; |
| |
| // Check 2: Tablet load stats (only versions [1-3] should be added) |
| auto tablet_load_stats_key = versioned::tablet_load_stats_key({instance_id, new_tablet_id}); |
| std::string tablet_load_stats_value; |
| ASSERT_EQ(versioned_get(txn.get(), tablet_load_stats_key, versionstamp, |
| &tablet_load_stats_value), |
| TxnErrorCode::TXN_OK) |
| << "Tablet load stats should exist for new tablet"; |
| |
| TabletStatsPB load_stats; |
| ASSERT_TRUE(load_stats.ParseFromString(tablet_load_stats_value)) |
| << "Tablet load stats should be valid protobuf"; |
| |
| // Schema change tablet load stats logic: |
| // 1. Existing load stats (versions 4-4, 5-5, 6-6, 7-7): 220 (40+50+60+70) |
| // 2. Apply schema change: output_rows 140 - remove_rows (versions 4-4, 5-5: 40+50=90) |
| // 3. Net change: 140 - 90 = 50 (effectively adding versions 2-2, 3-3: 20+30=50) |
| // 4. Final: 220 + 50 = 270 |
| int64_t expected_load_rows = 270; |
| ASSERT_EQ(load_stats.num_rows(), expected_load_rows) |
| << "Tablet load stats should show existing data plus schema change net: " |
| << load_stats.num_rows() << " vs expected " << expected_load_rows; |
| |
| // Check 3: Meta rowset compact keys for converted rowsets [2-2, 3-3, 4-4, 5-5] |
| for (int version = 2; version <= 5; ++version) { |
| auto meta_rowset_compact_key = |
| versioned::meta_rowset_compact_key({instance_id, new_tablet_id, version}); |
| std::string compact_rowset_value; |
| ASSERT_EQ(versioned_get(txn.get(), meta_rowset_compact_key, versionstamp, |
| &compact_rowset_value), |
| TxnErrorCode::TXN_OK) |
| << "Meta rowset compact key should exist for version " << version; |
| |
| // Parse and verify the compact rowset content |
| doris::RowsetMetaCloudPB compact_meta; |
| ASSERT_TRUE(compact_meta.ParseFromString(compact_rowset_value)) |
| << "Meta rowset compact value should be valid protobuf for version " << version; |
| ASSERT_EQ(compact_meta.tablet_id(), new_tablet_id) |
| << "Compact rowset tablet_id should match new tablet"; |
| ASSERT_EQ(compact_meta.end_version(), version) |
| << "Compact rowset version should match expected version " << version; |
| } |
| |
| // Note: Post-alter rowsets [6-6, 7-7] are normal data ingestion, not compaction/schema change |
| // They don't have meta_rowset_compact_keys, only the converted rowsets [2-2, 3-3, 4-4, 5-5] do |
| } |
| |
| // Verification Step 6: Ensure old tablet remains unaffected by schema change |
| { |
| std::unique_ptr<Transaction> txn; |
| ASSERT_EQ(txn_kv->create_txn(&txn), TxnErrorCode::TXN_OK); |
| |
| // Old tablet should still have all its rowsets [2-2, 3-3, 4-4, 5-5, 6-6, 7-7] |
| for (int version = 2; version <= 7; ++version) { |
| auto rowset_key = meta_rowset_key({instance_id, old_tablet_id, version}); |
| std::string rowset_value; |
| ASSERT_EQ(txn->get(rowset_key, &rowset_value), TxnErrorCode::TXN_OK) |
| << "Old tablet should still have rowset at version " << version; |
| } |
| |
| // Old tablet stats should remain unchanged |
| auto old_stats_key = |
| stats_tablet_key({instance_id, table_id, index_id, partition_id, old_tablet_id}); |
| std::string old_stats_value; |
| ASSERT_EQ(txn->get(old_stats_key, &old_stats_value), TxnErrorCode::TXN_OK) |
| << "Old tablet stats should still exist"; |
| |
| TabletStatsPB old_stats; |
| ASSERT_TRUE(old_stats.ParseFromString(old_stats_value)); |
| ASSERT_EQ(old_stats.num_rows(), 270) // Should remain unchanged |
| << "Old tablet stats should not be modified by schema change"; |
| } |
| } |
| |
| TEST(MetaServiceOperationLogTest, SchemaChangeLogFirstTimeTabletStats) { |
| // ========================================================================= |
| // Schema Change First-Time Multi-Version Tablet Stats Test |
| // ========================================================================= |
| // This test simulates schema change with first-time multi-version tablet stats creation |
| // and specifically tests the TXN_KEY_NOT_FOUND logic in process_schema_change_job |
| // |
| // Test Scenario Difference from SchemaChangeLog: |
| // - Instance starts in single version mode (MULTI_VERSION_DISABLED) |
| // - Switch to multi-version mode after data creation but before schema change |
| // - This triggers the first-time multi-version tablet stats creation path |
| // |
| // Expected Behavior (TXN_KEY_NOT_FOUND path): |
| // - Multi-version tablet load stats not found triggers first-time creation logic |
| // - new_tablet_load_stats.CopyFrom(*stats) copies single version baseline (220 rows) |
| // - schema_change_update_tablet_stats applies schema change updates (+50 net change) |
| // - Final result: 220 + 50 = 270 rows in multi-version tablet load stats |
| // ========================================================================= |
| |
| // Step 1: Initialize test environment and create old tablet with versions [2-2, 3-3] |
| auto meta_service = get_meta_service(false); |
| std::string instance_id = "first_time_tablet_stats"; |
| auto* sp = SyncPoint::get_instance(); |
| DORIS_CLOUD_DEFER { |
| SyncPoint::get_instance()->clear_all_call_backs(); |
| }; |
| sp->set_call_back("get_instance_id", [&](auto&& args) { |
| auto* ret = try_any_cast_ret<std::string>(args); |
| ret->first = instance_id; |
| ret->second = true; |
| }); |
| sp->enable_processing(); |
| |
| constexpr int64_t table_id = 40001; |
| constexpr int64_t index_id = 40002; |
| constexpr int64_t partition_id = 40003; |
| constexpr int64_t old_tablet_id = 40004; |
| constexpr int64_t new_tablet_id = 40005; |
| |
| // Create instance with single version mode initially (key difference!) |
| { |
| InstanceInfoPB instance_info; |
| instance_info.set_instance_id(instance_id); |
| instance_info.set_multi_version_status(MULTI_VERSION_DISABLED); // Start single version |
| std::unique_ptr<Transaction> txn; |
| ASSERT_EQ(meta_service->txn_kv()->create_txn(&txn), TxnErrorCode::TXN_OK); |
| txn->put(instance_key(instance_id), instance_info.SerializeAsString()); |
| ASSERT_EQ(txn->commit(), TxnErrorCode::TXN_OK); |
| |
| meta_service->resource_mgr()->refresh_instance(instance_id); |
| ASSERT_FALSE(meta_service->resource_mgr()->is_version_write_enabled(instance_id)); |
| } |
| |
| // Create old tablet |
| { create_tablet(meta_service.get(), table_id, index_id, partition_id, old_tablet_id); } |
| |
| auto txn_kv = meta_service->txn_kv(); |
| |
| // Create initial rowsets for old tablet: versions 2, 3 |
| std::vector<doris::RowsetMetaCloudPB> initial_rowsets; |
| { |
| std::unique_ptr<Transaction> txn; |
| ASSERT_EQ(txn_kv->create_txn(&txn), TxnErrorCode::TXN_OK); |
| |
| for (int version = 2; version <= 3; ++version) { |
| auto rowset = create_rowset(100 + version, old_tablet_id, partition_id, version, |
| version * 10); |
| initial_rowsets.push_back(rowset); |
| |
| auto rowset_key = meta_rowset_key({instance_id, old_tablet_id, version}); |
| auto rowset_val = rowset.SerializeAsString(); |
| txn->put(rowset_key, rowset_val); |
| } |
| ASSERT_EQ(txn->commit(), TxnErrorCode::TXN_OK); |
| } |
| |
| // Step 2: Create new tablet in NOTREADY state (empty, waiting for schema change) |
| { |
| brpc::Controller cntl; |
| CreateTabletsRequest req; |
| CreateTabletsResponse res; |
| req.set_cloud_unique_id("test_cloud_unique_id"); |
| add_tablet(req, table_id, index_id, partition_id, new_tablet_id); |
| |
| // Set new tablet state to NOTREADY (for schema change) |
| auto tablet_meta = req.mutable_tablet_metas(0); |
| tablet_meta->set_tablet_state(doris::TabletStatePB::PB_NOTREADY); |
| |
| meta_service->create_tablets(&cntl, &req, &res, nullptr); |
| ASSERT_EQ(res.status().code(), MetaServiceCode::OK); |
| } |
| |
| // Step 3: Add alter versions [4-4, 5-5] and start schema change job (alter_version = 5) |
| const std::string job_id = "first_time_schema_change_job"; |
| const std::string initiator = "test_be"; |
| constexpr int64_t alter_version = 5; // Determined when starting the job |
| |
| // Add versions 4-5 to old tablet (alter versions) |
| std::vector<doris::RowsetMetaCloudPB> alter_rowsets_old; |
| { |
| std::unique_ptr<Transaction> txn; |
| ASSERT_EQ(txn_kv->create_txn(&txn), TxnErrorCode::TXN_OK); |
| |
| for (int version = 4; version <= 5; ++version) { |
| auto rowset = create_rowset(200 + version, old_tablet_id, partition_id, version, |
| version * 10); |
| alter_rowsets_old.push_back(rowset); |
| |
| auto rowset_key = meta_rowset_key({instance_id, old_tablet_id, version}); |
| auto rowset_val = rowset.SerializeAsString(); |
| txn->put(rowset_key, rowset_val); |
| } |
| ASSERT_EQ(txn->commit(), TxnErrorCode::TXN_OK); |
| } |
| |
| // Add versions 4-5 to new tablet (alter versions) |
| std::vector<doris::RowsetMetaCloudPB> alter_rowsets_new; |
| { |
| std::unique_ptr<Transaction> txn; |
| ASSERT_EQ(txn_kv->create_txn(&txn), TxnErrorCode::TXN_OK); |
| |
| for (int version = 4; version <= 5; ++version) { |
| auto rowset = create_rowset(300 + version, new_tablet_id, partition_id, version, |
| version * 10); |
| alter_rowsets_new.push_back(rowset); |
| |
| auto rowset_key = meta_rowset_key({instance_id, new_tablet_id, version}); |
| auto rowset_val = rowset.SerializeAsString(); |
| txn->put(rowset_key, rowset_val); |
| } |
| ASSERT_EQ(txn->commit(), TxnErrorCode::TXN_OK); |
| } |
| |
| // Start schema change job |
| { |
| brpc::Controller cntl; |
| StartTabletJobRequest req; |
| StartTabletJobResponse res; |
| req.mutable_job()->mutable_idx()->set_tablet_id(old_tablet_id); |
| auto* schema_change = req.mutable_job()->mutable_schema_change(); |
| schema_change->set_id(job_id); |
| schema_change->set_initiator(initiator); |
| schema_change->mutable_new_tablet_idx()->set_table_id(table_id); |
| schema_change->mutable_new_tablet_idx()->set_index_id(index_id); |
| schema_change->mutable_new_tablet_idx()->set_partition_id(partition_id); |
| schema_change->mutable_new_tablet_idx()->set_tablet_id(new_tablet_id); |
| schema_change->set_alter_version(alter_version); |
| |
| long now = time(nullptr); |
| schema_change->set_expiration(now + 12); |
| |
| meta_service->start_tablet_job(&cntl, &req, &res, nullptr); |
| ASSERT_EQ(res.status().code(), MetaServiceCode::OK); |
| } |
| |
| // Step 4: Simulate ongoing data ingestion - add post-alter versions [6-6, 7-7] |
| std::vector<doris::RowsetMetaCloudPB> post_alter_rowsets_old; |
| std::vector<doris::RowsetMetaCloudPB> post_alter_rowsets_new; |
| |
| // Add versions 6-7 to old tablet |
| { |
| std::unique_ptr<Transaction> txn; |
| ASSERT_EQ(txn_kv->create_txn(&txn), TxnErrorCode::TXN_OK); |
| |
| for (int version = 6; version <= 7; ++version) { |
| auto rowset = create_rowset(400 + version, old_tablet_id, partition_id, version, |
| version * 10); |
| post_alter_rowsets_old.push_back(rowset); |
| |
| auto rowset_key = meta_rowset_key({instance_id, old_tablet_id, version}); |
| auto rowset_val = rowset.SerializeAsString(); |
| txn->put(rowset_key, rowset_val); |
| } |
| ASSERT_EQ(txn->commit(), TxnErrorCode::TXN_OK); |
| } |
| |
| // Add versions 6-7 to new tablet |
| { |
| std::unique_ptr<Transaction> txn; |
| ASSERT_EQ(txn_kv->create_txn(&txn), TxnErrorCode::TXN_OK); |
| |
| for (int version = 6; version <= 7; ++version) { |
| auto rowset = create_rowset(500 + version, new_tablet_id, partition_id, version, |
| version * 10); |
| post_alter_rowsets_new.push_back(rowset); |
| |
| auto rowset_key = meta_rowset_key({instance_id, new_tablet_id, version}); |
| auto rowset_val = rowset.SerializeAsString(); |
| txn->put(rowset_key, rowset_val); |
| } |
| ASSERT_EQ(txn->commit(), TxnErrorCode::TXN_OK); |
| } |
| |
| // Switch to multi-version mode before schema change completion (key difference!) |
| { |
| InstanceInfoPB instance_info; |
| instance_info.set_instance_id(instance_id); |
| instance_info.set_multi_version_status(MULTI_VERSION_WRITE_ONLY); |
| std::unique_ptr<Transaction> txn; |
| ASSERT_EQ(meta_service->txn_kv()->create_txn(&txn), TxnErrorCode::TXN_OK); |
| txn->put(instance_key(instance_id), instance_info.SerializeAsString()); |
| ASSERT_EQ(txn->commit(), TxnErrorCode::TXN_OK); |
| |
| meta_service->resource_mgr()->refresh_instance(instance_id); |
| ASSERT_TRUE(meta_service->resource_mgr()->is_version_write_enabled(instance_id)); |
| } |
| |
| // Step 5: Complete schema change - convert old tablet data via tmp rowsets |
| // Create tmp rowsets representing old tablet's [2-2, 3-3, 4-4, 5-5] data that will be converted to new tablet |
| std::vector<int64_t> tmp_txn_ids; |
| { |
| std::unique_ptr<Transaction> txn; |
| ASSERT_EQ(txn_kv->create_txn(&txn), TxnErrorCode::TXN_OK); |
| |
| // Create tmp rowsets for versions 2-2, 3-3, 4-4, 5-5 (simulating old tablet data conversion) |
| for (int version = 2; version <= 5; ++version) { |
| int64_t tmp_txn_id = 40000 + version; |
| tmp_txn_ids.push_back(tmp_txn_id); |
| |
| // Create tmp rowset with data from old tablet |
| auto tmp_rowset = create_rowset(600 + version, new_tablet_id, partition_id, version, |
| version * 10); |
| tmp_rowset.set_txn_id(tmp_txn_id); |
| |
| auto tmp_rowset_key = meta_rowset_tmp_key({instance_id, tmp_txn_id, new_tablet_id}); |
| auto tmp_rowset_val = tmp_rowset.SerializeAsString(); |
| txn->put(tmp_rowset_key, tmp_rowset_val); |
| } |
| |
| // Create initial tablet stats for both tablets |
| TabletStatsPB old_tablet_stats; |
| old_tablet_stats.set_num_rows(270); // sum of versions 2-7: 20+30+40+50+60+70 |
| old_tablet_stats.set_data_size(270 * 100); |
| old_tablet_stats.set_num_rowsets(6); |
| old_tablet_stats.set_num_segments(6); |
| old_tablet_stats.set_index_size(300); |
| old_tablet_stats.set_segment_size(600); |
| old_tablet_stats.set_cumulative_point(3); |
| |
| auto old_stats_key = |
| stats_tablet_key({instance_id, table_id, index_id, partition_id, old_tablet_id}); |
| txn->put(old_stats_key, old_tablet_stats.SerializeAsString()); |
| |
| TabletStatsPB new_tablet_stats; |
| new_tablet_stats.set_num_rows(220); // versions 4-7: 40+50+60+70 |
| new_tablet_stats.set_data_size(220 * 100); |
| new_tablet_stats.set_num_rowsets(4); |
| new_tablet_stats.set_num_segments(4); |
| new_tablet_stats.set_index_size(200); |
| new_tablet_stats.set_segment_size(400); |
| new_tablet_stats.set_cumulative_point(3); |
| |
| auto new_stats_key = |
| stats_tablet_key({instance_id, table_id, index_id, partition_id, new_tablet_id}); |
| txn->put(new_stats_key, new_tablet_stats.SerializeAsString()); |
| |
| // NOTE: Don't create versioned tablet load stats - this will trigger TXN_KEY_NOT_FOUND path |
| |
| ASSERT_EQ(txn->commit(), TxnErrorCode::TXN_OK); |
| } |
| |
| size_t num_logs_before = count_range(txn_kv.get(), versioned::log_key(instance_id), |
| versioned::log_key(instance_id) + "\xFF"); |
| |
| // Call finish_tablet_job to complete schema change (triggers process_schema_change_job) |
| { |
| brpc::Controller cntl; |
| FinishTabletJobRequest req; |
| FinishTabletJobResponse res; |
| |
| req.set_action(FinishTabletJobRequest::COMMIT); |
| req.mutable_job()->mutable_idx()->set_table_id(table_id); |
| req.mutable_job()->mutable_idx()->set_index_id(index_id); |
| req.mutable_job()->mutable_idx()->set_partition_id(partition_id); |
| req.mutable_job()->mutable_idx()->set_tablet_id(old_tablet_id); |
| |
| auto* schema_change = req.mutable_job()->mutable_schema_change(); |
| schema_change->set_id(job_id); |
| schema_change->set_initiator(initiator); |
| schema_change->mutable_new_tablet_idx()->set_table_id(table_id); |
| schema_change->mutable_new_tablet_idx()->set_index_id(index_id); |
| schema_change->mutable_new_tablet_idx()->set_partition_id(partition_id); |
| schema_change->mutable_new_tablet_idx()->set_tablet_id(new_tablet_id); |
| schema_change->set_alter_version(alter_version); |
| |
| // Specify tmp txn ids containing converted data from old tablet [2-2, 3-3, 4-4, 5-5] |
| for (int64_t tmp_txn_id : tmp_txn_ids) { |
| schema_change->add_txn_ids(tmp_txn_id); |
| } |
| |
| // Add output versions corresponding to tmp rowsets [2-2, 3-3, 4-4, 5-5] |
| for (int version = 2; version <= 5; ++version) { |
| schema_change->add_output_versions(version); |
| } |
| |
| // Add schema change statistics |
| // Output rowsets [2-2, 3-3, 4-4, 5-5]: 20+30+40+50 = 140 |
| schema_change->set_num_output_rows(140); |
| schema_change->set_num_output_rowsets(4); |
| schema_change->set_num_output_segments(4); |
| schema_change->set_size_output_rowsets(140 * 100); |
| schema_change->set_index_size_output_rowsets(200); |
| schema_change->set_segment_size_output_rowsets(400); |
| schema_change->set_output_cumulative_point(5); |
| |
| meta_service->finish_tablet_job(&cntl, &req, &res, nullptr); |
| ASSERT_EQ(res.status().code(), MetaServiceCode::OK); |
| } |
| |
| // Verification Step 1: Check schema change operation log was generated |
| size_t num_logs_after = count_range(txn_kv.get(), versioned::log_key(instance_id), |
| versioned::log_key(instance_id) + "\xFF"); |
| ASSERT_GT(num_logs_after, num_logs_before) |
| << "Expected new schema change operation log, but found no new logs"; |
| |
| // Verification Step 2: Validate first-time multi-version tablet stats creation |
| { |
| std::unique_ptr<Transaction> txn; |
| ASSERT_EQ(txn_kv->create_txn(&txn), TxnErrorCode::TXN_OK); |
| std::string log_key = versioned::log_key({instance_id}); |
| std::string value; |
| Versionstamp log_version; |
| ASSERT_EQ(versioned_get(txn.get(), log_key, &log_version, &value), TxnErrorCode::TXN_OK); |
| |
| OperationLogPB operation_log; |
| ASSERT_TRUE(operation_log.ParseFromString(value)); |
| ASSERT_TRUE(operation_log.has_schema_change()); |
| |
| const auto& schema_change_log = operation_log.schema_change(); |
| ASSERT_EQ(schema_change_log.old_tablet_id(), old_tablet_id); |
| ASSERT_EQ(schema_change_log.new_tablet_id(), new_tablet_id); |
| |
| // Verify first-time multi-version tablet load stats were created (TXN_KEY_NOT_FOUND logic) |
| auto tablet_load_stats_key = versioned::tablet_load_stats_key({instance_id, new_tablet_id}); |
| std::string tablet_load_stats_value; |
| Versionstamp* versionstamp = nullptr; |
| ASSERT_EQ(versioned_get(txn.get(), tablet_load_stats_key, versionstamp, |
| &tablet_load_stats_value), |
| TxnErrorCode::TXN_OK) |
| << "First-time multi-version tablet load stats should exist"; |
| |
| TabletStatsPB load_stats; |
| ASSERT_TRUE(load_stats.ParseFromString(tablet_load_stats_value)); |
| |
| // First-time multi-version stats logic (TXN_KEY_NOT_FOUND path): |
| // 1. CopyFrom single version stats: 220 rows (baseline from [4-4, 5-5, 6-6, 7-7]) |
| // 2. Apply schema change update: output_rows 140 - remove_rows 90 = +50 net change |
| // 3. Final: 220 + 50 = 270 rows |
| int64_t expected_load_rows = 270; |
| ASSERT_EQ(load_stats.num_rows(), expected_load_rows) |
| << "First-time multi-version stats should copy baseline then apply schema change: " |
| << load_stats.num_rows() << " vs expected " << expected_load_rows; |
| } |
| } |
| |
| } // namespace doris::cloud |