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

mrhhsg 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 faa3fdac16e [fix](be) Copy PipelineTask::_sink under the lifecycle 
lock in stop_if_finished (#69208)
faa3fdac16e is described below

commit faa3fdac16effc08881b099ff1c9f69c61eb5c3f
Author: Jerry Hu <[email protected]>
AuthorDate: Sat Oct 17 06:55:02 2026 +0800

    [fix](be) Copy PipelineTask::_sink under the lifecycle lock in 
stop_if_finished (#69208)
    
    ### What problem does this PR solve?
    
    Issue Number: None
    
    Related PR: None
    
    Problem Summary: `PipelineTask::stop_if_finished()` is called by
    `FragmentMgr::cancel_worker()` (through
    `PipelineFragmentContext::clear_finished_tasks()`) on the background
    cancel thread. It copied the `_sink` shared_ptr without any lock. At the
    same time a scheduler worker can run `PipelineTask::finalize()` for the
    same task, which resets `_sink` while holding
    `_dependency_lifecycle_lock`. Copying and resetting the same shared_ptr
    instance from two threads without synchronization is a data race
    (undefined behavior): the cancel thread can observe a torn
    pointer/control block and then call `is_finished()` on an object that is
    being released. Every other cross-thread reader of `_sink`
    (`debug_string()`, the hybrid scheduler submit path) is already
    synchronized; `stop_if_finished()` was the only one left.
    
    Fix: take `_dependency_lifecycle_lock` in `stop_if_finished()`, return
    if the task is already finalized, and copy `_sink` under the lock,
    following the pattern of `debug_string()`. `is_finished()` and
    `unblock_all_dependencies()` are called after the lock is released,
    because `unblock_all_dependencies()` acquires the same non-recursive
    mutex. A debug point inside the critical section lets the unit test hold
    the lock deterministically.
    
    ### Release note
    
    None
    
    ### Check List (For Author)
    
    - Test: Unit Test
    - Added `PipelineTaskTest.TEST_STOP_IF_FINISHED_EXCLUDES_FINALIZE`: it
    holds `stop_if_finished()` inside the critical section and checks that a
    concurrent `finalize()` is blocked and cannot reset `_sink`, and that a
    finalized task is skipped. It fails without the lock and passes with it
    (200 repeated runs). All `PipelineTaskTest` cases pass.
    - No regression test: the race window is a few instructions wide and
    cannot be triggered reliably from SQL.
    - Behavior changed: No
    - Does this need documentation: No
---
 be/src/exec/pipeline/pipeline_task.cpp       | 24 +++++++--
 be/test/exec/pipeline/pipeline_task_test.cpp | 79 ++++++++++++++++++++++++++++
 2 files changed, 99 insertions(+), 4 deletions(-)

diff --git a/be/src/exec/pipeline/pipeline_task.cpp 
b/be/src/exec/pipeline/pipeline_task.cpp
index aa5d392c19e..e614fa92816 100644
--- a/be/src/exec/pipeline/pipeline_task.cpp
+++ b/be/src/exec/pipeline/pipeline_task.cpp
@@ -883,11 +883,27 @@ void PipelineTask::stop_if_finished() {
         return;
     }
     
SCOPED_SWITCH_THREAD_MEM_TRACKER_LIMITER(fragment->get_query_ctx()->query_mem_tracker());
-    if (auto sink = _sink) {
-        if (sink->is_finished(_state)) {
-            set_wake_up_early();
-            unblock_all_dependencies();
+    DataSinkOperatorPtr sink;
+    {
+        // This runs on FragmentMgr's cancel thread while a scheduler worker 
may be in finalize(),
+        // which resets _sink under the same lock. Copy the shared_ptr under 
the lock so the two
+        // threads never access it concurrently. The lock is released before
+        // unblock_all_dependencies() because that function acquires it again.
+        std::unique_lock<std::mutex> lock(_dependency_lifecycle_lock);
+        
DBUG_EXECUTE_IF("PipelineTask::stop_if_finished.holding_lifecycle_lock", {
+            if (dp->callback.has_value()) {
+                DBUG_RUN_CALLBACK();
+            }
+        });
+        if (is_finalized()) {
+            return;
         }
+        sink = _sink;
+    }
+    DCHECK(sink);
+    if (sink->is_finished(_state)) {
+        set_wake_up_early();
+        unblock_all_dependencies();
     }
 }
 
diff --git a/be/test/exec/pipeline/pipeline_task_test.cpp 
b/be/test/exec/pipeline/pipeline_task_test.cpp
index b91c4c6917e..12bc33ad4e3 100644
--- a/be/test/exec/pipeline/pipeline_task_test.cpp
+++ b/be/test/exec/pipeline/pipeline_task_test.cpp
@@ -714,6 +714,85 @@ TEST_F(PipelineTaskTest, 
TEST_WAKE_UP_SUBMIT_PROTECTED_FROM_FINALIZE) {
     EXPECT_TRUE(task->_operators.empty());
 }
 
+TEST_F(PipelineTaskTest, TEST_STOP_IF_FINISHED_EXCLUDES_FINALIZE) {
+    auto num_instances = 1;
+    auto pip_id = 0;
+    auto task_id = 0;
+    auto pip = std::make_shared<Pipeline>(pip_id, num_instances, 
num_instances);
+    OperatorPtr source_op;
+    source_op.reset(new DummyOperator());
+    EXPECT_TRUE(pip->add_operator(source_op, num_instances).ok());
+
+    int op_id = 1;
+    int node_id = 2;
+    int dest_id = 3;
+    DataSinkOperatorPtr sink_op;
+    sink_op.reset(new DummySinkOperatorX(op_id, node_id, dest_id));
+    EXPECT_TRUE(pip->set_sink(sink_op).ok());
+
+    auto profile = std::make_shared<RuntimeProfile>("Pipeline : " + 
std::to_string(pip_id));
+    std::map<int,
+             std::pair<std::shared_ptr<BasicSharedState>, 
std::vector<std::shared_ptr<Dependency>>>>
+            shared_state_map;
+    _runtime_state->resize_op_id_to_local_state(-1);
+    auto task = std::make_shared<PipelineTask>(pip, task_id, 
_runtime_state.get(), _context,
+                                               profile.get(), 
shared_state_map, task_id);
+
+    std::vector<TScanRangeParams> scan_range;
+    int sender_id = 0;
+    TDataSink tsink;
+    EXPECT_TRUE(task->prepare(scan_range, sender_id, tsink).ok());
+    // A closed but not yet finalized task is what FragmentMgr's cancel thread 
usually scans.
+    EXPECT_TRUE(task->close(Status::OK()).ok());
+
+    auto origin_enable_debug_points = config::enable_debug_points;
+    config::enable_debug_points = true;
+    Defer debug_point_cleanup {[&]() {
+        
DebugPoints::instance()->remove("PipelineTask::stop_if_finished.holding_lifecycle_lock");
+        config::enable_debug_points = origin_enable_debug_points;
+    }};
+
+    std::promise<void> lock_held_promise;
+    auto lock_held = lock_held_promise.get_future();
+    std::promise<void> release_promise;
+    auto release = release_promise.get_future();
+    DebugPoints::instance()->add_with_callback(
+            "PipelineTask::stop_if_finished.holding_lifecycle_lock", 
std::function<void()>([&]() {
+                lock_held_promise.set_value();
+                release.wait();
+            }));
+
+    // Simulates FragmentMgr::cancel_worker -> clear_finished_tasks -> 
stop_if_finished.
+    std::thread stop_thread([&]() { task->stop_if_finished(); });
+    EXPECT_EQ(lock_held.wait_for(std::chrono::seconds(10)), 
std::future_status::ready);
+
+    // Simulates the scheduler worker finalizing the task. It must not reset 
_sink while
+    // stop_if_finished() is reading it.
+    std::promise<void> finalize_started_promise;
+    auto finalize_started = finalize_started_promise.get_future();
+    auto finalize_future = std::async(std::launch::async, [&]() {
+        finalize_started_promise.set_value();
+        return task->finalize();
+    });
+    EXPECT_EQ(finalize_started.wait_for(std::chrono::seconds(10)), 
std::future_status::ready);
+    EXPECT_EQ(finalize_future.wait_for(std::chrono::milliseconds(100)),
+              std::future_status::timeout);
+    EXPECT_NE(task->_sink, nullptr);
+
+    release_promise.set_value();
+    stop_thread.join();
+    EXPECT_TRUE(finalize_future.get().ok());
+
+    EXPECT_EQ(task->_exec_state, PipelineTask::State::FINALIZED);
+    EXPECT_EQ(task->_sink, nullptr);
+    EXPECT_FALSE(task->_wake_up_early);
+
+    // A finalized task is skipped without touching the cleared _sink.
+    
DebugPoints::instance()->remove("PipelineTask::stop_if_finished.holding_lifecycle_lock");
+    task->stop_if_finished();
+    EXPECT_FALSE(task->_wake_up_early);
+}
+
 TEST_F(PipelineTaskTest, 
TEST_CLOSED_TASK_REJECTS_HYBRID_SUBMIT_BEFORE_FINALIZE) {
     auto num_instances = 1;
     auto pip_id = 0;


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

Reply via email to