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 8dc7b8a6e17 branch-4.1: [fix](cloud) Avoid lock inversion in delete 
bitmap checks #69892 (#69968)
8dc7b8a6e17 is described below

commit 8dc7b8a6e173faad5e38e4f3c82ecdfd91349f58
Author: bobhan1 <[email protected]>
AuthorDate: Wed Nov 4 10:38:42 2026 +0800

    branch-4.1: [fix](cloud) Avoid lock inversion in delete bitmap checks 
#69892 (#69968)
    
    ### What problem does this PR solve?
    
    Related PR: #69892
    
    Backport #69892 to `branch-4.1` from commit
    `b2164ff1bacd3e467fd29f443ef9c03faa899658`.
    
    Background delete bitmap checks acquire the bitmap read lock before the
    tablet header lock, while compaction acquires them in the opposite
    order. The checks also run under the tablet map mutex, so a blocked
    tablet can stall unrelated tablet initialization, Rowsets scans,
    synchronization, and compaction scheduling.
    
    Read the sequence-column flag before bitmap traversal and run checks
    over the existing tablet snapshot outside the map mutex. Preserve the
    active and stale rowset version checks for sequence and non-sequence
    tables.
    
    Test context conflicts were adapted to the existing 4.1 fixture, and all
    five new upstream test instances are preserved. Explicitly include
    `cpp/sync_point.h` in `base_tablet.cpp` so production builds have the
    definition of `TEST_SYNC_POINT_CALLBACK`; the original unit-test
    configuration did not expose the missing include.
    
    ### Validation
    
    - ASAN BE unit tests after the include fix: **47 passed, 0 failures, 0
    errors**, verified in the GTest XML report.
    - Production ASAN BE build with `MAKE_TEST=OFF`, `ENABLE_PCH=OFF`, and
    `ENABLE_INJECTION_POINT=OFF`: **passed**, including compilation,
    linking, and packaging via `./build.sh --be -j100`.
    
    ```bash
    ./run-be-ut.sh --run 
--filter='SequenceModes/CloudTabletDeleteBitmapCheckTest.*:CloudTabletMgrTest.*:CloudTabletDeleteBitmapTest.*:TabletMetaTest.*:CloudCompactionTest.*'
 -j100
    ```
    
    - Changed-line clang-format 16 and `git diff --check
    upstream/branch-4.1...HEAD` passed.
    
    ### Release note
    
    Fix BE deadlocks and cascading write and metadata stalls when background
    delete bitmap checks are enabled.
    
    ### Check List (For Author)
    
    - Test
        - [x] Unit Test
    - Behavior changed:
    - [x] Yes. Delete bitmap checks avoid reversed lock ordering and run
    outside the tablet map mutex.
    - Does this need documentation?
        - [x] No. No new setting or syntax.
    
    ### Check List (For Reviewer who merge this PR)
    
    - [ ] Confirm the release note
    - [ ] Confirm test cases
    - [ ] Confirm document
    - [ ] Add branch pick label
---
 be/src/cloud/cloud_tablet_mgr.cpp       |   6 +-
 be/src/storage/tablet/base_tablet.cpp   |  10 ++-
 be/test/cloud/cloud_tablet_mgr_test.cpp |  48 ++++++++++++++
 be/test/cloud/cloud_tablet_test.cpp     | 111 ++++++++++++++++++++++++++++++++
 4 files changed, 172 insertions(+), 3 deletions(-)

diff --git a/be/src/cloud/cloud_tablet_mgr.cpp 
b/be/src/cloud/cloud_tablet_mgr.cpp
index 9b021c6b7dd..1e10cb243fd 100644
--- a/be/src/cloud/cloud_tablet_mgr.cpp
+++ b/be/src/cloud/cloud_tablet_mgr.cpp
@@ -349,7 +349,9 @@ void CloudTabletMgr::vacuum_stale_rowsets(const 
CountDownLatch& stop_latch) {
             int64_t max_useless_rowset_version_count = 0;
             int64_t tablet_id_with_max_useless_rowset_version_count = 0;
             OlapStopWatch watch;
-            _tablet_map->traverse([&](auto&& tablet) {
+            // Reuse the snapshot outside TabletMap's mutex: a check waiting 
on one tablet's
+            // header lock must not block loading or enumerating unrelated 
tablets.
+            for (const auto& tablet : tablets_to_remove_unused_rowsets) {
                 int64_t useless_rowset_count = 0;
                 int64_t useless_rowset_version_count = 0;
                 
tablet->check_agg_delete_bitmap_for_stale_rowsets(useless_rowset_count,
@@ -362,7 +364,7 @@ void CloudTabletMgr::vacuum_stale_rowsets(const 
CountDownLatch& stop_latch) {
                     max_useless_rowset_version_count = 
useless_rowset_version_count;
                     tablet_id_with_max_useless_rowset_version_count = 
tablet->tablet_id();
                 }
-            });
+            }
             
g_max_rowsets_with_useless_delete_bitmap.set_value(max_useless_rowset_count);
             g_max_rowsets_with_useless_delete_bitmap_version.set_value(
                     max_useless_rowset_version_count);
diff --git a/be/src/storage/tablet/base_tablet.cpp 
b/be/src/storage/tablet/base_tablet.cpp
index af494004bbc..5c6667087c3 100644
--- a/be/src/storage/tablet/base_tablet.cpp
+++ b/be/src/storage/tablet/base_tablet.cpp
@@ -37,6 +37,7 @@
 #include "common/status.h"
 #include "core/assert_cast.h"
 #include "core/data_type/data_type_factory.hpp"
+#include "cpp/sync_point.h"
 #include "load/memtable/memtable.h"
 #include "service/point_query_executor.h"
 #include "storage/compaction/cumulative_compaction_time_series_policy.h"
@@ -2074,6 +2075,13 @@ void 
BaseTablet::check_agg_delete_bitmap_for_stale_rowsets(int64_t& useless_rows
             },
             true);
 
+    // Compaction holds the header lock while merging the delete bitmap. Read 
the schema
+    // before taking the bitmap lock so the traversal cannot acquire these 
locks in reverse.
+    const bool has_sequence_col = tablet_schema()->has_sequence_col();
+    TEST_SYNC_POINT_CALLBACK(
+            
"BaseTablet::check_agg_delete_bitmap_for_stale_rowsets:before_traverse_delete_bitmap",
+            this);
+
     std::set<RowsetId> useless_rowsets;
     std::map<RowsetId, std::vector<int64_t>> useless_rowset_versions;
     {
@@ -2090,7 +2098,7 @@ void 
BaseTablet::check_agg_delete_bitmap_for_stale_rowsets(int64_t& useless_rows
                         return -1;
                     }
                     if (end_versions.find(version) == end_versions.end()) {
-                        if (tablet_schema()->has_sequence_col()) {
+                        if (has_sequence_col) {
                             auto rowset_version = rowset_it->second;
                             if (version >= rowset_version.first &&
                                 version <= rowset_version.second) {
diff --git a/be/test/cloud/cloud_tablet_mgr_test.cpp 
b/be/test/cloud/cloud_tablet_mgr_test.cpp
index ac956dbc19a..1838cc9c544 100644
--- a/be/test/cloud/cloud_tablet_mgr_test.cpp
+++ b/be/test/cloud/cloud_tablet_mgr_test.cpp
@@ -22,7 +22,9 @@
 #include <gtest/gtest.h>
 
 #include <atomic>
+#include <chrono>
 #include <condition_variable>
+#include <future>
 #include <memory>
 #include <mutex>
 #include <thread>
@@ -92,6 +94,52 @@ TEST_F(CloudTabletMgrTest, 
VacuumReclaimsWarmupStatesWithoutStaleOrUnusedRowsets
     EXPECT_EQ(tablet->get_rowset_warmup_state(rowset.rowset_id()).progress, 
WarmUpProgress::NONE);
 }
 
+TEST_F(CloudTabletMgrTest, DeleteBitmapCheckDoesNotHoldTabletMapLock) {
+    const bool old_check = 
config::enable_check_agg_and_remove_pre_rowsets_delete_bitmap;
+    auto sp = SyncPoint::get_instance();
+    sp->clear_all_call_backs();
+    Defer cleanup([&] {
+        sp->disable_processing();
+        sp->clear_all_call_backs();
+        config::enable_check_agg_and_remove_pre_rowsets_delete_bitmap = 
old_check;
+    });
+    config::enable_check_agg_and_remove_pre_rowsets_delete_bitmap = true;
+
+    CloudTabletMgr mgr(_engine);
+    auto tablet = std::make_shared<CloudTablet>(_engine, _tablet_meta);
+    mgr.put_tablet_for_UT(tablet);
+    TabletMetaSharedPtr other_meta(new TabletMeta(
+            1, 2, _tablet_meta->tablet_id() + 1, 15674, 4, 5, TTabletSchema(), 
6, {{7, 8}},
+            UniqueId(9, 10), TTabletType::TABLET_TYPE_DISK, 
TCompressionType::LZ4F));
+    auto other_tablet = std::make_shared<CloudTablet>(_engine, other_meta);
+
+    CountDownLatch check_entered(1);
+    CountDownLatch resume_check(1);
+    sp->set_call_back(
+            
"BaseTablet::check_agg_delete_bitmap_for_stale_rowsets:before_traverse_delete_bitmap",
+            [&](auto&&) {
+                check_entered.count_down();
+                resume_check.wait();
+            });
+    sp->enable_processing();
+
+    CountDownLatch stop_latch(1);
+    auto vacuum = std::async(std::launch::async, [&] { 
mgr.vacuum_stale_rowsets(stop_latch); });
+    Defer unblock_check([&] { resume_check.count_down(); });
+    ASSERT_TRUE(check_entered.wait_for(std::chrono::seconds(10)));
+
+    auto map_access = std::async(std::launch::async, [&] {
+        mgr.put_tablet_for_UT(other_tablet);
+        return mgr.get_weak_tablets().size();
+    });
+    const auto map_status = map_access.wait_for(std::chrono::seconds(10));
+    // Release the check before joining, so the old locking behavior fails 
without hanging UT.
+    resume_check.count_down();
+    vacuum.get();
+    EXPECT_EQ(map_status, std::future_status::ready);
+    EXPECT_EQ(map_access.get(), 2);
+}
+
 // Test concurrent get_tablet calls for the same tablet_id.
 // Reproduces bug where tablet ends up in cache but not in _tablet_map.
 //
diff --git a/be/test/cloud/cloud_tablet_test.cpp 
b/be/test/cloud/cloud_tablet_test.cpp
index 913d98e794b..533e39ca1b4 100644
--- a/be/test/cloud/cloud_tablet_test.cpp
+++ b/be/test/cloud/cloud_tablet_test.cpp
@@ -39,6 +39,8 @@
 #include "storage/rowset/rowset_factory.h"
 #include "storage/rowset/rowset_meta.h"
 #include "storage/tablet/tablet_meta.h"
+#include "util/countdown_latch.h"
+#include "util/defer_op.h"
 #include "util/uid_util.h"
 
 namespace doris {
@@ -337,6 +339,115 @@ TEST_F(CloudTabletDeleteBitmapTest, 
AggDeleteBitmapForCompactionReturnsPreRowset
     EXPECT_EQ(rowset_versions_without_stats, pre_rowset_to_versions);
 }
 
+class CloudTabletDeleteBitmapCheckTest : public CloudTabletWarmUpStateTest,
+                                         public 
testing::WithParamInterface<bool> {
+public:
+    void SetUp() override {
+        CloudTabletWarmUpStateTest::SetUp();
+        TColumn key;
+        key.__set_column_name("k");
+        key.__set_is_key(true);
+        key.column_type.type = TPrimitiveType::BIGINT;
+        TColumn value;
+        value.__set_column_name(GetParam() ? SEQUENCE_COL : "v");
+        value.__set_is_key(false);
+        value.__set_aggregation_type(TAggregationType::REPLACE);
+        value.column_type.type = TPrimitiveType::BIGINT;
+        TTabletSchema schema;
+        schema.__set_short_key_column_count(1);
+        schema.__set_schema_hash(4);
+        schema.__set_keys_type(TKeysType::UNIQUE_KEYS);
+        schema.__set_columns({key, value});
+        schema.__set_sequence_col_idx(GetParam() ? 1 : -1);
+        _tablet_meta.reset(new TabletMeta(1, 2, 15673, 15674, 4, 5, schema, 2, 
{{0, 0}, {1, 1}},
+                                          UniqueId(9, 10), 
TTabletType::TABLET_TYPE_DISK,
+                                          TCompressionType::LZ4F, 0, true));
+        _tablet = std::make_shared<CloudTablet>(_engine, _tablet_meta);
+    }
+};
+
+TEST_P(CloudTabletDeleteBitmapCheckTest, 
DoesNotAcquireHeaderLockDuringBitmapTraversal) {
+    auto rowset = create_rowset(Version(2, 4));
+    ASSERT_NE(rowset, nullptr);
+    _tablet->_rs_version_map.emplace(rowset->version(), rowset);
+    auto& delete_bitmap = _tablet->tablet_meta()->delete_bitmap();
+    roaring::Roaring bitmap;
+    bitmap.add(1);
+    // Version 3 is not a rowset end version, so both schema variants exercise 
the sequence check.
+    delete_bitmap.set({rowset->rowset_id(), 0, 3}, bitmap);
+
+    auto sp = SyncPoint::get_instance();
+    sp->clear_all_call_backs();
+    Defer cleanup([sp] {
+        sp->disable_processing();
+        sp->clear_all_call_backs();
+    });
+    CountDownLatch snapshot_ready(1);
+    CountDownLatch resume_check(1);
+    sp->set_call_back(
+            
"BaseTablet::check_agg_delete_bitmap_for_stale_rowsets:before_traverse_delete_bitmap",
+            [&](auto&&) {
+                snapshot_ready.count_down();
+                resume_check.wait();
+            });
+    sp->enable_processing();
+
+    int64_t useless_rowsets = 0;
+    int64_t useless_versions = 0;
+    auto check = std::async(std::launch::async, [&] {
+        _tablet->check_agg_delete_bitmap_for_stale_rowsets(useless_rowsets, 
useless_versions);
+    });
+    Defer unblock_check([&] { resume_check.count_down(); });
+    ASSERT_TRUE(snapshot_ready.wait_for(seconds(10)));
+
+    // Model compaction's exclusive header ownership after the check has 
captured its rowsets.
+    std::unique_lock header_lock(_tablet->get_header_lock());
+    resume_check.count_down();
+    const auto check_status = check.wait_for(seconds(10));
+    if (check_status == std::future_status::ready) {
+        // Even an empty compaction bitmap takes the DBM write lock while 
holding the header lock.
+        DeleteBitmap empty_bitmap(_tablet->tablet_id());
+        delete_bitmap.merge(empty_bitmap);
+    }
+    // Let the old callback finish after a timeout instead of leaving a 
deadlocked test thread.
+    header_lock.unlock();
+    check.get();
+    EXPECT_EQ(check_status, std::future_status::ready);
+    EXPECT_EQ(useless_rowsets, 0);
+    EXPECT_EQ(useless_versions, GetParam() ? 0 : 1);
+}
+
+TEST_P(CloudTabletDeleteBitmapCheckTest, 
PreservesActiveAndStaleRowsetVersionChecks) {
+    auto active = create_rowset(Version(2, 4));
+    auto stale = create_rowset(Version(5, 7));
+    auto orphan = create_rowset(Version(8, 8));
+    ASSERT_TRUE(active && stale && orphan);
+    _tablet->_rs_version_map.emplace(active->version(), active);
+    _tablet->_stale_rs_version_map.emplace(stale->version(), stale);
+    auto& delete_bitmap = _tablet->tablet_meta()->delete_bitmap();
+    roaring::Roaring bitmap;
+    bitmap.add(1);
+    delete_bitmap.set({active->rowset_id(), 0, 3}, bitmap);
+    delete_bitmap.set({active->rowset_id(), 0, 4}, bitmap);
+    delete_bitmap.set({stale->rowset_id(), 0, 6}, bitmap);
+    delete_bitmap.set({stale->rowset_id(), 0, 7}, bitmap);
+    delete_bitmap.set({orphan->rowset_id(), 0, 8}, bitmap);
+
+    int64_t useless_rowsets = 0;
+    int64_t useless_versions = 0;
+    _tablet->check_agg_delete_bitmap_for_stale_rowsets(useless_rowsets, 
useless_versions);
+    EXPECT_EQ(useless_rowsets, 1);
+    EXPECT_EQ(useless_versions, GetParam() ? 0 : 2);
+
+    // A version outside the rowset's range is still useless for a sequence 
table.
+    delete_bitmap.set({active->rowset_id(), 0, 1}, bitmap);
+    _tablet->check_agg_delete_bitmap_for_stale_rowsets(useless_rowsets, 
useless_versions);
+    EXPECT_EQ(useless_rowsets, 1);
+    EXPECT_EQ(useless_versions, GetParam() ? 1 : 2);
+}
+
+INSTANTIATE_TEST_SUITE_P(SequenceModes, CloudTabletDeleteBitmapCheckTest, 
testing::Bool());
+
 // Test get_rowset_warmup_state for non-existent rowset
 TEST_F(CloudTabletWarmUpStateTest, TestGetRowsetWarmupStateNonExistent) {
     auto rowset = create_rowset(Version(1, 1));


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

Reply via email to