| // 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 <butil/guid.h> | |
| #include <fmt/core.h> | |
| #include <gen_cpp/olap_file.pb.h> | |
| #include <gtest/gtest-spi.h> | |
| #include <gtest/gtest.h> | |
| #include <algorithm> | |
| #include <atomic> | |
| #include <chrono> | |
| #include <cstddef> | |
| #include <cstdint> | |
| #include <cstdlib> | |
| #include <exception> | |
| #include <iostream> | |
| #include <limits> | |
| #include <map> | |
| #include <memory> | |
| #include <string> | |
| #include <utility> | |
| #include "common/bvars.h" | |
| #include "common/config.h" | |
| #include "common/defer.h" | |
| #include "common/simple_thread_pool.h" | |
| #include "cpp/sync_point.h" | |
| #include "meta-service/meta_service_schema.h" | |
| #include "meta-service/txn_lazy_committer.h" | |
| #include "meta-store/blob_message.h" | |
| #include "meta-store/keys.h" | |
| #include "meta-store/mem_txn_kv.h" | |
| #include "meta-store/txn_kv.h" | |
| #include "meta-store/txn_kv_error.h" | |
| #include "recycler/recycler.h" | |
| #include "recycler/s3_accessor.h" | |
| #include "recycler/util.h" | |
| namespace doris::cloud { | |
| namespace { | |
| const std::string kBenchmarkInstanceId = "recycler_benchmark_instance"; | |
| const std::string kBenchmarkResourceId = "recycler_benchmark_resource"; | |
| std::string get_env(const char* name) { | |
| const char* value = std::getenv(name); | |
| return value == nullptr ? "" : value; | |
| } | |
| std::string get_env_with_fallback(const char* primary, const char* fallback) { | |
| const char* value = std::getenv(primary); | |
| return value == nullptr ? get_env(fallback) : std::string(value); | |
| } | |
| struct BenchmarkS3Config { | |
| bool enabled = false; | |
| std::string access_key; | |
| std::string secret_key; | |
| std::string role_arn; | |
| std::string external_id; | |
| std::string endpoint; | |
| std::string provider; | |
| std::string bucket; | |
| std::string region; | |
| std::string prefix; | |
| }; | |
| BenchmarkS3Config load_benchmark_s3_config() { | |
| BenchmarkS3Config config; | |
| config.enabled = get_env("ENABLE_S3_CLIENT") == "1"; | |
| if (!config.enabled) { | |
| return config; | |
| } | |
| config.access_key = get_env("S3_AK"); | |
| config.secret_key = get_env("S3_SK"); | |
| config.role_arn = get_env("AWS_ROLE_ARN"); | |
| config.external_id = get_env("AWS_EXTERNAL_ID"); | |
| config.endpoint = get_env_with_fallback("S3_ENDPOINT", "AWS_ENDPOINT"); | |
| config.provider = get_env("S3_PROVIDER"); | |
| config.bucket = get_env_with_fallback("S3_BUCKET", "AWS_BUCKET"); | |
| config.region = get_env_with_fallback("S3_REGION", "AWS_REGION"); | |
| config.prefix = get_env_with_fallback("S3_PREFIX", "AWS_PREFIX"); | |
| return config; | |
| } | |
| void set_obj_store_provider(const std::string& provider, ObjectStoreInfoPB* obj_info) { | |
| if (provider == "AZURE") { | |
| obj_info->set_provider(ObjectStoreInfoPB_Provider_AZURE); | |
| } else if (provider == "GCS") { | |
| obj_info->set_provider(ObjectStoreInfoPB_Provider_GCP); | |
| } else { | |
| obj_info->set_provider(ObjectStoreInfoPB_Provider_S3); | |
| } | |
| } | |
| // Number of recyclable rowsets seeded per branch. | |
| constexpr int64_t kRowsetsPerBranch = 5000; | |
| // Commit the seeded recycle rowset KVs in batches to keep each txn small. | |
| constexpr int64_t kSeedCommitBatch = 2000; | |
| constexpr int64_t kRowsetsPerPackedFile = 2; | |
| constexpr int64_t kPackedFileCount = kRowsetsPerBranch / kRowsetsPerPackedFile; | |
| constexpr int kMaxPackedRecyclePasses = 10; | |
| static_assert(kRowsetsPerBranch % kRowsetsPerPackedFile == 0); | |
| static_assert(kSeedCommitBatch % kRowsetsPerPackedFile == 0); | |
| constexpr int64_t kBenchmarkIndexId = 20000; | |
| constexpr int32_t kBenchmarkSchemaVersion = 1; | |
| constexpr int64_t kBenchmarkDbId = 10000; | |
| struct RecycleRowsetConfigGuard { | |
| RecycleRowsetConfigGuard() | |
| : old_enable_mark(config::enable_mark_delete_rowset_before_recycle), | |
| old_enable_abort(config::enable_abort_txn_and_job_for_delete_rowset_before_recycle) { | |
| config::enable_mark_delete_rowset_before_recycle = true; | |
| config::enable_abort_txn_and_job_for_delete_rowset_before_recycle = true; | |
| } | |
| ~RecycleRowsetConfigGuard() { | |
| config::enable_mark_delete_rowset_before_recycle = old_enable_mark; | |
| config::enable_abort_txn_and_job_for_delete_rowset_before_recycle = old_enable_abort; | |
| } | |
| bool old_enable_mark; | |
| bool old_enable_abort; | |
| }; | |
| doris::TabletSchemaCloudPB make_benchmark_schema() { | |
| doris::TabletSchemaCloudPB schema; | |
| schema.set_schema_version(kBenchmarkSchemaVersion); | |
| schema.set_inverted_index_storage_format(InvertedIndexStorageFormatPB::V1); | |
| auto* index = schema.add_index(); | |
| index->set_index_id(1); | |
| index->set_index_type(IndexType::INVERTED); | |
| return schema; | |
| } | |
| int put_benchmark_schema(TxnKv* txn_kv, const std::string& instance_id) { | |
| std::unique_ptr<Transaction> txn; | |
| if (txn_kv->create_txn(&txn) != TxnErrorCode::TXN_OK) { | |
| return -1; | |
| } | |
| std::string schema_key; | |
| meta_schema_key({instance_id, kBenchmarkIndexId, kBenchmarkSchemaVersion}, &schema_key); | |
| auto schema = make_benchmark_schema(); | |
| MetaServiceCode code = MetaServiceCode::OK; | |
| std::string msg; | |
| put_schema_kv(code, msg, txn.get(), schema_key, schema); | |
| if (code != MetaServiceCode::OK) { | |
| return -1; | |
| } | |
| return txn->commit() == TxnErrorCode::TXN_OK ? 0 : -1; | |
| } | |
| int remove_benchmark_schema(TxnKv* txn_kv, const std::string& instance_id) { | |
| std::unique_ptr<Transaction> txn; | |
| if (txn_kv->create_txn(&txn) != TxnErrorCode::TXN_OK) { | |
| return -1; | |
| } | |
| std::string schema_key; | |
| meta_schema_key({instance_id, kBenchmarkIndexId, kBenchmarkSchemaVersion}, &schema_key); | |
| ValueBuf schema_value; | |
| if (blob_get(txn.get(), schema_key, &schema_value) != TxnErrorCode::TXN_OK) { | |
| return -1; | |
| } | |
| schema_value.remove(txn.get()); | |
| return txn->commit() == TxnErrorCode::TXN_OK ? 0 : -1; | |
| } | |
| // Each value maps to one recyclable branch inside | |
| // InstanceRecycler::recycle_rowsets()::handle_rowset_kv. Both mark and abort | |
| // flags are enabled by RecycleRowsetConfigGuard for every scenario. | |
| enum class RecycleRowsetBranch { | |
| // Old-version RecycleRowsetPB (no `type`) whose resource_id is empty: the | |
| // recycler removes the KV only, without touching object storage. | |
| kLegacyEmptyResource, | |
| // Old-version RecycleRowsetPB with a real resource_id: recycled through the | |
| // single-rowset delete_rowset_data_by_prefix path. | |
| kLegacyWithResource, | |
| // Already-marked PREPARE rowset without a related txn/job: recycled directly. | |
| kPrepareDirect, | |
| // New PREPARE rowset queued to be marked as recycled first | |
| // (enable_mark_delete_rowset_before_recycle = true). | |
| kPrepareMark, | |
| // New PREPARE rowset carrying a load_id that triggers the abort-txn/job path | |
| // (enable_abort_txn_and_job_for_delete_rowset_before_recycle = true). | |
| kPrepareAbort, | |
| // New COMPACT/DROP rowset with segments: recycled via the batched | |
| // delete_rowset_data path. | |
| kCompactedWithData, | |
| // New COMPACT/DROP rowset without segments: treated as an empty rowset and | |
| // removed by KV delete only. | |
| kCompactedEmpty, | |
| }; | |
| // Build one RowsetMetaCloudPB for the given branch. Kept intentionally minimal | |
| // and self-contained so this benchmark does not depend on recycler_test.cpp. | |
| doris::RowsetMetaCloudPB make_rowset_meta(RecycleRowsetBranch branch, int64_t tablet_id, | |
| const std::string& rowset_id) { | |
| doris::RowsetMetaCloudPB meta; | |
| meta.set_rowset_id(0); // deprecated but required | |
| meta.set_rowset_id_v2(rowset_id); | |
| meta.set_tablet_id(tablet_id); | |
| meta.set_index_id(kBenchmarkIndexId); | |
| meta.set_schema_version(kBenchmarkSchemaVersion); | |
| meta.mutable_tablet_schema()->CopyFrom(make_benchmark_schema()); | |
| meta.set_start_version(2); | |
| meta.set_end_version(2); | |
| meta.set_data_disk_size(1024); | |
| meta.set_index_disk_size(512); | |
| meta.set_total_disk_size(1536); | |
| if (branch == RecycleRowsetBranch::kPrepareDirect) { | |
| meta.set_is_recycled(true); | |
| } | |
| switch (branch) { | |
| case RecycleRowsetBranch::kCompactedEmpty: | |
| meta.set_num_segments(0); | |
| break; | |
| default: | |
| meta.set_num_segments(1); | |
| break; | |
| } | |
| // Only the branches that reach delete_rowset_data need a resolvable resource. | |
| if (branch != RecycleRowsetBranch::kLegacyEmptyResource) { | |
| meta.set_resource_id(kBenchmarkResourceId); | |
| } | |
| if (branch == RecycleRowsetBranch::kPrepareAbort) { | |
| // A load_id makes make_related_txn_or_job_abort_task emit a TXN abort | |
| // task; end_version != 1 is required to enter the abort branch. | |
| meta.mutable_load_id()->set_hi(tablet_id); | |
| meta.mutable_load_id()->set_lo(1); | |
| meta.set_txn_id(tablet_id); | |
| } | |
| return meta; | |
| } | |
| // Wrap a RowsetMetaCloudPB into a RecycleRowsetPB shaped for the branch. | |
| RecycleRowsetPB make_recycle_rowset(RecycleRowsetBranch branch, | |
| const doris::RowsetMetaCloudPB& meta) { | |
| RecycleRowsetPB pb; | |
| pb.set_creation_time(1); // long expired once retention is 0 / immediate recycle | |
| pb.set_expiration(1); | |
| switch (branch) { | |
| case RecycleRowsetBranch::kLegacyEmptyResource: | |
| // Old-version layout: no `type`, resource_id left empty on purpose. | |
| pb.set_tablet_id(meta.tablet_id()); | |
| pb.set_resource_id(""); | |
| break; | |
| case RecycleRowsetBranch::kLegacyWithResource: | |
| // Old-version layout: no `type`, resource_id populated. | |
| pb.set_tablet_id(meta.tablet_id()); | |
| pb.set_resource_id(meta.resource_id()); | |
| break; | |
| case RecycleRowsetBranch::kPrepareDirect: | |
| case RecycleRowsetBranch::kPrepareMark: | |
| case RecycleRowsetBranch::kPrepareAbort: | |
| pb.set_type(RecycleRowsetPB::PREPARE); | |
| pb.mutable_rowset_meta()->CopyFrom(meta); | |
| break; | |
| case RecycleRowsetBranch::kCompactedWithData: | |
| case RecycleRowsetBranch::kCompactedEmpty: | |
| pb.set_type(RecycleRowsetPB::COMPACT); | |
| pb.mutable_rowset_meta()->CopyFrom(meta); | |
| if (branch == RecycleRowsetBranch::kCompactedWithData) { | |
| // Match production rowsets whose schema is stored separately in the schema KV. | |
| pb.mutable_rowset_meta()->clear_tablet_schema(); | |
| } | |
| break; | |
| } | |
| return pb; | |
| } | |
| enum class DeleteBitmapVersion { kNone, kV1, kV2 }; | |
| void put_benchmark_delete_bitmap(Transaction* txn, const std::string& instance_id, | |
| const doris::RowsetMetaCloudPB& meta, | |
| DeleteBitmapVersion version) { | |
| switch (version) { | |
| case DeleteBitmapVersion::kNone: | |
| break; | |
| case DeleteBitmapVersion::kV1: | |
| txn->put(meta_delete_bitmap_key({instance_id, meta.tablet_id(), meta.rowset_id_v2(), 0, 0}), | |
| "delete_bitmap_data"); | |
| break; | |
| case DeleteBitmapVersion::kV2: { | |
| // As with segment/index files, metadata is enough to exercise deletion of | |
| // the standalone .dbm file. Keep bitmap storage independent of data packing. | |
| DeleteBitmapStoragePB storage; | |
| storage.set_store_in_fdb(false); | |
| blob_put(txn, | |
| versioned::meta_delete_bitmap_key( | |
| {instance_id, meta.tablet_id(), meta.rowset_id_v2()}), | |
| storage, 0); | |
| break; | |
| } | |
| } | |
| } | |
| // Seed `count` recycle rowset KVs for one branch. Every rowset gets a distinct | |
| // tablet_id so the per-tablet recycle batch limit never truncates the workload. | |
| int seed_recycle_rowsets(TxnKv* txn_kv, const std::string& instance_id, RecycleRowsetBranch branch, | |
| int64_t count, int64_t tablet_id_base, bool write_schema_kv = true, | |
| DeleteBitmapVersion bitmap_version = DeleteBitmapVersion::kNone) { | |
| if (write_schema_kv && put_benchmark_schema(txn_kv, instance_id) != 0) { | |
| return -1; | |
| } | |
| std::unique_ptr<Transaction> txn; | |
| for (int64_t i = 0; i < count; ++i) { | |
| if (i % kSeedCommitBatch == 0) { | |
| if (txn) { | |
| if (txn->commit() != TxnErrorCode::TXN_OK) { | |
| return -1; | |
| } | |
| } | |
| if (txn_kv->create_txn(&txn) != TxnErrorCode::TXN_OK) { | |
| return -1; | |
| } | |
| } | |
| int64_t tablet_id = tablet_id_base + i; | |
| std::string rowset_id = fmt::format("{:018d}", i); | |
| auto meta = make_rowset_meta(branch, tablet_id, rowset_id); | |
| auto pb = make_recycle_rowset(branch, meta); | |
| std::string key; | |
| recycle_rowset_key({instance_id, tablet_id, rowset_id}, &key); | |
| std::string val; | |
| pb.SerializeToString(&val); | |
| txn->put(key, val); | |
| put_benchmark_delete_bitmap(txn.get(), instance_id, meta, bitmap_version); | |
| if (branch == RecycleRowsetBranch::kPrepareAbort) { | |
| const auto txn_id = meta.txn_id(); | |
| const auto label = fmt::format("recycler_benchmark_txn_{}", txn_id); | |
| TxnIndexPB index; | |
| index.mutable_tablet_index()->set_db_id(kBenchmarkDbId); | |
| index.mutable_tablet_index()->set_tablet_id(tablet_id); | |
| TxnInfoPB info; | |
| info.set_txn_id(txn_id); | |
| info.set_db_id(kBenchmarkDbId); | |
| info.set_label(label); | |
| info.set_status(TxnStatusPB::TXN_STATUS_PREPARED); | |
| info.set_prepare_time(1); | |
| info.set_timeout_ms(60000); | |
| TxnRunningPB running; | |
| running.set_timeout_time(info.prepare_time() + info.timeout_ms()); | |
| TxnLabelPB txn_label; | |
| txn_label.add_txn_ids(txn_id); | |
| txn->put(txn_index_key({instance_id, txn_id}), index.SerializeAsString()); | |
| txn->put(txn_info_key({instance_id, kBenchmarkDbId, txn_id}), info.SerializeAsString()); | |
| txn->put(txn_running_key({instance_id, kBenchmarkDbId, txn_id}), | |
| running.SerializeAsString()); | |
| txn->atomic_set_ver_value(txn_label_key({instance_id, kBenchmarkDbId, label}), | |
| txn_label.SerializeAsString()); | |
| } | |
| } | |
| if (txn && txn->commit() != TxnErrorCode::TXN_OK) { | |
| return -1; | |
| } | |
| return 0; | |
| } | |
| std::string benchmark_packed_file_path(int64_t file_id) { | |
| return fmt::format("data/packed_file/recycler_benchmark_{}.pack", file_id); | |
| } | |
| // Seed a complete packed file and its rowsets in the same transaction. | |
| void put_packed_recycle_rowsets(Transaction* txn, const std::string& instance_id, | |
| int64_t tablet_id_base, int64_t file_id, | |
| DeleteBitmapVersion bitmap_version) { | |
| const auto path = benchmark_packed_file_path(file_id); | |
| PackedFileInfoPB packed_info; | |
| packed_info.set_resource_id(kBenchmarkResourceId); | |
| packed_info.set_state(PackedFileInfoPB::NORMAL); | |
| int64_t offset = 0; | |
| for (int64_t j = 0; j < kRowsetsPerPackedFile; ++j) { | |
| // Spread references across scan batches so different recycler workers can | |
| // contend on the same packed file KV, instead of handling them serially. | |
| const auto rowset_number = file_id + j * kPackedFileCount; | |
| const auto tablet_id = tablet_id_base + rowset_number; | |
| const auto rowset_id = fmt::format("{:018d}", rowset_number); | |
| auto meta = make_rowset_meta(RecycleRowsetBranch::kCompactedWithData, tablet_id, rowset_id); | |
| const std::pair<std::string, int64_t> files[] = { | |
| {segment_path(tablet_id, rowset_id, 0), meta.data_disk_size()}, | |
| {inverted_index_path_v1(tablet_id, rowset_id, 0, 1, ""), meta.index_disk_size()}}; | |
| for (const auto& [small_path, size] : files) { | |
| auto& location = (*meta.mutable_packed_slice_locations())[small_path]; | |
| location.set_packed_file_path(path); | |
| location.set_offset(offset); | |
| location.set_size(size); | |
| auto* slice = packed_info.add_slices(); | |
| slice->set_path(small_path); | |
| slice->set_offset(offset); | |
| slice->set_size(size); | |
| slice->set_deleted(false); | |
| slice->set_tablet_id(tablet_id); | |
| slice->set_rowset_id(rowset_id); | |
| offset += size; | |
| } | |
| auto rowset = make_recycle_rowset(RecycleRowsetBranch::kCompactedWithData, meta); | |
| txn->put(recycle_rowset_key({instance_id, tablet_id, rowset_id}), | |
| rowset.SerializeAsString()); | |
| put_benchmark_delete_bitmap(txn, instance_id, meta, bitmap_version); | |
| } | |
| packed_info.set_ref_cnt(packed_info.slices_size()); | |
| packed_info.set_total_slice_num(packed_info.slices_size()); | |
| packed_info.set_total_slice_bytes(offset); | |
| packed_info.set_remaining_slice_bytes(offset); | |
| txn->put(packed_file_key({instance_id, path}), packed_info.SerializeAsString()); | |
| } | |
| class RecyclerBenchmarkTest : public ::testing::Test { | |
| protected: | |
| struct RecycleMetrics { | |
| int64_t num = 0; | |
| int64_t bytes = 0; | |
| }; | |
| struct TxnKvCounts { | |
| int64_t get = 0; | |
| int64_t put = 0; | |
| int64_t del = 0; | |
| int64_t total() const { return get + put + del; } | |
| TxnKvCounts& operator+=(const TxnKvCounts& rhs) { | |
| get += rhs.get; | |
| put += rhs.put; | |
| del += rhs.del; | |
| return *this; | |
| } | |
| }; | |
| struct BenchmarkResult { | |
| RecycleMetrics metrics; | |
| TxnKvCounts txn_kv_counts; | |
| double elapsed_ms = 0; | |
| }; | |
| using RecycleFunction = int (InstanceRecycler::*)(); | |
| static const std::map<std::string, RecycleFunction>& recycle_functions() { | |
| static const std::map<std::string, RecycleFunction> functions = { | |
| {"recycle_cluster_snapshots", &InstanceRecycler::recycle_cluster_snapshots}, | |
| {"recycle_operation_logs", &InstanceRecycler::recycle_operation_logs}, | |
| {"recycle_indexes", &InstanceRecycler::recycle_indexes}, | |
| {"recycle_partitions", &InstanceRecycler::recycle_partitions}, | |
| {"recycle_tmp_rowsets", &InstanceRecycler::recycle_tmp_rowsets}, | |
| {"recycle_rowsets", &InstanceRecycler::recycle_rowsets}, | |
| {"recycle_packed_files", &InstanceRecycler::recycle_packed_files}, | |
| {"abort_timeout_txn", &InstanceRecycler::abort_timeout_txn}, | |
| {"recycle_expired_txn_label", &InstanceRecycler::recycle_expired_txn_label}, | |
| {"recycle_copy_jobs", &InstanceRecycler::recycle_copy_jobs}, | |
| {"recycle_stage", &InstanceRecycler::recycle_stage}, | |
| {"recycle_expired_stage_objects", &InstanceRecycler::recycle_expired_stage_objects}, | |
| {"recycle_versions", &InstanceRecycler::recycle_versions}, | |
| {"recycle_restore_jobs", &InstanceRecycler::recycle_restore_jobs}, | |
| }; | |
| return functions; | |
| } | |
| void SetUp() override { | |
| old_force_immediate_recycle_ = config::force_immediate_recycle; | |
| old_retention_seconds_ = config::retention_seconds; | |
| config::force_immediate_recycle = true; | |
| config::retention_seconds = 0; | |
| instance_.set_instance_id(std::string(kBenchmarkInstanceId)); | |
| auto* obj_info = instance_.add_obj_info(); | |
| obj_info->set_id(kBenchmarkResourceId); | |
| const auto s3_config = load_benchmark_s3_config(); | |
| ASSERT_NO_FATAL_FAILURE(configure_obj_info(s3_config, obj_info)); | |
| s3_producer_pool_ = std::make_shared<SimpleThreadPool>( | |
| config::recycle_pool_parallelism, "recycler_benchmark_s3_producer_pool"); | |
| recycle_tablet_pool_ = std::make_shared<SimpleThreadPool>( | |
| config::recycle_pool_parallelism, "recycler_benchmark_recycle_tablet_pool"); | |
| group_recycle_function_pool_ = std::make_shared<SimpleThreadPool>( | |
| config::recycle_pool_parallelism, "recycler_benchmark_group_recycle_function_pool"); | |
| ASSERT_EQ(s3_producer_pool_->start(), 0); | |
| ASSERT_EQ(recycle_tablet_pool_->start(), 0); | |
| ASSERT_EQ(group_recycle_function_pool_->start(), 0); | |
| thread_pool_group_ = RecyclerThreadPoolGroup(s3_producer_pool_, recycle_tablet_pool_, | |
| group_recycle_function_pool_); | |
| ASSERT_NO_FATAL_FAILURE(init_recycler()); | |
| } | |
| void init_recycler() { | |
| recycler_.reset(); | |
| txn_lazy_committer_.reset(); | |
| config::fdb_cluster_file_path = "fdb.cluster"; | |
| txn_kv_ = std::make_shared<FdbTxnKv>(); | |
| ASSERT_EQ(txn_kv_->init(), 0); | |
| txn_lazy_committer_ = std::make_shared<TxnLazyCommitter>(txn_kv_); | |
| recycler_ = std::make_unique<InstanceRecycler>(txn_kv_, instance_, thread_pool_group_, | |
| txn_lazy_committer_); | |
| if (load_benchmark_s3_config().enabled) { | |
| auto s3_conf = S3Conf::from_obj_store_info(instance_.obj_info(0)); | |
| ASSERT_TRUE(s3_conf.has_value()); | |
| std::shared_ptr<S3Accessor> accessor; | |
| ASSERT_EQ(S3Accessor::create(std::move(*s3_conf), &accessor), 0); | |
| s3_accessor_ = std::move(accessor); | |
| recycler_->TEST_add_accessor(kBenchmarkResourceId, s3_accessor_); | |
| } | |
| ASSERT_EQ(recycler_->init(), 0); | |
| } | |
| template <typename Benchmark> | |
| void run_benchmark(const std::string& branch, Benchmark benchmark) { | |
| ::testing::TestPartResultArray failures; | |
| { | |
| ::testing::ScopedFakeTestPartResultReporter reporter( | |
| ::testing::ScopedFakeTestPartResultReporter::INTERCEPT_ALL_THREADS, &failures); | |
| SCOPED_TRACE(branch); | |
| try { | |
| // Fatal assertions return from this lambda or the scenario, not the whole test. | |
| [&] { benchmark(); }(); | |
| } catch (const std::exception& e) { | |
| ADD_FAILURE() << "benchmark threw: " << e.what(); | |
| } catch (...) { | |
| ADD_FAILURE() << "benchmark threw an unknown exception"; | |
| } | |
| } | |
| for (int i = 0; i < failures.size(); ++i) { | |
| const auto& failure = failures.GetTestPartResult(i); | |
| if (failure.failed()) { | |
| benchmark_failures_ += | |
| fmt::format("branch={}\n{}\n", branch, ::testing::PrintToString(failure)); | |
| } | |
| } | |
| } | |
| static void configure_obj_info(const BenchmarkS3Config& s3_config, | |
| ObjectStoreInfoPB* obj_info) { | |
| if (!s3_config.enabled) { | |
| obj_info->set_prefix(kBenchmarkResourceId); | |
| return; | |
| } | |
| ASSERT_FALSE(s3_config.endpoint.empty()); | |
| ASSERT_FALSE(s3_config.region.empty()); | |
| ASSERT_FALSE(s3_config.bucket.empty()); | |
| ASSERT_FALSE(s3_config.prefix.empty()); | |
| ASSERT_TRUE((s3_config.access_key.empty() && s3_config.secret_key.empty()) || | |
| (!s3_config.access_key.empty() && !s3_config.secret_key.empty())); | |
| obj_info->set_ak(s3_config.access_key); | |
| obj_info->set_sk(s3_config.secret_key); | |
| obj_info->set_endpoint(s3_config.endpoint); | |
| obj_info->set_region(s3_config.region); | |
| obj_info->set_bucket(s3_config.bucket); | |
| obj_info->set_prefix(fmt::format("{}{}recycler_benchmark/{}", s3_config.prefix, | |
| s3_config.prefix.ends_with('/') ? "" : "/", | |
| butil::GenerateGUID())); | |
| set_obj_store_provider(s3_config.provider, obj_info); | |
| if (s3_config.access_key.empty()) { | |
| obj_info->set_role_arn(s3_config.role_arn); | |
| obj_info->set_external_id(s3_config.external_id); | |
| obj_info->set_cred_provider_type(CredProviderTypePB::INSTANCE_PROFILE); | |
| } | |
| } | |
| void TearDown() override { | |
| recycler_.reset(); | |
| txn_lazy_committer_.reset(); | |
| if (s3_accessor_) { | |
| // The accessor is rooted at this run's GUID directory, never the external prefix. | |
| const int ret = s3_accessor_->delete_all(); | |
| if (ret != 0) { | |
| benchmark_failures_ += fmt::format("branch=s3_cleanup ret={} prefix={}\n", ret, | |
| instance_.obj_info(0).prefix()); | |
| } | |
| s3_accessor_.reset(); | |
| } | |
| std::string report; | |
| for (const auto& [operation_type, branches] : benchmark_results_) { | |
| size_t branch_width = 24; | |
| for (const auto& entry : branches) { | |
| branch_width = std::max(branch_width, entry.first.size()); | |
| } | |
| report += fmt::format("recycler benchmark: operation={}\n", operation_type); | |
| report += fmt::format( | |
| " {:<{}} {:>12} {:>13} {:>16} {:>12} {:>12} {:>12}" | |
| " {:>12}\n", | |
| "branch", branch_width, "total_ms", "recycled_num", "recycled_bytes", | |
| "get_keys", "put_keys", "del_keys", "total_keys"); | |
| double total_elapsed_ms = 0; | |
| RecycleMetrics total_recycle_metrics; | |
| TxnKvCounts total_txn_kv_counts; | |
| for (const auto& [branch, result] : branches) { | |
| report += fmt::format( | |
| " {:<{}} {:>12.2f} {:>13} {:>16} {:>12} {:>12} {:>12} {:>12}\n", | |
| branch, branch_width, result.elapsed_ms, result.metrics.num, | |
| result.metrics.bytes, result.txn_kv_counts.get, result.txn_kv_counts.put, | |
| result.txn_kv_counts.del, result.txn_kv_counts.total()); | |
| total_elapsed_ms += result.elapsed_ms; | |
| total_recycle_metrics.num += result.metrics.num; | |
| total_recycle_metrics.bytes += result.metrics.bytes; | |
| total_txn_kv_counts += result.txn_kv_counts; | |
| } | |
| report += fmt::format( | |
| " {:<{}} {:>12.2f} {:>13} {:>16} {:>12} {:>12} {:>12} {:>12}\n", | |
| "total", branch_width, total_elapsed_ms, total_recycle_metrics.num, | |
| total_recycle_metrics.bytes, total_txn_kv_counts.get, total_txn_kv_counts.put, | |
| total_txn_kv_counts.del, total_txn_kv_counts.total()); | |
| } | |
| std::cout << report << std::flush; | |
| if (!benchmark_failures_.empty()) { | |
| ADD_FAILURE() << "recycler benchmark failures:\n" << benchmark_failures_; | |
| } | |
| thread_pool_group_ = {}; | |
| if (group_recycle_function_pool_) { | |
| ASSERT_EQ(group_recycle_function_pool_->stop(), 0); | |
| } | |
| if (recycle_tablet_pool_) { | |
| ASSERT_EQ(recycle_tablet_pool_->stop(), 0); | |
| } | |
| if (s3_producer_pool_) { | |
| ASSERT_EQ(s3_producer_pool_->stop(), 0); | |
| } | |
| group_recycle_function_pool_.reset(); | |
| recycle_tablet_pool_.reset(); | |
| s3_producer_pool_.reset(); | |
| txn_kv_.reset(); | |
| config::force_immediate_recycle = old_force_immediate_recycle_; | |
| config::retention_seconds = old_retention_seconds_; | |
| } | |
| RecycleMetrics read_recycle_metrics(const std::string& operation_type) const { | |
| return {.num = g_bvar_recycler_instance_recycle_total_num_since_started.get( | |
| {kBenchmarkInstanceId, operation_type}), | |
| .bytes = g_bvar_recycler_instance_recycle_total_bytes_since_started.get( | |
| {kBenchmarkInstanceId, operation_type})}; | |
| } | |
| TxnKvCounts read_txn_kv_counts() const { | |
| return {.get = g_bvar_txn_kv_get_count_normalized.get_value(), | |
| .put = g_bvar_txn_kv_put.count() + g_bvar_txn_kv_atomic_set_ver_key.count() + | |
| g_bvar_txn_kv_atomic_set_ver_value.count() + | |
| g_bvar_txn_kv_atomic_add.count(), | |
| .del = g_bvar_txn_kv_remove.count() + g_bvar_txn_kv_range_remove.count()}; | |
| } | |
| void check_txn_kv_counts(const std::string& branch, const TxnKvCounts& actual, | |
| const TxnKvCounts& baseline) { | |
| if (actual.get != baseline.get || actual.put != baseline.put || | |
| actual.del != baseline.del) { | |
| benchmark_failures_ += fmt::format( | |
| "branch={} txn_kv_counts actual=[get={}, put={}, del={}] " | |
| "baseline=[get={}, put={}, del={}]\n", | |
| branch, actual.get, actual.put, actual.del, baseline.get, baseline.put, | |
| baseline.del); | |
| } | |
| } | |
| // Time only the recycler call; seeding, assertions and metrics reads are excluded. | |
| void measure(const std::string& operation_type, const std::string& branch, | |
| const std::string& phase) { | |
| SCOPED_TRACE(phase); | |
| const auto function_it = recycle_functions().find(operation_type); | |
| ASSERT_NE(function_it, recycle_functions().end()) | |
| << "unknown recycler operation: " << operation_type; | |
| const auto recycle_metrics_before = read_recycle_metrics(operation_type); | |
| const auto txn_kv_counts_before = read_txn_kv_counts(); | |
| const auto start = std::chrono::steady_clock::now(); | |
| const int ret = (recycler_.get()->*(function_it->second))(); | |
| const auto elapsed = | |
| std::chrono::duration<double, std::milli>(std::chrono::steady_clock::now() - start); | |
| const auto recycle_metrics_after = read_recycle_metrics(operation_type); | |
| const auto txn_kv_counts_after = read_txn_kv_counts(); | |
| const RecycleMetrics metrics { | |
| .num = recycle_metrics_after.num - recycle_metrics_before.num, | |
| .bytes = recycle_metrics_after.bytes - recycle_metrics_before.bytes}; | |
| const int64_t get_keys = txn_kv_counts_after.get - txn_kv_counts_before.get; | |
| const int64_t put_keys = txn_kv_counts_after.put - txn_kv_counts_before.put; | |
| const int64_t del_keys = txn_kv_counts_after.del - txn_kv_counts_before.del; | |
| const TxnKvCounts txn_kv_counts {.get = get_keys, .put = put_keys, .del = del_keys}; | |
| auto& result = benchmark_results_[operation_type][branch]; | |
| result.elapsed_ms += elapsed.count(); | |
| result.metrics.num += metrics.num; | |
| result.metrics.bytes += metrics.bytes; | |
| result.txn_kv_counts += txn_kv_counts; | |
| ASSERT_EQ(ret, 0) << "recycler operation failed: " << operation_type; | |
| } | |
| void seed_packed_recycle_rowsets(int64_t tablet_id_base, DeleteBitmapVersion bitmap_version) { | |
| ASSERT_EQ(put_benchmark_schema(txn_kv_.get(), kBenchmarkInstanceId), 0); | |
| std::unique_ptr<Transaction> txn; | |
| for (int64_t file_id = 0; file_id < kPackedFileCount; ++file_id) { | |
| if (file_id % (kSeedCommitBatch / kRowsetsPerPackedFile) == 0) { | |
| if (txn) { | |
| ASSERT_EQ(txn->commit(), TxnErrorCode::TXN_OK); | |
| } | |
| ASSERT_EQ(txn_kv_->create_txn(&txn), TxnErrorCode::TXN_OK); | |
| } | |
| put_packed_recycle_rowsets(txn.get(), kBenchmarkInstanceId, tablet_id_base, file_id, | |
| bitmap_version); | |
| } | |
| ASSERT_EQ(txn->commit(), TxnErrorCode::TXN_OK); | |
| } | |
| int64_t count_recycle_rowsets() { | |
| std::string begin = recycle_rowset_key({kBenchmarkInstanceId, 0, ""}); | |
| const auto end = | |
| recycle_rowset_key({kBenchmarkInstanceId, std::numeric_limits<int64_t>::max(), ""}); | |
| std::unique_ptr<Transaction> txn; | |
| if (txn_kv_->create_txn(&txn) != TxnErrorCode::TXN_OK) { | |
| return -1; | |
| } | |
| int64_t count = 0; | |
| std::unique_ptr<RangeGetIterator> it; | |
| do { | |
| if (txn->get(begin, end, &it) != TxnErrorCode::TXN_OK) { | |
| return -1; | |
| } | |
| count += it->size(); | |
| begin = it->next_begin_key(); | |
| } while (it->more()); | |
| return count; | |
| } | |
| void check_recycled_bitmap_range(int64_t tablet_id_base) { | |
| std::unique_ptr<Transaction> txn; | |
| ASSERT_EQ(txn_kv_->create_txn(&txn), TxnErrorCode::TXN_OK); | |
| std::unique_ptr<RangeGetIterator> it; | |
| ASSERT_EQ(txn->get(versioned::meta_delete_bitmap_key( | |
| {kBenchmarkInstanceId, tablet_id_base, ""}), | |
| versioned::meta_delete_bitmap_key( | |
| {kBenchmarkInstanceId, tablet_id_base + kRowsetsPerBranch, ""}), | |
| &it), | |
| TxnErrorCode::TXN_OK); | |
| ASSERT_FALSE(it->has_next()); | |
| } | |
| std::shared_ptr<TxnKv> txn_kv_; | |
| InstanceInfoPB instance_; | |
| RecyclerThreadPoolGroup thread_pool_group_; | |
| std::shared_ptr<SimpleThreadPool> s3_producer_pool_; | |
| std::shared_ptr<SimpleThreadPool> recycle_tablet_pool_; | |
| std::shared_ptr<SimpleThreadPool> group_recycle_function_pool_; | |
| std::shared_ptr<TxnLazyCommitter> txn_lazy_committer_; | |
| std::unique_ptr<InstanceRecycler> recycler_; | |
| std::shared_ptr<S3Accessor> s3_accessor_; | |
| std::map<std::string, std::map<std::string, BenchmarkResult>> benchmark_results_; | |
| std::string benchmark_failures_; | |
| bool reset_before_next_benchmark_ = false; | |
| double test_elapsed_ms_ = 0; | |
| bool old_force_immediate_recycle_ = false; | |
| int64_t old_retention_seconds_ = 0; | |
| }; | |
| // Non-overlapping tablet id ranges per branch so a single recycle_rowsets() run | |
| // can cover several branches at once without id collisions. | |
| constexpr int64_t kTabletBase = 1'000'000; | |
| constexpr int64_t kTabletStride = 1'000'000'000LL; | |
| int64_t tablet_base_for(RecycleRowsetBranch branch) { | |
| return kTabletBase + static_cast<int64_t>(branch) * kTabletStride; | |
| } | |
| TEST_F(RecyclerBenchmarkTest, RecycleRowsets) { | |
| RecycleRowsetConfigGuard config_guard; | |
| const auto test_start = std::chrono::steady_clock::now(); | |
| DORIS_CLOUD_DEFER { | |
| const auto elapsed = std::chrono::duration<double, std::milli>( | |
| std::chrono::steady_clock::now() - test_start); | |
| // Includes seeding and validation, unlike the sum of timed recycler calls. | |
| test_elapsed_ms_ = elapsed.count(); | |
| }; | |
| run_benchmark("legacy_empty_resource", [&] { | |
| ASSERT_EQ(seed_recycle_rowsets(txn_kv_.get(), kBenchmarkInstanceId, | |
| RecycleRowsetBranch::kLegacyEmptyResource, kRowsetsPerBranch, | |
| tablet_base_for(RecycleRowsetBranch::kLegacyEmptyResource)), | |
| 0); | |
| ASSERT_NO_FATAL_FAILURE(measure("recycle_rowsets", "legacy_empty_resource", "delete")); | |
| ASSERT_EQ(count_recycle_rowsets(), 0); | |
| }); | |
| run_benchmark("legacy_with_resource", [&] { | |
| ASSERT_EQ(seed_recycle_rowsets(txn_kv_.get(), kBenchmarkInstanceId, | |
| RecycleRowsetBranch::kLegacyWithResource, kRowsetsPerBranch, | |
| tablet_base_for(RecycleRowsetBranch::kLegacyWithResource)), | |
| 0); | |
| ASSERT_NO_FATAL_FAILURE(measure("recycle_rowsets", "legacy_with_resource", "delete")); | |
| ASSERT_EQ(count_recycle_rowsets(), 0); | |
| }); | |
| run_benchmark("prepare_direct", [&] { | |
| ASSERT_EQ(seed_recycle_rowsets(txn_kv_.get(), kBenchmarkInstanceId, | |
| RecycleRowsetBranch::kPrepareDirect, kRowsetsPerBranch, | |
| tablet_base_for(RecycleRowsetBranch::kPrepareDirect)), | |
| 0); | |
| ASSERT_NO_FATAL_FAILURE(measure("recycle_rowsets", "prepare_direct", "delete")); | |
| ASSERT_EQ(count_recycle_rowsets(), 0); | |
| }); | |
| run_benchmark("prepare_mark", [&] { | |
| const auto tablet_id_base = tablet_base_for(RecycleRowsetBranch::kPrepareMark); | |
| ASSERT_EQ(seed_recycle_rowsets(txn_kv_.get(), kBenchmarkInstanceId, | |
| RecycleRowsetBranch::kPrepareMark, kRowsetsPerBranch, | |
| tablet_id_base), | |
| 0); | |
| ASSERT_NO_FATAL_FAILURE(measure("recycle_rowsets", "prepare_mark", "mark")); | |
| ASSERT_EQ(count_recycle_rowsets(), kRowsetsPerBranch); | |
| ASSERT_NO_FATAL_FAILURE(measure("recycle_rowsets", "prepare_mark", "delete")); | |
| ASSERT_EQ(count_recycle_rowsets(), 0); | |
| }); | |
| run_benchmark("prepare_abort", [&] { | |
| const auto txn_id_base = tablet_base_for(RecycleRowsetBranch::kPrepareAbort); | |
| ASSERT_EQ(seed_recycle_rowsets(txn_kv_.get(), kBenchmarkInstanceId, | |
| RecycleRowsetBranch::kPrepareAbort, kRowsetsPerBranch, | |
| txn_id_base), | |
| 0); | |
| ASSERT_NO_FATAL_FAILURE(measure("recycle_rowsets", "prepare_abort", "mark")); | |
| ASSERT_EQ(count_recycle_rowsets(), kRowsetsPerBranch); | |
| ASSERT_NO_FATAL_FAILURE(measure("recycle_rowsets", "prepare_abort", "abort_and_delete")); | |
| ASSERT_EQ(count_recycle_rowsets(), 0); | |
| }); | |
| // Keep the missing-schema case separate from the bitmap ablation below. | |
| run_benchmark("compacted_without_schema", [&] { | |
| const auto tablet_id_base = tablet_base_for(RecycleRowsetBranch::kCompactedWithData); | |
| ASSERT_EQ(seed_recycle_rowsets(txn_kv_.get(), kBenchmarkInstanceId, | |
| RecycleRowsetBranch::kCompactedWithData, kRowsetsPerBranch, | |
| tablet_id_base), | |
| 0); | |
| ASSERT_EQ(remove_benchmark_schema(txn_kv_.get(), kBenchmarkInstanceId), 0); | |
| ASSERT_NO_FATAL_FAILURE(measure("recycle_rowsets", "compacted_without_schema", "delete")); | |
| ASSERT_EQ(count_recycle_rowsets(), 0); | |
| }); | |
| // Hold rowset count, schema and file sizes fixed; vary only data packing and | |
| // bitmap version. V1 bitmap KVs survive rowset recycling, so isolate each case. | |
| for (const auto& [bitmap_version, bitmap_name] : | |
| {std::pair {DeleteBitmapVersion::kNone, "data_only"}, | |
| std::pair {DeleteBitmapVersion::kV1, "delete_bitmap_v1"}, | |
| std::pair {DeleteBitmapVersion::kV2, "delete_bitmap_v2"}}) { | |
| SCOPED_TRACE(bitmap_name); | |
| const auto tablet_id_base = | |
| tablet_base_for(RecycleRowsetBranch::kCompactedWithData) + | |
| (1 + 2 * static_cast<int64_t>(bitmap_version)) * kRowsetsPerBranch; | |
| run_benchmark(fmt::format("compacted_with_data/{}", bitmap_name), [&] { | |
| ASSERT_EQ(seed_recycle_rowsets(txn_kv_.get(), kBenchmarkInstanceId, | |
| RecycleRowsetBranch::kCompactedWithData, | |
| kRowsetsPerBranch, tablet_id_base, true, bitmap_version), | |
| 0); | |
| ASSERT_EQ(count_recycle_rowsets(), kRowsetsPerBranch); | |
| ASSERT_NO_FATAL_FAILURE(measure("recycle_rowsets", | |
| fmt::format("compacted_with_data/{}", bitmap_name), | |
| "delete")); | |
| ASSERT_EQ(count_recycle_rowsets(), 0); | |
| ASSERT_NO_FATAL_FAILURE(check_recycled_bitmap_range(tablet_id_base)); | |
| }); | |
| run_benchmark(fmt::format("compacted_with_packed_data/{}", bitmap_name), [&] { | |
| ASSERT_NO_FATAL_FAILURE(seed_packed_recycle_rowsets(tablet_id_base + kRowsetsPerBranch, | |
| bitmap_version)); | |
| int64_t remaining = count_recycle_rowsets(); | |
| ASSERT_EQ(remaining, kRowsetsPerBranch); | |
| for (int pass = 1; pass <= kMaxPackedRecyclePasses && remaining > 0; ++pass) { | |
| const int64_t previous_remaining = remaining; | |
| // A worker may exhaust packed-file transaction retries while recycle_rowsets() | |
| // still returns success. Include every pass in the reported total elapsed time. | |
| ASSERT_NO_FATAL_FAILURE( | |
| measure("recycle_rowsets", | |
| fmt::format("compacted_with_packed_data/{}", bitmap_name), | |
| fmt::format("delete_pass_{}", pass))); | |
| remaining = count_recycle_rowsets(); | |
| ASSERT_GE(remaining, 0); | |
| ASSERT_LT(remaining, previous_remaining) | |
| << "packed rowset recycling made no progress, pass=" << pass; | |
| } | |
| ASSERT_EQ(remaining, 0) | |
| << "packed rowset recycling exceeded " << kMaxPackedRecyclePasses << " passes"; | |
| // Check outside the timed calls that every packed file reached zero references. | |
| std::unique_ptr<Transaction> txn; | |
| ASSERT_EQ(txn_kv_->create_txn(&txn), TxnErrorCode::TXN_OK); | |
| for (int64_t file_id = 0; file_id < kPackedFileCount; ++file_id) { | |
| std::string value; | |
| ASSERT_EQ(txn->get(packed_file_key({kBenchmarkInstanceId, | |
| benchmark_packed_file_path(file_id)}), | |
| &value), | |
| TxnErrorCode::TXN_KEY_NOT_FOUND); | |
| } | |
| ASSERT_NO_FATAL_FAILURE( | |
| check_recycled_bitmap_range(tablet_id_base + kRowsetsPerBranch)); | |
| }); | |
| } | |
| run_benchmark("compacted_empty", [&] { | |
| ASSERT_EQ(seed_recycle_rowsets(txn_kv_.get(), kBenchmarkInstanceId, | |
| RecycleRowsetBranch::kCompactedEmpty, kRowsetsPerBranch, | |
| tablet_base_for(RecycleRowsetBranch::kCompactedEmpty)), | |
| 0); | |
| ASSERT_NO_FATAL_FAILURE(measure("recycle_rowsets", "compacted_empty", "delete")); | |
| ASSERT_EQ(count_recycle_rowsets(), 0); | |
| }); | |
| // Validate ordering outside the timed calls so callback KV reads do not skew timings. | |
| run_benchmark("prepare_abort_before_delete", [&] { | |
| const auto txn_id = tablet_base_for(RecycleRowsetBranch::kPrepareAbort) + kRowsetsPerBranch; | |
| ASSERT_EQ(seed_recycle_rowsets(txn_kv_.get(), kBenchmarkInstanceId, | |
| RecycleRowsetBranch::kPrepareAbort, 1, txn_id), | |
| 0); | |
| ASSERT_EQ(recycler_->recycle_rowsets(), 0); | |
| ASSERT_EQ(count_recycle_rowsets(), 1); | |
| auto* sp = SyncPoint::get_instance(); | |
| DORIS_CLOUD_DEFER { | |
| sp->clear_all_call_backs(); | |
| sp->disable_processing(); | |
| }; | |
| sp->enable_processing(); | |
| ASSERT_EQ(recycler_->recycle_rowsets(), 0); | |
| ASSERT_EQ(count_recycle_rowsets(), 0); | |
| }); | |
| // Keep mixed-workload IDs separate from the transactions aborted above. | |
| // Already-marked direct rowsets are deleted in the first pass. | |
| run_benchmark("mixed", [&] { | |
| constexpr int64_t mixed_tablet_offset = 7 * kTabletStride; | |
| ASSERT_EQ(seed_recycle_rowsets( | |
| txn_kv_.get(), kBenchmarkInstanceId, | |
| RecycleRowsetBranch::kLegacyEmptyResource, kRowsetsPerBranch, | |
| mixed_tablet_offset + | |
| tablet_base_for(RecycleRowsetBranch::kLegacyEmptyResource)), | |
| 0); | |
| ASSERT_EQ(seed_recycle_rowsets( | |
| txn_kv_.get(), kBenchmarkInstanceId, | |
| RecycleRowsetBranch::kLegacyWithResource, kRowsetsPerBranch, | |
| mixed_tablet_offset + | |
| tablet_base_for(RecycleRowsetBranch::kLegacyWithResource)), | |
| 0); | |
| ASSERT_EQ( | |
| seed_recycle_rowsets( | |
| txn_kv_.get(), kBenchmarkInstanceId, RecycleRowsetBranch::kPrepareDirect, | |
| kRowsetsPerBranch, | |
| mixed_tablet_offset + tablet_base_for(RecycleRowsetBranch::kPrepareDirect)), | |
| 0); | |
| const auto mark_tablet_id_base = | |
| mixed_tablet_offset + tablet_base_for(RecycleRowsetBranch::kPrepareMark); | |
| ASSERT_EQ(seed_recycle_rowsets(txn_kv_.get(), kBenchmarkInstanceId, | |
| RecycleRowsetBranch::kPrepareMark, kRowsetsPerBranch, | |
| mark_tablet_id_base), | |
| 0); | |
| const auto txn_id_base = | |
| mixed_tablet_offset + tablet_base_for(RecycleRowsetBranch::kPrepareAbort); | |
| ASSERT_EQ(seed_recycle_rowsets(txn_kv_.get(), kBenchmarkInstanceId, | |
| RecycleRowsetBranch::kPrepareAbort, kRowsetsPerBranch, | |
| txn_id_base), | |
| 0); | |
| ASSERT_EQ(seed_recycle_rowsets( | |
| txn_kv_.get(), kBenchmarkInstanceId, | |
| RecycleRowsetBranch::kCompactedWithData, kRowsetsPerBranch, | |
| mixed_tablet_offset + | |
| tablet_base_for(RecycleRowsetBranch::kCompactedWithData)), | |
| 0); | |
| ASSERT_EQ( | |
| seed_recycle_rowsets(txn_kv_.get(), kBenchmarkInstanceId, | |
| RecycleRowsetBranch::kCompactedEmpty, kRowsetsPerBranch, | |
| mixed_tablet_offset + | |
| tablet_base_for(RecycleRowsetBranch::kCompactedEmpty)), | |
| 0); | |
| ASSERT_NO_FATAL_FAILURE(measure("recycle_rowsets", "mixed", "mark_and_delete_ready")); | |
| ASSERT_EQ(count_recycle_rowsets(), 2 * kRowsetsPerBranch); | |
| ASSERT_NO_FATAL_FAILURE(measure("recycle_rowsets", "mixed", "abort_and_delete_prepare")); | |
| ASSERT_EQ(count_recycle_rowsets(), 0); | |
| }); | |
| // Recorded with 5,000 rowsets per branch. Recalibrate if the workload changes. | |
| static_assert(kRowsetsPerBranch == 5000); | |
| const std::map<std::string, TxnKvCounts> baseline_txn_kv_counts = { | |
| {"compacted_empty", {.get = 5000, .put = 0, .del = 5000}}, | |
| // First schema cache fill reads one KV in addition to the 5,000 rowset KVs. | |
| {"compacted_with_data/data_only", {.get = 5001, .put = 0, .del = 5000}}, | |
| {"compacted_with_data/delete_bitmap_v1", {.get = 5000, .put = 0, .del = 5000}}, | |
| {"compacted_with_data/delete_bitmap_v2", {.get = 10000, .put = 0, .del = 10000}}, | |
| {"compacted_with_packed_data/data_only", {.get = 12500, .put = 5000, .del = 7500}}, | |
| {"compacted_with_packed_data/delete_bitmap_v1", | |
| {.get = 12500, .put = 5000, .del = 7500}}, | |
| {"compacted_with_packed_data/delete_bitmap_v2", | |
| {.get = 17500, .put = 5000, .del = 12500}}, | |
| {"compacted_without_schema", {.get = 5000, .put = 0, .del = 5000}}, | |
| {"legacy_empty_resource", {.get = 5000, .put = 0, .del = 5000}}, | |
| {"legacy_with_resource", {.get = 10000, .put = 0, .del = 10000}}, | |
| {"mixed", {.get = 100000, .put = 20000, .del = 60000}}, | |
| {"prepare_abort", {.get = 45000, .put = 15000, .del = 15000}}, | |
| {"prepare_direct", {.get = 10000, .put = 0, .del = 10000}}, | |
| {"prepare_mark", {.get = 20000, .put = 5000, .del = 10000}}, | |
| }; | |
| const auto& results = benchmark_results_["recycle_rowsets"]; | |
| for (const auto& [branch, baseline_counts] : baseline_txn_kv_counts) { | |
| const auto result = results.find(branch); | |
| if (result == results.end()) { | |
| benchmark_failures_ += | |
| fmt::format("branch={} did not produce a benchmark result\n", branch); | |
| continue; | |
| } | |
| check_txn_kv_counts(branch, result->second.txn_kv_counts, baseline_counts); | |
| } | |
| } | |
| } // namespace | |
| } // namespace doris::cloud |