github-actions[bot] commented on code in PR #68043:
URL: https://github.com/apache/doris/pull/68043#discussion_r4056547052
##########
cloud/test/CMakeLists.txt:
##########
@@ -41,6 +41,7 @@ set_target_properties(txn_kv_test PROPERTIES COMPILE_FLAGS
"-fno-access-control"
add_executable(recycler_test
recycler_test.cpp
+ recycler_benchmark_test.cpp
Review Comment:
**[P2] Keep the wall-clock benchmark out of default Cloud UT**
This source is linked into the ordinary `recycler_test`, and the default
runner executes every non-disabled test. The hard-coded recycler baselines
total 311,262.84 ms (before seeding and validation), then fail any branch or
total that is 50% slower than the machine where those numbers were captured.
That makes every correctness run absorb a workload recorded at over five
minutes and lets a slower or contended supported runner fail without a recycler
regression. Please keep a small deterministic functional case in normal UT and
gate the 30,000-rowset timing workload in a dedicated opt-in/performance job
with a controlled runner.
##########
cloud/src/recycler/recycler.cpp:
##########
@@ -5880,7 +5880,7 @@ int InstanceRecycler::recycle_rowsets() {
LOG(INFO) << "delete the recycle rowset kv that has empty
resource_id, key="
<< hex(k) << " value=" << proto_to_json(rowset);
rowset_keys.emplace_back(k);
- return -1;
+ return 0;
Review Comment:
**[P2] Apply this compatibility result to the multi-version paths too**
When `should_recycle_versioned_keys()` is true, this edited branch is
bypassed. `recycle_versioned_tablet()` has the same present-but-empty legacy
branch at lines 5532-5536 and still returns `-1`, so that tablet cleanup
attempt exits before the recycle-rowset range removal at lines 5685-5687. The
generic versioned sweep also retains `return -1` at lines 6357-6362. It can
eventually remove the key, but only after the old UNKNOWN-type rowset reaches
the 72-hour global retention; dropped indexes/partitions are eligible after 3
hours, so this can defer their cleanup by roughly 69 hours while reporting
failed recycler rounds. Please update both parallel handlers and add a
multi-version dropped-tablet regression; the new benchmark leaves
`multi_version_status` unset and cannot cover this.
##########
cloud/test/recycler_benchmark_test.cpp:
##########
@@ -0,0 +1,991 @@
+// 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 <cmath>
+#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";
Review Comment:
**[P2] Isolate each benchmark scenario's FDB state**
Every scenario and rerun uses this fixed instance plus deterministic
tablet/rowset/transaction IDs, but `run_benchmark()` intercepts fatal failures
and continues without clearing state, while `count_recycle_rowsets()` scans the
whole instance. If a seed commits one batch and then fails, or recycling leaves
keys for retry, later scenarios consume and time that leftover work; rerunning
against the supported persistent `--fdb` target is contaminated the same way.
Use a unique per-scenario/invocation namespace with RAII cleanup (or stop after
the first stateful failure) so branch counts and timings remain attributable.
##########
cloud/test/recycler_benchmark_test.cpp:
##########
@@ -0,0 +1,991 @@
+// 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 <cmath>
+#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";
+double recycler_benchmark_duration_tolerance_ratio = 0.5;
+
+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 = 30000;
+// Commit the seeded recycle rowset KVs in batches to keep each txn small.
+constexpr int64_t kSeedCommitBatch = 2000;
+constexpr int64_t kRowsetsPerPackedFile = 10;
+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 BenchmarkResult {
+ RecycleMetrics metrics;
+ 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;
+
+ const auto tolerance_env =
get_env("recycler_benchmark_duration_tolerance_ratio");
+ size_t parsed_size = 0;
+ recycler_benchmark_duration_tolerance_ratio =
+ tolerance_env.empty() ? 0.5 : std::stod(tolerance_env,
&parsed_size);
+ ASSERT_EQ(parsed_size, tolerance_env.size()) << "invalid recycler
benchmark tolerance";
+
ASSERT_TRUE(std::isfinite(recycler_benchmark_duration_tolerance_ratio));
+ ASSERT_GE(recycler_benchmark_duration_tolerance_ratio, 0);
+
+ 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}\n",
"branch", branch_width,
+ "total_ms", "recycled_num",
"recycled_bytes");
+ double total_elapsed_ms = 0;
+ for (const auto& [branch, result] : branches) {
+ report += fmt::format(" {:<{}} {:>12.2f} {:>13} {:>16}\n",
branch, branch_width,
+ result.elapsed_ms, result.metrics.num,
result.metrics.bytes);
+ total_elapsed_ms += result.elapsed_ms;
+ }
+ report += fmt::format(" {:<{}} {:>12.2f}\n", "total_elapsed_ms",
branch_width,
+ total_elapsed_ms);
+ }
+ 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})};
+ }
+
+ void check_elapsed_ms(const std::string& branch, double actual_ms, double
baseline_ms) {
+ const double limit_ms = baseline_ms * (1 +
recycler_benchmark_duration_tolerance_ratio);
+ if (actual_ms > limit_ms) {
+ benchmark_failures_ +=
+ fmt::format("branch={} actual_ms={:.2f} baseline_ms={:.2f}
limit_ms={:.2f}\n",
+ branch, actual_ms, baseline_ms, limit_ms);
+ }
+ }
+
+ // 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 before = read_recycle_metrics(operation_type);
+ 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 after = read_recycle_metrics(operation_type);
+ const RecycleMetrics metrics {.num = after.num - before.num,
+ .bytes = after.bytes - before.bytes};
+ auto& result = benchmark_results_[operation_type][branch];
+ result.elapsed_ms += elapsed.count();
+ result.metrics.num += metrics.num;
+ result.metrics.bytes += metrics.bytes;
+ 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,
Review Comment:
**[P2] Do not issue all 3,000 checks in one FDB transaction**
`kPackedFileCount` is 3,000, and this loop performs 3,000 sequential
blocking `get()` calls on the same transaction. Doris configures each FDB
transaction with a 10,000 ms timeout, so a supported remote or loaded cluster
averaging only ~3.3 ms per read times out here after the recycler itself has
already succeeded. Please validate the packed-file prefix with a range scan, or
recreate/batch transactions at a bounded size well below the timeout.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]