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

Yukang-Lian 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 cb0b56d408a [fix](cloud) Fix deadlock between tablet encryption check 
and meta lock writers (#69950)
cb0b56d408a is described below

commit cb0b56d408a76a59e5b7d248071907537796e47e
Author: Jamie <[email protected]>
AuthorDate: Tue Nov 3 14:41:49 2026 +0800

    [fix](cloud) Fix deadlock between tablet encryption check and meta lock 
writers (#69950)
    
    ### What problem does this PR solve?
    
    Issue Number: None
    
    Related PR: #64574
    
    Problem Summary:
    
    `is_tablet_encrypted()`, which serves `/api/check_tablet_encryption`,
    holds the tablet `_meta_lock` read lock in `traverse_rowsets()` and
    takes it again through `tablet_schema()` in the visitor. Since #64574
    `_meta_lock` is the writer-preferring `BthreadSharedMutex`, so a writer
    that arrives between the two reads deadlocks with the check:
    
    - The check holds the first read lock and waits for the second, which is
    blocked by the pending writer.
    - The writer waits for the first read lock to be released.
    
    The window is wide because each rowset's files are read from remote
    storage under the lock, and compaction and rowset sync take the write
    lock often. Once this happens the tablet stays stuck until the BE
    restarts:
    
    - The HTTP request never returns.
    - The compaction keeps renewing its lease but never finishes, so other
    BEs get `JOB_TABLET_BUSY` for this tablet.
    - Every later reader of this tablet's meta blocks behind the pending
    writer, including the periodic tablet sync.
    
    We hit this in a regression test: the compaction stopped right after its
    rowset sync, and the check timed out on all 10 retries over about 2h47m.
    
    This PR reads the schema once before the check and checks a snapshot of
    the rowsets (`get_snapshot_rowset()`) with no lock held, which also
    stops holding the meta lock across remote file reads. It adds a
    test-only sync point so a unit test can observe the lock at that moment.
    
    ### Release note
    
    Fix a deadlock between the tablet encryption check API and tablet meta
    writers such as compaction, which could block a tablet until the BE
    restarts.
    
    ### Check List (For Author)
    
    - Test <!-- At least one of them must be included. -->
        - [ ] Regression test
        - [x] Unit Test
        - [x] Manual test (add detailed scripts or steps below)
        - [ ] No need to test or manual test. Explain why:
    - [ ] This is a refactor/code format and no logic has been changed.
            - [ ] Previous test can cover this change.
            - [ ] No code files have been changed.
            - [ ] Other reason <!-- Add your reason?  -->
    
    Unit test
    `CheckEncryptionActionTest.RowsetsAreCheckedWithoutHoldingMetaLock`
    checks that `_meta_lock` is free while a rowset is being checked. It
    fails on the code before this PR and passes with it.
    
    Manual test: a temporary unit test (not committed) started a writer from
    inside the rowset check, then waited for the check to return. On the
    code before this PR the check had not returned after 20s. With this PR
    it returned at once.
    
    - Behavior changed:
        - [x] No.
        - [ ] Yes. <!-- Explain the behavior change -->
    
    - Does this need documentation?
        - [x] No.
    - [ ] Yes. <!-- Add document PR link here. eg:
    https://github.com/apache/doris-website/pull/1214
---
 .../http/action/check_encryption_action.cpp        | 17 ++++++++++---
 .../service/http/check_encryption_action_test.cpp  | 29 ++++++++++++++++++++++
 2 files changed, 42 insertions(+), 4 deletions(-)

diff --git a/be/src/service/http/action/check_encryption_action.cpp 
b/be/src/service/http/action/check_encryption_action.cpp
index ca3b61ec85b..d47f35538d2 100644
--- a/be/src/service/http/action/check_encryption_action.cpp
+++ b/be/src/service/http/action/check_encryption_action.cpp
@@ -32,6 +32,7 @@
 #include "cloud/cloud_tablet.h"
 #include "cloud/config.h"
 #include "common/status.h"
+#include "cpp/sync_point.h"
 #include "io/fs/file_reader.h"
 #include "io/fs/file_system.h"
 #include "io/fs/path.h"
@@ -64,7 +65,12 @@ Result<bool> is_tablet_encrypted(const BaseTabletSPtr& 
tablet) {
     }
     Status st;
     bool is_encrypted = true;
-    tablet->traverse_rowsets([&st, &tablet, &is_encrypted](const 
RowsetSharedPtr& rs) {
+    // The files are read without holding the tablet's meta lock: `_meta_lock` 
is writer-preferring,
+    // so taking it again for the schema while a writer is pending would 
deadlock, and holding it
+    // across remote reads would stall every other user of this tablet.
+    const auto tablet_schema = tablet->tablet_schema();
+    auto check_rowset = [&st, &tablet, &tablet_schema, &is_encrypted](const 
RowsetSharedPtr& rs) {
+        TEST_SYNC_POINT_CALLBACK("is_tablet_encrypted::check_rowset");
         if (!st) {
             return;
         }
@@ -100,8 +106,8 @@ Result<bool> is_tablet_encrypted(const BaseTabletSPtr& 
tablet) {
         std::vector<std::string> file_paths;
         const auto& first_seg_path = maybe_seg_path.value();
         file_paths.emplace_back(first_seg_path);
-        if (tablet->tablet_schema()->has_inverted_index() &&
-            tablet->tablet_schema()->get_inverted_index_storage_format() == 
V2) {
+        if (tablet_schema->has_inverted_index() &&
+            tablet_schema->get_inverted_index_storage_format() == V2) {
             
file_paths.emplace_back(InvertedIndexDescriptor::get_index_file_path_v2(
                     
InvertedIndexDescriptor::get_index_file_path_prefix(first_seg_path)));
         }
@@ -138,7 +144,10 @@ Result<bool> is_tablet_encrypted(const BaseTabletSPtr& 
tablet) {
                 LOG(INFO) << "found not encrypted segment, path=" << 
first_seg_path;
             }
         }
-    });
+    };
+    for (const auto& rs : tablet->get_snapshot_rowset()) {
+        check_rowset(rs);
+    }
 
     if (st) {
         return is_encrypted;
diff --git a/be/test/service/http/check_encryption_action_test.cpp 
b/be/test/service/http/check_encryption_action_test.cpp
index 2767b27ce63..3ca13503ca2 100644
--- a/be/test/service/http/check_encryption_action_test.cpp
+++ b/be/test/service/http/check_encryption_action_test.cpp
@@ -27,6 +27,7 @@
 
 #include "cloud/cloud_storage_engine.h"
 #include "cloud/cloud_tablet.h"
+#include "cpp/sync_point.h"
 #include "storage/rowset/rowset.h"
 #include "storage/rowset/rowset_factory.h"
 #include "storage/rowset/rowset_meta.h"
@@ -89,4 +90,32 @@ TEST_F(CheckEncryptionActionTest, 
UncheckableRowsetReportsError) {
             << res.error();
 }
 
+// `_meta_lock` is writer-preferring: if the check held it while reading files 
and took it again for
+// the tablet schema, a writer arriving in between would deadlock with the 
check.
+TEST_F(CheckEncryptionActionTest, RowsetsAreCheckedWithoutHoldingMetaLock) {
+    add_rowset(Version(2, 2), 1, "not_exist_resource");
+
+    bool checked = false;
+    bool meta_lock_is_free = false;
+    auto* sp = SyncPoint::get_instance();
+    SyncPoint::CallbackGuard guard;
+    sp->set_call_back(
+            "is_tablet_encrypted::check_rowset",
+            [&](auto&&) {
+                checked = true;
+                meta_lock_is_free = _tablet->get_header_lock().try_lock();
+                if (meta_lock_is_free) {
+                    _tablet->get_header_lock().unlock();
+                }
+            },
+            &guard);
+    sp->enable_processing();
+    auto res = is_tablet_encrypted(_tablet);
+    sp->disable_processing();
+
+    ASSERT_FALSE(res.has_value());
+    EXPECT_TRUE(checked);
+    EXPECT_TRUE(meta_lock_is_free);
+}
+
 } // namespace doris


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

Reply via email to