This is an automated email from the ASF dual-hosted git repository.
bobhan1 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/doris.git
The following commit(s) were added to refs/heads/master by this push:
new b2164ff1bac [fix](cloud) Avoid lock inversion in delete bitmap checks
(#69892)
b2164ff1bac is described below
commit b2164ff1bacd3e467fd29f443ef9c03faa899658
Author: bobhan1 <[email protected]>
AuthorDate: Mon Nov 2 11:23:53 2026 +0800
[fix](cloud) Avoid lock inversion in delete bitmap checks (#69892)
### What problem does this PR solve?
Issue Number: N/A
Problem Summary:
With background delete bitmap checks enabled, the checker holds the
delete bitmap read lock while `tablet_schema()` acquires the tablet
header lock. Compaction takes these locks in the opposite order, causing
a deadlock on the same tablet. The checker also runs inside
`TabletMap::traverse()`, so its outer map mutex blocks unrelated tablet
initialization, Rowsets scans, synchronization, and compaction
scheduling.
Read the sequence-column flag before traversing the delete bitmap, and
run checks outside the map mutex by reusing the existing tablet
snapshot. Preserve the active/stale rowset version checks for sequence
and non-sequence tables.
### Release note
Fix BE deadlocks and cascading write and metadata stalls when background
delete bitmap checks are enabled.
### Check List (For Author)
- Test
- [ ] Regression test
- [x] Unit Test
- [ ] Manual test
- [ ] No need to test
- Behavior changed:
- [ ] No.
- [x] Yes. Delete bitmap checks avoid the reversed lock order and no
longer hold the tablet map mutex while checking individual tablets.
- Does this need documentation?
- [x] No.
- [ ] Yes.
Validation: 72 ASAN unit tests passed, including five new instances
covering lock ordering, an empty compaction bitmap merge, unrelated
tablet map operations, and sequence/non-sequence version semantics.
`build-support/check-format.sh`, `build-support/check-build-hygiene.sh`,
and `git diff --check` passed.
```bash
./run-be-ut.sh --run
--filter='SequenceModes/CloudTabletDeleteBitmapCheckTest.*:CloudTabletMgrTest.*:CloudTabletDeleteBitmapTest.*:PrefillModes/CloudTabletDeleteBitmapPrefillTest.*:TabletMetaTest.*:CloudCompactionTest.*'
-j100
```
### 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 | 9 ++-
be/test/cloud/cloud_tablet_mgr_test.cpp | 48 ++++++++++++++
be/test/cloud/cloud_tablet_test.cpp | 110 ++++++++++++++++++++++++++++++++
4 files changed, 170 insertions(+), 3 deletions(-)
diff --git a/be/src/cloud/cloud_tablet_mgr.cpp
b/be/src/cloud/cloud_tablet_mgr.cpp
index 61c78473b93..91201f9bd42 100644
--- a/be/src/cloud/cloud_tablet_mgr.cpp
+++ b/be/src/cloud/cloud_tablet_mgr.cpp
@@ -362,7 +362,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,
@@ -375,7 +377,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 0ed6d7430f2..75270ef450f 100644
--- a/be/src/storage/tablet/base_tablet.cpp
+++ b/be/src/storage/tablet/base_tablet.cpp
@@ -2346,6 +2346,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;
{
@@ -2362,7 +2369,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 5f2d8c4314f..fa98540d259 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 9e169ece5da..a5869d2ca24 100644
--- a/be/test/cloud/cloud_tablet_test.cpp
+++ b/be/test/cloud/cloud_tablet_test.cpp
@@ -39,6 +39,7 @@
#include "storage/rowset/rowset_meta.h"
#include "storage/rowset/unique_rowset_id_generator.h"
#include "storage/tablet/tablet_meta.h"
+#include "util/countdown_latch.h"
#include "util/defer_op.h"
#include "util/uid_util.h"
@@ -338,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());
+
class CloudTabletDeleteBitmapPrefillTest
: public CloudTabletWarmUpStateTest,
public testing::WithParamInterface<std::tuple<bool, bool>> {
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]