This is an automated email from the ASF dual-hosted git repository.

yiguolei pushed a commit to branch branch-4.1
in repository https://gitbox.apache.org/repos/asf/doris.git


The following commit(s) were added to refs/heads/branch-4.1 by this push:
     new 1430ed67f4a branch-4.1: [fix](checker) Avoid false-positive leaked 
delete bitmaps for unexpired job tmp rowsets #64313 (#65012)
1430ed67f4a is described below

commit 1430ed67f4ac5b3bdc7dced9497e204ae306a00f
Author: github-actions[bot] 
<41898282+github-actions[bot]@users.noreply.github.com>
AuthorDate: Tue Jun 30 19:37:20 2026 +0800

    branch-4.1: [fix](checker) Avoid false-positive leaked delete bitmaps for 
unexpired job tmp rowsets #64313 (#65012)
    
    Cherry-picked from #64313
    
    Co-authored-by: meiyi <[email protected]>
    Co-authored-by: Claude Opus 4.8 <[email protected]>
---
 cloud/src/recycler/checker.cpp | 127 ++++++++++++++++++++++++++++++++++++++++-
 cloud/src/recycler/checker.h   |   2 +
 cloud/src/recycler/recycler.h  |   5 ++
 cloud/test/recycler_test.cpp   | 125 ++++++++++++++++++++++++++++++++++++++++
 4 files changed, 258 insertions(+), 1 deletion(-)

diff --git a/cloud/src/recycler/checker.cpp b/cloud/src/recycler/checker.cpp
index 82ed8ab5a2b..3a938601121 100644
--- a/cloud/src/recycler/checker.cpp
+++ b/cloud/src/recycler/checker.cpp
@@ -1227,6 +1227,11 @@ int InstanceChecker::do_delete_bitmap_inverted_check() {
         std::unordered_set<std::string> pending_delete_bitmaps {};
     } tablet_rowsets_cache {};
 
+    std::unordered_map<int64_t, std::unordered_set<std::string>> 
unexpired_tmp_rowsets;
+    if (int ret = collect_unexpired_job_tmp_rowsets(unexpired_tmp_rowsets); 
ret < 0) {
+        return ret;
+    }
+
     std::unique_ptr<RangeGetIterator> it;
     auto begin = meta_delete_bitmap_key({instance_id_, 0, "", 0, 0});
     auto end =
@@ -1267,6 +1272,9 @@ int InstanceChecker::do_delete_bitmap_inverted_check() {
 
             if (tablet_rowsets_cache.tablet_id == -1 ||
                 tablet_rowsets_cache.tablet_id != tablet_id) {
+                if (tablet_rowsets_cache.tablet_id != -1) {
+                    
unexpired_tmp_rowsets.erase(tablet_rowsets_cache.tablet_id);
+                }
                 TabletMetaCloudPB tablet_meta;
                 int ret = get_tablet_meta(txn_kv_.get(), instance_id_, 
tablet_id, tablet_meta);
                 if (ret < 0) {
@@ -1319,8 +1327,15 @@ int InstanceChecker::do_delete_bitmap_inverted_check() {
                 continue;
             }
 
+            bool belongs_to_unexpired_tmp_rowset = false;
+            auto tmp_rowsets_it = unexpired_tmp_rowsets.find(tablet_id);
+            if (tmp_rowsets_it != unexpired_tmp_rowsets.end()) {
+                belongs_to_unexpired_tmp_rowset = 
tmp_rowsets_it->second.contains(rowset_id);
+            }
+
             if (!tablet_rowsets_cache.rowsets.contains(rowset_id) &&
-                
!tablet_rowsets_cache.pending_delete_bitmaps.contains(std::string(k))) {
+                
!tablet_rowsets_cache.pending_delete_bitmaps.contains(std::string(k)) &&
+                !belongs_to_unexpired_tmp_rowset) {
                 TEST_SYNC_POINT_CALLBACK(
                         
"InstanceChecker::do_delete_bitmap_inverted_check.get_leaked_delete_bitmap",
                         &tablet_id, &rowset_id, &version, &segment_id);
@@ -1338,6 +1353,116 @@ int InstanceChecker::do_delete_bitmap_inverted_check() {
     return (leaked_delete_bitmaps > 0 || abnormal_delete_bitmaps > 0) ? 1 : 0;
 }
 
+int InstanceChecker::collect_unexpired_job_tmp_rowsets(
+        std::unordered_map<int64_t, std::unordered_set<std::string>>& 
tmp_rowsets) {
+    static constexpr int64_t max_unexpired_tmp_rowsets = 1000;
+    auto begin = meta_rowset_tmp_key({instance_id_, 0, 0});
+    auto end = meta_rowset_tmp_key({instance_id_, INT64_MAX, 0});
+    std::unique_ptr<RangeGetIterator> it;
+    int64_t num_scanned = 0;
+    int64_t num_non_job = 0;
+    int64_t num_skipped_non_job_txns = 0;
+    int64_t num_unexpired = 0;
+    int64_t num_expired = 0;
+    int64_t last_txn_id = -1;
+    int64_t current_time = 
duration_cast<seconds>(system_clock::now().time_since_epoch()).count();
+
+    while (it == nullptr /* may be not init */ || (it->more() && !stopped())) {
+        std::unique_ptr<Transaction> txn;
+        TxnErrorCode err = txn_kv_->create_txn(&txn);
+        if (err != TxnErrorCode::TXN_OK) {
+            LOG(WARNING) << "failed to create txn";
+            return -1;
+        }
+        err = txn->get(begin, end, &it);
+        if (err != TxnErrorCode::TXN_OK) {
+            LOG(WARNING) << "failed to get tmp rowset kv, err=" << err;
+            return -1;
+        }
+        if (!it->has_next()) {
+            break;
+        }
+        while (it->has_next() && !stopped()) {
+            auto [k, v] = it->next();
+            ++num_scanned;
+
+            std::string_view k1 = k;
+            k1.remove_prefix(1);
+            std::vector<std::tuple<std::variant<int64_t, std::string>, int, 
int>> out;
+            if (decode_key(&k1, &out) != 0 || out.size() < 5) {
+                LOG(WARNING) << "malformed tmp rowset key, key=" << hex(k);
+                return -1;
+            }
+            // 0x01 "meta" ${instance_id} "rowset_tmp" ${txn_id} ${tablet_id} 
-> RowsetMetaCloudPB
+            auto txn_id = std::get<int64_t>(std::get<0>(out[3]));
+            bool is_first_rowset_of_txn = last_txn_id != txn_id;
+            last_txn_id = txn_id;
+
+            doris::RowsetMetaCloudPB rowset;
+            if (!rowset.ParseFromArray(v.data(), v.size())) {
+                LOG(WARNING) << "malformed tmp rowset meta, key=" << hex(k);
+                return -1;
+            }
+            if (!rowset.has_job_id() || rowset.job_id().empty()) {
+                ++num_non_job;
+                if (is_first_rowset_of_txn) {
+                    ++num_skipped_non_job_txns;
+                    if (txn_id == INT64_MAX) {
+                        begin = end;
+                    } else {
+                        begin = meta_rowset_tmp_key({instance_id_, txn_id + 1, 
0});
+                    }
+                    it.reset();
+                    break;
+                }
+                if (!it->has_next()) {
+                    begin = k;
+                    begin.push_back('\x00');
+                }
+                continue;
+            }
+
+            // Must use the same threshold as the recycler so that a delete 
bitmap is never
+            // reported as leaked while its tmp rowset is still alive from the 
recycler's view.
+            // `earlest_ts` is a local sentinel initialized to 0 on purpose: 
it keeps the value
+            // below any real expiration so the helper never updates the 
recycler's
+            // earliest-ts bvar (the checker must not touch the recycler's 
metrics).
+            int64_t earlest_ts = 0;
+            int64_t expiration =
+                    calculate_tmp_rowset_expired_time(instance_id_, rowset, 
&earlest_ts);
+            if (current_time < expiration) {
+                tmp_rowsets[rowset.tablet_id()].insert(rowset.rowset_id_v2());
+                ++num_unexpired;
+                if (num_unexpired >= max_unexpired_tmp_rowsets) {
+                    LOG(WARNING)
+                            << "collect unexpired tmp rowsets for delete 
bitmap checker reached "
+                            << "limit, remaining tmp rowsets will not be 
considered and may cause "
+                            << "false positives, instance_id=" << instance_id_
+                            << ", num_scanned=" << num_scanned << ", 
num_non_job=" << num_non_job
+                            << ", num_skipped_non_job_txns=" << 
num_skipped_non_job_txns
+                            << ", num_unexpired=" << num_unexpired
+                            << ", num_expired=" << num_expired
+                            << ", limit=" << max_unexpired_tmp_rowsets;
+                    return 0;
+                }
+            } else {
+                ++num_expired;
+            }
+
+            if (!it->has_next()) {
+                begin = k;
+                begin.push_back('\x00');
+            }
+        }
+    }
+
+    LOG(INFO) << "collect unexpired tmp rowsets for delete bitmap checker 
finished, instance_id="
+              << instance_id_ << ", num_scanned=" << num_scanned << ", 
num_non_job=" << num_non_job
+              << ", num_skipped_non_job_txns=" << num_skipped_non_job_txns
+              << ", num_unexpired=" << num_unexpired << ", num_expired=" << 
num_expired;
+    return 0;
+}
+
 int InstanceChecker::get_pending_delete_bitmap_keys(
         int64_t tablet_id, std::unordered_set<std::string>& 
pending_delete_bitmaps) {
     std::unique_ptr<Transaction> txn;
diff --git a/cloud/src/recycler/checker.h b/cloud/src/recycler/checker.h
index ba26d07a70e..67ce7930b5c 100644
--- a/cloud/src/recycler/checker.h
+++ b/cloud/src/recycler/checker.h
@@ -185,6 +185,8 @@ private:
     int collect_tablet_rowsets(
             int64_t tablet_id,
             const std::function<void(const doris::RowsetMetaCloudPB&)>& 
collect_cb);
+    int collect_unexpired_job_tmp_rowsets(
+            std::unordered_map<int64_t, std::unordered_set<std::string>>& 
tmp_rowsets);
     int get_pending_delete_bitmap_keys(int64_t tablet_id,
                                        std::unordered_set<std::string>& 
pending_delete_bitmaps);
     int check_delete_bitmap_storage_optimize_v2(int64_t tablet_id, bool 
has_sequence_col,
diff --git a/cloud/src/recycler/recycler.h b/cloud/src/recycler/recycler.h
index d65d2c3860c..0dc3f270de8 100644
--- a/cloud/src/recycler/recycler.h
+++ b/cloud/src/recycler/recycler.h
@@ -55,6 +55,11 @@ class SimpleThreadPool;
 class RecyclerMetricsContext;
 class TabletRecyclerMetricsContext;
 class SegmentRecyclerMetricsContext;
+
+int64_t calculate_tmp_rowset_expired_time(
+        const std::string& instance_id_, const doris::RowsetMetaCloudPB& 
tmp_rowset_meta_pb,
+        int64_t* earlest_ts /* tmp_rowset earliest expiration ts */);
+
 struct RecyclerThreadPoolGroup {
     RecyclerThreadPoolGroup() = default;
     RecyclerThreadPoolGroup(std::shared_ptr<SimpleThreadPool> s3_producer_pool,
diff --git a/cloud/test/recycler_test.cpp b/cloud/test/recycler_test.cpp
index 3d349ffad78..f84eed2bab2 100644
--- a/cloud/test/recycler_test.cpp
+++ b/cloud/test/recycler_test.cpp
@@ -4444,6 +4444,131 @@ TEST(CheckerTest, delete_bitmap_inverted_check_normal) {
     ASSERT_EQ(checker.do_delete_bitmap_inverted_check(), 0);
 }
 
+TEST(CheckerTest, delete_bitmap_inverted_check_unexpired_tmp_rowset) {
+    auto retention_seconds = config::retention_seconds;
+    auto force_immediate_recycle = config::force_immediate_recycle;
+    DORIS_CLOUD_DEFER {
+        config::retention_seconds = retention_seconds;
+        config::force_immediate_recycle = force_immediate_recycle;
+    };
+    config::retention_seconds = 3600;
+    config::force_immediate_recycle = false;
+
+    auto txn_kv = std::make_shared<MemTxnKv>();
+    ASSERT_EQ(txn_kv->init(), 0);
+
+    InstanceInfoPB instance;
+    instance.set_instance_id(instance_id);
+    auto obj_info = instance.add_obj_info();
+    obj_info->set_id("1");
+
+    InstanceChecker checker(txn_kv, instance_id);
+    ASSERT_EQ(checker.init(instance), 0);
+    auto accessor = checker.accessor_map_.begin()->second;
+
+    constexpr int64_t table_id = 10000;
+    constexpr int64_t index_id = 10001;
+    constexpr int64_t partition_id = 10002;
+    constexpr int64_t tablet_id = 600011;
+    ASSERT_EQ(0, create_tablet(txn_kv.get(), table_id, index_id, partition_id, 
tablet_id, true));
+
+    doris::TabletSchemaCloudPB schema;
+    schema.set_schema_version(1);
+    auto tmp_rowset = create_rowset("1", tablet_id, index_id, 1, schema, 
100001);
+    tmp_rowset.set_creation_time(current_time);
+    tmp_rowset.set_txn_expiration(current_time);
+    tmp_rowset.set_start_version(10);
+    tmp_rowset.set_end_version(10);
+    tmp_rowset.set_job_id("compaction-job-1");
+    ASSERT_EQ(0, create_tmp_rowset(txn_kv.get(), accessor.get(), tmp_rowset, 
false));
+    ASSERT_EQ(0, create_delete_bitmaps_v1(txn_kv.get(), tablet_id, 
tmp_rowset.rowset_id_v2()));
+
+    ASSERT_EQ(checker.do_delete_bitmap_inverted_check(), 0);
+}
+
+TEST(CheckerTest, delete_bitmap_inverted_check_unexpired_non_job_tmp_rowset) {
+    auto retention_seconds = config::retention_seconds;
+    auto force_immediate_recycle = config::force_immediate_recycle;
+    DORIS_CLOUD_DEFER {
+        config::retention_seconds = retention_seconds;
+        config::force_immediate_recycle = force_immediate_recycle;
+    };
+    config::retention_seconds = 3600;
+    config::force_immediate_recycle = false;
+
+    auto txn_kv = std::make_shared<MemTxnKv>();
+    ASSERT_EQ(txn_kv->init(), 0);
+
+    InstanceInfoPB instance;
+    instance.set_instance_id(instance_id);
+    auto obj_info = instance.add_obj_info();
+    obj_info->set_id("1");
+
+    InstanceChecker checker(txn_kv, instance_id);
+    ASSERT_EQ(checker.init(instance), 0);
+    auto accessor = checker.accessor_map_.begin()->second;
+
+    constexpr int64_t table_id = 10000;
+    constexpr int64_t index_id = 10001;
+    constexpr int64_t partition_id = 10002;
+    constexpr int64_t tablet_id = 600013;
+    ASSERT_EQ(0, create_tablet(txn_kv.get(), table_id, index_id, partition_id, 
tablet_id, true));
+
+    doris::TabletSchemaCloudPB schema;
+    schema.set_schema_version(1);
+    auto tmp_rowset = create_rowset("1", tablet_id, index_id, 1, schema, 
100003);
+    tmp_rowset.set_creation_time(current_time);
+    tmp_rowset.set_txn_expiration(current_time);
+    tmp_rowset.set_start_version(10);
+    tmp_rowset.set_end_version(10);
+    ASSERT_EQ(0, create_tmp_rowset(txn_kv.get(), accessor.get(), tmp_rowset, 
false));
+    ASSERT_EQ(0, create_delete_bitmaps_v1(txn_kv.get(), tablet_id, 
tmp_rowset.rowset_id_v2()));
+
+    ASSERT_EQ(checker.do_delete_bitmap_inverted_check(), 1);
+}
+
+TEST(CheckerTest, delete_bitmap_inverted_check_expired_tmp_rowset) {
+    auto retention_seconds = config::retention_seconds;
+    auto force_immediate_recycle = config::force_immediate_recycle;
+    DORIS_CLOUD_DEFER {
+        config::retention_seconds = retention_seconds;
+        config::force_immediate_recycle = force_immediate_recycle;
+    };
+    config::retention_seconds = 3600;
+    config::force_immediate_recycle = false;
+
+    auto txn_kv = std::make_shared<MemTxnKv>();
+    ASSERT_EQ(txn_kv->init(), 0);
+
+    InstanceInfoPB instance;
+    instance.set_instance_id(instance_id);
+    auto obj_info = instance.add_obj_info();
+    obj_info->set_id("1");
+
+    InstanceChecker checker(txn_kv, instance_id);
+    ASSERT_EQ(checker.init(instance), 0);
+    auto accessor = checker.accessor_map_.begin()->second;
+
+    constexpr int64_t table_id = 10000;
+    constexpr int64_t index_id = 10001;
+    constexpr int64_t partition_id = 10002;
+    constexpr int64_t tablet_id = 600012;
+    ASSERT_EQ(0, create_tablet(txn_kv.get(), table_id, index_id, partition_id, 
tablet_id, true));
+
+    doris::TabletSchemaCloudPB schema;
+    schema.set_schema_version(1);
+    auto tmp_rowset = create_rowset("1", tablet_id, index_id, 1, schema, 
100002);
+    tmp_rowset.set_creation_time(current_time - config::retention_seconds - 
10);
+    tmp_rowset.set_txn_expiration(current_time - config::retention_seconds - 
10);
+    tmp_rowset.set_start_version(10);
+    tmp_rowset.set_end_version(10);
+    tmp_rowset.set_job_id("compaction-job-2");
+    ASSERT_EQ(0, create_tmp_rowset(txn_kv.get(), accessor.get(), tmp_rowset, 
false));
+    ASSERT_EQ(0, create_delete_bitmaps_v1(txn_kv.get(), tablet_id, 
tmp_rowset.rowset_id_v2()));
+
+    ASSERT_EQ(checker.do_delete_bitmap_inverted_check(), 1);
+}
+
 TEST(CheckerTest, delete_bitmap_inverted_check_abnormal) {
     // abnormal case, some delete bitmaps arem leaked
     auto txn_kv = std::make_shared<MemTxnKv>();


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to