// 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 #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "common/config.h" #include "common/defer.h" #include "common/lexical_util.h" #include "cpp/sync_point.h" #include "meta-service/meta_service.h" #include "meta-store/keys.h" #include "meta-store/txn_kv.h" #include "meta-store/txn_kv_error.h" #include "meta-store/versioned_value.h" #include "recycler/recycler.h" #include "resource-manager/resource_manager.h" #include "snapshot/snapshot_manager.h" namespace doris::cloud { extern std::unique_ptr get_meta_service(bool mock_resource_mgr); extern void add_tablet(CreateTabletsRequest& request, 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 TxnErrorCode read_operation_log(Transaction* txn, std::string_view log_key, Versionstamp* log_version, OperationLogPB* operation_log); class MetaServiceTableStreamTest : public ::testing::Test { protected: void SetUp() override { service_ = get_meta_service(false); instance_id_ = "table_stream_read_state_instance"; cloud_unique_id_ = "1:" + instance_id_ + ":test"; identity_.set_base_db_id(1001); identity_.set_base_table_id(1002); identity_.set_stream_db_id(1003); identity_.set_stream_id(1004); } void set_multi_version_status(MultiVersionStatus mode) { multi_version_status_ = mode; std::unique_ptr txn; ASSERT_EQ(service_->txn_kv()->create_txn(&txn), TxnErrorCode::TXN_OK); InstanceInfoPB instance; instance.set_instance_id(instance_id_); instance.set_multi_version_status(mode); txn->put(instance_key({instance_id_}), instance.SerializeAsString()); ASSERT_EQ(txn->commit(), TxnErrorCode::TXN_OK); auto [code, message] = service_->resource_mgr()->refresh_instance(instance_id_); ASSERT_EQ(code, MetaServiceCode::OK) << message; } void put_partition_mapping(Transaction* txn, int64_t partition_id) { PartitionIndexPB partition; partition.set_db_id(identity_.base_db_id()); partition.set_table_id(identity_.base_table_id()); txn->put(versioned::partition_index_key({instance_id_, partition_id}), partition.SerializeAsString()); } void put_latest_partition_state(Transaction* txn, int64_t partition_id, int64_t visible_version, int64_t commit_tso, std::optional offset_tso) { VersionPB version; version.set_version(visible_version); version.set_commit_tso(commit_tso); txn->put(partition_version_key({instance_id_, identity_.base_db_id(), identity_.base_table_id(), partition_id}), version.SerializeAsString()); if (offset_tso.has_value()) { TableStreamOffsetPB offset; offset.set_partition_id(partition_id); offset.set_state(TABLE_STREAM_OFFSET_CONSUMED); offset.set_offset_tso(*offset_tso); offset.set_last_consumption_time_ms(1234); txn->put(table_stream_offset_key({instance_id_, identity_.base_db_id(), identity_.base_table_id(), identity_.stream_db_id(), identity_.stream_id(), partition_id}), offset.SerializeAsString()); } } void put_versioned_partition_state(Transaction* txn, int64_t partition_id, int64_t visible_version, int64_t commit_tso, std::optional offset_tso) { put_partition_mapping(txn, partition_id); versioned_put(txn, versioned::meta_partition_key({instance_id_, partition_id}), Versionstamp(41, 0), ""); VersionPB version; version.set_version(visible_version); version.set_commit_tso(commit_tso); versioned_put(txn, versioned::partition_version_key({instance_id_, partition_id}), Versionstamp(42, 0), version.SerializeAsString()); if (offset_tso.has_value()) { TableStreamOffsetPB offset; offset.set_partition_id(partition_id); offset.set_state(TABLE_STREAM_OFFSET_INITIAL_SNAPSHOT_PENDING); offset.set_offset_tso(*offset_tso); versioned_put(txn, versioned::table_stream_offset_key( {instance_id_, identity_.base_db_id(), identity_.base_table_id(), identity_.stream_db_id(), identity_.stream_id(), partition_id}), Versionstamp(43, 0), offset.SerializeAsString()); } } GetTableStreamOffsetResponse get_read_state(MetaServiceProxy* service, const std::vector& partitions) { GetTableStreamOffsetRequest request; request.set_cloud_unique_id(cloud_unique_id_); auto* binding = request.add_bindings(); binding->mutable_identity()->CopyFrom(identity_); for (int64_t partition_id : partitions) { binding->add_partition_ids(partition_id); } GetTableStreamOffsetResponse response; brpc::Controller controller; service->get_table_stream_offset(&controller, &request, &response, nullptr); return response; } GetTableStreamOffsetResponse get_read_state(const std::vector& partitions) { return get_read_state(service_.get(), partitions); } GetTableStreamOffsetResponse get_read_state(std::initializer_list partitions) { return get_read_state(std::vector(partitions)); } int64_t begin_target_transaction(const std::string& label) { return begin_transaction(cloud_unique_id_, label); } int64_t begin_transaction(const std::string& cloud_unique_id, const std::string& label) { BeginTxnRequest request; request.set_cloud_unique_id(cloud_unique_id); TxnInfoPB* txn_info = request.mutable_txn_info(); txn_info->set_db_id(3001); txn_info->set_label(label); txn_info->add_table_ids(3002); txn_info->set_timeout_ms(36000); BeginTxnResponse response; brpc::Controller controller; service_->begin_txn(&controller, &request, &response, nullptr); EXPECT_EQ(response.status().code(), MetaServiceCode::OK) << response.status().msg(); return response.txn_id(); } void register_instance(const std::string& instance_id, MultiVersionStatus mode) { std::unique_ptr txn; ASSERT_EQ(service_->txn_kv()->create_txn(&txn), TxnErrorCode::TXN_OK); InstanceInfoPB instance; instance.set_instance_id(instance_id); instance.set_multi_version_status(mode); txn->put(instance_key({instance_id}), instance.SerializeAsString()); ASSERT_EQ(txn->commit(), TxnErrorCode::TXN_OK); auto [code, message] = service_->resource_mgr()->refresh_instance(instance_id); ASSERT_EQ(code, MetaServiceCode::OK) << message; } void mark_transaction_visible(const std::string& instance_id, int64_t txn_id, Transaction* txn) { std::string txn_info_value; ASSERT_EQ(txn->get(txn_info_key({instance_id, 3001, txn_id}), &txn_info_value), TxnErrorCode::TXN_OK); TxnInfoPB txn_info; ASSERT_TRUE(txn_info.ParseFromString(txn_info_value)); txn_info.set_status(TXN_STATUS_VISIBLE); txn->put(txn_info_key({instance_id, 3001, txn_id}), txn_info.SerializeAsString()); } CommitTxnRequest make_consume_request(int64_t txn_id, int64_t partition_id, TableStreamOffsetStatePB expected_state, int64_t expected_tso, int64_t next_tso) { CommitTxnRequest request; request.set_cloud_unique_id(cloud_unique_id_); request.set_db_id(3001); request.set_txn_id(txn_id); TableStreamUpdatePB* stream_update = request.add_table_stream_updates(); stream_update->mutable_identity()->CopyFrom(identity_); TableStreamPartitionUpdatePB* partition_update = stream_update->add_partition_updates(); partition_update->set_partition_id(partition_id); partition_update->set_expected_state(expected_state); if (expected_state != TABLE_STREAM_OFFSET_UNKNOWN) { partition_update->set_expected_offset_tso(expected_tso); } partition_update->set_next_offset_tso(next_tso); return request; } CommitTxnResponse commit_transaction(const CommitTxnRequest& request) { CommitTxnResponse response; brpc::Controller controller; service_->commit_txn(&controller, &request, &response, nullptr); return response; } std::pair commit_concurrently( const CommitTxnRequest& first_request, const CommitTxnRequest& second_request) { std::mutex mutex; std::condition_variable condition; int commits_ready = 0; bool release_commits = false; auto* sync_point = SyncPoint::get_instance(); DORIS_CLOUD_DEFER { sync_point->clear_all_call_backs(); sync_point->clear_trace(); sync_point->disable_processing(); }; sync_point->set_call_back("commit_txn_immediately::before_commit", [&](auto&&) { std::unique_lock lock(mutex); ++commits_ready; condition.notify_all(); condition.wait(lock, [&] { return release_commits; }); }); sync_point->enable_processing(); CommitTxnResponse first_response; CommitTxnResponse second_response; std::thread first_thread([&] { first_response = commit_transaction(first_request); }); std::thread second_thread([&] { second_response = commit_transaction(second_request); }); bool both_commits_ready = false; { std::unique_lock lock(mutex); both_commits_ready = condition.wait_for(lock, std::chrono::seconds(10), [&] { return commits_ready == 2; }); release_commits = true; condition.notify_all(); } first_thread.join(); second_thread.join(); EXPECT_TRUE(both_commits_ready); EXPECT_EQ(commits_ready, 2); return {std::move(first_response), std::move(second_response)}; } CommitTxnResponse consume_partition(int64_t txn_id, int64_t partition_id, TableStreamOffsetStatePB expected_state, int64_t expected_tso, int64_t next_tso) { return commit_transaction( make_consume_request(txn_id, partition_id, expected_state, expected_tso, next_tso)); } void create_target_tablet(int64_t db_id, int64_t table_id, int64_t index_id, int64_t partition_id, int64_t tablet_id) { CreateTabletsRequest request; request.set_cloud_unique_id(cloud_unique_id_); request.set_db_id(db_id); add_tablet(request, table_id, index_id, partition_id, tablet_id); CreateTabletsResponse response; brpc::Controller controller; service_->create_tablets(&controller, &request, &response, nullptr); ASSERT_EQ(response.status().code(), MetaServiceCode::OK) << response.status().msg(); std::unique_ptr txn; ASSERT_EQ(service_->txn_kv()->create_txn(&txn), TxnErrorCode::TXN_OK); VersionPB version; version.set_version(1); txn->put(partition_version_key({instance_id_, db_id, table_id, partition_id}), version.SerializeAsString()); ASSERT_EQ(txn->commit(), TxnErrorCode::TXN_OK); } void stage_target_rowset(int64_t txn_id, int64_t partition_id, int64_t tablet_id) { const doris::RowsetMetaCloudPB rowset = create_rowset(txn_id, tablet_id, partition_id, -1, 100); auto call = [&](bool prepare) { CreateRowsetRequest request; request.set_cloud_unique_id(cloud_unique_id_); request.mutable_rowset_meta()->CopyFrom(rowset); CreateRowsetResponse response; brpc::Controller controller; if (prepare) { service_->prepare_rowset(&controller, &request, &response, nullptr); } else { service_->commit_rowset(&controller, &request, &response, nullptr); } ASSERT_EQ(response.status().code(), MetaServiceCode::OK) << response.status().msg(); }; ASSERT_NO_FATAL_FAILURE(call(true)); ASSERT_NO_FATAL_FAILURE(call(false)); } TableStreamOffsetPB get_latest_offset(const TableStreamIdentityPB& identity, int64_t partition_id) { std::unique_ptr txn; EXPECT_EQ(service_->txn_kv()->create_txn(&txn), TxnErrorCode::TXN_OK); std::string value; EXPECT_EQ(txn->get(table_stream_offset_key( {instance_id_, identity.base_db_id(), identity.base_table_id(), identity.stream_db_id(), identity.stream_id(), partition_id}), &value), TxnErrorCode::TXN_OK); TableStreamOffsetPB offset; EXPECT_TRUE(offset.ParseFromString(value)); return offset; } TableStreamOffsetPB get_latest_offset(int64_t partition_id) { return get_latest_offset(identity_, partition_id); } size_t count_keys(std::string_view begin, std::string_view end) { std::unique_ptr txn; EXPECT_EQ(service_->txn_kv()->create_txn(&txn), TxnErrorCode::TXN_OK); std::unique_ptr iter; EXPECT_EQ(txn->get(begin, end, &iter, true, 0), TxnErrorCode::TXN_OK); return iter->size(); } std::vector> read_keys(std::string_view begin, std::string_view end) { std::unique_ptr txn; EXPECT_EQ(service_->txn_kv()->create_txn(&txn), TxnErrorCode::TXN_OK); FullRangeGetOptions options; options.txn = txn.get(); auto iter = service_->txn_kv()->full_range_get(std::string(begin), std::string(end), std::move(options)); std::vector> entries; for (auto kv = iter->next(); kv.has_value(); kv = iter->next()) { entries.emplace_back(kv->first, kv->second); } EXPECT_TRUE(iter->is_valid()); return entries; } std::unique_ptr restart_meta_service() { const std::shared_ptr txn_kv = service_->txn_kv(); auto resource_manager = std::make_shared(txn_kv); auto [code, message] = resource_manager->refresh_instance(instance_id_); EXPECT_EQ(code, MetaServiceCode::OK) << message; auto rate_limiter = std::make_shared(); auto snapshot_manager = std::make_shared(txn_kv); auto restarted_impl = std::make_unique(txn_kv, resource_manager, rate_limiter, snapshot_manager); return std::make_unique(std::move(restarted_impl)); } void set_clone_source(const std::string& source_instance_id, Versionstamp snapshot_version) { multi_version_status_ = MULTI_VERSION_READ_WRITE; std::unique_ptr txn; ASSERT_EQ(service_->txn_kv()->create_txn(&txn), TxnErrorCode::TXN_OK); InstanceInfoPB instance; instance.set_instance_id(instance_id_); instance.set_multi_version_status(MULTI_VERSION_READ_WRITE); instance.set_source_instance_id(source_instance_id); instance.set_source_snapshot_id(SnapshotManager::serialize_snapshot_id(snapshot_version)); txn->put(instance_key({instance_id_}), instance.SerializeAsString()); ASSERT_EQ(txn->commit(), TxnErrorCode::TXN_OK); auto [code, message] = service_->resource_mgr()->refresh_instance(instance_id_); ASSERT_EQ(code, MetaServiceCode::OK) << message; } Versionstamp put_auto_versioned_offset(const std::string& target_instance_id, const TableStreamIdentityPB& identity, int64_t partition_id, int64_t offset_tso, bool write_latest = false) { std::unique_ptr txn; EXPECT_EQ(service_->txn_kv()->create_txn(&txn), TxnErrorCode::TXN_OK); TableStreamOffsetPB offset; offset.set_partition_id(partition_id); offset.set_state(TABLE_STREAM_OFFSET_CONSUMED); offset.set_offset_tso(offset_tso); const TableStreamOffsetKeyInfo key_info {target_instance_id, identity.base_db_id(), identity.base_table_id(), identity.stream_db_id(), identity.stream_id(), partition_id}; const std::string value = offset.SerializeAsString(); if (write_latest) { txn->put(table_stream_offset_key(key_info), value); } txn->enable_get_versionstamp(); versioned_put( txn.get(), versioned::table_stream_offset_key(versioned::TableStreamOffsetKeyInfo(key_info)), value); EXPECT_EQ(txn->commit(), TxnErrorCode::TXN_OK); Versionstamp version; EXPECT_EQ(txn->get_versionstamp(&version), TxnErrorCode::TXN_OK); return version; } std::unique_ptr service_; std::string instance_id_; std::string cloud_unique_id_; TableStreamIdentityPB identity_; MultiVersionStatus multi_version_status_ = MULTI_VERSION_DISABLED; }; TEST_F(MetaServiceTableStreamTest, ReadLatestAndUnknownOffsets) { set_multi_version_status(MULTI_VERSION_DISABLED); std::unique_ptr txn; ASSERT_EQ(service_->txn_kv()->create_txn(&txn), TxnErrorCode::TXN_OK); put_latest_partition_state(txn.get(), 2001, 8, 130, 100); put_latest_partition_state(txn.get(), 2002, 9, 140, std::nullopt); ASSERT_EQ(txn->commit(), TxnErrorCode::TXN_OK); GetTableStreamOffsetResponse response = get_read_state({2001, 2002}); ASSERT_EQ(response.status().code(), MetaServiceCode::OK) << response.status().msg(); ASSERT_EQ(response.bindings_size(), 1); ASSERT_EQ(response.bindings(0).partition_states_size(), 2); const auto& consumed = response.bindings(0).partition_states(0); EXPECT_EQ(consumed.partition_id(), 2001); EXPECT_EQ(consumed.offset_state(), TABLE_STREAM_OFFSET_CONSUMED); EXPECT_EQ(consumed.offset_tso(), 100); EXPECT_EQ(consumed.end_tso(), 130); EXPECT_EQ(consumed.visible_version(), 8); EXPECT_EQ(consumed.last_consumption_time_ms(), 1234); const auto& unknown = response.bindings(0).partition_states(1); EXPECT_EQ(unknown.partition_id(), 2002); EXPECT_EQ(unknown.offset_state(), TABLE_STREAM_OFFSET_UNKNOWN); EXPECT_FALSE(unknown.has_offset_tso()); EXPECT_EQ(unknown.end_tso(), 140); EXPECT_EQ(unknown.visible_version(), 9); } TEST_F(MetaServiceTableStreamTest, ReadVersionAndOffsetFromOneSnapshot) { set_multi_version_status(MULTI_VERSION_DISABLED); constexpr int64_t kPartitionId = 2001; std::unique_ptr txn; ASSERT_EQ(service_->txn_kv()->create_txn(&txn), TxnErrorCode::TXN_OK); put_latest_partition_state(txn.get(), kPartitionId, 8, 130, 100); ASSERT_EQ(txn->commit(), TxnErrorCode::TXN_OK); bool updated = false; auto* sync_point = SyncPoint::get_instance(); DORIS_CLOUD_DEFER { sync_point->disable_processing(); sync_point->clear_all_call_backs(); }; sync_point->set_call_back("get_table_stream_offset::after_read_versions", [&](auto&&) { std::unique_ptr update_txn; ASSERT_EQ(service_->txn_kv()->create_txn(&update_txn), TxnErrorCode::TXN_OK); put_latest_partition_state(update_txn.get(), kPartitionId, 9, 140, 120); ASSERT_EQ(update_txn->commit(), TxnErrorCode::TXN_OK); updated = true; }); sync_point->enable_processing(); const GetTableStreamOffsetResponse response = get_read_state({kPartitionId}); ASSERT_EQ(response.status().code(), MetaServiceCode::OK) << response.status().msg(); ASSERT_TRUE(updated); ASSERT_EQ(response.bindings_size(), 1); ASSERT_EQ(response.bindings(0).partition_states_size(), 1); const TableStreamPartitionReadStatePB& state = response.bindings(0).partition_states(0); EXPECT_EQ(state.visible_version(), 8); EXPECT_EQ(state.end_tso(), 130); EXPECT_EQ(state.offset_tso(), 100); sync_point->disable_processing(); sync_point->clear_all_call_backs(); const GetTableStreamOffsetResponse next_response = get_read_state({kPartitionId}); ASSERT_EQ(next_response.status().code(), MetaServiceCode::OK) << next_response.status().msg(); const TableStreamPartitionReadStatePB& next_state = next_response.bindings(0).partition_states(0); EXPECT_EQ(next_state.visible_version(), 9); EXPECT_EQ(next_state.end_tso(), 140); EXPECT_EQ(next_state.offset_tso(), 120); } TEST_F(MetaServiceTableStreamTest, ReadStateSurvivesMetaServiceRestartAndDoesNotWriteKv) { set_multi_version_status(MULTI_VERSION_DISABLED); std::unique_ptr txn; ASSERT_EQ(service_->txn_kv()->create_txn(&txn), TxnErrorCode::TXN_OK); put_latest_partition_state(txn.get(), 2001, 8, 130, 100); put_latest_partition_state(txn.get(), 2002, 9, 140, std::nullopt); ASSERT_EQ(txn->commit(), TxnErrorCode::TXN_OK); const std::string all_keys_begin(1, '\x00'); const std::string all_keys_end(1, '\xff'); const auto keys_before = read_keys(all_keys_begin, all_keys_end); const GetTableStreamOffsetResponse first_response = get_read_state({2001, 2002}); ASSERT_EQ(first_response.status().code(), MetaServiceCode::OK) << first_response.status().msg(); EXPECT_EQ(read_keys(all_keys_begin, all_keys_end), keys_before); const std::shared_ptr txn_kv = service_->txn_kv(); auto resource_manager = std::make_shared(txn_kv); auto [code, message] = resource_manager->refresh_instance(instance_id_); ASSERT_EQ(code, MetaServiceCode::OK) << message; auto rate_limiter = std::make_shared(); auto snapshot_manager = std::make_shared(txn_kv); auto restarted_impl = std::make_unique(txn_kv, resource_manager, rate_limiter, snapshot_manager); auto restarted_service = std::make_unique(std::move(restarted_impl)); const GetTableStreamOffsetResponse restarted_response = get_read_state(restarted_service.get(), {2001, 2002}); ASSERT_EQ(restarted_response.status().code(), MetaServiceCode::OK) << restarted_response.status().msg(); EXPECT_EQ(restarted_response.SerializeAsString(), first_response.SerializeAsString()); EXPECT_EQ(read_keys(all_keys_begin, all_keys_end), keys_before); } TEST_F(MetaServiceTableStreamTest, CreateResumesAcrossMetaServiceRestarts) { set_multi_version_status(MULTI_VERSION_DISABLED); constexpr int64_t kFirstPartitionId = 2001; constexpr int64_t kSecondPartitionId = 2002; std::unique_ptr txn; ASSERT_EQ(service_->txn_kv()->create_txn(&txn), TxnErrorCode::TXN_OK); put_latest_partition_state(txn.get(), kFirstPartitionId, 8, 130, std::nullopt); put_latest_partition_state(txn.get(), kSecondPartitionId, 9, 140, std::nullopt); ASSERT_EQ(txn->commit(), TxnErrorCode::TXN_OK); IndexRequest index_request; index_request.set_cloud_unique_id(cloud_unique_id_); index_request.set_db_id(identity_.base_db_id()); index_request.set_table_id(identity_.base_table_id()); index_request.add_index_ids(identity_.stream_id()); index_request.set_object_type(IndexObjectTypePB::TABLE_STREAM); index_request.set_stream_db_id(identity_.stream_db_id()); index_request.set_expiration(::time(nullptr) + 3600); IndexResponse index_response; brpc::Controller prepare_controller; service_->prepare_index(&prepare_controller, &index_request, &index_response, nullptr); ASSERT_EQ(index_response.status().code(), MetaServiceCode::OK) << index_response.status().msg(); auto make_partition_request = [&](int64_t partition_id, int64_t offset_tso) { PartitionRequest request; request.set_cloud_unique_id(cloud_unique_id_); request.set_db_id(identity_.base_db_id()); request.set_table_id(identity_.base_table_id()); request.add_index_ids(identity_.stream_id()); request.set_object_type(IndexObjectTypePB::TABLE_STREAM); request.set_stream_db_id(identity_.stream_db_id()); request.add_partition_ids(partition_id); TableStreamOffsetPB* offset = request.add_table_stream_offsets(); offset->set_partition_id(partition_id); offset->set_state(TABLE_STREAM_OFFSET_INITIAL_SNAPSHOT_PENDING); offset->set_offset_tso(offset_tso); return request; }; const PartitionRequest first_partition_request = make_partition_request(kFirstPartitionId, 100); const PartitionRequest second_partition_request = make_partition_request(kSecondPartitionId, 110); auto restarted_after_prepare = restart_meta_service(); PartitionResponse partition_response; brpc::Controller first_partition_controller; restarted_after_prepare->commit_partition(&first_partition_controller, &first_partition_request, &partition_response, nullptr); ASSERT_EQ(partition_response.status().code(), MetaServiceCode::OK) << partition_response.status().msg(); auto restarted_after_partial_commit = restart_meta_service(); partition_response.Clear(); brpc::Controller retry_partition_controller; restarted_after_partial_commit->commit_partition( &retry_partition_controller, &first_partition_request, &partition_response, nullptr); ASSERT_EQ(partition_response.status().code(), MetaServiceCode::OK) << partition_response.status().msg(); partition_response.Clear(); brpc::Controller second_partition_controller; restarted_after_partial_commit->commit_partition( &second_partition_controller, &second_partition_request, &partition_response, nullptr); ASSERT_EQ(partition_response.status().code(), MetaServiceCode::OK) << partition_response.status().msg(); index_response.Clear(); brpc::Controller commit_index_controller; restarted_after_partial_commit->commit_index(&commit_index_controller, &index_request, &index_response, nullptr); ASSERT_EQ(index_response.status().code(), MetaServiceCode::OK) << index_response.status().msg(); EXPECT_EQ(get_latest_offset(kFirstPartitionId).offset_tso(), 100); EXPECT_EQ(get_latest_offset(kSecondPartitionId).offset_tso(), 110); ASSERT_EQ(service_->txn_kv()->create_txn(&txn), TxnErrorCode::TXN_OK); std::string value; EXPECT_EQ(txn->get(recycle_index_key({instance_id_, identity_.stream_id()}), &value, true), TxnErrorCode::TXN_KEY_NOT_FOUND); Versionstamp offset_version; EXPECT_EQ(versioned_get( txn.get(), versioned::table_stream_offset_key( {instance_id_, identity_.base_db_id(), identity_.base_table_id(), identity_.stream_db_id(), identity_.stream_id(), kFirstPartitionId}), &offset_version, &value), TxnErrorCode::TXN_KEY_NOT_FOUND); } TEST_F(MetaServiceTableStreamTest, ReadVersionedState) { set_multi_version_status(MULTI_VERSION_READ_WRITE); std::unique_ptr txn; ASSERT_EQ(service_->txn_kv()->create_txn(&txn), TxnErrorCode::TXN_OK); put_versioned_partition_state(txn.get(), 2001, 18, 230, 200); ASSERT_EQ(txn->commit(), TxnErrorCode::TXN_OK); GetTableStreamOffsetResponse response = get_read_state({2001}); ASSERT_EQ(response.status().code(), MetaServiceCode::OK) << response.status().msg(); ASSERT_EQ(response.bindings_size(), 1); ASSERT_EQ(response.bindings(0).partition_states_size(), 1); const auto& state = response.bindings(0).partition_states(0); EXPECT_EQ(state.offset_state(), TABLE_STREAM_OFFSET_INITIAL_SNAPSHOT_PENDING); EXPECT_EQ(state.offset_tso(), 200); EXPECT_EQ(state.end_tso(), 230); EXPECT_EQ(state.visible_version(), 18); } TEST_F(MetaServiceTableStreamTest, ReadVersionedStateFromCloneChainInBatchSurvivesRestartAndDoesNotWriteKv) { const std::string source_instance_id = "table_stream_batch_read_source"; set_clone_source(source_instance_id, Versionstamp(100, 0)); const std::vector partition_ids = {2001, 2002, 2003}; std::unique_ptr txn; ASSERT_EQ(service_->txn_kv()->create_txn(&txn), TxnErrorCode::TXN_OK); for (size_t i = 0; i < partition_ids.size(); ++i) { int64_t partition_id = partition_ids[i]; PartitionIndexPB partition; partition.set_db_id(identity_.base_db_id()); partition.set_table_id(identity_.base_table_id()); txn->put(versioned::partition_index_key({source_instance_id, partition_id}), partition.SerializeAsString()); versioned_put(txn.get(), versioned::meta_partition_key({source_instance_id, partition_id}), Versionstamp(51 + i, 0), ""); VersionPB version; version.set_version(10 + i); version.set_commit_tso(1000 + i); versioned_put(txn.get(), versioned::partition_version_key({source_instance_id, partition_id}), Versionstamp(60 + i, 0), version.SerializeAsString()); } auto put_offset = [&](const std::string& target_instance_id, int64_t partition_id, Versionstamp versionstamp, int64_t offset_tso) { TableStreamOffsetPB offset; offset.set_partition_id(partition_id); offset.set_state(TABLE_STREAM_OFFSET_CONSUMED); offset.set_offset_tso(offset_tso); versioned_put( txn.get(), versioned::table_stream_offset_key( {target_instance_id, identity_.base_db_id(), identity_.base_table_id(), identity_.stream_db_id(), identity_.stream_id(), partition_id}), versionstamp, offset.SerializeAsString()); }; put_offset(source_instance_id, 2001, Versionstamp(70, 0), 701); put_offset(source_instance_id, 2002, Versionstamp(71, 0), 702); put_offset(instance_id_, 2002, Versionstamp(200, 0), 802); ASSERT_EQ(txn->commit(), TxnErrorCode::TXN_OK); const std::string all_keys_begin(1, '\x00'); const std::string all_keys_end(1, '\xff'); const auto keys_before = read_keys(all_keys_begin, all_keys_end); const GetTableStreamOffsetResponse response = get_read_state(partition_ids); ASSERT_EQ(response.status().code(), MetaServiceCode::OK) << response.status().msg(); ASSERT_EQ(response.bindings_size(), 1); ASSERT_EQ(response.bindings(0).partition_states_size(), partition_ids.size()); EXPECT_EQ(response.bindings(0).partition_states(0).offset_tso(), 701); EXPECT_EQ(response.bindings(0).partition_states(1).offset_tso(), 802); EXPECT_EQ(response.bindings(0).partition_states(2).offset_state(), TABLE_STREAM_OFFSET_UNKNOWN); EXPECT_EQ(read_keys(all_keys_begin, all_keys_end), keys_before); auto restarted_service = restart_meta_service(); const GetTableStreamOffsetResponse restarted_response = get_read_state(restarted_service.get(), partition_ids); ASSERT_EQ(restarted_response.status().code(), MetaServiceCode::OK) << restarted_response.status().msg(); EXPECT_EQ(restarted_response.SerializeAsString(), response.SerializeAsString()); EXPECT_EQ(read_keys(all_keys_begin, all_keys_end), keys_before); } TEST_F(MetaServiceTableStreamTest, ReadLargePartitionBatch) { set_multi_version_status(MULTI_VERSION_DISABLED); constexpr int kPartitionCount = 1201; std::vector partition_ids; partition_ids.reserve(kPartitionCount); std::unique_ptr txn; ASSERT_EQ(service_->txn_kv()->create_txn(&txn), TxnErrorCode::TXN_OK); for (int i = 0; i < kPartitionCount; ++i) { int64_t partition_id = 10000 + i; partition_ids.push_back(partition_id); put_latest_partition_state(txn.get(), partition_id, i + 1, 100000 + i, 90000 + i); } ASSERT_EQ(txn->commit(), TxnErrorCode::TXN_OK); GetTableStreamOffsetResponse response = get_read_state(partition_ids); ASSERT_EQ(response.status().code(), MetaServiceCode::OK) << response.status().msg(); ASSERT_EQ(response.bindings(0).partition_states_size(), kPartitionCount); EXPECT_EQ(response.bindings(0).partition_states(0).partition_id(), partition_ids.front()); EXPECT_EQ(response.bindings(0).partition_states(0).offset_tso(), 90000); EXPECT_EQ(response.bindings(0).partition_states(kPartitionCount - 1).partition_id(), partition_ids.back()); EXPECT_EQ(response.bindings(0).partition_states(kPartitionCount - 1).offset_tso(), 90000 + kPartitionCount - 1); } TEST_F(MetaServiceTableStreamTest, ReadMultipleBindingsInBatch) { set_multi_version_status(MULTI_VERSION_READ_WRITE); constexpr int kBindingCount = 101; constexpr int64_t kPartitionId = 2001; std::unique_ptr txn; ASSERT_EQ(service_->txn_kv()->create_txn(&txn), TxnErrorCode::TXN_OK); PartitionIndexPB partition; partition.set_db_id(identity_.base_db_id()); partition.set_table_id(identity_.base_table_id()); txn->put(versioned::partition_index_key({instance_id_, kPartitionId}), partition.SerializeAsString()); versioned_put(txn.get(), versioned::meta_partition_key({instance_id_, kPartitionId}), Versionstamp(20, 0), ""); VersionPB version; version.set_version(8); version.set_commit_tso(130); versioned_put(txn.get(), versioned::partition_version_key({instance_id_, kPartitionId}), Versionstamp(21, 0), version.SerializeAsString()); GetTableStreamOffsetRequest request; request.set_cloud_unique_id(cloud_unique_id_); for (int i = 0; i < kBindingCount; ++i) { int64_t stream_id = identity_.stream_id() + i; TableStreamPartitionSetPB* binding = request.add_bindings(); binding->mutable_identity()->CopyFrom(identity_); binding->mutable_identity()->set_stream_id(stream_id); binding->add_partition_ids(kPartitionId); } ASSERT_EQ(txn->commit(), TxnErrorCode::TXN_OK); GetTableStreamOffsetResponse response; brpc::Controller controller; service_->get_table_stream_offset(&controller, &request, &response, nullptr); ASSERT_EQ(response.status().code(), MetaServiceCode::OK) << response.status().msg(); ASSERT_EQ(response.bindings_size(), kBindingCount); for (const TableStreamReadBindingResultPB& binding : response.bindings()) { ASSERT_EQ(binding.partition_states_size(), 1); EXPECT_EQ(binding.partition_states(0).offset_state(), TABLE_STREAM_OFFSET_UNKNOWN); } } TEST_F(MetaServiceTableStreamTest, ReadWriteOnlyState) { set_multi_version_status(MULTI_VERSION_WRITE_ONLY); std::unique_ptr txn; ASSERT_EQ(service_->txn_kv()->create_txn(&txn), TxnErrorCode::TXN_OK); put_latest_partition_state(txn.get(), 2001, 28, 330, 300); ASSERT_EQ(txn->commit(), TxnErrorCode::TXN_OK); GetTableStreamOffsetResponse response = get_read_state({2001}); ASSERT_EQ(response.status().code(), MetaServiceCode::OK) << response.status().msg(); ASSERT_EQ(response.bindings_size(), 1); ASSERT_EQ(response.bindings(0).partition_states_size(), 1); const auto& state = response.bindings(0).partition_states(0); EXPECT_EQ(state.offset_state(), TABLE_STREAM_OFFSET_CONSUMED); EXPECT_EQ(state.offset_tso(), 300); EXPECT_EQ(state.end_tso(), 330); EXPECT_EQ(state.visible_version(), 28); } TEST_F(MetaServiceTableStreamTest, WaitsForPendingSourceTxnAndRereadsSnapshot) { set_multi_version_status(MULTI_VERSION_DISABLED); const int64_t pending_txn_id = begin_target_transaction("pending-source-version"); std::unique_ptr txn; ASSERT_EQ(service_->txn_kv()->create_txn(&txn), TxnErrorCode::TXN_OK); std::string txn_info_value; ASSERT_EQ(txn->get(txn_info_key({instance_id_, 3001, pending_txn_id}), &txn_info_value), TxnErrorCode::TXN_OK); TxnInfoPB txn_info; ASSERT_TRUE(txn_info.ParseFromString(txn_info_value)); txn_info.set_status(TXN_STATUS_VISIBLE); txn->put(txn_info_key({instance_id_, 3001, pending_txn_id}), txn_info.SerializeAsString()); VersionPB pending_version; pending_version.set_commit_tso(130); pending_version.add_pending_txn_ids(pending_txn_id); txn->put(partition_version_key( {instance_id_, identity_.base_db_id(), identity_.base_table_id(), 2001}), pending_version.SerializeAsString()); TableStreamOffsetPB offset; offset.set_partition_id(2001); offset.set_state(TABLE_STREAM_OFFSET_CONSUMED); offset.set_offset_tso(100); txn->put(table_stream_offset_key({instance_id_, identity_.base_db_id(), identity_.base_table_id(), identity_.stream_db_id(), identity_.stream_id(), 2001}), offset.SerializeAsString()); ASSERT_EQ(txn->commit(), TxnErrorCode::TXN_OK); bool after_wait_called = false; auto* sync_point = SyncPoint::get_instance(); DORIS_CLOUD_DEFER { sync_point->disable_processing(); sync_point->clear_all_call_backs(); }; sync_point->set_call_back("get_table_stream_offset::after_wait_for_pending_txns", [&](auto&&) { std::unique_ptr update_txn; ASSERT_EQ(service_->txn_kv()->create_txn(&update_txn), TxnErrorCode::TXN_OK); put_latest_partition_state(update_txn.get(), 2001, 2, 130, 120); ASSERT_EQ(update_txn->commit(), TxnErrorCode::TXN_OK); after_wait_called = true; }); sync_point->enable_processing(); GetTableStreamOffsetResponse response = get_read_state({2001}); ASSERT_EQ(response.status().code(), MetaServiceCode::OK) << response.status().msg(); ASSERT_TRUE(after_wait_called); ASSERT_EQ(response.bindings_size(), 1); ASSERT_EQ(response.bindings(0).partition_states_size(), 1); const TableStreamPartitionReadStatePB& state = response.bindings(0).partition_states(0); EXPECT_EQ(state.visible_version(), 2); EXPECT_EQ(state.end_tso(), 130); EXPECT_EQ(state.offset_tso(), 120); } TEST_F(MetaServiceTableStreamTest, WaitsForPendingVersionFromCloneSourceInstance) { const std::string source_instance_id = "table_stream_pending_version_source"; register_instance(source_instance_id, MULTI_VERSION_READ_WRITE); set_clone_source(source_instance_id, Versionstamp(100, 0)); const int64_t pending_txn_id = begin_transaction(fmt::format("1:{}:test", source_instance_id), "pending-clone-source-version"); std::unique_ptr txn; ASSERT_EQ(service_->txn_kv()->create_txn(&txn), TxnErrorCode::TXN_OK); mark_transaction_visible(source_instance_id, pending_txn_id, txn.get()); PartitionIndexPB partition; partition.set_db_id(identity_.base_db_id()); partition.set_table_id(identity_.base_table_id()); txn->put(versioned::partition_index_key({source_instance_id, 2001}), partition.SerializeAsString()); versioned_put(txn.get(), versioned::meta_partition_key({source_instance_id, 2001}), Versionstamp(50, 0), ""); VersionPB pending_version; pending_version.set_commit_tso(130); pending_version.add_pending_txn_ids(pending_txn_id); versioned_put(txn.get(), versioned::partition_version_key({source_instance_id, 2001}), Versionstamp(60, 0), pending_version.SerializeAsString()); TableStreamOffsetPB offset; offset.set_partition_id(2001); offset.set_state(TABLE_STREAM_OFFSET_CONSUMED); offset.set_offset_tso(100); versioned_put(txn.get(), versioned::table_stream_offset_key( {source_instance_id, identity_.base_db_id(), identity_.base_table_id(), identity_.stream_db_id(), identity_.stream_id(), 2001}), Versionstamp(70, 0), offset.SerializeAsString()); ASSERT_EQ(txn->commit(), TxnErrorCode::TXN_OK); bool after_wait_called = false; auto* sync_point = SyncPoint::get_instance(); DORIS_CLOUD_DEFER { sync_point->disable_processing(); sync_point->clear_all_call_backs(); }; sync_point->set_call_back("get_table_stream_offset::after_wait_for_pending_txns", [&](auto&&) { std::unique_ptr update_txn; ASSERT_EQ(service_->txn_kv()->create_txn(&update_txn), TxnErrorCode::TXN_OK); VersionPB committed_version; committed_version.set_version(2); committed_version.set_commit_tso(130); versioned_put(update_txn.get(), versioned::partition_version_key({source_instance_id, 2001}), Versionstamp(60, 0), committed_version.SerializeAsString()); offset.set_offset_tso(120); versioned_put( update_txn.get(), versioned::table_stream_offset_key( {source_instance_id, identity_.base_db_id(), identity_.base_table_id(), identity_.stream_db_id(), identity_.stream_id(), 2001}), Versionstamp(70, 0), offset.SerializeAsString()); ASSERT_EQ(update_txn->commit(), TxnErrorCode::TXN_OK); after_wait_called = true; }); sync_point->enable_processing(); GetTableStreamOffsetResponse response = get_read_state({2001}); ASSERT_EQ(response.status().code(), MetaServiceCode::OK) << response.status().msg(); ASSERT_TRUE(after_wait_called); ASSERT_EQ(response.bindings_size(), 1); ASSERT_EQ(response.bindings(0).partition_states_size(), 1); const TableStreamPartitionReadStatePB& state = response.bindings(0).partition_states(0); EXPECT_EQ(state.visible_version(), 2); EXPECT_EQ(state.end_tso(), 130); EXPECT_EQ(state.offset_tso(), 120); } TEST_F(MetaServiceTableStreamTest, SingleGetVersionWaitsUsingCloneSourceInstance) { const std::string source_instance_id = "table_stream_single_pending_version_source"; register_instance(source_instance_id, MULTI_VERSION_READ_WRITE); set_clone_source(source_instance_id, Versionstamp(100, 0)); const int64_t pending_txn_id = begin_transaction(fmt::format("1:{}:test", source_instance_id), "pending-clone-single-version"); std::unique_ptr txn; ASSERT_EQ(service_->txn_kv()->create_txn(&txn), TxnErrorCode::TXN_OK); mark_transaction_visible(source_instance_id, pending_txn_id, txn.get()); VersionPB pending_version; pending_version.set_commit_tso(130); pending_version.add_pending_txn_ids(pending_txn_id); versioned_put(txn.get(), versioned::partition_version_key({source_instance_id, 2001}), Versionstamp(60, 0), pending_version.SerializeAsString()); ASSERT_EQ(txn->commit(), TxnErrorCode::TXN_OK); GetVersionRequest request; request.set_cloud_unique_id(cloud_unique_id_); request.set_db_id(identity_.base_db_id()); request.set_table_id(identity_.base_table_id()); request.set_partition_id(2001); request.set_wait_for_pending_txn(true); GetVersionResponse response; brpc::Controller controller; service_->get_version(&controller, &request, &response, nullptr); ASSERT_EQ(response.status().code(), MetaServiceCode::OK) << response.status().msg(); EXPECT_EQ(response.version(), 2); ASSERT_EQ(response.commit_tsos_size(), 1); EXPECT_EQ(response.commit_tsos(0), 130); } TEST_F(MetaServiceTableStreamTest, BatchGetVersionWaitsUsingCloneSourceInstance) { const std::string source_instance_id = "table_stream_batch_pending_version_source"; register_instance(source_instance_id, MULTI_VERSION_READ_WRITE); set_clone_source(source_instance_id, Versionstamp(100, 0)); const int64_t pending_txn_id = begin_transaction(fmt::format("1:{}:test", source_instance_id), "pending-clone-batch-version"); std::unique_ptr txn; ASSERT_EQ(service_->txn_kv()->create_txn(&txn), TxnErrorCode::TXN_OK); mark_transaction_visible(source_instance_id, pending_txn_id, txn.get()); PartitionIndexPB partition; partition.set_db_id(identity_.base_db_id()); partition.set_table_id(identity_.base_table_id()); txn->put(versioned::partition_index_key({source_instance_id, 2001}), partition.SerializeAsString()); VersionPB pending_version; pending_version.set_commit_tso(130); pending_version.add_pending_txn_ids(pending_txn_id); versioned_put(txn.get(), versioned::partition_version_key({source_instance_id, 2001}), Versionstamp(60, 0), pending_version.SerializeAsString()); ASSERT_EQ(txn->commit(), TxnErrorCode::TXN_OK); GetVersionRequest request; request.set_cloud_unique_id(cloud_unique_id_); request.set_batch_mode(true); request.set_wait_for_pending_txn(true); request.add_db_ids(identity_.base_db_id()); request.add_table_ids(identity_.base_table_id()); request.add_partition_ids(2001); GetVersionResponse response; brpc::Controller controller; service_->get_version(&controller, &request, &response, nullptr); ASSERT_EQ(response.status().code(), MetaServiceCode::OK) << response.status().msg(); ASSERT_EQ(response.versions_size(), 1); ASSERT_EQ(response.commit_tsos_size(), 1); EXPECT_EQ(response.versions(0), 2); EXPECT_EQ(response.commit_tsos(0), 130); } TEST_F(MetaServiceTableStreamTest, RejectDuplicateBindingsAndPartitions) { GetTableStreamOffsetRequest request; request.set_cloud_unique_id(cloud_unique_id_); auto* binding = request.add_bindings(); binding->mutable_identity()->CopyFrom(identity_); binding->add_partition_ids(2001); binding->add_partition_ids(2001); GetTableStreamOffsetResponse response; brpc::Controller controller; service_->get_table_stream_offset(&controller, &request, &response, nullptr); EXPECT_EQ(response.status().code(), MetaServiceCode::INVALID_ARGUMENT); request.mutable_bindings()->Clear(); for (int i = 0; i < 2; ++i) { binding = request.add_bindings(); binding->mutable_identity()->CopyFrom(identity_); binding->add_partition_ids(2001 + i); } response.Clear(); brpc::Controller second_controller; service_->get_table_stream_offset(&second_controller, &request, &response, nullptr); EXPECT_EQ(response.status().code(), MetaServiceCode::INVALID_ARGUMENT); } TEST_F(MetaServiceTableStreamTest, RejectEnabledModeAndRecyclingStream) { set_multi_version_status(MULTI_VERSION_DISABLED); std::unique_ptr txn; ASSERT_EQ(service_->txn_kv()->create_txn(&txn), TxnErrorCode::TXN_OK); InstanceInfoPB instance; instance.set_instance_id(instance_id_); instance.set_multi_version_status(MULTI_VERSION_ENABLED); txn->put(instance_key({instance_id_}), instance.SerializeAsString()); ASSERT_EQ(txn->commit(), TxnErrorCode::TXN_OK); GetTableStreamOffsetResponse response = get_read_state({2001}); EXPECT_EQ(response.status().code(), MetaServiceCode::INVALID_ARGUMENT); set_multi_version_status(MULTI_VERSION_DISABLED); ASSERT_EQ(service_->txn_kv()->create_txn(&txn), TxnErrorCode::TXN_OK); put_latest_partition_state(txn.get(), 2001, 8, 130, 100); RecycleIndexPB recycle_index; recycle_index.set_table_id(identity_.base_table_id()); recycle_index.set_state(RecycleIndexPB::RECYCLING); recycle_index.set_object_type(TABLE_STREAM); txn->put(recycle_index_key({instance_id_, identity_.stream_id()}), recycle_index.SerializeAsString()); ASSERT_EQ(txn->commit(), TxnErrorCode::TXN_OK); response = get_read_state({2001}); EXPECT_EQ(response.status().code(), MetaServiceCode::INVALID_ARGUMENT); } TEST_F(MetaServiceTableStreamTest, CommitUpdatesLatestOffsetAndIsIdempotent) { set_multi_version_status(MULTI_VERSION_DISABLED); std::unique_ptr txn; ASSERT_EQ(service_->txn_kv()->create_txn(&txn), TxnErrorCode::TXN_OK); put_latest_partition_state(txn.get(), 2001, 8, 130, 100); put_latest_partition_state(txn.get(), 2002, 9, 140, std::nullopt); ASSERT_EQ(txn->commit(), TxnErrorCode::TXN_OK); int64_t txn_id = begin_target_transaction("consume-existing-offset"); CommitTxnResponse response = consume_partition(txn_id, 2001, TABLE_STREAM_OFFSET_CONSUMED, 100, 120); ASSERT_EQ(response.status().code(), MetaServiceCode::OK) << response.status().msg(); TableStreamOffsetPB offset = get_latest_offset(2001); EXPECT_EQ(offset.state(), TABLE_STREAM_OFFSET_CONSUMED); EXPECT_EQ(offset.offset_tso(), 120); EXPECT_GT(offset.last_consumption_time_ms(), 0); response = consume_partition(txn_id, 2001, TABLE_STREAM_OFFSET_CONSUMED, 100, 120); ASSERT_EQ(response.status().code(), MetaServiceCode::OK) << response.status().msg(); EXPECT_EQ(get_latest_offset(2001).offset_tso(), 120); txn_id = begin_target_transaction("consume-unknown-offset"); response = consume_partition(txn_id, 2002, TABLE_STREAM_OFFSET_UNKNOWN, 0, 140); ASSERT_EQ(response.status().code(), MetaServiceCode::OK) << response.status().msg(); EXPECT_EQ(get_latest_offset(2002).offset_tso(), 140); } TEST_F(MetaServiceTableStreamTest, CommitMultipleStreamsAtomically) { set_multi_version_status(MULTI_VERSION_DISABLED); TableStreamIdentityPB second_identity = identity_; second_identity.set_stream_db_id(1005); second_identity.set_stream_id(1006); std::unique_ptr txn; ASSERT_EQ(service_->txn_kv()->create_txn(&txn), TxnErrorCode::TXN_OK); put_latest_partition_state(txn.get(), 2001, 8, 130, 100); TableStreamOffsetPB second_offset; second_offset.set_partition_id(2001); second_offset.set_state(TABLE_STREAM_OFFSET_CONSUMED); second_offset.set_offset_tso(105); second_offset.set_last_consumption_time_ms(1234); txn->put(table_stream_offset_key( {instance_id_, second_identity.base_db_id(), second_identity.base_table_id(), second_identity.stream_db_id(), second_identity.stream_id(), 2001}), second_offset.SerializeAsString()); ASSERT_EQ(txn->commit(), TxnErrorCode::TXN_OK); auto make_request = [&](int64_t txn_id, int64_t second_expected_tso) { CommitTxnRequest request = make_consume_request(txn_id, 2001, TABLE_STREAM_OFFSET_CONSUMED, 100, 120); TableStreamUpdatePB* second_stream_update = request.add_table_stream_updates(); second_stream_update->mutable_identity()->CopyFrom(second_identity); TableStreamPartitionUpdatePB* second_partition_update = second_stream_update->add_partition_updates(); second_partition_update->set_partition_id(2001); second_partition_update->set_expected_state(TABLE_STREAM_OFFSET_CONSUMED); second_partition_update->set_expected_offset_tso(second_expected_tso); second_partition_update->set_next_offset_tso(125); return request; }; CommitTxnResponse response = commit_transaction( make_request(begin_target_transaction("consume-multiple-streams-stale"), 104)); EXPECT_EQ(response.status().code(), MetaServiceCode::INVALID_ARGUMENT); EXPECT_EQ(get_latest_offset(identity_, 2001).offset_tso(), 100); EXPECT_EQ(get_latest_offset(second_identity, 2001).offset_tso(), 105); response = commit_transaction( make_request(begin_target_transaction("consume-multiple-streams"), 105)); ASSERT_EQ(response.status().code(), MetaServiceCode::OK) << response.status().msg(); EXPECT_EQ(get_latest_offset(identity_, 2001).offset_tso(), 120); EXPECT_EQ(get_latest_offset(second_identity, 2001).offset_tso(), 125); } TEST_F(MetaServiceTableStreamTest, CommitTargetRowsetAndOffsetAtomically) { set_multi_version_status(MULTI_VERSION_DISABLED); constexpr int64_t kSourcePartitionId = 2001; constexpr int64_t kTargetDbId = 3001; constexpr int64_t kTargetTableId = 3002; constexpr int64_t kTargetIndexId = 3003; constexpr int64_t kTargetPartitionId = 4001; constexpr int64_t kTargetTabletId = 5001; std::unique_ptr txn; ASSERT_EQ(service_->txn_kv()->create_txn(&txn), TxnErrorCode::TXN_OK); put_latest_partition_state(txn.get(), kSourcePartitionId, 8, 130, 100); ASSERT_EQ(txn->commit(), TxnErrorCode::TXN_OK); create_target_tablet(kTargetDbId, kTargetTableId, kTargetIndexId, kTargetPartitionId, kTargetTabletId); int64_t stale_txn_id = begin_target_transaction("consume-target-rowset-stale"); stage_target_rowset(stale_txn_id, kTargetPartitionId, kTargetTabletId); CommitTxnRequest stale_request = make_consume_request(stale_txn_id, kSourcePartitionId, TABLE_STREAM_OFFSET_CONSUMED, 99, 120); CommitTxnResponse response = commit_transaction(stale_request); ASSERT_EQ(response.status().code(), MetaServiceCode::INVALID_ARGUMENT); EXPECT_EQ(get_latest_offset(kSourcePartitionId).offset_tso(), 100); ASSERT_EQ(service_->txn_kv()->create_txn(&txn), TxnErrorCode::TXN_OK); std::string value; EXPECT_EQ(txn->get(meta_rowset_tmp_key({instance_id_, stale_txn_id, kTargetTabletId}), &value), TxnErrorCode::TXN_OK); EXPECT_EQ(txn->get(meta_rowset_key({instance_id_, kTargetTabletId, 2}), &value), TxnErrorCode::TXN_KEY_NOT_FOUND); ASSERT_EQ(txn->get(partition_version_key( {instance_id_, kTargetDbId, kTargetTableId, kTargetPartitionId}), &value), TxnErrorCode::TXN_OK); VersionPB target_version; ASSERT_TRUE(target_version.ParseFromString(value)); EXPECT_EQ(target_version.version(), 1); int64_t committed_txn_id = begin_target_transaction("consume-target-rowset"); stage_target_rowset(committed_txn_id, kTargetPartitionId, kTargetTabletId); CommitTxnRequest commit_request = make_consume_request(committed_txn_id, kSourcePartitionId, TABLE_STREAM_OFFSET_CONSUMED, 100, 120); commit_request.set_commit_tso(999); response = commit_transaction(commit_request); ASSERT_EQ(response.status().code(), MetaServiceCode::OK) << response.status().msg(); EXPECT_EQ(get_latest_offset(kSourcePartitionId).offset_tso(), 120); ASSERT_EQ(service_->txn_kv()->create_txn(&txn), TxnErrorCode::TXN_OK); EXPECT_EQ(txn->get(meta_rowset_tmp_key({instance_id_, committed_txn_id, kTargetTabletId}), &value), TxnErrorCode::TXN_KEY_NOT_FOUND); ASSERT_EQ(txn->get(meta_rowset_key({instance_id_, kTargetTabletId, 2}), &value), TxnErrorCode::TXN_OK); doris::RowsetMetaCloudPB committed_rowset; ASSERT_TRUE(committed_rowset.ParseFromString(value)); EXPECT_EQ(committed_rowset.txn_id(), committed_txn_id); ASSERT_EQ(txn->get(partition_version_key( {instance_id_, kTargetDbId, kTargetTableId, kTargetPartitionId}), &value), TxnErrorCode::TXN_OK); ASSERT_TRUE(target_version.ParseFromString(value)); EXPECT_EQ(target_version.version(), 2); EXPECT_EQ(target_version.commit_tso(), 999); response = commit_transaction(commit_request); ASSERT_EQ(response.status().code(), MetaServiceCode::OK) << response.status().msg(); EXPECT_EQ(get_latest_offset(kSourcePartitionId).offset_tso(), 120); ASSERT_EQ(service_->txn_kv()->create_txn(&txn), TxnErrorCode::TXN_OK); ASSERT_EQ(txn->get(meta_rowset_key({instance_id_, kTargetTabletId, 2}), &value), TxnErrorCode::TXN_OK); ASSERT_TRUE(committed_rowset.ParseFromString(value)); EXPECT_EQ(committed_rowset.txn_id(), committed_txn_id); ASSERT_EQ(txn->get(partition_version_key( {instance_id_, kTargetDbId, kTargetTableId, kTargetPartitionId}), &value), TxnErrorCode::TXN_OK); ASSERT_TRUE(target_version.ParseFromString(value)); EXPECT_EQ(target_version.version(), 2); EXPECT_EQ(txn->get(meta_rowset_key({instance_id_, kTargetTabletId, 3}), &value), TxnErrorCode::TXN_KEY_NOT_FOUND); } TEST_F(MetaServiceTableStreamTest, SourcePublishDoesNotConflictWithConsumptionCommit) { set_multi_version_status(MULTI_VERSION_DISABLED); constexpr int64_t kSourcePartitionId = 2001; constexpr int64_t kTargetDbId = 3001; constexpr int64_t kTargetTableId = 3002; constexpr int64_t kTargetIndexId = 3003; constexpr int64_t kTargetPartitionId = 4001; constexpr int64_t kTargetTabletId = 5001; std::unique_ptr txn; ASSERT_EQ(service_->txn_kv()->create_txn(&txn), TxnErrorCode::TXN_OK); put_latest_partition_state(txn.get(), kSourcePartitionId, 8, 130, 100); ASSERT_EQ(txn->commit(), TxnErrorCode::TXN_OK); create_target_tablet(kTargetDbId, kTargetTableId, kTargetIndexId, kTargetPartitionId, kTargetTabletId); const int64_t consume_txn_id = begin_target_transaction("consume-during-source-publish"); stage_target_rowset(consume_txn_id, kTargetPartitionId, kTargetTabletId); CommitTxnRequest consume_request = make_consume_request(consume_txn_id, kSourcePartitionId, TABLE_STREAM_OFFSET_CONSUMED, 100, 120); consume_request.set_commit_tso(999); std::mutex mutex; std::condition_variable condition; bool consume_ready = false; bool release_consume = false; auto* sync_point = SyncPoint::get_instance(); DORIS_CLOUD_DEFER { { std::lock_guard lock(mutex); release_consume = true; } condition.notify_all(); sync_point->disable_processing(); sync_point->clear_all_call_backs(); }; sync_point->set_call_back("commit_txn_immediately::before_commit", [&](auto&&) { std::unique_lock lock(mutex); consume_ready = true; condition.notify_all(); condition.wait(lock, [&] { return release_consume; }); }); sync_point->enable_processing(); CommitTxnResponse consume_response; std::thread consume_thread([&] { consume_response = commit_transaction(consume_request); }); bool reached_commit = false; { std::unique_lock lock(mutex); reached_commit = condition.wait_for(lock, std::chrono::seconds(10), [&] { return consume_ready; }); } TxnErrorCode create_publish_txn = TxnErrorCode::TXN_OK; TxnErrorCode commit_publish_txn = TxnErrorCode::TXN_OK; if (reached_commit) { create_publish_txn = service_->txn_kv()->create_txn(&txn); if (create_publish_txn == TxnErrorCode::TXN_OK) { VersionPB published_version; published_version.set_version(9); published_version.set_commit_tso(160); txn->put(partition_version_key({instance_id_, identity_.base_db_id(), identity_.base_table_id(), kSourcePartitionId}), published_version.SerializeAsString()); commit_publish_txn = txn->commit(); } } { std::lock_guard lock(mutex); release_consume = true; } condition.notify_all(); consume_thread.join(); ASSERT_TRUE(reached_commit); ASSERT_EQ(create_publish_txn, TxnErrorCode::TXN_OK); ASSERT_EQ(commit_publish_txn, TxnErrorCode::TXN_OK); ASSERT_EQ(consume_response.status().code(), MetaServiceCode::OK) << consume_response.status().msg(); EXPECT_EQ(get_latest_offset(kSourcePartitionId).offset_tso(), 120); ASSERT_EQ(service_->txn_kv()->create_txn(&txn), TxnErrorCode::TXN_OK); std::string value; ASSERT_EQ(txn->get(partition_version_key({instance_id_, identity_.base_db_id(), identity_.base_table_id(), kSourcePartitionId}), &value), TxnErrorCode::TXN_OK); VersionPB source_version; ASSERT_TRUE(source_version.ParseFromString(value)); EXPECT_EQ(source_version.version(), 9); EXPECT_EQ(source_version.commit_tso(), 160); EXPECT_EQ( txn->get(meta_rowset_tmp_key({instance_id_, consume_txn_id, kTargetTabletId}), &value), TxnErrorCode::TXN_KEY_NOT_FOUND); ASSERT_EQ(txn->get(meta_rowset_key({instance_id_, kTargetTabletId, 2}), &value), TxnErrorCode::TXN_OK); doris::RowsetMetaCloudPB committed_rowset; ASSERT_TRUE(committed_rowset.ParseFromString(value)); EXPECT_EQ(committed_rowset.txn_id(), consume_txn_id); ASSERT_EQ(txn->get(partition_version_key( {instance_id_, kTargetDbId, kTargetTableId, kTargetPartitionId}), &value), TxnErrorCode::TXN_OK); VersionPB target_version; ASSERT_TRUE(target_version.ParseFromString(value)); EXPECT_EQ(target_version.version(), 2); EXPECT_EQ(target_version.commit_tso(), 999); ASSERT_EQ(txn->get(txn_info_key({instance_id_, kTargetDbId, consume_txn_id}), &value), TxnErrorCode::TXN_OK); TxnInfoPB txn_info; ASSERT_TRUE(txn_info.ParseFromString(value)); EXPECT_EQ(txn_info.status(), TXN_STATUS_VISIBLE); } TEST_F(MetaServiceTableStreamTest, CommitRetryAfterMetaServiceRestartIsExactlyOnce) { set_multi_version_status(MULTI_VERSION_DISABLED); constexpr int64_t kSourcePartitionId = 2001; constexpr int64_t kTargetDbId = 3001; constexpr int64_t kTargetTableId = 3002; constexpr int64_t kTargetIndexId = 3003; constexpr int64_t kTargetPartitionId = 4001; constexpr int64_t kTargetTabletId = 5001; std::unique_ptr txn; ASSERT_EQ(service_->txn_kv()->create_txn(&txn), TxnErrorCode::TXN_OK); put_latest_partition_state(txn.get(), kSourcePartitionId, 8, 130, 100); ASSERT_EQ(txn->commit(), TxnErrorCode::TXN_OK); create_target_tablet(kTargetDbId, kTargetTableId, kTargetIndexId, kTargetPartitionId, kTargetTabletId); const int64_t txn_id = begin_target_transaction("consume-retry-after-ms-restart"); stage_target_rowset(txn_id, kTargetPartitionId, kTargetTabletId); CommitTxnRequest request = make_consume_request(txn_id, kSourcePartitionId, TABLE_STREAM_OFFSET_CONSUMED, 100, 120); request.set_commit_tso(999); // The commit succeeds, but its response is lost before the caller observes it. const CommitTxnResponse lost_response = commit_transaction(request); ASSERT_EQ(lost_response.status().code(), MetaServiceCode::OK) << lost_response.status().msg(); auto restarted_service = restart_meta_service(); CommitTxnResponse retry_response; brpc::Controller retry_controller; restarted_service->commit_txn(&retry_controller, &request, &retry_response, nullptr); ASSERT_EQ(retry_response.status().code(), MetaServiceCode::OK) << retry_response.status().msg(); EXPECT_EQ(get_latest_offset(kSourcePartitionId).offset_tso(), 120); ASSERT_EQ(service_->txn_kv()->create_txn(&txn), TxnErrorCode::TXN_OK); std::string value; EXPECT_EQ(txn->get(meta_rowset_tmp_key({instance_id_, txn_id, kTargetTabletId}), &value), TxnErrorCode::TXN_KEY_NOT_FOUND); ASSERT_EQ(txn->get(meta_rowset_key({instance_id_, kTargetTabletId, 2}), &value), TxnErrorCode::TXN_OK); doris::RowsetMetaCloudPB committed_rowset; ASSERT_TRUE(committed_rowset.ParseFromString(value)); EXPECT_EQ(committed_rowset.txn_id(), txn_id); EXPECT_EQ(txn->get(meta_rowset_key({instance_id_, kTargetTabletId, 3}), &value), TxnErrorCode::TXN_KEY_NOT_FOUND); ASSERT_EQ(txn->get(partition_version_key( {instance_id_, kTargetDbId, kTargetTableId, kTargetPartitionId}), &value), TxnErrorCode::TXN_OK); VersionPB target_version; ASSERT_TRUE(target_version.ParseFromString(value)); EXPECT_EQ(target_version.version(), 2); EXPECT_EQ(target_version.commit_tso(), 999); } TEST_F(MetaServiceTableStreamTest, CommitRejectsRecyclingStreamBeforePublishingRowsetAndOffset) { set_multi_version_status(MULTI_VERSION_DISABLED); constexpr int64_t kSourcePartitionId = 2001; constexpr int64_t kTargetDbId = 3001; constexpr int64_t kTargetTableId = 3002; constexpr int64_t kTargetIndexId = 3003; constexpr int64_t kTargetPartitionId = 4001; constexpr int64_t kTargetTabletId = 5001; std::unique_ptr txn; ASSERT_EQ(service_->txn_kv()->create_txn(&txn), TxnErrorCode::TXN_OK); put_latest_partition_state(txn.get(), kSourcePartitionId, 8, 130, 100); ASSERT_EQ(txn->commit(), TxnErrorCode::TXN_OK); create_target_tablet(kTargetDbId, kTargetTableId, kTargetIndexId, kTargetPartitionId, kTargetTabletId); const int64_t txn_id = begin_target_transaction("consume-recycling-stream"); stage_target_rowset(txn_id, kTargetPartitionId, kTargetTabletId); ASSERT_EQ(service_->txn_kv()->create_txn(&txn), TxnErrorCode::TXN_OK); RecycleIndexPB recycle_index; recycle_index.set_db_id(identity_.base_db_id()); recycle_index.set_table_id(identity_.base_table_id()); recycle_index.set_stream_db_id(identity_.stream_db_id()); recycle_index.set_state(RecycleIndexPB::RECYCLING); recycle_index.set_object_type(IndexObjectTypePB::TABLE_STREAM); txn->put(recycle_index_key({instance_id_, identity_.stream_id()}), recycle_index.SerializeAsString()); ASSERT_EQ(txn->commit(), TxnErrorCode::TXN_OK); const CommitTxnRequest request = make_consume_request(txn_id, kSourcePartitionId, TABLE_STREAM_OFFSET_CONSUMED, 100, 120); const CommitTxnResponse response = commit_transaction(request); EXPECT_EQ(response.status().code(), MetaServiceCode::INVALID_ARGUMENT); EXPECT_EQ(get_latest_offset(kSourcePartitionId).offset_tso(), 100); ASSERT_EQ(service_->txn_kv()->create_txn(&txn), TxnErrorCode::TXN_OK); std::string value; EXPECT_EQ(txn->get(meta_rowset_tmp_key({instance_id_, txn_id, kTargetTabletId}), &value), TxnErrorCode::TXN_OK); EXPECT_EQ(txn->get(meta_rowset_key({instance_id_, kTargetTabletId, 2}), &value), TxnErrorCode::TXN_KEY_NOT_FOUND); ASSERT_EQ(txn->get(partition_version_key( {instance_id_, kTargetDbId, kTargetTableId, kTargetPartitionId}), &value), TxnErrorCode::TXN_OK); VersionPB target_version; ASSERT_TRUE(target_version.ParseFromString(value)); EXPECT_EQ(target_version.version(), 1); } TEST_F(MetaServiceTableStreamTest, DropPartitionConflictsWithInFlightConsumption) { set_multi_version_status(MULTI_VERSION_DISABLED); constexpr int64_t kSourcePartitionId = 2001; constexpr int64_t kTargetDbId = 3001; constexpr int64_t kTargetTableId = 3002; constexpr int64_t kTargetIndexId = 3003; constexpr int64_t kTargetPartitionId = 4001; constexpr int64_t kTargetTabletId = 5001; std::unique_ptr txn; ASSERT_EQ(service_->txn_kv()->create_txn(&txn), TxnErrorCode::TXN_OK); put_latest_partition_state(txn.get(), kSourcePartitionId, 8, 130, 100); ASSERT_EQ(txn->commit(), TxnErrorCode::TXN_OK); create_target_tablet(kTargetDbId, kTargetTableId, kTargetIndexId, kTargetPartitionId, kTargetTabletId); const int64_t txn_id = begin_target_transaction("consume-while-dropping-source-partition"); stage_target_rowset(txn_id, kTargetPartitionId, kTargetTabletId); const CommitTxnRequest consume_request = make_consume_request( txn_id, kSourcePartitionId, TABLE_STREAM_OFFSET_CONSUMED, 100, 120); std::mutex mutex; std::condition_variable condition; bool commit_ready = false; bool release_commit = false; auto* sync_point = SyncPoint::get_instance(); DORIS_CLOUD_DEFER { { std::lock_guard lock(mutex); release_commit = true; } condition.notify_all(); sync_point->disable_processing(); sync_point->clear_all_call_backs(); }; sync_point->set_call_back("commit_txn_immediately::before_commit", [&](auto&&) { std::unique_lock lock(mutex); commit_ready = true; condition.notify_all(); condition.wait(lock, [&] { return release_commit; }); }); sync_point->enable_processing(); CommitTxnResponse consume_response; std::thread consume_thread([&] { consume_response = commit_transaction(consume_request); }); bool reached_commit = false; { std::unique_lock lock(mutex); reached_commit = condition.wait_for(lock, std::chrono::seconds(10), [&] { return commit_ready; }); } PartitionRequest drop_request; PartitionResponse drop_response; if (reached_commit) { drop_request.set_cloud_unique_id(cloud_unique_id_); drop_request.set_db_id(identity_.base_db_id()); drop_request.set_table_id(identity_.base_table_id()); drop_request.add_index_ids(identity_.base_table_id() + 1); drop_request.add_partition_ids(kSourcePartitionId); drop_request.mutable_table_streams()->Add()->CopyFrom(identity_); drop_request.set_expiration(::time(nullptr) + 3600); brpc::Controller drop_controller; service_->drop_partition(&drop_controller, &drop_request, &drop_response, nullptr); } { std::lock_guard lock(mutex); release_commit = true; } condition.notify_all(); consume_thread.join(); ASSERT_TRUE(reached_commit); ASSERT_EQ(drop_response.status().code(), MetaServiceCode::OK) << drop_response.status().msg(); EXPECT_EQ(consume_response.status().code(), MetaServiceCode::KV_TXN_CONFLICT) << consume_response.status().msg(); EXPECT_EQ(get_latest_offset(kSourcePartitionId).offset_tso(), 100); ASSERT_EQ(service_->txn_kv()->create_txn(&txn), TxnErrorCode::TXN_OK); std::string value; ASSERT_EQ(txn->get(recycle_partition_key({instance_id_, kSourcePartitionId}), &value), TxnErrorCode::TXN_OK); RecyclePartitionPB recycle_partition; ASSERT_TRUE(recycle_partition.ParseFromString(value)); EXPECT_EQ(recycle_partition.state(), RecyclePartitionPB::DROPPED); EXPECT_EQ(txn->get(meta_rowset_tmp_key({instance_id_, txn_id, kTargetTabletId}), &value), TxnErrorCode::TXN_OK); EXPECT_EQ(txn->get(meta_rowset_key({instance_id_, kTargetTabletId, 2}), &value), TxnErrorCode::TXN_KEY_NOT_FOUND); ASSERT_EQ(txn->get(partition_version_key( {instance_id_, kTargetDbId, kTargetTableId, kTargetPartitionId}), &value), TxnErrorCode::TXN_OK); VersionPB target_version; ASSERT_TRUE(target_version.ParseFromString(value)); EXPECT_EQ(target_version.version(), 1); } TEST_F(MetaServiceTableStreamTest, ConcurrentOffsetCasAllowsOnlyOneCommit) { set_multi_version_status(MULTI_VERSION_DISABLED); constexpr int64_t kPartitionId = 2001; std::unique_ptr txn; ASSERT_EQ(service_->txn_kv()->create_txn(&txn), TxnErrorCode::TXN_OK); put_latest_partition_state(txn.get(), kPartitionId, 8, 130, 100); ASSERT_EQ(txn->commit(), TxnErrorCode::TXN_OK); const CommitTxnRequest first_request = make_consume_request(begin_target_transaction("concurrent-cas-first"), kPartitionId, TABLE_STREAM_OFFSET_CONSUMED, 100, 120); const CommitTxnRequest second_request = make_consume_request(begin_target_transaction("concurrent-cas-second"), kPartitionId, TABLE_STREAM_OFFSET_CONSUMED, 100, 125); auto [first_response, second_response] = commit_concurrently(first_request, second_request); const int successful_commits = (first_response.status().code() == MetaServiceCode::OK ? 1 : 0) + (second_response.status().code() == MetaServiceCode::OK ? 1 : 0); const int conflicted_commits = (first_response.status().code() == MetaServiceCode::KV_TXN_CONFLICT ? 1 : 0) + (second_response.status().code() == MetaServiceCode::KV_TXN_CONFLICT ? 1 : 0); EXPECT_EQ(successful_commits, 1); EXPECT_EQ(conflicted_commits, 1); const int64_t final_offset = get_latest_offset(kPartitionId).offset_tso(); EXPECT_TRUE(final_offset == 120 || final_offset == 125); } TEST_F(MetaServiceTableStreamTest, ConcurrentUnknownOffsetCasAllowsOnlyOneFirstConsumer) { set_multi_version_status(MULTI_VERSION_DISABLED); constexpr int64_t kPartitionId = 2001; std::unique_ptr txn; ASSERT_EQ(service_->txn_kv()->create_txn(&txn), TxnErrorCode::TXN_OK); put_latest_partition_state(txn.get(), kPartitionId, 8, 130, std::nullopt); ASSERT_EQ(txn->commit(), TxnErrorCode::TXN_OK); const CommitTxnRequest first_request = make_consume_request(begin_target_transaction("concurrent-unknown-first"), kPartitionId, TABLE_STREAM_OFFSET_UNKNOWN, 0, 120); const CommitTxnRequest second_request = make_consume_request(begin_target_transaction("concurrent-unknown-second"), kPartitionId, TABLE_STREAM_OFFSET_UNKNOWN, 0, 125); auto [first_response, second_response] = commit_concurrently(first_request, second_request); const int successful_commits = (first_response.status().code() == MetaServiceCode::OK ? 1 : 0) + (second_response.status().code() == MetaServiceCode::OK ? 1 : 0); const int conflicted_commits = (first_response.status().code() == MetaServiceCode::KV_TXN_CONFLICT ? 1 : 0) + (second_response.status().code() == MetaServiceCode::KV_TXN_CONFLICT ? 1 : 0); EXPECT_EQ(successful_commits, 1); EXPECT_EQ(conflicted_commits, 1); const int64_t final_offset = get_latest_offset(kPartitionId).offset_tso(); EXPECT_TRUE(final_offset == 120 || final_offset == 125); } TEST_F(MetaServiceTableStreamTest, ConcurrentIndependentOffsetUpdatesDoNotConflict) { set_multi_version_status(MULTI_VERSION_DISABLED); constexpr int64_t kSharedPartitionId = 2101; constexpr int64_t kFirstPartitionId = 2201; constexpr int64_t kSecondPartitionId = 2202; TableStreamIdentityPB second_identity = identity_; second_identity.set_stream_db_id(identity_.stream_db_id() + 1); second_identity.set_stream_id(identity_.stream_id() + 1); std::unique_ptr txn; ASSERT_EQ(service_->txn_kv()->create_txn(&txn), TxnErrorCode::TXN_OK); put_latest_partition_state(txn.get(), kSharedPartitionId, 8, 130, std::nullopt); put_latest_partition_state(txn.get(), kFirstPartitionId, 9, 140, std::nullopt); put_latest_partition_state(txn.get(), kSecondPartitionId, 10, 150, std::nullopt); ASSERT_EQ(txn->commit(), TxnErrorCode::TXN_OK); CommitTxnRequest first_request = make_consume_request(begin_target_transaction("concurrent-stream-first"), kSharedPartitionId, TABLE_STREAM_OFFSET_UNKNOWN, 0, 120); CommitTxnRequest second_request = make_consume_request(begin_target_transaction("concurrent-stream-second"), kSharedPartitionId, TABLE_STREAM_OFFSET_UNKNOWN, 0, 125); second_request.mutable_table_stream_updates(0)->mutable_identity()->CopyFrom(second_identity); auto [first_stream_response, second_stream_response] = commit_concurrently(first_request, second_request); ASSERT_EQ(first_stream_response.status().code(), MetaServiceCode::OK) << first_stream_response.status().msg(); ASSERT_EQ(second_stream_response.status().code(), MetaServiceCode::OK) << second_stream_response.status().msg(); EXPECT_EQ(get_latest_offset(identity_, kSharedPartitionId).offset_tso(), 120); EXPECT_EQ(get_latest_offset(second_identity, kSharedPartitionId).offset_tso(), 125); first_request = make_consume_request(begin_target_transaction("concurrent-partition-first"), kFirstPartitionId, TABLE_STREAM_OFFSET_UNKNOWN, 0, 135); second_request = make_consume_request(begin_target_transaction("concurrent-partition-second"), kSecondPartitionId, TABLE_STREAM_OFFSET_UNKNOWN, 0, 145); auto [first_partition_response, second_partition_response] = commit_concurrently(first_request, second_request); ASSERT_EQ(first_partition_response.status().code(), MetaServiceCode::OK) << first_partition_response.status().msg(); ASSERT_EQ(second_partition_response.status().code(), MetaServiceCode::OK) << second_partition_response.status().msg(); EXPECT_EQ(get_latest_offset(kFirstPartitionId).offset_tso(), 135); EXPECT_EQ(get_latest_offset(kSecondPartitionId).offset_tso(), 145); } TEST_F(MetaServiceTableStreamTest, MultiVersionModeChangeConflictsWithInFlightConsumption) { set_multi_version_status(MULTI_VERSION_DISABLED); constexpr int64_t kPartitionId = 2001; std::unique_ptr txn; ASSERT_EQ(service_->txn_kv()->create_txn(&txn), TxnErrorCode::TXN_OK); put_latest_partition_state(txn.get(), kPartitionId, 8, 130, 100); ASSERT_EQ(txn->commit(), TxnErrorCode::TXN_OK); const CommitTxnRequest request = make_consume_request( begin_target_transaction("consume-while-changing-multi-version-mode"), kPartitionId, TABLE_STREAM_OFFSET_CONSUMED, 100, 120); std::mutex mutex; std::condition_variable condition; bool commit_ready = false; bool release_commit = false; auto* sync_point = SyncPoint::get_instance(); DORIS_CLOUD_DEFER { { std::lock_guard lock(mutex); release_commit = true; } condition.notify_all(); sync_point->disable_processing(); sync_point->clear_all_call_backs(); }; sync_point->set_call_back("commit_txn_immediately::before_commit", [&](auto&&) { std::unique_lock lock(mutex); commit_ready = true; condition.notify_all(); condition.wait(lock, [&] { return release_commit; }); }); sync_point->enable_processing(); CommitTxnResponse response; std::thread commit_thread([&] { response = commit_transaction(request); }); bool reached_commit = false; { std::unique_lock lock(mutex); reached_commit = condition.wait_for(lock, std::chrono::seconds(10), [&] { return commit_ready; }); } TxnErrorCode create_mode_txn = TxnErrorCode::TXN_OK; TxnErrorCode commit_mode_txn = TxnErrorCode::TXN_OK; if (reached_commit) { create_mode_txn = service_->txn_kv()->create_txn(&txn); if (create_mode_txn == TxnErrorCode::TXN_OK) { InstanceInfoPB instance; instance.set_instance_id(instance_id_); instance.set_multi_version_status(MULTI_VERSION_WRITE_ONLY); txn->put(instance_key({instance_id_}), instance.SerializeAsString()); commit_mode_txn = txn->commit(); } } { std::lock_guard lock(mutex); release_commit = true; } condition.notify_all(); commit_thread.join(); ASSERT_TRUE(reached_commit); ASSERT_EQ(create_mode_txn, TxnErrorCode::TXN_OK); ASSERT_EQ(commit_mode_txn, TxnErrorCode::TXN_OK); EXPECT_EQ(response.status().code(), MetaServiceCode::KV_TXN_CONFLICT) << response.status().msg(); EXPECT_EQ(get_latest_offset(kPartitionId).offset_tso(), 100); } TEST_F(MetaServiceTableStreamTest, CommitLargePartitionBatch) { set_multi_version_status(MULTI_VERSION_WRITE_ONLY); constexpr int kPartitionCount = 1201; std::unique_ptr txn; ASSERT_EQ(service_->txn_kv()->create_txn(&txn), TxnErrorCode::TXN_OK); for (int i = 0; i < kPartitionCount; ++i) { int64_t partition_id = 30000 + i; put_latest_partition_state(txn.get(), partition_id, i + 1, 40000 + i, std::nullopt); } ASSERT_EQ(txn->commit(), TxnErrorCode::TXN_OK); CommitTxnRequest request; request.set_cloud_unique_id(cloud_unique_id_); request.set_db_id(3001); request.set_txn_id(begin_target_transaction("consume-large-partition-batch")); TableStreamUpdatePB* stream_update = request.add_table_stream_updates(); stream_update->mutable_identity()->CopyFrom(identity_); for (int i = 0; i < kPartitionCount; ++i) { int64_t partition_id = 30000 + i; TableStreamPartitionUpdatePB* update = stream_update->add_partition_updates(); update->set_partition_id(partition_id); update->set_expected_state(TABLE_STREAM_OFFSET_UNKNOWN); update->set_next_offset_tso(40000 + i); } CommitTxnResponse response = commit_transaction(request); ASSERT_EQ(response.status().code(), MetaServiceCode::OK) << response.status().msg(); EXPECT_EQ(get_latest_offset(30000).offset_tso(), 40000); EXPECT_EQ(get_latest_offset(30000 + kPartitionCount / 2).offset_tso(), 40000 + kPartitionCount / 2); EXPECT_EQ(get_latest_offset(30000 + kPartitionCount - 1).offset_tso(), 40000 + kPartitionCount - 1); } TEST_F(MetaServiceTableStreamTest, CommitRejectsStaleAndInvalidOffsets) { set_multi_version_status(MULTI_VERSION_DISABLED); std::unique_ptr txn; ASSERT_EQ(service_->txn_kv()->create_txn(&txn), TxnErrorCode::TXN_OK); put_latest_partition_state(txn.get(), 2001, 8, 130, 100); ASSERT_EQ(txn->commit(), TxnErrorCode::TXN_OK); int64_t txn_id = begin_target_transaction("consume-stale-offset"); CommitTxnResponse response = consume_partition(txn_id, 2001, TABLE_STREAM_OFFSET_CONSUMED, 99, 120); EXPECT_EQ(response.status().code(), MetaServiceCode::INVALID_ARGUMENT); EXPECT_EQ(get_latest_offset(2001).offset_tso(), 100); txn_id = begin_target_transaction("consume-backward-offset"); response = consume_partition(txn_id, 2001, TABLE_STREAM_OFFSET_CONSUMED, 100, 90); EXPECT_EQ(response.status().code(), MetaServiceCode::INVALID_ARGUMENT); EXPECT_EQ(get_latest_offset(2001).offset_tso(), 100); txn_id = begin_target_transaction("consume-beyond-visible-tso"); response = consume_partition(txn_id, 2001, TABLE_STREAM_OFFSET_CONSUMED, 100, 131); EXPECT_EQ(response.status().code(), MetaServiceCode::INVALID_ARGUMENT); EXPECT_EQ(get_latest_offset(2001).offset_tso(), 100); } TEST_F(MetaServiceTableStreamTest, CommitValidatesOffsetTsoDomain) { set_multi_version_status(MULTI_VERSION_DISABLED); std::unique_ptr txn; ASSERT_EQ(service_->txn_kv()->create_txn(&txn), TxnErrorCode::TXN_OK); put_latest_partition_state(txn.get(), 2001, 8, 130, -1); put_latest_partition_state(txn.get(), 2002, 9, 140, std::nullopt); ASSERT_EQ(txn->commit(), TxnErrorCode::TXN_OK); CommitTxnResponse response = consume_partition(begin_target_transaction("consume-empty-boundary-existing"), 2001, TABLE_STREAM_OFFSET_CONSUMED, -1, -1); ASSERT_EQ(response.status().code(), MetaServiceCode::OK) << response.status().msg(); EXPECT_EQ(get_latest_offset(2001).offset_tso(), -1); response = consume_partition(begin_target_transaction("consume-empty-boundary-unknown"), 2002, TABLE_STREAM_OFFSET_UNKNOWN, 0, -1); ASSERT_EQ(response.status().code(), MetaServiceCode::OK) << response.status().msg(); EXPECT_EQ(get_latest_offset(2002).offset_tso(), -1); for (int64_t invalid_tso : {0, -2}) { response = consume_partition( begin_target_transaction("consume-invalid-expected-" + std::to_string(invalid_tso)), 2001, TABLE_STREAM_OFFSET_CONSUMED, invalid_tso, -1); EXPECT_EQ(response.status().code(), MetaServiceCode::INVALID_ARGUMENT); EXPECT_EQ(get_latest_offset(2001).offset_tso(), -1); response = consume_partition( begin_target_transaction("consume-invalid-next-" + std::to_string(invalid_tso)), 2001, TABLE_STREAM_OFFSET_CONSUMED, -1, invalid_tso); EXPECT_EQ(response.status().code(), MetaServiceCode::INVALID_ARGUMENT); EXPECT_EQ(get_latest_offset(2001).offset_tso(), -1); } } TEST_F(MetaServiceTableStreamTest, CommitUsesCloneEffectiveOffset) { const std::string source_instance_id = "table_stream_read_state_source"; set_clone_source(source_instance_id, Versionstamp(100, 0)); std::unique_ptr txn; ASSERT_EQ(service_->txn_kv()->create_txn(&txn), TxnErrorCode::TXN_OK); PartitionIndexPB partition_index; partition_index.set_db_id(identity_.base_db_id()); partition_index.set_table_id(identity_.base_table_id()); txn->put(versioned::partition_index_key({source_instance_id, 2001}), partition_index.SerializeAsString()); versioned_put(txn.get(), versioned::meta_partition_key({source_instance_id, 2001}), Versionstamp(41, 0), ""); VersionPB version; version.set_version(18); version.set_commit_tso(230); versioned_put(txn.get(), versioned::partition_version_key({source_instance_id, 2001}), Versionstamp(42, 0), version.SerializeAsString()); TableStreamOffsetPB inherited_offset; inherited_offset.set_partition_id(2001); inherited_offset.set_state(TABLE_STREAM_OFFSET_INITIAL_SNAPSHOT_PENDING); inherited_offset.set_offset_tso(200); versioned_put(txn.get(), versioned::table_stream_offset_key( {source_instance_id, identity_.base_db_id(), identity_.base_table_id(), identity_.stream_db_id(), identity_.stream_id(), 2001}), Versionstamp(43, 0), inherited_offset.SerializeAsString()); ASSERT_EQ(txn->commit(), TxnErrorCode::TXN_OK); int64_t txn_id = begin_target_transaction("consume-inherited-offset-as-unknown"); CommitTxnResponse response = consume_partition(txn_id, 2001, TABLE_STREAM_OFFSET_UNKNOWN, 0, 220); EXPECT_EQ(response.status().code(), MetaServiceCode::INVALID_ARGUMENT); txn_id = begin_target_transaction("consume-inherited-offset"); response = consume_partition(txn_id, 2001, TABLE_STREAM_OFFSET_INITIAL_SNAPSHOT_PENDING, 200, 220); ASSERT_EQ(response.status().code(), MetaServiceCode::OK) << response.status().msg(); EXPECT_EQ(get_latest_offset(2001).offset_tso(), 220); ASSERT_EQ(service_->txn_kv()->create_txn(&txn), TxnErrorCode::TXN_OK); std::string value; Versionstamp offset_version; ASSERT_EQ( versioned_get(txn.get(), versioned::table_stream_offset_key( {instance_id_, identity_.base_db_id(), identity_.base_table_id(), identity_.stream_db_id(), identity_.stream_id(), 2001}), &offset_version, &value), TxnErrorCode::TXN_OK); TableStreamOffsetPB versioned_offset; ASSERT_TRUE(versioned_offset.ParseFromString(value)); EXPECT_EQ(versioned_offset.offset_tso(), 220); OperationLogPB operation_log; Versionstamp log_version; ASSERT_EQ(read_operation_log(txn.get(), versioned::log_key({instance_id_}), &log_version, &operation_log), TxnErrorCode::TXN_OK); ASSERT_TRUE(operation_log.has_commit_txn()); ASSERT_EQ(operation_log.commit_txn().table_stream_offset_gc_size(), 1); const TableStreamPartitionSetPB& offset_gc = operation_log.commit_txn().table_stream_offset_gc(0); EXPECT_EQ(offset_gc.identity().SerializeAsString(), identity_.SerializeAsString()); ASSERT_EQ(offset_gc.partition_ids_size(), 1); EXPECT_EQ(offset_gc.partition_ids(0), 2001); } TEST_F(MetaServiceTableStreamTest, CommitRejectsLatestOffsetWithoutVersionedOffset) { set_multi_version_status(MULTI_VERSION_READ_WRITE); std::unique_ptr txn; ASSERT_EQ(service_->txn_kv()->create_txn(&txn), TxnErrorCode::TXN_OK); put_versioned_partition_state(txn.get(), 2001, 8, 130, std::nullopt); put_latest_partition_state(txn.get(), 2001, 8, 130, 100); ASSERT_EQ(txn->commit(), TxnErrorCode::TXN_OK); int64_t txn_id = begin_target_transaction("consume-inconsistent-local-offset"); CommitTxnResponse response = consume_partition(txn_id, 2001, TABLE_STREAM_OFFSET_UNKNOWN, 0, 120); EXPECT_EQ(response.status().code(), MetaServiceCode::INVALID_ARGUMENT); EXPECT_EQ(get_latest_offset(2001).offset_tso(), 100); } TEST_F(MetaServiceTableStreamTest, CommitRejectsDifferentLatestAndVersionedOffsetValues) { set_multi_version_status(MULTI_VERSION_READ_WRITE); std::unique_ptr txn; ASSERT_EQ(service_->txn_kv()->create_txn(&txn), TxnErrorCode::TXN_OK); put_versioned_partition_state(txn.get(), 2001, 8, 130, std::nullopt); TableStreamOffsetPB latest_offset; latest_offset.set_partition_id(2001); latest_offset.set_state(TABLE_STREAM_OFFSET_CONSUMED); latest_offset.set_offset_tso(100); latest_offset.set_last_consumption_time_ms(1234); TableStreamOffsetKeyInfo key_info {instance_id_, identity_.base_db_id(), identity_.base_table_id(), identity_.stream_db_id(), identity_.stream_id(), 2001}; txn->put(table_stream_offset_key(key_info), latest_offset.SerializeAsString()); TableStreamOffsetPB versioned_offset = latest_offset; versioned_offset.set_last_consumption_time_ms(1235); versioned_put(txn.get(), versioned::table_stream_offset_key(versioned::TableStreamOffsetKeyInfo(key_info)), Versionstamp(43, 0), versioned_offset.SerializeAsString()); ASSERT_EQ(txn->commit(), TxnErrorCode::TXN_OK); int64_t txn_id = begin_target_transaction("consume-inconsistent-offset-values"); CommitTxnResponse response = consume_partition(txn_id, 2001, TABLE_STREAM_OFFSET_CONSUMED, 100, 120); EXPECT_EQ(response.status().code(), MetaServiceCode::INVALID_ARGUMENT); EXPECT_EQ(get_latest_offset(2001).last_consumption_time_ms(), 1234); } TEST_F(MetaServiceTableStreamTest, CommitRejectsUnsupportedModeAndMissingCommitTso) { set_multi_version_status(MULTI_VERSION_DISABLED); std::unique_ptr txn; ASSERT_EQ(service_->txn_kv()->create_txn(&txn), TxnErrorCode::TXN_OK); VersionPB version; version.set_version(8); txn->put(partition_version_key( {instance_id_, identity_.base_db_id(), identity_.base_table_id(), 2001}), version.SerializeAsString()); TableStreamOffsetPB offset; offset.set_partition_id(2001); offset.set_state(TABLE_STREAM_OFFSET_CONSUMED); offset.set_offset_tso(100); txn->put(table_stream_offset_key({instance_id_, identity_.base_db_id(), identity_.base_table_id(), identity_.stream_db_id(), identity_.stream_id(), 2001}), offset.SerializeAsString()); ASSERT_EQ(txn->commit(), TxnErrorCode::TXN_OK); int64_t txn_id = begin_target_transaction("consume-missing-visible-tso"); CommitTxnResponse response = consume_partition(txn_id, 2001, TABLE_STREAM_OFFSET_CONSUMED, 100, 110); EXPECT_EQ(response.status().code(), MetaServiceCode::VERSION_NOT_FOUND); EXPECT_EQ(get_latest_offset(2001).offset_tso(), 100); set_multi_version_status(MULTI_VERSION_ENABLED); txn_id = begin_target_transaction("consume-enabled-mode"); response = consume_partition(txn_id, 2001, TABLE_STREAM_OFFSET_CONSUMED, 100, 110); EXPECT_EQ(response.status().code(), MetaServiceCode::INVALID_ARGUMENT); EXPECT_EQ(get_latest_offset(2001).offset_tso(), 100); } TEST_F(MetaServiceTableStreamTest, CommitRejectsUnsupportedTxnModesAndForcesImmediate) { set_multi_version_status(MULTI_VERSION_DISABLED); std::unique_ptr txn; ASSERT_EQ(service_->txn_kv()->create_txn(&txn), TxnErrorCode::TXN_OK); put_latest_partition_state(txn.get(), 2001, 8, 130, 100); ASSERT_EQ(txn->commit(), TxnErrorCode::TXN_OK); CommitTxnRequest request = make_consume_request(begin_target_transaction("consume-2pc"), 2001, TABLE_STREAM_OFFSET_CONSUMED, 100, 120); request.set_is_2pc(true); EXPECT_EQ(commit_transaction(request).status().code(), MetaServiceCode::INVALID_ARGUMENT); request = make_consume_request(begin_target_transaction("consume-txn-load"), 2001, TABLE_STREAM_OFFSET_CONSUMED, 100, 120); request.set_is_txn_load(true); EXPECT_EQ(commit_transaction(request).status().code(), MetaServiceCode::INVALID_ARGUMENT); request = make_consume_request(begin_target_transaction("consume-sub-txn"), 2001, TABLE_STREAM_OFFSET_CONSUMED, 100, 120); request.add_sub_txn_infos()->set_sub_txn_id(1); EXPECT_EQ(commit_transaction(request).status().code(), MetaServiceCode::INVALID_ARGUMENT); constexpr int64_t kTargetDbId = 3001; constexpr int64_t kTargetTableId = 3002; constexpr int64_t kTargetIndexId = 3003; constexpr int64_t kTargetPartitionId = 4001; constexpr int64_t kTargetTabletId = 5001; create_target_tablet(kTargetDbId, kTargetTableId, kTargetIndexId, kTargetPartitionId, kTargetTabletId); const int old_rowset_threshold = config::txn_lazy_commit_rowsets_thresold; const int old_fuzzy_possibility = config::cloud_txn_lazy_commit_fuzzy_possibility; const bool old_lazy_commit_enabled = config::enable_cloud_txn_lazy_commit; config::txn_lazy_commit_rowsets_thresold = 0; config::cloud_txn_lazy_commit_fuzzy_possibility = 100; config::enable_cloud_txn_lazy_commit = true; DORIS_CLOUD_DEFER { config::txn_lazy_commit_rowsets_thresold = old_rowset_threshold; config::cloud_txn_lazy_commit_fuzzy_possibility = old_fuzzy_possibility; config::enable_cloud_txn_lazy_commit = old_lazy_commit_enabled; SyncPoint::get_instance()->clear_all_call_backs(); SyncPoint::get_instance()->clear_trace(); SyncPoint::get_instance()->disable_processing(); }; bool eventually_committed = false; SyncPoint::get_instance()->set_call_back("commit_txn_eventually::finish", [&](auto&&) { eventually_committed = true; }); SyncPoint::get_instance()->enable_processing(); const int64_t immediate_txn_id = begin_target_transaction("consume-immediate"); stage_target_rowset(immediate_txn_id, kTargetPartitionId, kTargetTabletId); request = make_consume_request(immediate_txn_id, 2001, TABLE_STREAM_OFFSET_CONSUMED, 100, 120); request.set_enable_txn_lazy_commit(true); request.set_commit_tso(999); CommitTxnResponse response = commit_transaction(request); ASSERT_EQ(response.status().code(), MetaServiceCode::OK) << response.status().msg(); EXPECT_FALSE(response.is_lazy_commit()); EXPECT_FALSE(eventually_committed); EXPECT_EQ(get_latest_offset(2001).offset_tso(), 120); ASSERT_EQ(service_->txn_kv()->create_txn(&txn), TxnErrorCode::TXN_OK); std::string value; EXPECT_EQ(txn->get(meta_rowset_tmp_key({instance_id_, immediate_txn_id, kTargetTabletId}), &value), TxnErrorCode::TXN_KEY_NOT_FOUND); EXPECT_EQ(txn->get(meta_rowset_key({instance_id_, kTargetTabletId, 2}), &value), TxnErrorCode::TXN_OK); } TEST_F(MetaServiceTableStreamTest, CommitTxnTooLargeDoesNotFallBackToLazyCommit) { set_multi_version_status(MULTI_VERSION_DISABLED); constexpr int64_t kSourcePartitionId = 2001; constexpr int64_t kTargetDbId = 3001; constexpr int64_t kTargetTableId = 3002; constexpr int64_t kTargetIndexId = 3003; constexpr int64_t kTargetPartitionId = 4001; constexpr int64_t kTargetTabletId = 5001; std::unique_ptr txn; ASSERT_EQ(service_->txn_kv()->create_txn(&txn), TxnErrorCode::TXN_OK); put_latest_partition_state(txn.get(), kSourcePartitionId, 8, 130, 100); ASSERT_EQ(txn->commit(), TxnErrorCode::TXN_OK); create_target_tablet(kTargetDbId, kTargetTableId, kTargetIndexId, kTargetPartitionId, kTargetTabletId); const int64_t txn_id = begin_target_transaction("consume-too-large"); stage_target_rowset(txn_id, kTargetPartitionId, kTargetTabletId); const int old_rowset_threshold = config::txn_lazy_commit_rowsets_thresold; const int old_fuzzy_possibility = config::cloud_txn_lazy_commit_fuzzy_possibility; const bool old_lazy_commit_enabled = config::enable_cloud_txn_lazy_commit; config::txn_lazy_commit_rowsets_thresold = 0; config::cloud_txn_lazy_commit_fuzzy_possibility = 100; config::enable_cloud_txn_lazy_commit = true; DORIS_CLOUD_DEFER { config::txn_lazy_commit_rowsets_thresold = old_rowset_threshold; config::cloud_txn_lazy_commit_fuzzy_possibility = old_fuzzy_possibility; config::enable_cloud_txn_lazy_commit = old_lazy_commit_enabled; SyncPoint::get_instance()->clear_all_call_backs(); SyncPoint::get_instance()->clear_trace(); SyncPoint::get_instance()->disable_processing(); }; bool immediate_commit_injected = false; bool eventually_committed = false; SyncPoint::get_instance()->set_call_back( "commit_txn_immediately::before_commit", [&](auto&& args) { auto* err = try_any_cast(args[0]); *err = TxnErrorCode::TXN_BYTES_TOO_LARGE; auto* code = try_any_cast(args[1]); *code = cast_as(*err); auto* pred = try_any_cast(args.back()); *pred = true; immediate_commit_injected = true; }); SyncPoint::get_instance()->set_call_back("commit_txn_eventually::finish", [&](auto&&) { eventually_committed = true; }); SyncPoint::get_instance()->enable_processing(); CommitTxnRequest request = make_consume_request(txn_id, kSourcePartitionId, TABLE_STREAM_OFFSET_CONSUMED, 100, 120); request.set_enable_txn_lazy_commit(true); request.set_commit_tso(999); const CommitTxnResponse response = commit_transaction(request); EXPECT_EQ(response.status().code(), MetaServiceCode::INVALID_ARGUMENT); EXPECT_NE(response.status().msg().find("table stream offset updates cannot use lazy commit"), std::string::npos); EXPECT_TRUE(immediate_commit_injected); EXPECT_FALSE(eventually_committed); EXPECT_FALSE(response.is_lazy_commit()); EXPECT_EQ(get_latest_offset(kSourcePartitionId).offset_tso(), 100); ASSERT_EQ(service_->txn_kv()->create_txn(&txn), TxnErrorCode::TXN_OK); std::string value; EXPECT_EQ(txn->get(meta_rowset_tmp_key({instance_id_, txn_id, kTargetTabletId}), &value), TxnErrorCode::TXN_OK); EXPECT_EQ(txn->get(meta_rowset_key({instance_id_, kTargetTabletId, 2}), &value), TxnErrorCode::TXN_KEY_NOT_FOUND); ASSERT_EQ(txn->get(partition_version_key( {instance_id_, kTargetDbId, kTargetTableId, kTargetPartitionId}), &value), TxnErrorCode::TXN_OK); VersionPB target_version; ASSERT_TRUE(target_version.ParseFromString(value)); EXPECT_EQ(target_version.version(), 1); } TEST_F(MetaServiceTableStreamTest, CommitWritesOffsetsForEachMultiVersionMode) { struct ModeCase { MultiVersionStatus mode; const char* label; bool succeeds; bool writes_versioned; }; const std::vector mode_cases = { {MULTI_VERSION_DISABLED, "disabled", true, false}, {MULTI_VERSION_WRITE_ONLY, "write-only", true, true}, {MULTI_VERSION_READ_WRITE, "read-write", true, true}, {MULTI_VERSION_ENABLED, "enabled", false, false}, }; for (size_t i = 0; i < mode_cases.size(); ++i) { const ModeCase& mode_case = mode_cases[i]; SCOPED_TRACE(mode_case.label); const int64_t partition_id = 2101 + i; const std::string log_prefix = versioned::log_key({instance_id_}); if (mode_case.mode == MULTI_VERSION_READ_WRITE) { set_multi_version_status(mode_case.mode); std::unique_ptr txn; ASSERT_EQ(service_->txn_kv()->create_txn(&txn), TxnErrorCode::TXN_OK); put_versioned_partition_state(txn.get(), partition_id, 8, 130, std::nullopt); TableStreamOffsetPB latest_offset; latest_offset.set_partition_id(partition_id); latest_offset.set_state(TABLE_STREAM_OFFSET_INITIAL_SNAPSHOT_PENDING); latest_offset.set_offset_tso(100); const TableStreamOffsetKeyInfo key_info {instance_id_, identity_.base_db_id(), identity_.base_table_id(), identity_.stream_db_id(), identity_.stream_id(), partition_id}; txn->put(table_stream_offset_key(key_info), latest_offset.SerializeAsString()); versioned_put(txn.get(), versioned::table_stream_offset_key( versioned::TableStreamOffsetKeyInfo(key_info)), latest_offset.SerializeAsString()); ASSERT_EQ(txn->commit(), TxnErrorCode::TXN_OK); } else { set_multi_version_status(MULTI_VERSION_DISABLED); std::unique_ptr txn; ASSERT_EQ(service_->txn_kv()->create_txn(&txn), TxnErrorCode::TXN_OK); put_latest_partition_state(txn.get(), partition_id, 8, 130, 100); ASSERT_EQ(txn->commit(), TxnErrorCode::TXN_OK); if (mode_case.mode != MULTI_VERSION_DISABLED) { set_multi_version_status(mode_case.mode); } } const size_t log_count_before = count_keys(log_prefix, lexical_end(log_prefix)); const TableStreamOffsetStatePB expected_state = mode_case.mode == MULTI_VERSION_READ_WRITE ? TABLE_STREAM_OFFSET_INITIAL_SNAPSHOT_PENDING : TABLE_STREAM_OFFSET_CONSUMED; const CommitTxnResponse response = consume_partition( begin_target_transaction(fmt::format("consume-mode-{}", mode_case.label)), partition_id, expected_state, 100, 120); EXPECT_EQ(response.status().code(), mode_case.succeeds ? MetaServiceCode::OK : MetaServiceCode::INVALID_ARGUMENT) << response.status().msg(); EXPECT_EQ(get_latest_offset(partition_id).offset_tso(), mode_case.succeeds ? 120 : 100); std::unique_ptr txn; ASSERT_EQ(service_->txn_kv()->create_txn(&txn), TxnErrorCode::TXN_OK); Versionstamp value_version; std::string value; const TxnErrorCode versioned_err = versioned_get( txn.get(), versioned::table_stream_offset_key( {instance_id_, identity_.base_db_id(), identity_.base_table_id(), identity_.stream_db_id(), identity_.stream_id(), partition_id}), &value_version, &value); if (mode_case.writes_versioned) { ASSERT_EQ(versioned_err, TxnErrorCode::TXN_OK); TableStreamOffsetPB versioned_offset; ASSERT_TRUE(versioned_offset.ParseFromString(value)); EXPECT_EQ(versioned_offset.state(), TABLE_STREAM_OFFSET_CONSUMED); EXPECT_EQ(versioned_offset.offset_tso(), 120); } else { EXPECT_EQ(versioned_err, TxnErrorCode::TXN_KEY_NOT_FOUND); } EXPECT_EQ(count_keys(log_prefix, lexical_end(log_prefix)), log_count_before + (mode_case.writes_versioned ? 1 : 0)); } } TEST_F(MetaServiceTableStreamTest, DropWritesTypedRecycleIndexInDisabledMode) { set_multi_version_status(MULTI_VERSION_DISABLED); std::unique_ptr txn; ASSERT_EQ(service_->txn_kv()->create_txn(&txn), TxnErrorCode::TXN_OK); IndexRequest request; request.set_cloud_unique_id(cloud_unique_id_); request.add_index_ids(identity_.stream_id()); request.set_db_id(identity_.base_db_id()); request.set_table_id(identity_.base_table_id()); request.set_object_type(IndexObjectTypePB::TABLE_STREAM); request.set_stream_db_id(identity_.stream_db_id()); request.set_expiration(0); IndexResponse response; brpc::Controller controller; service_->drop_index(&controller, &request, &response, nullptr); ASSERT_EQ(response.status().code(), MetaServiceCode::OK) << response.status().msg(); ASSERT_EQ(service_->txn_kv()->create_txn(&txn), TxnErrorCode::TXN_OK); std::string value; ASSERT_EQ(txn->get(recycle_index_key({instance_id_, identity_.stream_id()}), &value), TxnErrorCode::TXN_OK); RecycleIndexPB recycle_index; ASSERT_TRUE(recycle_index.ParseFromString(value)); EXPECT_EQ(recycle_index.state(), RecycleIndexPB::DROPPED); EXPECT_EQ(recycle_index.object_type(), IndexObjectTypePB::TABLE_STREAM); EXPECT_EQ(recycle_index.db_id(), identity_.base_db_id()); EXPECT_EQ(recycle_index.table_id(), identity_.base_table_id()); EXPECT_EQ(recycle_index.stream_db_id(), identity_.stream_db_id()); } TEST_F(MetaServiceTableStreamTest, DropWritesTypedOperationLogInVersionedMode) { set_multi_version_status(MULTI_VERSION_READ_WRITE); std::unique_ptr txn; ASSERT_EQ(service_->txn_kv()->create_txn(&txn), TxnErrorCode::TXN_OK); IndexRequest request; request.set_cloud_unique_id(cloud_unique_id_); request.add_index_ids(identity_.stream_id()); request.set_db_id(identity_.base_db_id()); request.set_table_id(identity_.base_table_id()); request.set_object_type(IndexObjectTypePB::TABLE_STREAM); request.set_stream_db_id(identity_.stream_db_id()); request.set_expiration(0); IndexResponse response; brpc::Controller controller; service_->drop_index(&controller, &request, &response, nullptr); ASSERT_EQ(response.status().code(), MetaServiceCode::OK) << response.status().msg(); ASSERT_EQ(service_->txn_kv()->create_txn(&txn), TxnErrorCode::TXN_OK); OperationLogPB operation_log; Versionstamp log_version; ASSERT_EQ(read_operation_log(txn.get(), versioned::log_key({instance_id_}), &log_version, &operation_log), TxnErrorCode::TXN_OK); ASSERT_TRUE(operation_log.has_drop_index()); ASSERT_TRUE(operation_log.has_min_timestamp()); EXPECT_EQ(operation_log.min_timestamp(), static_cast(std::numeric_limits::max())); const DropIndexLogPB& drop_index = operation_log.drop_index(); EXPECT_EQ(drop_index.object_type(), IndexObjectTypePB::TABLE_STREAM); EXPECT_EQ(drop_index.db_id(), identity_.base_db_id()); EXPECT_EQ(drop_index.table_id(), identity_.base_table_id()); EXPECT_EQ(drop_index.stream_db_id(), identity_.stream_db_id()); ASSERT_EQ(drop_index.index_ids_size(), 1); EXPECT_EQ(drop_index.index_ids(0), identity_.stream_id()); std::string value; EXPECT_EQ(txn->get(recycle_index_key({instance_id_, identity_.stream_id()}), &value, true), TxnErrorCode::TXN_KEY_NOT_FOUND); } TEST_F(MetaServiceTableStreamTest, DropRetryAfterMetaServiceRestartConverges) { const bool old_force_immediate_recycle = config::force_immediate_recycle; config::force_immediate_recycle = true; DORIS_CLOUD_DEFER { config::force_immediate_recycle = old_force_immediate_recycle; }; set_multi_version_status(MULTI_VERSION_READ_WRITE); constexpr int64_t kPartitionId = 2001; put_auto_versioned_offset(instance_id_, identity_, kPartitionId, 100, true); IndexRequest request; request.set_cloud_unique_id(cloud_unique_id_); request.add_index_ids(identity_.stream_id()); request.set_db_id(identity_.base_db_id()); request.set_table_id(identity_.base_table_id()); request.set_object_type(IndexObjectTypePB::TABLE_STREAM); request.set_stream_db_id(identity_.stream_db_id()); request.set_expiration(0); // The first DROP succeeds, but its response is lost before the caller observes it. IndexResponse lost_response; brpc::Controller first_drop_controller; service_->drop_index(&first_drop_controller, &request, &lost_response, nullptr); ASSERT_EQ(lost_response.status().code(), MetaServiceCode::OK) << lost_response.status().msg(); const std::string operation_log_prefix = versioned::log_key({instance_id_}); const size_t first_drop_log_count = count_keys(operation_log_prefix, lexical_end(operation_log_prefix)); ASSERT_GT(first_drop_log_count, 0); auto restarted_service = restart_meta_service(); IndexResponse retry_response; brpc::Controller retry_drop_controller; restarted_service->drop_index(&retry_drop_controller, &request, &retry_response, nullptr); ASSERT_EQ(retry_response.status().code(), MetaServiceCode::OK) << retry_response.status().msg(); EXPECT_GE(count_keys(operation_log_prefix, lexical_end(operation_log_prefix)), first_drop_log_count); InstanceInfoPB instance; instance.set_instance_id(instance_id_); instance.set_status(InstanceInfoPB::NORMAL); instance.set_multi_version_status(MULTI_VERSION_READ_WRITE); InstanceRecycler recycler(service_->txn_kv(), instance, RecyclerThreadPoolGroup {}, std::make_shared(service_->txn_kv())); ASSERT_EQ(recycler.init(), 0); ASSERT_EQ(recycler.recycle_operation_logs(), 0); EXPECT_EQ(count_keys(operation_log_prefix, lexical_end(operation_log_prefix)), 0); std::unique_ptr txn; ASSERT_EQ(service_->txn_kv()->create_txn(&txn), TxnErrorCode::TXN_OK); std::string value; ASSERT_EQ(txn->get(recycle_index_key({instance_id_, identity_.stream_id()}), &value, true), TxnErrorCode::TXN_OK); RecycleIndexPB recycle_index; ASSERT_TRUE(recycle_index.ParseFromString(value)); EXPECT_EQ(recycle_index.state(), RecycleIndexPB::DROPPED); EXPECT_EQ(recycle_index.object_type(), IndexObjectTypePB::TABLE_STREAM); ASSERT_EQ(recycler.recycle_indexes(), 0); ASSERT_EQ(service_->txn_kv()->create_txn(&txn), TxnErrorCode::TXN_OK); EXPECT_EQ(txn->get(recycle_index_key({instance_id_, identity_.stream_id()}), &value, true), TxnErrorCode::TXN_KEY_NOT_FOUND); const std::string latest_offset_prefix = table_stream_offset_key_prefix( instance_id_, identity_.base_db_id(), identity_.base_table_id(), identity_.stream_db_id(), identity_.stream_id()); const std::string versioned_offset_prefix = versioned::table_stream_offset_key_prefix( instance_id_, identity_.base_db_id(), identity_.base_table_id(), identity_.stream_db_id(), identity_.stream_id()); EXPECT_EQ(count_keys(latest_offset_prefix, lexical_end(latest_offset_prefix)), 0); EXPECT_EQ(count_keys(versioned_offset_prefix, lexical_end(versioned_offset_prefix)), 0); } TEST_F(MetaServiceTableStreamTest, DropUsesEarliestLocalVersionedOffset) { const std::string source_instance_id = "table_stream_drop_source"; const Versionstamp source_version = put_auto_versioned_offset(source_instance_id, identity_, 2001, 10); set_clone_source(source_instance_id, Versionstamp(source_version.version() + 1, 0)); // Partition 2002 sorts after partition 2001 but contains the earliest local version. const Versionstamp min_local_version = put_auto_versioned_offset(instance_id_, identity_, 2002, 30); put_auto_versioned_offset(instance_id_, identity_, 2001, 10); put_auto_versioned_offset(instance_id_, identity_, 2001, 20); IndexRequest request; request.set_cloud_unique_id(cloud_unique_id_); request.add_index_ids(identity_.stream_id()); request.set_db_id(identity_.base_db_id()); request.set_table_id(identity_.base_table_id()); request.set_object_type(IndexObjectTypePB::TABLE_STREAM); request.set_stream_db_id(identity_.stream_db_id()); request.set_expiration(0); IndexResponse response; brpc::Controller controller; service_->drop_index(&controller, &request, &response, nullptr); ASSERT_EQ(response.status().code(), MetaServiceCode::OK) << response.status().msg(); std::unique_ptr txn; ASSERT_EQ(service_->txn_kv()->create_txn(&txn), TxnErrorCode::TXN_OK); OperationLogPB operation_log; Versionstamp log_version; ASSERT_EQ(read_operation_log(txn.get(), versioned::log_key({instance_id_}), &log_version, &operation_log), TxnErrorCode::TXN_OK); ASSERT_TRUE(operation_log.has_drop_index()); ASSERT_TRUE(operation_log.has_min_timestamp()); EXPECT_LT(source_version.version(), min_local_version.version()); EXPECT_EQ(operation_log.min_timestamp(), min_local_version.version()); EXPECT_LT(operation_log.min_timestamp(), log_version.version()); } TEST_F(MetaServiceTableStreamTest, DropFindsMinimumInLargeVersionedOffsetHistory) { set_multi_version_status(MULTI_VERSION_READ_WRITE); const int default_full_range_batch_size = FullRangeGetOptions().batch_limit; std::unique_ptr txn; ASSERT_EQ(service_->txn_kv()->create_txn(&txn), TxnErrorCode::TXN_OK); TableStreamOffsetPB offset; offset.set_state(TABLE_STREAM_OFFSET_CONSUMED); offset.set_offset_tso(100); for (int i = 0; i < default_full_range_batch_size; ++i) { offset.set_partition_id(2001); versioned_put( txn.get(), versioned::table_stream_offset_key( {instance_id_, identity_.base_db_id(), identity_.base_table_id(), identity_.stream_db_id(), identity_.stream_id(), offset.partition_id()}), Versionstamp(1000 + i, 0), offset.SerializeAsString()); } // This key sorts after all partition 2001 history but carries the global minimum version. offset.set_partition_id(2002); versioned_put(txn.get(), versioned::table_stream_offset_key( {instance_id_, identity_.base_db_id(), identity_.base_table_id(), identity_.stream_db_id(), identity_.stream_id(), offset.partition_id()}), Versionstamp(50, 0), offset.SerializeAsString()); ASSERT_EQ(txn->commit(), TxnErrorCode::TXN_OK); IndexRequest request; request.set_cloud_unique_id(cloud_unique_id_); request.add_index_ids(identity_.stream_id()); request.set_db_id(identity_.base_db_id()); request.set_table_id(identity_.base_table_id()); request.set_object_type(IndexObjectTypePB::TABLE_STREAM); request.set_stream_db_id(identity_.stream_db_id()); request.set_expiration(0); IndexResponse response; brpc::Controller controller; service_->drop_index(&controller, &request, &response, nullptr); ASSERT_EQ(response.status().code(), MetaServiceCode::OK) << response.status().msg(); ASSERT_EQ(service_->txn_kv()->create_txn(&txn), TxnErrorCode::TXN_OK); OperationLogPB operation_log; Versionstamp log_version; ASSERT_EQ(read_operation_log(txn.get(), versioned::log_key({instance_id_}), &log_version, &operation_log), TxnErrorCode::TXN_OK); ASSERT_TRUE(operation_log.has_drop_index()); ASSERT_TRUE(operation_log.has_min_timestamp()); EXPECT_EQ(operation_log.min_timestamp(), 50); } TEST_F(MetaServiceTableStreamTest, DropAndRecycleCloneChildOffsets) { const bool old_force_immediate_recycle = config::force_immediate_recycle; config::force_immediate_recycle = true; DORIS_CLOUD_DEFER { config::force_immediate_recycle = old_force_immediate_recycle; }; const std::string source_instance_id = "table_stream_drop_recycle_source"; TableStreamIdentityPB local_identity = identity_; TableStreamIdentityPB inherited_identity = identity_; inherited_identity.set_stream_id(identity_.stream_id() + 1); put_auto_versioned_offset(source_instance_id, local_identity, 2001, 90); const Versionstamp source_version = put_auto_versioned_offset(source_instance_id, inherited_identity, 2001, 80); const Versionstamp source_snapshot = Versionstamp::next(source_version); set_clone_source(source_instance_id, source_snapshot); put_auto_versioned_offset(instance_id_, local_identity, 2001, 100, true); auto drop_stream = [&](const TableStreamIdentityPB& identity) { IndexRequest request; request.set_cloud_unique_id(cloud_unique_id_); request.add_index_ids(identity.stream_id()); request.set_db_id(identity.base_db_id()); request.set_table_id(identity.base_table_id()); request.set_object_type(IndexObjectTypePB::TABLE_STREAM); request.set_stream_db_id(identity.stream_db_id()); request.set_expiration(0); IndexResponse response; brpc::Controller controller; service_->drop_index(&controller, &request, &response, nullptr); EXPECT_EQ(response.status().code(), MetaServiceCode::OK) << response.status().msg(); }; drop_stream(local_identity); drop_stream(inherited_identity); InstanceInfoPB child_instance; child_instance.set_instance_id(instance_id_); child_instance.set_status(InstanceInfoPB::NORMAL); child_instance.set_multi_version_status(MULTI_VERSION_READ_WRITE); child_instance.set_source_instance_id(source_instance_id); child_instance.set_source_snapshot_id(SnapshotManager::serialize_snapshot_id(source_snapshot)); InstanceRecycler recycler(service_->txn_kv(), child_instance, RecyclerThreadPoolGroup {}, std::make_shared(service_->txn_kv())); ASSERT_EQ(recycler.init(), 0); ASSERT_EQ(recycler.recycle_operation_logs(), 0); auto key_exists = [&](std::string_view key) { std::unique_ptr txn; EXPECT_EQ(service_->txn_kv()->create_txn(&txn), TxnErrorCode::TXN_OK); std::string value; return txn->get(key, &value, true) == TxnErrorCode::TXN_OK; }; EXPECT_TRUE(key_exists(recycle_index_key({instance_id_, local_identity.stream_id()}))); EXPECT_TRUE(key_exists(recycle_index_key({instance_id_, inherited_identity.stream_id()}))); ASSERT_EQ(recycler.recycle_indexes(), 0); const auto offset_count = [&](const std::string& prefix) { std::unique_ptr txn; EXPECT_EQ(service_->txn_kv()->create_txn(&txn), TxnErrorCode::TXN_OK); std::unique_ptr iter; EXPECT_EQ(txn->get(prefix, lexical_end(prefix), &iter, true, 0), TxnErrorCode::TXN_OK); return iter->size(); }; EXPECT_EQ(offset_count(table_stream_offset_key_prefix( instance_id_, local_identity.base_db_id(), local_identity.base_table_id(), local_identity.stream_db_id(), local_identity.stream_id())), 0); EXPECT_EQ(offset_count(versioned::table_stream_offset_key_prefix( instance_id_, local_identity.base_db_id(), local_identity.base_table_id(), local_identity.stream_db_id(), local_identity.stream_id())), 0); EXPECT_EQ(offset_count(versioned::table_stream_offset_key_prefix( instance_id_, inherited_identity.base_db_id(), inherited_identity.base_table_id(), inherited_identity.stream_db_id(), inherited_identity.stream_id())), 0); EXPECT_FALSE(key_exists(recycle_index_key({instance_id_, local_identity.stream_id()}))); EXPECT_FALSE(key_exists(recycle_index_key({instance_id_, inherited_identity.stream_id()}))); EXPECT_GT( offset_count(versioned::table_stream_offset_key_prefix( source_instance_id, local_identity.base_db_id(), local_identity.base_table_id(), local_identity.stream_db_id(), local_identity.stream_id())), 0); EXPECT_GT(offset_count(versioned::table_stream_offset_key_prefix( source_instance_id, inherited_identity.base_db_id(), inherited_identity.base_table_id(), inherited_identity.stream_db_id(), inherited_identity.stream_id())), 0); } } // namespace doris::cloud