This is an automated email from the ASF dual-hosted git repository.
liaoxin01 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 48d9e57b5e5 [fix](cloud) Use bthread-aware shared mutex for tablet
header lock (#64574)
48d9e57b5e5 is described below
commit 48d9e57b5e5e3a914216a9582d82d6fc38563e0d
Author: Xin Liao <[email protected]>
AuthorDate: Thu Jun 25 15:49:48 2026 +0800
[fix](cloud) Use bthread-aware shared mutex for tablet header lock (#64574)
## Problem
The tablet header lock (`_meta_lock`) is a `std::shared_mutex`, which
under libstdc++ wraps `pthread_rwlock_t` and is **thread-affine**:
unlocking it from an OS thread other than the one that acquired it is
undefined behavior.
In cloud mode the header **write** lock can be held across a
**suspending** call. Concretely, when a query-driven rowset sync pulls
an overlapping (compacted) rowset, `CloudTablet::add_rowsets` warms up
the new remote file **while holding the write lock**; on a
cold-restarted BE, resolving the storage vault / building the S3 client
issues a meta-service RPC. The holding bthread suspends and may
**migrate to another worker pthread**, so the matching unlock runs on a
**different OS thread**. This corrupts the glibc rwlock (the
write-locked bit is left set with no owner), **permanently wedging** the
lock — all readers/writers on that tablet pile up and queries time out
(~90s).
Observed in graceful-restart tests: a tablet's header lock stuck with
`active_writer=[none]` yet every `try_lock`/`try_lock_shared` failing
for >70 min, and the last writer's acquire OS-tid differing from its
release OS-tid (proof of the cross-thread unlock).
## Fix
Replace `_meta_lock` with `BthreadSharedMutex`, a port of libc++'s
`std::shared_mutex` (the two-gate condition-variable algorithm) onto
`bthread::Mutex` / `bthread::ConditionVariable`:
- Ownership is an integer state guarded by a briefly held internal mutex
and carries **no OS-thread identity**, so locking on one worker and
unlocking on another after a bthread migration is well defined — the
permanent wedge can no longer happen.
- Waiting blocks on a bthread condition variable, suspending the bthread
instead of blocking the worker.
- Writer-preferring; satisfies the C++ SharedMutex requirements, so it
is a drop-in with `std::unique_lock` / `std::shared_lock`.
Only the tablet header lock is switched; unrelated `std::shared_mutex`
members (e.g. `TabletMeta::_meta_lock`) are left untouched. Call sites
that named the type explicitly are converted to class template argument
deduction or to `BthreadSharedMutex`.
---
be/src/cloud/cloud_base_compaction.cpp | 2 +-
be/src/cloud/cloud_cumulative_compaction.cpp | 2 +-
be/src/cloud/cloud_full_compaction.cpp | 2 +-
be/src/cloud/cloud_meta_mgr.cpp | 4 +-
be/src/cloud/cloud_meta_mgr.h | 2 +-
be/src/cloud/cloud_tablet.cpp | 51 ++--
be/src/cloud/cloud_tablet.h | 16 +-
be/src/storage/compaction/binlog_compaction.cpp | 2 +-
be/src/storage/compaction/compaction.cpp | 4 +-
.../cumulative_compaction_time_series_policy.cpp | 5 +-
be/src/storage/compaction/full_compaction.cpp | 2 +-
be/src/storage/rowset_builder.cpp | 2 +-
be/src/storage/schema_change/schema_change.cpp | 4 +-
be/src/storage/tablet/base_tablet.cpp | 4 +
be/src/storage/tablet/base_tablet.h | 9 +-
be/src/storage/tablet/tablet.cpp | 65 +++--
be/src/storage/tablet/tablet.h | 9 +-
be/src/storage/tablet/tablet_manager.cpp | 2 +-
be/src/storage/task/engine_clone_task.cpp | 2 +-
be/src/storage/task/index_builder.cpp | 4 +-
be/src/util/bthread_shared_mutex.h | 125 ++++++++++
.../cloud/cloud_empty_rowset_compaction_test.cpp | 2 +-
be/test/cloud/cloud_meta_mgr_test.cpp | 28 +--
be/test/cloud/cloud_tablet_test.cpp | 2 +-
be/test/util/bthread_shared_mutex_test.cpp | 264 +++++++++++++++++++++
25 files changed, 519 insertions(+), 95 deletions(-)
diff --git a/be/src/cloud/cloud_base_compaction.cpp
b/be/src/cloud/cloud_base_compaction.cpp
index f6e98e8fb31..33fca48daed 100644
--- a/be/src/cloud/cloud_base_compaction.cpp
+++ b/be/src/cloud/cloud_base_compaction.cpp
@@ -189,7 +189,7 @@ Status CloudBaseCompaction::pick_rowsets_to_compact() {
std::shared_lock rlock(_tablet->get_header_lock());
_base_compaction_cnt = cloud_tablet()->base_compaction_cnt();
_cumulative_compaction_cnt =
cloud_tablet()->cumulative_compaction_cnt();
- _input_rowsets =
cloud_tablet()->pick_candidate_rowsets_to_base_compaction();
+ _input_rowsets =
cloud_tablet()->pick_candidate_rowsets_to_base_compaction_unlocked();
}
if (auto st = check_version_continuity(_input_rowsets); !st.ok()) {
DCHECK(false) << st;
diff --git a/be/src/cloud/cloud_cumulative_compaction.cpp
b/be/src/cloud/cloud_cumulative_compaction.cpp
index e7229e6f12d..d136944ae4b 100644
--- a/be/src/cloud/cloud_cumulative_compaction.cpp
+++ b/be/src/cloud/cloud_cumulative_compaction.cpp
@@ -503,7 +503,7 @@ Status CloudCumulativeCompaction::pick_rowsets_to_compact()
{
std::max(cloud_tablet()->cumulative_layer_point(),
_max_conflict_version + 1),
cloud_tablet()->alter_version() + 1);
// Get all rowsets whose version >= `candidate_version` as candidate
rowsets
- cloud_tablet()->traverse_rowsets(
+ cloud_tablet()->traverse_rowsets_unlocked(
[&candidate_rowsets, candidate_version](const RowsetSharedPtr&
rs) {
if (rs->start_version() >= candidate_version) {
candidate_rowsets.push_back(rs);
diff --git a/be/src/cloud/cloud_full_compaction.cpp
b/be/src/cloud/cloud_full_compaction.cpp
index 56d81e98a9f..12ea9c87c99 100644
--- a/be/src/cloud/cloud_full_compaction.cpp
+++ b/be/src/cloud/cloud_full_compaction.cpp
@@ -128,7 +128,7 @@ Status CloudFullCompaction::pick_rowsets_to_compact() {
std::shared_lock rlock(_tablet->get_header_lock());
_base_compaction_cnt = cloud_tablet()->base_compaction_cnt();
_cumulative_compaction_cnt =
cloud_tablet()->cumulative_compaction_cnt();
- _input_rowsets =
cloud_tablet()->pick_candidate_rowsets_to_full_compaction();
+ _input_rowsets =
cloud_tablet()->pick_candidate_rowsets_to_full_compaction_unlocked();
}
if (auto st = check_version_continuity(_input_rowsets); !st.ok()) {
DCHECK(false) << st;
diff --git a/be/src/cloud/cloud_meta_mgr.cpp b/be/src/cloud/cloud_meta_mgr.cpp
index 4eb56aa4237..479e9fb153d 100644
--- a/be/src/cloud/cloud_meta_mgr.cpp
+++ b/be/src/cloud/cloud_meta_mgr.cpp
@@ -618,7 +618,7 @@ Status CloudMetaMgr::_log_mow_delete_bitmap(CloudTablet*
tablet, GetRowsetRespon
std::vector<RowsetSharedPtr> old_rowsets;
RowsetIdUnorderedSet old_rowset_ids;
{
- std::lock_guard<std::shared_mutex>
rlock(tablet->get_header_lock());
+ std::lock_guard rlock(tablet->get_header_lock());
RETURN_IF_ERROR(tablet->get_all_rs_id_unlocked(old_max_version,
&old_rowset_ids));
old_rowsets = tablet->get_rowset_by_ids(&old_rowset_ids);
}
@@ -2367,7 +2367,7 @@ int64_t
CloudMetaMgr::get_inverted_index_file_size(RowsetMeta& rs_meta) {
}
Status CloudMetaMgr::fill_version_holes(CloudTablet* tablet, int64_t
max_version,
- std::unique_lock<std::shared_mutex>&
wlock) {
+ std::unique_lock<BthreadSharedMutex>&
wlock) {
if (max_version <= 0) {
return Status::OK();
}
diff --git a/be/src/cloud/cloud_meta_mgr.h b/be/src/cloud/cloud_meta_mgr.h
index eb2a0f1ab9c..657265594bc 100644
--- a/be/src/cloud/cloud_meta_mgr.h
+++ b/be/src/cloud/cloud_meta_mgr.h
@@ -160,7 +160,7 @@ public:
// Fill version holes by creating empty rowsets for missing versions
Status fill_version_holes(CloudTablet* tablet, int64_t max_version,
- std::unique_lock<std::shared_mutex>& wlock);
+ std::unique_lock<BthreadSharedMutex>& wlock);
// Create an empty rowset to fill a version hole
Status create_empty_rowset_for_hole(CloudTablet* tablet, int64_t version,
diff --git a/be/src/cloud/cloud_tablet.cpp b/be/src/cloud/cloud_tablet.cpp
index 3522871f557..7827c438bf9 100644
--- a/be/src/cloud/cloud_tablet.cpp
+++ b/be/src/cloud/cloud_tablet.cpp
@@ -187,7 +187,10 @@ Result<std::vector<Version>>
CloudTablet::capture_versions_prefer_cache(
const Version& spec_version) const {
g_capture_prefer_cache_count << 1;
Versions version_path;
- std::shared_lock rlock(_meta_lock);
+ // Caller (capture_consistent_versions_unlocked) already holds shared
+ // `_meta_lock`; do NOT re-acquire it here. The lock is writer-preferring,
+ // so a recursive shared acquisition self-deadlocks if a writer queues in
+ // between the outer and inner lock.
auto st =
_timestamped_version_tracker.capture_consistent_versions_prefer_cache(
spec_version, version_path,
[&](int64_t start, int64_t end) { return
rowset_is_warmed_up_unlocked(start, end); });
@@ -239,7 +242,10 @@ Result<std::vector<Version>>
CloudTablet::capture_versions_with_freshness_tolera
auto freshness_limit_tp = system_clock::now() -
milliseconds(query_freshness_tolerance_ms);
// find a version path where every edge(rowset) has been warmuped
Versions version_path;
- std::shared_lock rlock(_meta_lock);
+ // Caller (capture_consistent_versions_unlocked) already holds shared
+ // `_meta_lock`; do NOT re-acquire it here. The lock is writer-preferring,
+ // so a recursive shared acquisition self-deadlocks if a writer queues in
+ // between the outer and inner lock.
if (enable_unique_key_merge_on_write()) {
// For merge-on-write table, newly generated delete bitmap marks will
be on the rowsets which are in newest layout.
// So we can ony capture rowsets which are in newest data layout.
Otherwise there may be data correctness issue.
@@ -265,7 +271,8 @@ Result<std::vector<Version>>
CloudTablet::capture_versions_with_freshness_tolera
std::ranges::any_of(std::views::values(_tablet_meta->all_rs_metas()), check_fn)
||
std::ranges::any_of(std::views::values(_tablet_meta->all_stale_rs_metas()),
check_fn);
if (should_fallback) {
- rlock.unlock();
+ // The outer caller still holds the shared `_meta_lock`; the base
+ // unlocked fallback below runs under that lock.
g_capture_with_freshness_tolerance_fallback_count << 1;
// if there exists a rowset which satisfies freshness tolerance and
its start version is larger than the path max version
// but has not been warmuped up yet, fallback to capture rowsets as
usual
@@ -411,7 +418,7 @@ Status CloudTablet::sync_if_not_running(SyncRowsetStats*
stats) {
}
void CloudTablet::add_rowsets(std::vector<RowsetSharedPtr> to_add, bool
version_overlap,
- std::unique_lock<std::shared_mutex>& meta_lock,
+ std::unique_lock<BthreadSharedMutex>& meta_lock,
bool warmup_delta_data) {
if (to_add.empty()) {
return;
@@ -493,7 +500,7 @@ void CloudTablet::add_rowsets(std::vector<RowsetSharedPtr>
to_add, bool version_
}
void CloudTablet::delete_rowsets(const std::vector<RowsetSharedPtr>& to_delete,
- std::unique_lock<std::shared_mutex>&) {
+ std::unique_lock<BthreadSharedMutex>&) {
if (to_delete.empty()) {
return;
}
@@ -514,7 +521,7 @@ void CloudTablet::delete_rowsets(const
std::vector<RowsetSharedPtr>& to_delete,
}
void CloudTablet::delete_rowsets_for_schema_change(const
std::vector<RowsetSharedPtr>& to_delete,
-
std::unique_lock<std::shared_mutex>&,
+
std::unique_lock<BthreadSharedMutex>&,
bool
recycle_deleted_rowsets) {
if (to_delete.empty()) {
return;
@@ -546,7 +553,7 @@ void CloudTablet::delete_rowsets_for_schema_change(const
std::vector<RowsetShare
void CloudTablet::replace_rowsets_with_schema_change_output(
const std::vector<RowsetSharedPtr>& output_rowsets, int64_t
alter_version,
- std::unique_lock<std::shared_mutex>& meta_lock, const char* stage,
+ std::unique_lock<BthreadSharedMutex>& meta_lock, const char* stage,
bool recycle_deleted_rowsets) {
std::vector<RowsetSharedPtr> to_delete;
for (auto& [v, rs] : _rs_version_map) {
@@ -895,7 +902,7 @@ int64_t CloudTablet::get_cloud_base_compaction_score()
const {
if (_tablet_meta->compaction_policy() == CUMULATIVE_TIME_SERIES_POLICY) {
bool has_delete = false;
int64_t point = cumulative_layer_point();
- std::shared_lock<std::shared_mutex> rlock(_meta_lock);
+ std::shared_lock rlock(_meta_lock);
for (const auto& [_, rs_meta] : _tablet_meta->all_rs_metas()) {
if (rs_meta->start_version() >= point) {
continue;
@@ -1156,30 +1163,24 @@ Result<RowsetSharedPtr>
CloudTablet::pick_a_rowset_for_index_change(int schema_v
return ret_rowset;
}
-std::vector<RowsetSharedPtr>
CloudTablet::pick_candidate_rowsets_to_base_compaction() {
+std::vector<RowsetSharedPtr>
CloudTablet::pick_candidate_rowsets_to_base_compaction_unlocked() {
std::vector<RowsetSharedPtr> candidate_rowsets;
- {
- std::shared_lock rlock(_meta_lock);
- for (const auto& [version, rs] : _rs_version_map) {
- if (version.first != 0 && version.first < _cumulative_point &&
- (_alter_version == -1 || version.second <= _alter_version)) {
- candidate_rowsets.push_back(rs);
- }
+ for (const auto& [version, rs] : _rs_version_map) {
+ if (version.first != 0 && version.first < _cumulative_point &&
+ (_alter_version == -1 || version.second <= _alter_version)) {
+ candidate_rowsets.push_back(rs);
}
}
std::sort(candidate_rowsets.begin(), candidate_rowsets.end(),
Rowset::comparator);
return candidate_rowsets;
}
-std::vector<RowsetSharedPtr>
CloudTablet::pick_candidate_rowsets_to_full_compaction() {
+std::vector<RowsetSharedPtr>
CloudTablet::pick_candidate_rowsets_to_full_compaction_unlocked() {
std::vector<RowsetSharedPtr> candidate_rowsets;
- {
- std::shared_lock rlock(_meta_lock);
- for (auto& [v, rs] : _rs_version_map) {
- // MUST NOT compact rowset [0-1] for some historical reasons (see
cloud_schema_change)
- if (v.first != 0) {
- candidate_rowsets.push_back(rs);
- }
+ for (auto& [v, rs] : _rs_version_map) {
+ // MUST NOT compact rowset [0-1] for some historical reasons (see
cloud_schema_change)
+ if (v.first != 0) {
+ candidate_rowsets.push_back(rs);
}
}
std::sort(candidate_rowsets.begin(), candidate_rowsets.end(),
Rowset::comparator);
@@ -2051,7 +2052,7 @@ void CloudTablet::apply_visible_pending_rowsets() {
Defer defer {[&] { clear_unused_visible_pending_rowsets(); }};
std::unique_lock lock(_sync_meta_lock);
- std::unique_lock<std::shared_mutex> meta_wlock(_meta_lock);
+ std::unique_lock meta_wlock(_meta_lock);
int64_t next_version = _max_version + 1;
std::vector<RowsetSharedPtr> to_add;
std::lock_guard<std::mutex> pending_lock(_visible_pending_rs_lock);
diff --git a/be/src/cloud/cloud_tablet.h b/be/src/cloud/cloud_tablet.h
index 93ffc40488d..7fc3dec1451 100644
--- a/be/src/cloud/cloud_tablet.h
+++ b/be/src/cloud/cloud_tablet.h
@@ -55,7 +55,7 @@ struct SyncRowsetStats {
int64_t tablet_meta_cache_miss {0};
int64_t bthread_schedule_delay_ns {0};
- int64_t meta_lock_wait_ns {0}; // _meta_lock (std::shared_mutex) wait
across all acquisitions
+ int64_t meta_lock_wait_ns {0}; // _meta_lock (BthreadSharedMutex) wait
across all acquisitions
int64_t sync_meta_lock_wait_ns {
0}; // _sync_meta_lock (bthread::Mutex) wait across all
acquisitions
};
@@ -152,19 +152,19 @@ public:
// If 'need_download_data_async' is true, it means that we need to
download the new version
// rowsets datum async.
void add_rowsets(std::vector<RowsetSharedPtr> to_add, bool version_overlap,
- std::unique_lock<std::shared_mutex>& meta_lock,
+ std::unique_lock<BthreadSharedMutex>& meta_lock,
bool warmup_delta_data = false);
// MUST hold EXCLUSIVE `_meta_lock`.
void delete_rowsets(const std::vector<RowsetSharedPtr>& to_delete,
- std::unique_lock<std::shared_mutex>& meta_lock);
+ std::unique_lock<BthreadSharedMutex>& meta_lock);
// Like delete_rowsets, but also removes edges from the version graph.
// Used by schema change to prevent the greedy capture algorithm from
// preferring stale compaction rowsets over individual SC output rowsets.
// MUST hold EXCLUSIVE `_meta_lock`.
void delete_rowsets_for_schema_change(const std::vector<RowsetSharedPtr>&
to_delete,
- std::unique_lock<std::shared_mutex>&
meta_lock,
+
std::unique_lock<BthreadSharedMutex>& meta_lock,
bool recycle_deleted_rowsets = true);
// Replace local rowsets in [2, alter_version] with schema change output
rowsets.
@@ -175,7 +175,7 @@ public:
// MUST hold EXCLUSIVE `_meta_lock`.
void replace_rowsets_with_schema_change_output(
const std::vector<RowsetSharedPtr>& output_rowsets, int64_t
alter_version,
- std::unique_lock<std::shared_mutex>& meta_lock, const char* stage,
+ std::unique_lock<BthreadSharedMutex>& meta_lock, const char* stage,
bool recycle_deleted_rowsets);
// When the tablet is dropped, we need to recycle cached data:
@@ -285,7 +285,8 @@ public:
_last_active_time_ms = time_ms;
}
- std::vector<RowsetSharedPtr> pick_candidate_rowsets_to_base_compaction();
+ // MUST hold SHARED `_meta_lock`.
+ std::vector<RowsetSharedPtr>
pick_candidate_rowsets_to_base_compaction_unlocked();
inline Version max_version() const {
std::shared_lock rdlock(_meta_lock);
@@ -294,7 +295,8 @@ public:
int64_t base_size() const { return _base_size; }
- std::vector<RowsetSharedPtr> pick_candidate_rowsets_to_full_compaction();
+ // MUST hold SHARED `_meta_lock`.
+ std::vector<RowsetSharedPtr>
pick_candidate_rowsets_to_full_compaction_unlocked();
Result<RowsetSharedPtr> pick_a_rowset_for_index_change(int schema_version,
bool&
is_base_rowset);
Status check_rowset_schema_for_build_index(std::vector<TColumn>& columns,
int schema_version);
diff --git a/be/src/storage/compaction/binlog_compaction.cpp
b/be/src/storage/compaction/binlog_compaction.cpp
index b41782e4df9..7ee72cf7615 100644
--- a/be/src/storage/compaction/binlog_compaction.cpp
+++ b/be/src/storage/compaction/binlog_compaction.cpp
@@ -149,7 +149,7 @@ Status BinlogCompaction::modify_rowsets() {
_output_rowset);
{
std::lock_guard<std::mutex>
wrlock_(tablet()->get_rowset_update_lock());
- std::lock_guard<std::shared_mutex> wrlock(_tablet->get_header_lock());
+ std::lock_guard wrlock(_tablet->get_header_lock());
SCOPED_SIMPLE_TRACE_IF_TIMEOUT(TRACE_TABLET_LOCK_THRESHOLD);
RETURN_IF_ERROR(tablet()->modify_row_binlog_rowsets(output_rowsets,
_input_rowsets));
}
diff --git a/be/src/storage/compaction/compaction.cpp
b/be/src/storage/compaction/compaction.cpp
index ff87dbd6764..df2fee8b114 100644
--- a/be/src/storage/compaction/compaction.cpp
+++ b/be/src/storage/compaction/compaction.cpp
@@ -1504,7 +1504,7 @@ Status CompactionMixin::modify_rowsets() {
{
std::lock_guard<std::mutex>
wrlock_(tablet()->get_rowset_update_lock());
- std::lock_guard<std::shared_mutex>
wrlock(_tablet->get_header_lock());
+ std::lock_guard wrlock(_tablet->get_header_lock());
SCOPED_SIMPLE_TRACE_IF_TIMEOUT(TRACE_TABLET_LOCK_THRESHOLD);
// Here we will calculate all the rowsets delete bitmaps which are
committed but not published to reduce the calculation pressure
@@ -1559,7 +1559,7 @@ Status CompactionMixin::modify_rowsets() {
RETURN_IF_ERROR(tablet()->modify_rowsets(output_rowsets,
_input_rowsets, true));
}
} else {
- std::lock_guard<std::shared_mutex> wrlock(_tablet->get_header_lock());
+ std::lock_guard wrlock(_tablet->get_header_lock());
SCOPED_SIMPLE_TRACE_IF_TIMEOUT(TRACE_TABLET_LOCK_THRESHOLD);
RETURN_IF_ERROR(tablet()->modify_rowsets(output_rowsets,
_input_rowsets, true));
}
diff --git
a/be/src/storage/compaction/cumulative_compaction_time_series_policy.cpp
b/be/src/storage/compaction/cumulative_compaction_time_series_policy.cpp
index ab23c43904e..af023f6beca 100644
--- a/be/src/storage/compaction/cumulative_compaction_time_series_policy.cpp
+++ b/be/src/storage/compaction/cumulative_compaction_time_series_policy.cpp
@@ -154,9 +154,12 @@ uint32_t
TimeSeriesCumulativeCompactionPolicy::calc_cumulative_compaction_score(
}
// Condition 6: If there is a continuous set of empty rowsets, prioritize
merging.
+ // This is reached via Tablet::suitable_for_compaction() ->
_calc_cumulative_compaction_score(),
+ // which already holds a shared `_meta_lock`. Use the unlocked variant to
avoid recursively
+ // re-acquiring the (writer-preferring) header lock, which would
self-deadlock.
std::vector<RowsetSharedPtr> input_rowsets;
std::vector<RowsetSharedPtr> candidate_rowsets =
- tablet->pick_candidate_rowsets_to_cumulative_compaction();
+ tablet->pick_candidate_rowsets_to_cumulative_compaction_unlocked();
tablet->calc_consecutive_empty_rowsets(
&input_rowsets, candidate_rowsets,
tablet->tablet_meta()->time_series_compaction_empty_rowsets_threshold());
diff --git a/be/src/storage/compaction/full_compaction.cpp
b/be/src/storage/compaction/full_compaction.cpp
index 9d730e0856c..260badefe4c 100644
--- a/be/src/storage/compaction/full_compaction.cpp
+++ b/be/src/storage/compaction/full_compaction.cpp
@@ -169,7 +169,7 @@ Status FullCompaction::modify_rowsets() {
} else {
DBUG_EXECUTE_IF("FullCompaction.modify_rowsets.before.block",
DBUG_BLOCK);
std::lock_guard<std::mutex>
rowset_update_wlock(tablet()->get_rowset_update_lock());
- std::lock_guard<std::shared_mutex>
meta_wlock(_tablet->get_header_lock());
+ std::lock_guard meta_wlock(_tablet->get_header_lock());
RETURN_IF_ERROR(tablet()->modify_rowsets(output_rowsets,
_input_rowsets, true));
DBUG_EXECUTE_IF("FullCompaction.modify_rowsets.sleep", { sleep(5); })
tablet()->save_meta();
diff --git a/be/src/storage/rowset_builder.cpp
b/be/src/storage/rowset_builder.cpp
index 1be1d0873dc..bab3016dd02 100644
--- a/be/src/storage/rowset_builder.cpp
+++ b/be/src/storage/rowset_builder.cpp
@@ -147,7 +147,7 @@ void RowsetBuilder::_garbage_collection(bool cancel_txn) {
Status BaseRowsetBuilder::init_mow_context(std::shared_ptr<MowContext>&
mow_context) {
DCHECK(is_data_builder());
- std::lock_guard<std::shared_mutex> lck(tablet()->get_header_lock());
+ std::lock_guard lck(tablet()->get_header_lock());
_max_version_in_flush_phase = tablet()->max_version_unlocked();
std::vector<RowsetSharedPtr> rowset_ptrs;
// tablet is under alter process. The delete bitmap will be calculated
after conversion.
diff --git a/be/src/storage/schema_change/schema_change.cpp
b/be/src/storage/schema_change/schema_change.cpp
index 13082f89401..8b51e539089 100644
--- a/be/src/storage/schema_change/schema_change.cpp
+++ b/be/src/storage/schema_change/schema_change.cpp
@@ -978,7 +978,7 @@ Status SchemaChangeJob::_do_process_alter_tablet(const
TAlterTabletReqV2& reques
std::lock_guard new_tablet_lock(_new_tablet->get_push_lock());
std::lock_guard base_tablet_wlock(_base_tablet->get_header_lock());
SCOPED_SIMPLE_TRACE_IF_TIMEOUT(TRACE_TABLET_LOCK_THRESHOLD);
- std::lock_guard<std::shared_mutex>
new_tablet_wlock(_new_tablet->get_header_lock());
+ std::lock_guard new_tablet_wlock(_new_tablet->get_header_lock());
do {
RowsetSharedPtr max_rowset;
@@ -1174,7 +1174,7 @@ Status SchemaChangeJob::_do_process_alter_tablet(const
TAlterTabletReqV2& reques
}
} else {
// set state to ready
- std::lock_guard<std::shared_mutex>
new_wlock(_new_tablet->get_header_lock());
+ std::lock_guard new_wlock(_new_tablet->get_header_lock());
SCOPED_SIMPLE_TRACE_IF_TIMEOUT(TRACE_TABLET_LOCK_THRESHOLD);
res = _new_tablet->set_tablet_state(TabletState::TABLET_RUNNING);
if (!res) {
diff --git a/be/src/storage/tablet/base_tablet.cpp
b/be/src/storage/tablet/base_tablet.cpp
index 78088e394d1..09f73ec43d2 100644
--- a/be/src/storage/tablet/base_tablet.cpp
+++ b/be/src/storage/tablet/base_tablet.cpp
@@ -188,6 +188,10 @@ void BaseTablet::update_max_version_schema(const
TabletSchemaSPtr& tablet_schema
uint32_t BaseTablet::get_real_compaction_score() const {
std::shared_lock l(_meta_lock);
+ return get_real_compaction_score_unlocked();
+}
+
+uint32_t BaseTablet::get_real_compaction_score_unlocked() const {
const auto& rs_metas = _tablet_meta->all_rs_metas();
return std::accumulate(rs_metas.begin(), rs_metas.end(), 0, [](uint32_t
score, const auto& it) {
return score + it.second->get_compaction_score();
diff --git a/be/src/storage/tablet/base_tablet.h
b/be/src/storage/tablet/base_tablet.h
index cf8af9a5a27..a835e2465ee 100644
--- a/be/src/storage/tablet/base_tablet.h
+++ b/be/src/storage/tablet/base_tablet.h
@@ -35,6 +35,7 @@
#include "storage/tablet/tablet_meta.h"
#include "storage/tablet/tablet_schema.h"
#include "storage/version_graph.h"
+#include "util/bthread_shared_mutex.h"
namespace doris {
struct RowSetSplits;
@@ -94,7 +95,7 @@ public:
int32_t max_version_config();
// FIXME(plat1ko): It is not appropriate to expose this lock
- std::shared_mutex& get_header_lock() { return _meta_lock; }
+ BthreadSharedMutex& get_header_lock() { return _meta_lock; }
void update_max_version_schema(const TabletSchemaSPtr& tablet_schema);
@@ -127,6 +128,10 @@ public:
// this method just return the compaction sum on each rowset
// note(tsy): we should unify the compaction score calculation finally
uint32_t get_real_compaction_score() const;
+ // MUST hold shared `_meta_lock`. Use this variant when the caller already
+ // holds the header lock to avoid recursively re-acquiring the (now
+ // writer-preferring) `_meta_lock`, which would self-deadlock.
+ uint32_t get_real_compaction_score_unlocked() const;
// MUST hold shared meta lock
Status capture_rs_readers_unlocked(const Versions& version_path,
@@ -372,7 +377,7 @@ protected:
Result<CaptureRowsetResult> _remote_capture_rowsets(const Version&
version_range) const;
- mutable std::shared_mutex _meta_lock;
+ mutable BthreadSharedMutex _meta_lock;
TimestampedVersionTracker _timestamped_version_tracker;
TimestampedVersionTracker _row_binlog_version_tracker;
diff --git a/be/src/storage/tablet/tablet.cpp b/be/src/storage/tablet/tablet.cpp
index d782574638b..84b083d112b 100644
--- a/be/src/storage/tablet/tablet.cpp
+++ b/be/src/storage/tablet/tablet.cpp
@@ -529,7 +529,7 @@ Status Tablet::revise_tablet_meta(const
std::vector<RowsetSharedPtr>& to_add,
Status Tablet::add_rowset(RowsetSharedPtr rowset, RowsetSharedPtr
row_binlog_rowset) {
DCHECK(rowset != nullptr);
- std::lock_guard<std::shared_mutex> wrlock(_meta_lock);
+ std::lock_guard wrlock(_meta_lock);
SCOPED_SIMPLE_TRACE_IF_TIMEOUT(TRACE_TABLET_LOCK_THRESHOLD);
// If the rowset already exist, just return directly. The rowset_id is an
unique-id,
// we can use it to check this situation.
@@ -845,7 +845,7 @@ RowsetSharedPtr Tablet::_rowset_with_largest_size() {
Status Tablet::add_inc_rowset(const RowsetSharedPtr& rowset,
const RowsetSharedPtr& row_binlog_rowset) {
DCHECK(rowset != nullptr);
- std::lock_guard<std::shared_mutex> wrlock(_meta_lock);
+ std::lock_guard wrlock(_meta_lock);
SCOPED_SIMPLE_TRACE_IF_TIMEOUT(TRACE_TABLET_LOCK_THRESHOLD);
if (_contains_rowset(rowset->rowset_id())) {
// Ensure binlog<row> is also added on retry.
@@ -882,7 +882,7 @@ void Tablet::delete_expired_stale_rowset() {
std::vector<std::pair<Version, std::vector<RowsetId>>>
deleted_stale_rowsets;
// hold write lock while processing stable rowset
{
- std::lock_guard<std::shared_mutex> wrlock(_meta_lock);
+ std::lock_guard wrlock(_meta_lock);
SCOPED_SIMPLE_TRACE_IF_TIMEOUT(TRACE_TABLET_LOCK_THRESHOLD);
// Compute the end time to delete rowsets, when a expired rowset
createtime less then this time, it will be deleted.
int64_t expired_stale_sweep_endtime =
@@ -1064,7 +1064,7 @@ void Tablet::delete_expired_stale_rowset() {
}
#ifndef BE_TEST
{
- std::shared_lock<std::shared_mutex> rlock(_meta_lock);
+ std::shared_lock rlock(_meta_lock);
save_meta();
}
#endif
@@ -1191,7 +1191,10 @@ uint32_t Tablet::calc_compaction_score(CompactionType
compaction_type,
{
// Need meta lock, because it will iterator "all_rs_metas" of tablet
meta.
std::shared_lock rdlock(_meta_lock);
- int32_t score = get_real_compaction_score();
+ // Use the unlocked variant: we already hold a shared `_meta_lock`
here,
+ // and the lock is writer-preferring, so recursively re-acquiring it
+ // would self-deadlock if a writer queues in between.
+ int32_t score = get_real_compaction_score_unlocked();
if (_compaction_score > 0 && _compaction_score != score) {
LOG(WARNING) << "cumu cache score not equal real score, cache
score; "
<< _compaction_score << ", real score: " << score
@@ -1208,7 +1211,7 @@ bool Tablet::suitable_for_compaction(
#ifndef BE_TEST
if (compaction_type == CompactionType::CUMULATIVE_COMPACTION &&
cumulative_compaction_policy != nullptr) {
- std::lock_guard<std::shared_mutex> wrlock(_meta_lock);
+ std::lock_guard wrlock(_meta_lock);
if (_cumulative_compaction_policy == nullptr ||
_cumulative_compaction_policy->name() !=
cumulative_compaction_policy->name()) {
_cumulative_compaction_policy = cumulative_compaction_policy;
@@ -1338,7 +1341,7 @@ void
Tablet::_max_continuous_version_from_beginning_unlocked(Version* version, V
}
void Tablet::calculate_cumulative_point() {
- std::lock_guard<std::shared_mutex> wrlock(_meta_lock);
+ std::lock_guard wrlock(_meta_lock);
SCOPED_SIMPLE_TRACE_IF_TIMEOUT(TRACE_TABLET_LOCK_THRESHOLD);
int64_t ret_cumulative_point;
_cumulative_compaction_policy->calculate_cumulative_point(
@@ -1396,12 +1399,17 @@ Status Tablet::_contains_version(const Version&
version) {
}
std::vector<RowsetSharedPtr>
Tablet::pick_candidate_rowsets_to_cumulative_compaction() {
+ std::shared_lock rlock(_meta_lock);
+ return pick_candidate_rowsets_to_cumulative_compaction_unlocked();
+}
+
+std::vector<RowsetSharedPtr>
Tablet::pick_candidate_rowsets_to_cumulative_compaction_unlocked() {
std::vector<RowsetSharedPtr> candidate_rowsets;
if (_cumulative_point == K_INVALID_CUMULATIVE_POINT) {
return candidate_rowsets;
}
- return _pick_visible_rowsets_to_compaction(_cumulative_point,
-
std::numeric_limits<int64_t>::max());
+ return _pick_visible_rowsets_to_compaction_unlocked(_cumulative_point,
+
std::numeric_limits<int64_t>::max());
}
std::vector<RowsetSharedPtr>
Tablet::pick_candidate_rowsets_to_base_compaction() {
@@ -1411,6 +1419,12 @@ std::vector<RowsetSharedPtr>
Tablet::pick_candidate_rowsets_to_base_compaction()
std::vector<RowsetSharedPtr> Tablet::_pick_visible_rowsets_to_compaction(
int64_t min_start_version, int64_t max_start_version) {
+ std::shared_lock rlock(_meta_lock);
+ return _pick_visible_rowsets_to_compaction_unlocked(min_start_version,
max_start_version);
+}
+
+std::vector<RowsetSharedPtr>
Tablet::_pick_visible_rowsets_to_compaction_unlocked(
+ int64_t min_start_version, int64_t max_start_version) {
auto [visible_version, update_ts] = get_visible_version_and_time();
bool update_time_long = MonotonicMillis() - update_ts >
config::compaction_keep_invisible_version_timeout_sec * 1000L;
@@ -1418,25 +1432,24 @@ std::vector<RowsetSharedPtr>
Tablet::_pick_visible_rowsets_to_compaction(
update_time_long ?
config::compaction_keep_invisible_version_min_count
:
config::compaction_keep_invisible_version_max_count;
+ // Caller MUST hold shared `_meta_lock`; do not re-acquire it here (the
lock
+ // is writer-preferring, recursive shared acquisition self-deadlocks).
std::vector<RowsetSharedPtr> candidate_rowsets;
- {
- std::shared_lock rlock(_meta_lock);
- for (const auto& [version, rs] : _rs_version_map) {
- int64_t version_start = version.first;
- // rowset is remote or rowset is not in given range
- if (!rs->is_local() || version_start < min_start_version ||
- version_start > max_start_version) {
- continue;
- }
+ for (const auto& [version, rs] : _rs_version_map) {
+ int64_t version_start = version.first;
+ // rowset is remote or rowset is not in given range
+ if (!rs->is_local() || version_start < min_start_version ||
+ version_start > max_start_version) {
+ continue;
+ }
- // can compact, met one of the conditions:
- // 1. had been visible;
- // 2. exceeds the limit of keep invisible versions.
- int64_t version_end = version.second;
- if (version_end <= visible_version ||
- version_end > visible_version + keep_invisible_version_limit) {
- candidate_rowsets.push_back(rs);
- }
+ // can compact, met one of the conditions:
+ // 1. had been visible;
+ // 2. exceeds the limit of keep invisible versions.
+ int64_t version_end = version.second;
+ if (version_end <= visible_version ||
+ version_end > visible_version + keep_invisible_version_limit) {
+ candidate_rowsets.push_back(rs);
}
}
std::sort(candidate_rowsets.begin(), candidate_rowsets.end(),
Rowset::comparator);
diff --git a/be/src/storage/tablet/tablet.h b/be/src/storage/tablet/tablet.h
index e4f2b32e964..918d892fdeb 100644
--- a/be/src/storage/tablet/tablet.h
+++ b/be/src/storage/tablet/tablet.h
@@ -214,7 +214,7 @@ public:
Versions calc_missed_versions(int64_t spec_version, Versions
existing_versions) const override;
// meta lock
- std::shared_mutex& get_header_lock() { return _meta_lock; }
+ BthreadSharedMutex& get_header_lock() { return _meta_lock; }
std::mutex& get_rowset_update_lock() { return _rowset_update_lock; }
std::mutex& get_push_lock() { return _ingest_lock; }
std::mutex& get_base_compaction_lock() { return _base_compaction_lock; }
@@ -310,6 +310,10 @@ public:
void check_tablet_path_exists();
std::vector<RowsetSharedPtr>
pick_candidate_rowsets_to_cumulative_compaction();
+ // MUST hold shared `_meta_lock`. Use this when the caller already holds
the
+ // header lock (e.g. the time-series cumulative score path under
+ // suitable_for_compaction) to avoid recursive shared acquisition.
+ std::vector<RowsetSharedPtr>
pick_candidate_rowsets_to_cumulative_compaction_unlocked();
std::vector<RowsetSharedPtr> pick_candidate_rowsets_to_base_compaction();
std::vector<RowsetSharedPtr> pick_candidate_rowsets_to_full_compaction();
std::vector<RowsetSharedPtr> pick_candidate_rowsets_to_binlog_compaction();
@@ -577,6 +581,9 @@ private:
std::vector<RowsetSharedPtr> _pick_visible_rowsets_to_compaction(int64_t
min_start_version,
int64_t
max_start_version);
+ // MUST hold shared `_meta_lock`.
+ std::vector<RowsetSharedPtr> _pick_visible_rowsets_to_compaction_unlocked(
+ int64_t min_start_version, int64_t max_start_version);
void _init_context_common_fields(RowsetWriterContext& context);
diff --git a/be/src/storage/tablet/tablet_manager.cpp
b/be/src/storage/tablet/tablet_manager.cpp
index 4371864ce49..f53ff7a9017 100644
--- a/be/src/storage/tablet/tablet_manager.cpp
+++ b/be/src/storage/tablet/tablet_manager.cpp
@@ -570,7 +570,7 @@ Status TabletManager::_drop_tablet(TTabletId tablet_id,
TReplicaId replica_id, b
{
// drop tablet will update tablet meta, should lock
- std::lock_guard<std::shared_mutex>
wrlock(to_drop_tablet->get_header_lock());
+ std::lock_guard wrlock(to_drop_tablet->get_header_lock());
SCOPED_SIMPLE_TRACE_IF_TIMEOUT(TRACE_TABLET_LOCK_THRESHOLD);
// NOTE: has to update tablet here, but must not update tablet meta
directly.
// because other thread may hold the tablet object, they may save meta
too.
diff --git a/be/src/storage/task/engine_clone_task.cpp
b/be/src/storage/task/engine_clone_task.cpp
index f8cac18b04a..e6fe3d3031d 100644
--- a/be/src/storage/task/engine_clone_task.cpp
+++ b/be/src/storage/task/engine_clone_task.cpp
@@ -951,7 +951,7 @@ Status EngineCloneTask::_finish_clone(Tablet* tablet, const
std::string& clone_d
std::lock_guard
build_inverted_index_lock(tablet->get_build_inverted_index_lock());
std::lock_guard<std::mutex> push_lock(tablet->get_push_lock());
std::lock_guard<std::mutex> rwlock(tablet->get_rowset_update_lock());
- std::lock_guard<std::shared_mutex> wrlock(tablet->get_header_lock());
+ std::lock_guard wrlock(tablet->get_header_lock());
SCOPED_SIMPLE_TRACE_IF_TIMEOUT(TRACE_TABLET_LOCK_THRESHOLD);
if (is_incremental_clone) {
status = _finish_incremental_clone(tablet, cloned_tablet_meta,
version, copy_row_binlog);
diff --git a/be/src/storage/task/index_builder.cpp
b/be/src/storage/task/index_builder.cpp
index b7626d553ed..ef49626e143 100644
--- a/be/src/storage/task/index_builder.cpp
+++ b/be/src/storage/task/index_builder.cpp
@@ -931,7 +931,7 @@ Status IndexBuilder::modify_rowsets(const
Merger::Statistics* stats) {
if (_tablet->keys_type() == KeysType::UNIQUE_KEYS &&
_tablet->enable_unique_key_merge_on_write()) {
std::lock_guard<std::mutex>
rowset_update_wlock(_tablet->get_rowset_update_lock());
- std::lock_guard<std::shared_mutex>
meta_wlock(_tablet->get_header_lock());
+ std::lock_guard meta_wlock(_tablet->get_header_lock());
SCOPED_SIMPLE_TRACE_IF_TIMEOUT(TRACE_TABLET_LOCK_THRESHOLD);
DeleteBitmapPtr delete_bitmap =
std::make_shared<DeleteBitmap>(_tablet->tablet_id());
for (auto i = 0; i < _input_rowsets.size(); ++i) {
@@ -953,7 +953,7 @@ Status IndexBuilder::modify_rowsets(const
Merger::Statistics* stats) {
// should call it after merge delete_bitmap
RETURN_IF_ERROR(_tablet->modify_rowsets(_output_rowsets,
_input_rowsets, true));
} else {
- std::lock_guard<std::shared_mutex> wrlock(_tablet->get_header_lock());
+ std::lock_guard wrlock(_tablet->get_header_lock());
RETURN_IF_ERROR(_tablet->modify_rowsets(_output_rowsets,
_input_rowsets, true));
}
diff --git a/be/src/util/bthread_shared_mutex.h
b/be/src/util/bthread_shared_mutex.h
new file mode 100644
index 00000000000..a993b5b7867
--- /dev/null
+++ b/be/src/util/bthread_shared_mutex.h
@@ -0,0 +1,125 @@
+// 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.
+
+#pragma once
+
+#include <bthread/condition_variable.h>
+#include <bthread/mutex.h>
+
+#include <climits>
+#include <mutex>
+
+namespace doris {
+
+// A reader-writer lock for bthread contexts. It is a port of libc++'s
+// std::shared_mutex (the two-gate condition-variable algorithm) onto
+// bthread::Mutex/bthread::ConditionVariable. Unlike std::shared_mutex
+// (pthread_rwlock_t), ownership carries no OS-thread identity, so it is safe
to
+// lock on one bthread worker and unlock on another after a bthread migrates.
+// Satisfies the C++ SharedMutex requirements (usable with std::unique_lock /
+// std::shared_lock). Writer-preferring.
+class BthreadSharedMutex {
+public:
+ BthreadSharedMutex() = default;
+ ~BthreadSharedMutex() = default;
+
+ BthreadSharedMutex(const BthreadSharedMutex&) = delete;
+ BthreadSharedMutex& operator=(const BthreadSharedMutex&) = delete;
+
+ void lock() {
+ std::unique_lock<bthread::Mutex> lk(_mutex);
+ while (_state & _write_entered) {
+ _gate1.wait(lk);
+ }
+ _state |= _write_entered;
+ while (_state & _n_readers) {
+ _gate2.wait(lk);
+ }
+ }
+
+ bool try_lock() {
+ std::unique_lock<bthread::Mutex> lk(_mutex);
+ if (_state == 0) {
+ _state = _write_entered;
+ return true;
+ }
+ return false;
+ }
+
+ void unlock() {
+ std::lock_guard<bthread::Mutex> lk(_mutex);
+ _state = 0;
+ // Notify while still holding `_mutex`. Releasing the mutex before
+ // notifying can lose a wakeup with bthread::ConditionVariable when the
+ // waiter is a pthread and the notifier is a bthread (or vice versa).
+ _gate1.notify_all();
+ }
+
+ void lock_shared() {
+ std::unique_lock<bthread::Mutex> lk(_mutex);
+ while ((_state & _write_entered) || (_state & _n_readers) ==
_n_readers) {
+ _gate1.wait(lk);
+ }
+ unsigned num_readers = (_state & _n_readers) + 1;
+ _state &= ~_n_readers;
+ _state |= num_readers;
+ }
+
+ bool try_lock_shared() {
+ std::unique_lock<bthread::Mutex> lk(_mutex);
+ unsigned num_readers = _state & _n_readers;
+ if (!(_state & _write_entered) && num_readers != _n_readers) {
+ ++num_readers;
+ _state &= ~_n_readers;
+ _state |= num_readers;
+ return true;
+ }
+ return false;
+ }
+
+ void unlock_shared() {
+ std::unique_lock<bthread::Mutex> lk(_mutex);
+ unsigned num_readers = (_state & _n_readers) - 1;
+ _state &= ~_n_readers;
+ _state |= num_readers;
+ // Notify while still holding `_mutex` (do not unlock first): with
+ // bthread::ConditionVariable a notify issued after releasing the mutex
+ // can be lost across pthread/bthread contexts, leaving a writer parked
+ // forever on a reader count that is already zero.
+ if (_state & _write_entered) {
+ if (num_readers == 0) {
+ _gate2.notify_one();
+ }
+ } else {
+ if (num_readers == _n_readers - 1) {
+ _gate1.notify_one();
+ }
+ }
+ }
+
+private:
+ bthread::Mutex _mutex;
+ bthread::ConditionVariable _gate1;
+ bthread::ConditionVariable _gate2;
+ unsigned _state {0};
+
+ // High bit: a writer has entered. Remaining bits: active reader count.
+ static constexpr unsigned _write_entered = 1U << (sizeof(unsigned) *
CHAR_BIT - 1);
+ static constexpr unsigned _n_readers = ~_write_entered;
+};
+
+} // namespace doris
diff --git a/be/test/cloud/cloud_empty_rowset_compaction_test.cpp
b/be/test/cloud/cloud_empty_rowset_compaction_test.cpp
index 63794bf5ec0..8fb67d04a06 100644
--- a/be/test/cloud/cloud_empty_rowset_compaction_test.cpp
+++ b/be/test/cloud/cloud_empty_rowset_compaction_test.cpp
@@ -123,7 +123,7 @@ public:
version2_meta,
&version2_rowset);
EXPECT_TRUE(status.ok());
{
- std::unique_lock<std::shared_mutex>
lock(_tablet->get_header_lock());
+ std::unique_lock lock(_tablet->get_header_lock());
_tablet->add_rowsets({version2_rowset}, false, lock, false);
}
diff --git a/be/test/cloud/cloud_meta_mgr_test.cpp
b/be/test/cloud/cloud_meta_mgr_test.cpp
index ff4539f5969..a6149cae9c5 100644
--- a/be/test/cloud/cloud_meta_mgr_test.cpp
+++ b/be/test/cloud/cloud_meta_mgr_test.cpp
@@ -204,12 +204,12 @@ TEST_F(CloudMetaMgrTest,
test_fill_version_holes_no_holes) {
// Add all rowsets to tablet
{
- std::unique_lock<std::shared_mutex> lock(tablet->get_header_lock());
+ std::unique_lock lock(tablet->get_header_lock());
tablet->add_rowsets(std::move(rowsets), false, lock, false);
}
// Test fill_version_holes directly - should not add any rowsets since
there are no holes
- std::unique_lock<std::shared_mutex> wlock(tablet->get_header_lock());
+ std::unique_lock wlock(tablet->get_header_lock());
Status status = meta_mgr.fill_version_holes(tablet.get(), 4, wlock);
EXPECT_TRUE(status.ok());
@@ -256,7 +256,7 @@ TEST_F(CloudMetaMgrTest,
test_fill_version_holes_with_holes) {
// Add all rowsets to tablet
{
- std::unique_lock<std::shared_mutex> lock(tablet->get_header_lock());
+ std::unique_lock lock(tablet->get_header_lock());
tablet->add_rowsets(std::move(rowsets), false, lock, false);
}
@@ -264,7 +264,7 @@ TEST_F(CloudMetaMgrTest,
test_fill_version_holes_with_holes) {
EXPECT_EQ(tablet->tablet_meta()->all_rs_metas().size(), 3);
// Test fill_version_holes directly to fill missing versions 1 and 3
- std::unique_lock<std::shared_mutex> wlock(tablet->get_header_lock());
+ std::unique_lock wlock(tablet->get_header_lock());
Status status = meta_mgr.fill_version_holes(tablet.get(), 4, wlock);
EXPECT_TRUE(status.ok());
@@ -372,7 +372,7 @@ TEST_F(CloudMetaMgrTest,
test_fill_version_holes_edge_cases) {
auto tablet =
std::make_shared<CloudTablet>(engine,
std::make_shared<TabletMeta>(*tablet_meta));
- std::unique_lock<std::shared_mutex> wlock(tablet->get_header_lock());
+ std::unique_lock wlock(tablet->get_header_lock());
Status status = meta_mgr.fill_version_holes(tablet.get(), 0, wlock);
EXPECT_TRUE(status.ok());
@@ -390,7 +390,7 @@ TEST_F(CloudMetaMgrTest,
test_fill_version_holes_edge_cases) {
auto tablet =
std::make_shared<CloudTablet>(engine,
std::make_shared<TabletMeta>(*tablet_meta));
- std::unique_lock<std::shared_mutex> wlock(tablet->get_header_lock());
+ std::unique_lock wlock(tablet->get_header_lock());
Status status = meta_mgr.fill_version_holes(tablet.get(), 5, wlock);
EXPECT_TRUE(status.ok());
@@ -432,7 +432,7 @@ TEST_F(CloudMetaMgrTest,
test_fill_version_holes_trailing_holes) {
// Add all rowsets to tablet
{
- std::unique_lock<std::shared_mutex> lock(tablet->get_header_lock());
+ std::unique_lock lock(tablet->get_header_lock());
tablet->add_rowsets(std::move(rowsets), false, lock, false);
}
@@ -440,7 +440,7 @@ TEST_F(CloudMetaMgrTest,
test_fill_version_holes_trailing_holes) {
EXPECT_EQ(tablet->tablet_meta()->all_rs_metas().size(), 3);
// Test fill_version_holes to fill trailing holes (versions 3, 4, 5)
- std::unique_lock<std::shared_mutex> wlock(tablet->get_header_lock());
+ std::unique_lock wlock(tablet->get_header_lock());
Status status = meta_mgr.fill_version_holes(tablet.get(), 5, wlock);
EXPECT_TRUE(status.ok());
@@ -507,7 +507,7 @@ TEST_F(CloudMetaMgrTest,
test_fill_version_holes_single_hole) {
// Add all rowsets to tablet
{
- std::unique_lock<std::shared_mutex> lock(tablet->get_header_lock());
+ std::unique_lock lock(tablet->get_header_lock());
tablet->add_rowsets(std::move(rowsets), false, lock, false);
}
@@ -515,7 +515,7 @@ TEST_F(CloudMetaMgrTest,
test_fill_version_holes_single_hole) {
EXPECT_EQ(tablet->tablet_meta()->all_rs_metas().size(), 2);
// Test fill_version_holes to fill single hole (version 1)
- std::unique_lock<std::shared_mutex> wlock(tablet->get_header_lock());
+ std::unique_lock wlock(tablet->get_header_lock());
Status status = meta_mgr.fill_version_holes(tablet.get(), 2, wlock);
EXPECT_TRUE(status.ok());
@@ -580,7 +580,7 @@ TEST_F(CloudMetaMgrTest,
test_fill_version_holes_multiple_consecutive_holes) {
// Add all rowsets to tablet
{
- std::unique_lock<std::shared_mutex> lock(tablet->get_header_lock());
+ std::unique_lock lock(tablet->get_header_lock());
tablet->add_rowsets(std::move(rowsets), false, lock, false);
}
@@ -588,7 +588,7 @@ TEST_F(CloudMetaMgrTest,
test_fill_version_holes_multiple_consecutive_holes) {
EXPECT_EQ(tablet->tablet_meta()->all_rs_metas().size(), 2);
// Test fill_version_holes to fill multiple consecutive holes (versions 1,
2, 3, 4)
- std::unique_lock<std::shared_mutex> wlock(tablet->get_header_lock());
+ std::unique_lock wlock(tablet->get_header_lock());
Status status = meta_mgr.fill_version_holes(tablet.get(), 5, wlock);
EXPECT_TRUE(status.ok());
@@ -655,7 +655,7 @@ TEST_F(CloudMetaMgrTest,
test_fill_version_holes_mixed_holes) {
// Add all rowsets to tablet
{
- std::unique_lock<std::shared_mutex> lock(tablet->get_header_lock());
+ std::unique_lock lock(tablet->get_header_lock());
tablet->add_rowsets(std::move(rowsets), false, lock, false);
}
@@ -663,7 +663,7 @@ TEST_F(CloudMetaMgrTest,
test_fill_version_holes_mixed_holes) {
EXPECT_EQ(tablet->tablet_meta()->all_rs_metas().size(), 4);
// Test fill_version_holes with max_version = 8 (should fill 1, 3, 4, 7, 8)
- std::unique_lock<std::shared_mutex> wlock(tablet->get_header_lock());
+ std::unique_lock wlock(tablet->get_header_lock());
Status status = meta_mgr.fill_version_holes(tablet.get(), 8, wlock);
EXPECT_TRUE(status.ok());
diff --git a/be/test/cloud/cloud_tablet_test.cpp
b/be/test/cloud/cloud_tablet_test.cpp
index 23045f20d5e..228e7421ee3 100644
--- a/be/test/cloud/cloud_tablet_test.cpp
+++ b/be/test/cloud/cloud_tablet_test.cpp
@@ -1006,7 +1006,7 @@ public:
}
void add_initial_rowsets(const std::vector<RowsetSharedPtr>& rowsets) {
- std::unique_lock<std::shared_mutex>
meta_wlock(_tablet->get_header_lock());
+ std::unique_lock meta_wlock(_tablet->get_header_lock());
_tablet->add_rowsets(std::vector<RowsetSharedPtr>(rowsets), false,
meta_wlock, false);
}
diff --git a/be/test/util/bthread_shared_mutex_test.cpp
b/be/test/util/bthread_shared_mutex_test.cpp
new file mode 100644
index 00000000000..8382ecdd006
--- /dev/null
+++ b/be/test/util/bthread_shared_mutex_test.cpp
@@ -0,0 +1,264 @@
+// 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 "util/bthread_shared_mutex.h"
+
+#include <bthread/bthread.h>
+#include <gtest/gtest.h>
+
+#include <atomic>
+#include <shared_mutex>
+#include <thread>
+#include <vector>
+
+namespace doris {
+
+// 1) Basic SharedMutex contract. try_lock/try_lock_shared are non-blocking, so
+// this can be checked single-threaded.
+TEST(BthreadSharedMutexTest, BasicContract) {
+ BthreadSharedMutex m;
+
+ // Multiple shared holders can coexist.
+ ASSERT_TRUE(m.try_lock_shared());
+ ASSERT_TRUE(m.try_lock_shared());
+ // A writer cannot acquire while any reader is held.
+ ASSERT_FALSE(m.try_lock());
+ m.unlock_shared();
+ ASSERT_FALSE(m.try_lock()); // still one reader
+ m.unlock_shared();
+
+ // Exclusive ownership.
+ ASSERT_TRUE(m.try_lock());
+ ASSERT_FALSE(m.try_lock()); // exclusive: a second writer fails
+ ASSERT_FALSE(m.try_lock_shared()); // a reader is blocked while the writer
holds
+ m.unlock();
+
+ // Free again.
+ ASSERT_TRUE(m.try_lock_shared());
+ m.unlock_shared();
+}
+
+// 2) Writer-pending behavior of the two-gate algorithm: once a writer has set
+// the write-entered bit and is waiting for existing readers to drain, new
+// readers must block until the writer acquires and releases the lock.
+namespace {
+struct WriterPendingCtx {
+ BthreadSharedMutex* m;
+ std::atomic<bool> acquired {false};
+ std::atomic<bool> released {false};
+};
+
+void* writer_pending_fn(void* arg) {
+ auto* c = static_cast<WriterPendingCtx*>(arg);
+ c->m->lock(); // sets write-entered, then blocks until the reader drains
+ c->acquired.store(true);
+ bthread_usleep(20 * 1000);
+ c->m->unlock();
+ c->released.store(true);
+ return nullptr;
+}
+} // namespace
+
+TEST(BthreadSharedMutexTest, WriterPendingBlocksNewReaders) {
+ bthread_setconcurrency(8);
+ BthreadSharedMutex m;
+ m.lock_shared(); // reader R1 holds a shared lock
+
+ WriterPendingCtx ctx;
+ ctx.m = &m;
+ bthread_t w;
+ ASSERT_EQ(0, bthread_start_background(&w, nullptr, writer_pending_fn,
&ctx));
+
+ // Wait until the writer has reached lock() and set the write-entered bit,
+ // observed via try_lock_shared() beginning to fail (writer-preferring: a
new
+ // reader must not barge ahead of a pending writer). Probe instead of a
fixed
+ // sleep so a slow CI worker doesn't flake; bounded so a stuck writer fails
+ // the test rather than hanging. Successful probes are released
immediately so
+ // they don't add a lingering reader.
+ bool writer_pending = false;
+ for (int i = 0; i < 1000; ++i) { // up to ~10s
+ if (!m.try_lock_shared()) {
+ writer_pending = true;
+ break;
+ }
+ m.unlock_shared();
+ bthread_usleep(10 * 1000);
+ }
+ ASSERT_TRUE(writer_pending); // writer set _write_entered, blocking
new readers
+ ASSERT_FALSE(ctx.acquired.load()); // still waiting on R1 to drain
+
+ m.unlock_shared(); // drain R1 -> writer can now acquire
+ ASSERT_EQ(0, bthread_join(w, nullptr));
+ ASSERT_TRUE(ctx.acquired.load());
+ ASSERT_TRUE(ctx.released.load());
+
+ // The lock is usable afterwards.
+ ASSERT_TRUE(m.try_lock_shared());
+ m.unlock_shared();
+}
+
+// 3) Cross-thread unlock regression. Ownership is not tied to the OS thread
that
+// acquired the lock, so unlocking from a different thread is well defined
+// (with std::shared_mutex / pthread_rwlock_t this is undefined behavior and
+// can permanently wedge the lock).
+TEST(BthreadSharedMutexTest, CrossThreadUnlockExclusive) {
+ BthreadSharedMutex m;
+ m.lock(); // acquired on this thread
+ std::thread t([&] { m.unlock(); }); // released on a different OS thread
+ t.join();
+ // Not wedged: the lock can be acquired again.
+ ASSERT_TRUE(m.try_lock());
+ m.unlock();
+}
+
+TEST(BthreadSharedMutexTest, CrossThreadUnlockShared) {
+ BthreadSharedMutex m;
+ m.lock_shared();
+ std::thread t([&] { m.unlock_shared(); });
+ t.join();
+ // The reader was fully released, so a writer can acquire.
+ ASSERT_TRUE(m.try_lock());
+ m.unlock();
+}
+
+// 3b) bthread migration variant: hold the lock across a suspension point so
the
+// bthread may migrate to another worker pthread, and unlock from the
resumed
+// context (potentially a different OS thread). Many bthreads run
concurrently
+// to force migration; the test passing means the lock never wedges.
+namespace {
+struct MigrateCtx {
+ BthreadSharedMutex* m;
+ std::atomic<int>* done;
+ int iters;
+};
+
+void* migrate_fn(void* arg) {
+ auto* c = static_cast<MigrateCtx*>(arg);
+ for (int i = 0; i < c->iters; ++i) {
+ c->m->lock();
+ bthread_usleep(100); // suspension point while holding the write lock
+ c->m->unlock(); // may run on a different worker than lock()
+ c->m->lock_shared();
+ bthread_usleep(100);
+ c->m->unlock_shared();
+ }
+ c->done->fetch_add(1);
+ return nullptr;
+}
+} // namespace
+
+TEST(BthreadSharedMutexTest, BthreadMigrationNoWedge) {
+ bthread_setconcurrency(8);
+ BthreadSharedMutex m;
+ std::atomic<int> done {0};
+ MigrateCtx ctx {&m, &done, 100};
+
+ constexpr int kBthreads = 16;
+ std::vector<bthread_t> tids(kBthreads);
+ for (auto& t : tids) {
+ ASSERT_EQ(0, bthread_start_background(&t, nullptr, migrate_fn, &ctx));
+ }
+ for (auto t : tids) {
+ ASSERT_EQ(0, bthread_join(t, nullptr));
+ }
+ ASSERT_EQ(kBthreads, done.load()); // all finished -> no permanent wedge
+ ASSERT_TRUE(m.try_lock());
+ m.unlock();
+}
+
+// 4) Mixed reader/writer stress with invariant counters, also exercising the
+// RAII wrappers std::shared_lock / std::unique_lock (point 5).
+namespace {
+struct StressCtx {
+ BthreadSharedMutex* m;
+ std::atomic<int>* active_readers;
+ std::atomic<int>* active_writers;
+ std::atomic<bool>* failed;
+ int iters;
+ int id;
+};
+
+void* stress_fn(void* arg) {
+ auto* c = static_cast<StressCtx*>(arg);
+ for (int i = 0; i < c->iters; ++i) {
+ if ((c->id + i) % 4 == 0) { // ~25% writers
+ std::unique_lock<BthreadSharedMutex> wlock(*c->m);
+ int aw = c->active_writers->fetch_add(1) + 1;
+ // Invariant: at most one writer, and no readers while writing.
+ if (aw != 1 || c->active_readers->load() != 0) {
+ c->failed->store(true);
+ }
+ bthread_usleep(10);
+ c->active_writers->fetch_sub(1);
+ } else {
+ std::shared_lock<BthreadSharedMutex> rlock(*c->m);
+ c->active_readers->fetch_add(1);
+ // Invariant: no writer while readers are active.
+ if (c->active_writers->load() != 0) {
+ c->failed->store(true);
+ }
+ bthread_usleep(10);
+ c->active_readers->fetch_sub(1);
+ }
+ }
+ return nullptr;
+}
+} // namespace
+
+TEST(BthreadSharedMutexTest, MixedReaderWriterInvariants) {
+ bthread_setconcurrency(8);
+ BthreadSharedMutex m;
+ std::atomic<int> active_readers {0};
+ std::atomic<int> active_writers {0};
+ std::atomic<bool> failed {false};
+
+ constexpr int kBthreads = 16;
+ constexpr int kIters = 500;
+ std::vector<bthread_t> tids(kBthreads);
+ std::vector<StressCtx> ctxs(kBthreads);
+ for (int i = 0; i < kBthreads; ++i) {
+ ctxs[i] = {&m, &active_readers, &active_writers, &failed, kIters, i};
+ ASSERT_EQ(0, bthread_start_background(&tids[i], nullptr, stress_fn,
&ctxs[i]));
+ }
+ for (auto t : tids) {
+ ASSERT_EQ(0, bthread_join(t, nullptr));
+ }
+ ASSERT_FALSE(failed.load());
+ ASSERT_EQ(0, active_readers.load());
+ ASSERT_EQ(0, active_writers.load());
+}
+
+// 5) RAII drop-in coverage: BthreadSharedMutex must work with std::shared_lock
+// and std::unique_lock, which is what the call sites rely on.
+TEST(BthreadSharedMutexTest, RaiiWrappers) {
+ BthreadSharedMutex m;
+ {
+ std::shared_lock<BthreadSharedMutex> rlock(m);
+ ASSERT_FALSE(m.try_lock()); // writer blocked while shared lock held
+ }
+ {
+ std::unique_lock<BthreadSharedMutex> wlock(m);
+ ASSERT_FALSE(m.try_lock_shared()); // reader blocked while exclusive
lock held
+ }
+ // Adopt an already-held lock, mirroring the call-site pattern.
+ ASSERT_TRUE(m.try_lock());
+ { std::unique_lock<BthreadSharedMutex> wlock(m, std::adopt_lock); }
+ ASSERT_TRUE(m.try_lock_shared()); // released by the adopting guard
+ m.unlock_shared();
+}
+
+} // namespace doris
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]