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

Mryange 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 039a1f0909f [refine](exec) simplify DataQueue eos handling (#65324)
039a1f0909f is described below

commit 039a1f0909f14a2c1942e9605fe904e2b22d34e9
Author: Mryange <[email protected]>
AuthorDate: Fri Jul 17 14:41:11 2026 +0800

    [refine](exec) simplify DataQueue eos handling (#65324)
    
    ### What problem does this PR solve?
    
    DataQueue exposed its internal child-finish and queue-selection protocol
    to source and sink operators, which made EOS handling duplicated and
    difficult to follow.
    
    This change carries EOS through `push_block()` and `DataQueueBlock`,
    moves child selection into DataQueue, and updates Union and Cache
    operators to consume the unified result. Union sink also forwards each
    input block immediately instead of retaining a cross-call output
    
    
    ### Release note
    
    None
---
 be/src/exec/operator/cache_sink_operator.cpp   |  13 ++-
 be/src/exec/operator/cache_source_operator.cpp |  17 +--
 be/src/exec/operator/cache_source_operator.h   |   4 -
 be/src/exec/operator/data_queue.cpp            | 103 ++++++++++-------
 be/src/exec/operator/data_queue.h              |  31 ++++--
 be/src/exec/operator/union_sink_operator.cpp   |  37 ++-----
 be/src/exec/operator/union_sink_operator.h     |   8 +-
 be/src/exec/operator/union_source_operator.cpp |  41 ++-----
 be/src/exec/operator/union_source_operator.h   |   9 +-
 be/test/exec/operator/union_operator_test.cpp  |   6 +-
 be/test/exec/pipeline/data_queue_test.cpp      | 148 ++++++++++++++-----------
 11 files changed, 201 insertions(+), 216 deletions(-)

diff --git a/be/src/exec/operator/cache_sink_operator.cpp 
b/be/src/exec/operator/cache_sink_operator.cpp
index 97c42b99c25..b8c7204ec1a 100644
--- a/be/src/exec/operator/cache_sink_operator.cpp
+++ b/be/src/exec/operator/cache_sink_operator.cpp
@@ -54,12 +54,13 @@ Status CacheSinkOperatorX::sink_impl(RuntimeState* state, 
Block* in_block, bool
     SCOPED_TIMER(local_state.exec_time_counter());
     COUNTER_UPDATE(local_state.rows_input_counter(), 
(int64_t)in_block->rows());
 
-    if (in_block->rows() > 0) {
-        RETURN_IF_ERROR(local_state._shared_state->data_queue.push_block(
-                Block::create_unique(std::move(*in_block)), 0));
-    }
-    if (UNLIKELY(eos)) {
-        local_state._shared_state->data_queue.set_finish(0);
+    if (in_block->rows() > 0 || eos) {
+        std::unique_ptr<Block> output_block;
+        if (in_block->rows() > 0) {
+            output_block = Block::create_unique(std::move(*in_block));
+        }
+        RETURN_IF_ERROR(
+                
local_state._shared_state->data_queue.push_block(std::move(output_block), 0, 
eos));
     }
     return Status::OK();
 }
diff --git a/be/src/exec/operator/cache_source_operator.cpp 
b/be/src/exec/operator/cache_source_operator.cpp
index ec7d947680a..6b68e300ff9 100644
--- a/be/src/exec/operator/cache_source_operator.cpp
+++ b/be/src/exec/operator/cache_source_operator.cpp
@@ -111,9 +111,8 @@ std::string CacheSourceLocalState::debug_string(int 
indentation_level) const {
     fmt::memory_buffer debug_string_buffer;
     fmt::format_to(debug_string_buffer, "{}", 
Base::debug_string(indentation_level));
     if (_shared_state) {
-        fmt::format_to(debug_string_buffer, ", data_queue: (is_all_finish = 
{}, has_data = {})",
-                       _shared_state->data_queue.is_all_finish(),
-                       _shared_state->data_queue.has_more_data());
+        fmt::format_to(debug_string_buffer, ", data_queue: {}",
+                       _shared_state->data_queue.debug_string());
     }
     return fmt::to_string(debug_string_buffer);
 }
@@ -140,18 +139,14 @@ Status CacheSourceOperatorX::get_block_impl(RuntimeState* 
state, Block* block, b
             }
         });
 
-        std::unique_ptr<Block> output_block;
-        int child_idx = 0;
-        
RETURN_IF_ERROR(local_state._shared_state->data_queue.get_block_from_queue(&output_block,
-                                                                               
    &child_idx));
-        // Here, check the value of `_has_data(state)` again after 
`data_queue.is_all_finish()` is TRUE
-        // as there may be one or more blocks when 
`data_queue.is_all_finish()` is TRUE.
-        *eos = !_has_data(state) && 
local_state._shared_state->data_queue.is_all_finish();
+        auto queue_block = 
DORIS_TRY(local_state._shared_state->data_queue.get_block_from_queue());
+        *eos = queue_block.eos;
 
-        if (!output_block) {
+        if (!queue_block.block) {
             return Status::OK();
         }
 
+        auto& output_block = queue_block.block;
         if (local_state._need_insert_cache) {
             if (need_clone_empty) {
                 *block = output_block->clone_empty();
diff --git a/be/src/exec/operator/cache_source_operator.h 
b/be/src/exec/operator/cache_source_operator.h
index 3a68bd337fc..864f6301217 100644
--- a/be/src/exec/operator/cache_source_operator.h
+++ b/be/src/exec/operator/cache_source_operator.h
@@ -90,10 +90,6 @@ public:
 
 private:
     TQueryCacheParam _cache_param;
-    bool _has_data(RuntimeState* state) const {
-        auto& local_state = get_local_state(state);
-        return local_state._shared_state->data_queue.remaining_has_data();
-    }
     friend class CacheSourceLocalState;
 };
 
diff --git a/be/src/exec/operator/data_queue.cpp 
b/be/src/exec/operator/data_queue.cpp
index 752e972c3e6..feed0bb79fa 100644
--- a/be/src/exec/operator/data_queue.cpp
+++ b/be/src/exec/operator/data_queue.cpp
@@ -25,6 +25,7 @@
 #include "common/thread_safety_annotations.h"
 #include "core/block/block.h"
 #include "exec/pipeline/dependency.h"
+#include "fmt/format.h"
 
 namespace doris {
 
@@ -97,6 +98,10 @@ bool DataQueue::has_more_data() const {
     return _cur_blocks_total_nums.load() > 0;
 }
 
+std::string DataQueue::debug_string() const {
+    return fmt::format("(is_all_finish = {}, has_data = {})", is_all_finish(), 
has_more_data());
+}
+
 void DataQueue::set_source_dependency(std::shared_ptr<Dependency> 
source_dependency)
         NO_THREAD_SAFETY_ANALYSIS {
     _source_dependency = std::move(source_dependency);
@@ -134,13 +139,16 @@ std::unique_ptr<Block> DataQueue::get_free_block(int 
child_idx) {
     return Block::create_unique();
 }
 
-void DataQueue::push_free_block(std::unique_ptr<Block> block, int child_idx) {
-    DCHECK(block->rows() == 0);
+void DataQueue::push_free_block(DataQueueBlock&& queue_block) {
+    if (!queue_block.block) {
+        return;
+    }
+    DCHECK(queue_block.block->rows() == 0);
 
     if (!_is_low_memory_mode) {
-        auto& sub = *_sub_queues[child_idx];
+        auto& sub = *_sub_queues[queue_block.child_idx];
         LockGuard l(sub.free_lock);
-        sub.free_blocks.emplace_back(std::move(block));
+        sub.free_blocks.emplace_back(std::move(queue_block.block));
     }
 }
 
@@ -154,73 +162,75 @@ void DataQueue::clear_free_blocks() {
 
 void DataQueue::terminate() {
     for (int i = 0; i < _child_count; ++i) {
-        set_finish(i);
+        mark_finish(i);
         _sub_queues[i]->clear_blocks();
     }
+    _cur_blocks_total_nums = 0;
     clear_free_blocks();
+    set_source_always_ready();
 }
 
-//check which queue have data, and save the idx in _flag_queue_idx,
-//so next loop, will check the record idx + 1 first
-//maybe it's useful with many queue, others maybe always 0
-bool DataQueue::remaining_has_data() {
-    int count = _child_count;
-    while (--count >= 0) {
-        _flag_queue_idx++;
-        if (_flag_queue_idx == _child_count) {
-            _flag_queue_idx = 0;
-        }
-        if (_sub_queues[_flag_queue_idx]->blocks_in_queue.load() > 0) {
-            return true;
+Result<DataQueueBlock> DataQueue::get_block_from_queue() {
+    DataQueueBlock result;
+    const int start_idx = (_flag_queue_idx + 1) % _child_count;
+    for (int offset = 0; offset < _child_count; ++offset) {
+        const int idx = (start_idx + offset) % _child_count;
+        if (_sub_queues[idx]->blocks_in_queue.load() == 0) {
+            continue;
         }
-    }
-    return false;
-}
-
-//the _flag_queue_idx indicate which queue has data, and in check can_read
-//will be set idx in remaining_has_data function
-Status DataQueue::get_block_from_queue(std::unique_ptr<Block>* output_block, 
int* child_idx) {
-    const int idx = _flag_queue_idx;
-    auto& sub = *_sub_queues[idx];
 
-    sub.try_pop(output_block);
-    if (*output_block) {
-        if (child_idx) {
-            *child_idx = idx;
+        auto& sub = *_sub_queues[idx];
+        sub.try_pop(&result.block);
+        if (!result.block) {
+            continue;
         }
+        result.child_idx = idx;
+        _flag_queue_idx = idx;
         auto old_total = _cur_blocks_total_nums.fetch_sub(1);
         if (old_total == 1) {
             set_source_block();
         }
+        break;
     }
-    return Status::OK();
+
+    // A producer enqueues its final block before marking the child finished. 
Observe completion
+    // first, then recheck queued data to avoid reporting EOS while the final 
block is still queued.
+    result.eos = is_all_finish() && !has_more_data();
+    return result;
 }
 
-Status DataQueue::push_block(std::unique_ptr<Block> block, int child_idx) {
-    if (!block) {
+Status DataQueue::push_block(std::unique_ptr<Block> block, int child_idx, bool 
eos) {
+    DCHECK(block || eos);
+    if (!block && !eos) {
         return Status::OK();
     }
-    auto& sub = *_sub_queues[child_idx];
-    // total_counter is incremented inside try_push under queue_lock, only 
when the
-    // block is actually enqueued. This ensures get_block_from_queue() always 
observes
-    // _cur_blocks_total_nums >= 1 when it successfully pops a block, with no 
risk of
-    // underflow or the need for a rollback on failure.
-    if (!sub.try_push(std::move(block), _cur_blocks_total_nums)) {
-        return Status::EndOfFile("SubQueue already finished");
+
+    if (block) {
+        auto& sub = *_sub_queues[child_idx];
+        // total_counter is incremented inside try_push under queue_lock, only 
when the
+        // block is actually enqueued. This ensures get_block_from_queue() 
always observes
+        // _cur_blocks_total_nums >= 1 when it successfully pops a block, with 
no risk of
+        // underflow or the need for a rollback on failure.
+        if (!sub.try_push(std::move(block), _cur_blocks_total_nums)) {
+            return Status::EndOfFile("SubQueue already finished");
+        }
+    }
+
+    if (eos) {
+        mark_finish(child_idx);
     }
     set_source_ready();
     return Status::OK();
 }
 
-void DataQueue::set_finish(int child_idx) {
+void DataQueue::mark_finish(int child_idx) {
     auto& sub = *_sub_queues[child_idx];
     if (!sub.mark_finished(_un_finished_counter, _is_all_finished)) {
         return;
     }
-    set_source_ready();
 }
 
-bool DataQueue::is_all_finish() {
+bool DataQueue::is_all_finish() const {
     return _is_all_finished;
 }
 
@@ -231,6 +241,13 @@ void DataQueue::set_source_ready() {
     }
 }
 
+void DataQueue::set_source_always_ready() {
+    LockGuard lc(_source_lock);
+    if (_source_dependency) {
+        _source_dependency->set_always_ready();
+    }
+}
+
 void DataQueue::set_source_block() {
     // Re-check under _source_lock to avoid blocking the source when a 
concurrent push
     // has already added new blocks (or all children have finished) since we 
observed
diff --git a/be/src/exec/operator/data_queue.h 
b/be/src/exec/operator/data_queue.h
index 1443e2ed520..49a3bcaf00d 100644
--- a/be/src/exec/operator/data_queue.h
+++ b/be/src/exec/operator/data_queue.h
@@ -20,6 +20,7 @@
 #include <cstdint>
 #include <deque>
 #include <memory>
+#include <string>
 #include <vector>
 
 #include "common/status.h"
@@ -30,6 +31,15 @@ namespace doris {
 
 class Dependency;
 
+struct DataQueueBlock {
+    std::unique_ptr<Block> block;
+    bool eos = false;
+
+private:
+    int child_idx = 0;
+    friend class DataQueue;
+};
+
 // Per child sub-queue. Groups all parallel state so that the lock/field
 // relationship is explicit and can be checked by clang -Wthread-safety.
 struct SubQueue {
@@ -46,7 +56,7 @@ struct SubQueue {
     AnnotatedMutex free_lock;
     std::deque<std::unique_ptr<Block>> free_blocks GUARDED_BY(free_lock);
 
-    // blocks_in_queue is readable from lock-free fast paths 
(remaining_has_data),
+    // blocks_in_queue is readable from lock-free fast paths 
(get_block_from_queue),
     // so it remains atomic and is intentionally not GUARDED_BY.
     std::atomic_uint32_t blocks_in_queue {0};
 
@@ -82,18 +92,13 @@ public:
     DataQueue(int child_count = 1);
     ~DataQueue() = default;
 
-    Status get_block_from_queue(std::unique_ptr<Block>* block, int* child_idx 
= nullptr);
-    Status push_block(std::unique_ptr<Block> block, int child_idx = 0);
+    Result<DataQueueBlock> get_block_from_queue();
+    Status push_block(std::unique_ptr<Block> block, int child_idx, bool eos);
 
     std::unique_ptr<Block> get_free_block(int child_idx = 0);
-    void push_free_block(std::unique_ptr<Block> output_block, int child_idx = 
0);
+    void push_free_block(DataQueueBlock&& queue_block);
 
-    void set_finish(int child_idx = 0);
-    bool is_all_finish();
-
-    // This function is not thread safe, should be called in 
Operator::get_block()
-    bool remaining_has_data();
-    bool has_more_data() const;
+    std::string debug_string() const;
 
     void set_source_dependency(std::shared_ptr<Dependency> source_dependency)
             NO_THREAD_SAFETY_ANALYSIS;
@@ -104,8 +109,12 @@ public:
     void terminate();
 
 private:
+    bool is_all_finish() const;
+    bool has_more_data() const;
     void clear_free_blocks();
+    void mark_finish(int child_idx);
     void set_source_ready();
+    void set_source_always_ready();
     void set_source_block();
 
     std::vector<std::unique_ptr<SubQueue>> _sub_queues;
@@ -117,7 +126,7 @@ private:
     std::atomic_uint32_t _cur_blocks_total_nums = 0;
 
     //this will be indicate which queue has data, it's useful when have many 
queues
-    std::atomic_int _flag_queue_idx = 0;
+    int _flag_queue_idx = 0;
     // only used by streaming agg source operator
 
     std::atomic_bool _is_low_memory_mode = false;
diff --git a/be/src/exec/operator/union_sink_operator.cpp 
b/be/src/exec/operator/union_sink_operator.cpp
index b0f7cf619a8..81881fcf576 100644
--- a/be/src/exec/operator/union_sink_operator.cpp
+++ b/be/src/exec/operator/union_sink_operator.cpp
@@ -100,44 +100,21 @@ Status UnionSinkOperatorX::sink_impl(RuntimeState* state, 
Block* in_block, bool
     }
     SCOPED_TIMER(local_state.exec_time_counter());
     COUNTER_UPDATE(local_state.rows_input_counter(), 
(int64_t)in_block->rows());
-    if (local_state._output_block == nullptr) {
-        local_state._output_block =
-                
local_state._shared_state->data_queue.get_free_block(_cur_child_id);
-    }
-    if (_cur_child_id < _get_first_materialized_child_idx()) { //pass_through
-        if (in_block->rows() > 0) {
-            local_state._output_block->swap(*in_block);
-            RETURN_IF_ERROR(local_state._shared_state->data_queue.push_block(
-                    std::move(local_state._output_block), _cur_child_id));
-        }
+    auto output_block = 
local_state._shared_state->data_queue.get_free_block(_cur_child_id);
+    if (is_child_passthrough(_cur_child_id)) { //pass_through
+        output_block->swap(*in_block);
     } else if (_get_first_materialized_child_idx() != children_count() &&
                _cur_child_id < children_count()) { //need materialized
-        RETURN_IF_ERROR(materialize_child_block(state, _cur_child_id, in_block,
-                                                
local_state._output_block.get()));
+        RETURN_IF_ERROR(
+                materialize_child_block(state, _cur_child_id, in_block, 
output_block.get()));
     } else {
         return Status::InternalError("maybe can't reach here, execute const 
expr: {}, {}, {}",
                                      _cur_child_id, 
_get_first_materialized_child_idx(),
                                      children_count());
     }
-    if (UNLIKELY(eos)) {
-        //if _cur_child_id eos, need check to push block
-        //Now here can't check _output_block rows, even it's row==0, also need 
push block
-        //because maybe sink is eos and queue have none data, if not push block
-        //the source can't can_read again and can't set source finished
-        if (local_state._output_block) {
-            RETURN_IF_ERROR(local_state._shared_state->data_queue.push_block(
-                    std::move(local_state._output_block), _cur_child_id));
-        }
 
-        local_state._shared_state->data_queue.set_finish(_cur_child_id);
-        return Status::OK();
-    }
-    // not eos and block rows is enough to output,so push block
-    if (local_state._output_block && (local_state._output_block->rows() >= 
state->batch_size())) {
-        RETURN_IF_ERROR(local_state._shared_state->data_queue.push_block(
-                std::move(local_state._output_block), _cur_child_id));
-    }
-    return Status::OK();
+    return 
local_state._shared_state->data_queue.push_block(std::move(output_block), 
_cur_child_id,
+                                                            eos);
 }
 
 } // namespace doris
diff --git a/be/src/exec/operator/union_sink_operator.h 
b/be/src/exec/operator/union_sink_operator.h
index 2bf0e7b3e75..7f2ec5bbb2f 100644
--- a/be/src/exec/operator/union_sink_operator.h
+++ b/be/src/exec/operator/union_sink_operator.h
@@ -51,8 +51,7 @@ class UnionSinkOperatorX;
 class UnionSinkLocalState final : public 
PipelineXSinkLocalState<UnionSharedState> {
 public:
     ENABLE_FACTORY_CREATOR(UnionSinkLocalState);
-    UnionSinkLocalState(DataSinkOperatorXBase* parent, RuntimeState* state)
-            : Base(parent, state), _child_row_idx(0) {}
+    UnionSinkLocalState(DataSinkOperatorXBase* parent, RuntimeState* state) : 
Base(parent, state) {}
     Status init(RuntimeState* state, LocalSinkStateInfo& info) override;
     Status open(RuntimeState* state) override;
     friend class UnionSinkOperatorX;
@@ -60,8 +59,6 @@ public:
     using Parent = UnionSinkOperatorX;
 
 private:
-    std::unique_ptr<Block> _output_block;
-
     /// Const exprs materialized by this node. These exprs don't refer to any 
children.
     /// Only materialized by the first fragment instance to avoid duplication.
     VExprContextSPtrs _const_expr;
@@ -69,8 +66,6 @@ private:
     /// Exprs materialized by this node. The i-th result expr list refers to 
the i-th child.
     VExprContextSPtrs _child_expr;
 
-    /// Index of current row in child_row_block_.
-    int _child_row_idx;
     RuntimeProfile::Counter* _expr_timer = nullptr;
 };
 
@@ -168,7 +163,6 @@ private:
                 RETURN_IF_ERROR(
                         materialize_block(local_state._child_expr, 
input_block, &res, false));
             }
-            local_state._child_row_idx += res.rows();
             RETURN_IF_ERROR(mblock.merge(res));
         }
         return Status::OK();
diff --git a/be/src/exec/operator/union_source_operator.cpp 
b/be/src/exec/operator/union_source_operator.cpp
index 6cc39ebb7ce..926fa5487db 100644
--- a/be/src/exec/operator/union_source_operator.cpp
+++ b/be/src/exec/operator/union_source_operator.cpp
@@ -30,7 +30,6 @@
 #include "exec/operator/union_sink_operator.h"
 #include "exec/pipeline/dependency.h"
 #include "runtime/descriptors.h"
-#include "util/defer_op.h"
 
 namespace doris {
 class RuntimeState;
@@ -93,49 +92,33 @@ std::string UnionSourceLocalState::debug_string(int 
indentation_level) const {
     fmt::memory_buffer debug_string_buffer;
     fmt::format_to(debug_string_buffer, "{}", 
Base::debug_string(indentation_level));
     if (_shared_state) {
-        fmt::format_to(debug_string_buffer, ", data_queue: (is_all_finish = 
{}, has_data = {})",
-                       _shared_state->data_queue.is_all_finish(),
-                       _shared_state->data_queue.has_more_data());
+        fmt::format_to(debug_string_buffer, ", data_queue: {}",
+                       _shared_state->data_queue.debug_string());
     }
     return fmt::to_string(debug_string_buffer);
 }
 
 Status UnionSourceOperatorX::get_block_impl(RuntimeState* state, Block* block, 
bool* eos) {
     auto& local_state = get_local_state(state);
-    Defer set_eos {[&]() {
-        // the eos check of union operator is complex, need check all logical 
if you want modify
-        // could ref this PR: https://github.com/apache/doris/pull/29677
-        // have executing const expr, queue have no data anymore, and child 
could be closed
-        if (_child_size == 0 && !local_state._need_read_for_const_expr) {
-            *eos = true;
-        } else if (_has_data(state)) {
-            *eos = false;
-        } else if (local_state._shared_state->data_queue.is_all_finish()) {
-            // Here, check the value of `_has_data(state)` again after 
`data_queue.is_all_finish()` is TRUE
-            // as there may be one or more blocks when 
`data_queue.is_all_finish()` is TRUE.
-            *eos = !_has_data(state);
-        } else {
-            *eos = false;
-        }
-    }};
-
+    *eos = false;
     SCOPED_TIMER(local_state.exec_time_counter());
     if (local_state._need_read_for_const_expr) {
         if (has_more_const(state)) {
             RETURN_IF_ERROR(get_next_const(state, block));
         }
         local_state._need_read_for_const_expr = has_more_const(state);
+        if (_child_size == 0 && !local_state._need_read_for_const_expr) {
+            *eos = true;
+        }
     } else if (_child_size != 0) {
-        std::unique_ptr<Block> output_block;
-        int child_idx = 0;
-        
RETURN_IF_ERROR(local_state._shared_state->data_queue.get_block_from_queue(&output_block,
-                                                                               
    &child_idx));
-        if (!output_block) {
+        auto queue_block = 
DORIS_TRY(local_state._shared_state->data_queue.get_block_from_queue());
+        *eos = queue_block.eos;
+        if (!queue_block.block) {
             return Status::OK();
         }
-        block->swap(*output_block);
-        
output_block->clear_column_data(row_descriptor().num_materialized_slots());
-        
local_state._shared_state->data_queue.push_free_block(std::move(output_block), 
child_idx);
+        block->swap(*queue_block.block);
+        
queue_block.block->clear_column_data(row_descriptor().num_materialized_slots());
+        
local_state._shared_state->data_queue.push_free_block(std::move(queue_block));
     }
     local_state.reached_limit(block, eos);
     return Status::OK();
diff --git a/be/src/exec/operator/union_source_operator.h 
b/be/src/exec/operator/union_source_operator.h
index c1006e46693..67b405b1002 100644
--- a/be/src/exec/operator/union_source_operator.h
+++ b/be/src/exec/operator/union_source_operator.h
@@ -122,13 +122,6 @@ public:
     }
 
 private:
-    bool _has_data(RuntimeState* state) const {
-        auto& local_state = get_local_state(state);
-        if (_child_size == 0) {
-            return local_state._need_read_for_const_expr;
-        }
-        return local_state._shared_state->data_queue.remaining_has_data();
-    }
     bool has_more_const(RuntimeState* state) const {
         auto& local_state = get_local_state(state);
         return state->per_fragment_instance_idx() == 0 &&
@@ -140,4 +133,4 @@ private:
     std::vector<VExprContextSPtrs> _const_expr_lists;
 };
 
-} // namespace doris
\ No newline at end of file
+} // namespace doris
diff --git a/be/test/exec/operator/union_operator_test.cpp 
b/be/test/exec/operator/union_operator_test.cpp
index 3009e977a47..8590c86f732 100644
--- a/be/test/exec/operator/union_operator_test.cpp
+++ b/be/test/exec/operator/union_operator_test.cpp
@@ -254,7 +254,9 @@ TEST_F(UnionOperatorTest, test_sink_and_source) {
 
     {
         for (int i = 0; i < child_size; i++) {
-            sink_state[i]->_batch_size = 2;
+            // Each sink input should be queued immediately, including 
materialized children whose
+            // input is smaller than the runtime batch size.
+            sink_state[i]->_batch_size = 10;
             Block block = ColumnHelper::create_block<DataTypeInt64>({1, 2}, 
{3, 4});
             EXPECT_TRUE(sink_ops[i]->sink(sink_state[i].get(), &block, false));
         }
@@ -291,4 +293,4 @@ TEST_F(UnionOperatorTest, test_sink_and_source) {
                 block, ColumnHelper::create_block<DataTypeInt64>({1, 2}, {3, 
4})));
     }
 }
-} // namespace doris
\ No newline at end of file
+} // namespace doris
diff --git a/be/test/exec/pipeline/data_queue_test.cpp 
b/be/test/exec/pipeline/data_queue_test.cpp
index f8ba26bc664..db1c3cccef4 100644
--- a/be/test/exec/pipeline/data_queue_test.cpp
+++ b/be/test/exec/pipeline/data_queue_test.cpp
@@ -221,57 +221,59 @@ public:
 
 // Initial state: no data, no finish.
 TEST_F(DataQueueTest, InitialState) {
-    EXPECT_FALSE(data_queue->has_more_data());
-    EXPECT_FALSE(data_queue->is_all_finish());
-    EXPECT_FALSE(data_queue->remaining_has_data());
+    EXPECT_EQ(data_queue->debug_string(), "(is_all_finish = false, has_data = 
false)");
+    auto queue_block = TEST_TRY(data_queue->get_block_from_queue());
+    EXPECT_EQ(queue_block.block, nullptr);
+    EXPECT_FALSE(queue_block.eos);
 }
 
 // Push one block and retrieve it.
 TEST_F(DataQueueTest, SinglePushPop) {
-    EXPECT_TRUE(data_queue->push_block(make_block(), 0).ok());
-    EXPECT_TRUE(data_queue->has_more_data());
+    EXPECT_TRUE(data_queue->push_block(make_block(), 0, false).ok());
+    EXPECT_EQ(data_queue->debug_string(), "(is_all_finish = false, has_data = 
true)");
 
-    // Find the queue with data.
-    EXPECT_TRUE(data_queue->remaining_has_data());
-
-    std::unique_ptr<Block> out;
-    int child_idx = -1;
-    EXPECT_TRUE(data_queue->get_block_from_queue(&out, &child_idx).ok());
-    EXPECT_NE(out, nullptr);
-    EXPECT_EQ(child_idx, 0);
-    EXPECT_FALSE(data_queue->has_more_data());
+    auto queue_block = TEST_TRY(data_queue->get_block_from_queue());
+    EXPECT_NE(queue_block.block, nullptr);
+    EXPECT_FALSE(queue_block.eos);
+    EXPECT_EQ(data_queue->debug_string(), "(is_all_finish = false, has_data = 
false)");
 }
 
-// is_all_finish only becomes true after all children call set_finish.
+// is_all_finish only becomes true after all children push eos.
 TEST_F(DataQueueTest, IsAllFinishAfterAllChildren) {
-    data_queue->set_finish(0);
-    EXPECT_FALSE(data_queue->is_all_finish());
-    data_queue->set_finish(1);
-    EXPECT_FALSE(data_queue->is_all_finish());
-    data_queue->set_finish(2);
-    EXPECT_TRUE(data_queue->is_all_finish());
+    EXPECT_TRUE(data_queue->push_block(nullptr, 0, true).ok());
+    auto first_queue_block = TEST_TRY(data_queue->get_block_from_queue());
+    EXPECT_FALSE(first_queue_block.eos);
+    EXPECT_TRUE(data_queue->push_block(nullptr, 1, true).ok());
+    auto second_queue_block = TEST_TRY(data_queue->get_block_from_queue());
+    EXPECT_FALSE(second_queue_block.eos);
+    EXPECT_TRUE(data_queue->push_block(nullptr, 2, true).ok());
+    auto last_queue_block = TEST_TRY(data_queue->get_block_from_queue());
+    EXPECT_TRUE(last_queue_block.eos);
 }
 
-// set_finish is idempotent.
-TEST_F(DataQueueTest, SetFinishIdempotent) {
-    data_queue->set_finish(0);
-    data_queue->set_finish(0); // second call must not double-decrement
-    data_queue->set_finish(1);
-    data_queue->set_finish(2);
-    EXPECT_TRUE(data_queue->is_all_finish());
+// eos push is idempotent.
+TEST_F(DataQueueTest, EosPushIdempotent) {
+    EXPECT_TRUE(data_queue->push_block(nullptr, 0, true).ok());
+    EXPECT_TRUE(data_queue->push_block(nullptr, 0, true).ok());
+    EXPECT_TRUE(data_queue->push_block(nullptr, 1, true).ok());
+    EXPECT_TRUE(data_queue->push_block(nullptr, 2, true).ok());
+    auto queue_block = TEST_TRY(data_queue->get_block_from_queue());
+    EXPECT_TRUE(queue_block.eos);
 }
 
-// child_idx returned by get_block_from_queue reflects the actual queue.
-TEST_F(DataQueueTest, ChildIdxReturned) {
+// DataQueueBlock can return a popped block to the originating child free list.
+TEST_F(DataQueueTest, ReturnBlockToChildFreeList) {
     // Push to child 1 only.
-    EXPECT_TRUE(data_queue->push_block(make_block(), 1).ok());
-    data_queue->remaining_has_data(); // advance _flag_queue_idx to find child 
1
+    EXPECT_TRUE(data_queue->push_block(make_block(), 1, false).ok());
 
-    std::unique_ptr<Block> out;
-    int child_idx = -1;
-    EXPECT_TRUE(data_queue->get_block_from_queue(&out, &child_idx).ok());
-    EXPECT_NE(out, nullptr);
-    EXPECT_EQ(child_idx, 1);
+    auto queue_block = TEST_TRY(data_queue->get_block_from_queue());
+    EXPECT_NE(queue_block.block, nullptr);
+    queue_block.block->clear();
+    auto* returned_block = queue_block.block.get();
+    data_queue->push_free_block(std::move(queue_block));
+
+    auto reused_block = data_queue->get_free_block(1);
+    EXPECT_EQ(reused_block.get(), returned_block);
 }
 
 // get_free_block returns a new block when free list is empty, reuses when not.
@@ -282,7 +284,9 @@ TEST_F(DataQueueTest, FreeBlockReuse) {
 
     // Return it to the free list.
     block->clear(); // ensure rows == 0
-    data_queue->push_free_block(std::move(block), 0);
+    DataQueueBlock queue_block;
+    queue_block.block = std::move(block);
+    data_queue->push_free_block(std::move(queue_block));
 
     // Second call: must return the recycled block.
     auto block2 = data_queue->get_free_block(0);
@@ -292,7 +296,9 @@ TEST_F(DataQueueTest, FreeBlockReuse) {
 // In low-memory mode push_free_block discards blocks and max drops to 1.
 TEST_F(DataQueueTest, LowMemoryMode) {
     // Pre-populate the free list.
-    data_queue->push_free_block(Block::create_unique(), 0);
+    DataQueueBlock queue_block;
+    queue_block.block = Block::create_unique();
+    data_queue->push_free_block(std::move(queue_block));
 
     data_queue->set_low_memory_mode();
 
@@ -303,7 +309,9 @@ TEST_F(DataQueueTest, LowMemoryMode) {
 
     // push_free_block now discards.
     block->clear();
-    data_queue->push_free_block(std::move(block), 0);
+    DataQueueBlock low_memory_block;
+    low_memory_block.block = std::move(block);
+    data_queue->push_free_block(std::move(low_memory_block));
     auto block2 = data_queue->get_free_block(0);
     // Still gets a fresh allocation (free list stays empty).
     EXPECT_NE(block2, nullptr);
@@ -311,15 +319,17 @@ TEST_F(DataQueueTest, LowMemoryMode) {
 
 // terminate() finishes all children and clears pending blocks from sub-queues.
 TEST_F(DataQueueTest, Terminate) {
-    EXPECT_TRUE(data_queue->push_block(make_block(), 0).ok());
-    EXPECT_TRUE(data_queue->push_block(make_block(), 1).ok());
+    EXPECT_TRUE(data_queue->push_block(make_block(), 0, false).ok());
+    EXPECT_TRUE(data_queue->push_block(make_block(), 1, false).ok());
 
     data_queue->terminate();
 
-    EXPECT_TRUE(data_queue->is_all_finish());
-    // remaining_has_data() checks blocks_in_queue per sub-queue,
-    // which clear_blocks() resets to 0.
-    EXPECT_FALSE(data_queue->remaining_has_data());
+    EXPECT_EQ(data_queue->debug_string(), "(is_all_finish = true, has_data = 
false)");
+    source_dep->block();
+    EXPECT_TRUE(source_dep->ready());
+    auto queue_block = TEST_TRY(data_queue->get_block_from_queue());
+    EXPECT_EQ(queue_block.block, nullptr);
+    EXPECT_TRUE(queue_block.eos);
 }
 
 // set_max_blocks_in_sub_queue propagates to every sub-queue.
@@ -327,12 +337,12 @@ TEST_F(DataQueueTest, SetMaxBlocksInSubQueue) {
     data_queue->set_max_blocks_in_sub_queue(5);
     // Push 5 blocks to child 0 — sink must stay ready (not over the limit 
yet).
     for (int i = 0; i < 5; i++) {
-        EXPECT_TRUE(data_queue->push_block(make_block(), 0).ok());
+        EXPECT_TRUE(data_queue->push_block(make_block(), 0, false).ok());
     }
     EXPECT_TRUE(sink_deps[0]->ready());
 
     // 6th push exceeds limit → sink blocked.
-    EXPECT_TRUE(data_queue->push_block(make_block(), 0).ok());
+    EXPECT_TRUE(data_queue->push_block(make_block(), 0, false).ok());
     EXPECT_FALSE(sink_deps[0]->ready());
 }
 
@@ -341,10 +351,28 @@ TEST_F(DataQueueTest, SourceReadyOnPush) {
     source_dep->block(); // start blocked
     EXPECT_FALSE(source_dep->ready());
 
-    EXPECT_TRUE(data_queue->push_block(make_block(), 0).ok());
+    EXPECT_TRUE(data_queue->push_block(make_block(), 0, false).ok());
     EXPECT_TRUE(source_dep->ready());
 }
 
+TEST_F(DataQueueTest, SourceReadyOnEosOnly) {
+    source_dep->block();
+    EXPECT_FALSE(source_dep->ready());
+
+    EXPECT_TRUE(data_queue->push_block(nullptr, 0, true).ok());
+    EXPECT_TRUE(source_dep->ready());
+}
+
+TEST_F(DataQueueTest, EosOnlyAfterAllChildren) {
+    EXPECT_TRUE(data_queue->push_block(nullptr, 0, true).ok());
+    EXPECT_TRUE(data_queue->push_block(nullptr, 1, true).ok());
+    EXPECT_TRUE(data_queue->push_block(nullptr, 2, true).ok());
+
+    auto queue_block = TEST_TRY(data_queue->get_block_from_queue());
+    EXPECT_EQ(queue_block.block, nullptr);
+    EXPECT_TRUE(queue_block.eos);
+}
+
 // ---------------------------------------------------------------------------
 // Multi-threaded integration test (existing)
 // ---------------------------------------------------------------------------
@@ -355,19 +383,9 @@ TEST_F(DataQueueTest, MultiTest) {
         while (true) {
             bool eos = false;
             if (source_dep->ready()) {
-                Defer set_eos {[&]() {
-                    if (data_queue->remaining_has_data()) {
-                        eos = false;
-                    } else if (data_queue->is_all_finish()) {
-                        eos = !data_queue->remaining_has_data();
-                    } else {
-                        eos = false;
-                    }
-                }};
-                std::unique_ptr<Block> output_block;
-                int child_idx = 0;
-                EXPECT_TRUE(data_queue->get_block_from_queue(&output_block, 
&child_idx));
-                if (output_block) {
+                auto queue_block = 
TEST_TRY(data_queue->get_block_from_queue());
+                eos = queue_block.eos;
+                if (queue_block.block) {
                     output_count++;
                 }
             }
@@ -392,11 +410,11 @@ TEST_F(DataQueueTest, MultiTest) {
         int i = 0;
         while (i < 50) {
             if (sink_deps[id]->ready()) {
-                
EXPECT_TRUE(data_queue->push_block(std::move(input_blocks[id][i]), id).ok());
+                
EXPECT_TRUE(data_queue->push_block(std::move(input_blocks[id][i]), id, 
false).ok());
                 i++;
             }
         }
-        data_queue->set_finish(id);
+        EXPECT_TRUE(data_queue->push_block(nullptr, id, true).ok());
     };
 
     std::thread input1(input_func, 0);
@@ -409,7 +427,7 @@ TEST_F(DataQueueTest, MultiTest) {
     output1.join();
 
     EXPECT_EQ(output_count, 150);
-    EXPECT_TRUE(data_queue->is_all_finish());
+    EXPECT_EQ(data_queue->debug_string(), "(is_all_finish = true, has_data = 
false)");
 }
 
 // ./run-be-ut.sh --run --filter=DataQueueTest.*


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


Reply via email to