github-actions[bot] commented on code in PR #67140:
URL: https://github.com/apache/doris/pull/67140#discussion_r3860482267


##########
be/src/exec/operator/distinct_streaming_aggregation_operator.cpp:
##########
@@ -176,8 +195,13 @@ Status 
DistinctStreamingAggLocalState::_distinct_pre_agg_with_serialized_key(
     const uint32_t rows = (uint32_t)in_block->rows();
     _distinct_row.clear();
 
-    if (_parent->cast<DistinctStreamingAggOperatorX>()._is_streaming_preagg && 
low_memory_mode()) {
-        _stop_emplace_flag = true;
+    auto& parent = _parent->cast<DistinctStreamingAggOperatorX>();
+    if (parent._is_streaming_preagg) {
+        const auto memory_limit = parent._memory_limit(state());
+        COUNTER_SET(_memory_use_limit, static_cast<int64_t>(memory_limit));
+        if (low_memory_mode() || (memory_limit > 0 && _memory_usage() > 
memory_limit)) {

Review Comment:
   [P1] Do not count raw pass-through duplicates toward a distinct limit. A 
plain local `LIMIT N` is pushed to this lower distinct aggregation. Once this 
cap condition switches it to pass-through, duplicates are emitted raw, but 
`push()` still truncates and reaches `_limit` by raw row count and then stops 
child input. With limit 2, after key `A` was emitted, a triggering `[A, B]` 
block lets duplicate `A` consume the final allowance and prevents `B` from 
reaching the global distinct, returning too few rows. Keep deduplicating while 
the pushed distinct limit is active or remove/disable that local limit for 
pass-through, and add this transition case.



##########
be/src/exec/operator/streaming_aggregation_operator.cpp:
##########
@@ -289,9 +296,9 @@ bool StreamingAggLocalState::_should_not_do_pre_agg(size_t 
rows) {
     // But for fixed hash map, it never need to expand
     auto& p = Base::_parent->template cast<StreamingAggOperatorX>();
     bool ret_flag = false;
-    const auto spill_streaming_agg_mem_limit = 
p._spill_streaming_agg_mem_limit;
-    const bool used_too_much_memory =
-            spill_streaming_agg_mem_limit > 0 && _memory_usage() > 
spill_streaming_agg_mem_limit;
+    const auto memory_limit = p._memory_limit(state());

Review Comment:
   [P1] Protect the regular pure-distinct fallback from the same early-EOS bug. 
With `enable_distinct_streaming_aggregation=false`, the lower pure-distinct 
stage is still streaming and uses this regular operator. When this dynamic cap 
selects pass-through, it copies duplicate group keys verbatim, while generic 
`reached_limit()` counts those raw rows against the pushed local distinct limit 
and reports EOS. With limit 2, retained `A`, and input `[A, A, B]`, the 
duplicates stop the child before `B`, so the global distinct returns too few 
keys. Fix the pushed-limit/pass-through interaction in this operator too and 
cover the disabled-specialized-operator plan.



##########
be/src/exec/operator/streaming_agg_memory_limit.h:
##########
@@ -0,0 +1,62 @@
+// 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 <algorithm>
+#include <cstddef>
+#include <cstdint>
+
+#include "exec/operator/streaming_agg_min_reduction.h"
+
+namespace doris {
+
+// Per-task memory budget of a streaming pre-aggregation (hash table + arenas).
+//
+// 1. The budget is one fifth of the current query memory limit, shared evenly 
among the
+//    `parallel_tasks` instances of the operator on this BE. The query limit 
is read on every
+//    call, so a limit lowered or restored by the workload group manager takes 
effect at once.
+// 2. It never drops below `min_memory_limit`, which is the last cache tier of 
the min-reduction
+//    table doubled (the budget also counts the key/state arenas, not only the 
bucket array), so a
+//    small query limit does not disable pre-aggregation altogether. The floor 
itself is capped by
+//    the per-task share of the query limit, so the pre-aggregation alone can 
never exceed it.
+// 3. `fixed_limit` is an explicit upper bound on top of that; 0 means "no 
fixed bound". Callers
+//    pass the session variable `spill_streaming_agg_mem_limit` when spilling 
is enabled (the
+//    downstream agg can spill, the pre-agg cannot, so it must stay small) and 
0 otherwise. It is
+//    applied last so that a user who sets it explicitly always gets what they 
asked for.
+//
+// Returns 0 when neither limit is known, which callers treat as "no cap".
+inline size_t streaming_agg_memory_limit(int64_t query_memory_limit, int 
parallel_tasks,
+                                         int64_t fixed_limit) {
+    if (query_memory_limit <= 0) {
+        return fixed_limit > 0 ? static_cast<size_t>(fixed_limit) : 0;
+    }
+
+    constexpr int64_t memory_limit_divisor = 5;
+    constexpr int64_t min_memory_limit =
+            2LL * STREAMING_HT_MIN_REDUCTION[STREAMING_HT_MIN_REDUCTION_SIZE - 
1].min_ht_mem;
+
+    const int64_t per_task_query_limit = query_memory_limit / 
std::max(parallel_tasks, 1);

Review Comment:
   [P2] Keep a positive share distinct from the no-cap sentinel. For 
`query_memory_limit=1` and `parallel_tasks=2`, integer division makes 
`per_task_query_limit` zero, and the helper returns zero even if `fixed_limit` 
is positive. Both callers interpret zero as unlimited, so a known positive 
query limit disables the new gate. Clamp a known-positive per-task share to at 
least one byte or represent unknown/no-cap separately, and add `query_limit < 
parallel_tasks` cases.



##########
be/src/exec/operator/distinct_streaming_aggregation_operator.cpp:
##########
@@ -176,8 +195,13 @@ Status 
DistinctStreamingAggLocalState::_distinct_pre_agg_with_serialized_key(
     const uint32_t rows = (uint32_t)in_block->rows();
     _distinct_row.clear();
 
-    if (_parent->cast<DistinctStreamingAggOperatorX>()._is_streaming_preagg && 
low_memory_mode()) {
-        _stop_emplace_flag = true;
+    auto& parent = _parent->cast<DistinctStreamingAggOperatorX>();
+    if (parent._is_streaming_preagg) {
+        const auto memory_limit = parent._memory_limit(state());
+        COUNTER_SET(_memory_use_limit, static_cast<int64_t>(memory_limit));
+        if (low_memory_mode() || (memory_limit > 0 && _memory_usage() > 
memory_limit)) {
+            _stop_emplace_flag = true;

Review Comment:
   [P2] Re-evaluate the cap stop after a restored query limit. This assignment 
permanently latches `_stop_emplace_flag`, but workload-group query limits can 
fall and later rise, and `_memory_limit()` is intentionally refreshed every 
block. After one transient breach, every later block skips distinct emplacement 
even when retained usage is below the restored cap, so the remainder of a long 
query loses all local deduplication and can flood the exchange. Keep 
cap-triggered pass-through separate from the permanent low-reduction/low-memory 
stop, and add a lower-then-restore test.



##########
fe/fe-core/src/main/java/org/apache/doris/qe/SessionVariable.java:
##########
@@ -3262,7 +3262,9 @@ public void setDetailShapePlanNodes(String 
detailShapePlanNodes) {
     @VarAttrDef.VarAttr(name = LOW_MEMORY_MODE_BUFFER_LIMIT, fuzzy = false)
     public long lowMemoryModeBufferLimit = 33554432;
 
-    // The memory limit of streaming agg when spilling is enabled
+    // The memory limit of streaming agg when spilling is enabled. It is 
applied on top of the

Review Comment:
   [P1] Forward this explicit cap to the planning master. The annotation lacks 
`needForward=true`, so `getForwardVariables()` omits the setting. A follower 
session with `enable_spill=true` and a 1 MiB cap that forwards an insert/select 
sends `enable_spill` but reconstructs this value as the master's 256 MiB 
default before `toThrift()`; both newly covered streaming operators then use 
the wrong upper bound. Mark the variable for forwarding and add an 
observer-to-master non-default-value test.



##########
be/test/exec/operator/streaming_agg_operator_test.cpp:
##########
@@ -144,9 +175,26 @@ TEST_F(StreamingAggOperatorTest, test1) {
 
         EXPECT_EQ(local_state->_get_hash_table_size(), 3);
         EXPECT_TRUE(op->need_more_input_data(state.get()));
+        
EXPECT_EQ(local_state->custom_profile()->get_counter("MemoryUseLimit")->value(),
+                  100 * 1024 * 1024);
+    }
+
+    {
+        // With spilling enabled, spill_streaming_agg_mem_limit caps the 
budget.
+        state->set_enable_spill(true);
+        op->_spill_streaming_agg_mem_limit = 16 * 1024 * 1024;
+        Block block {ColumnHelper::create_column_with_name<DataTypeInt64>({1, 
2, 3}),
+                     ColumnHelper::create_column_with_name<DataTypeInt64>({1, 
100, 1000})};
+        auto st = op->push(state.get(), &block, false);

Review Comment:
   [P1] Exercise the production cap decision in this test. 
`MockStreamingAggLocalState::_should_not_do_pre_agg()` calls the base method 
but discards its return value and then returns `should_not_do_pre_agg`, which 
remains false in `test1`. Consequently this `push()` never follows the new 
cap-triggered pass-through path; these additions only prove the profile value 
and hash-table growth, and remain green if production enforcement is broken. 
Use a real local state or a fixture that returns the base decision, then lower 
the live/fixed cap below measured usage and assert pass-through/no new hash 
entries (plus restoration).



##########
be/src/exec/operator/streaming_aggregation_operator.cpp:
##########
@@ -289,9 +296,9 @@ bool StreamingAggLocalState::_should_not_do_pre_agg(size_t 
rows) {
     // But for fixed hash map, it never need to expand
     auto& p = Base::_parent->template cast<StreamingAggOperatorX>();
     bool ret_flag = false;
-    const auto spill_streaming_agg_mem_limit = 
p._spill_streaming_agg_mem_limit;
-    const bool used_too_much_memory =
-            spill_streaming_agg_mem_limit > 0 && _memory_usage() > 
spill_streaming_agg_mem_limit;
+    const auto memory_limit = p._memory_limit(state());
+    COUNTER_SET(_memory_use_limit, static_cast<int64_t>(memory_limit));

Review Comment:
   [P2] Restore TopN pruning after a transient cap pass-through. If a pushed 
TopN aggregate hits the cap before its hash table reaches `_sort_limit`, the 
pass-through branch changes `need_do_sort_limit` from -1 to 0. After 
workload-group limit restoration, aggregation resumes, but heap construction 
only runs while state is -1, so state 0 can never activate 
`_do_limit_filter()`; the local stage retains and shuffles all later groups. 
Keep the state re-eligible or rebuild it on recovery, and test lower/restore 
below the TopN threshold.



##########
be/src/exec/operator/streaming_aggregation_operator.cpp:
##########
@@ -289,9 +296,9 @@ bool StreamingAggLocalState::_should_not_do_pre_agg(size_t 
rows) {
     // But for fixed hash map, it never need to expand
     auto& p = Base::_parent->template cast<StreamingAggOperatorX>();
     bool ret_flag = false;
-    const auto spill_streaming_agg_mem_limit = 
p._spill_streaming_agg_mem_limit;
-    const bool used_too_much_memory =
-            spill_streaming_agg_mem_limit > 0 && _memory_usage() > 
spill_streaming_agg_mem_limit;
+    const auto memory_limit = p._memory_limit(state());
+    COUNTER_SET(_memory_use_limit, static_cast<int64_t>(memory_limit));
+    const bool used_too_much_memory = memory_limit > 0 && _memory_usage() > 
memory_limit;

Review Comment:
   [P1] Include all retained state in this memory cap. This comparison is not 
an upper bound for supported aggregates. `_memory_usage()` counts hash buffers 
and arenas but not heap owned by states stored in fixed slots; for example, 
`bitmap_union`'s per-group `BitmapValue`/Roaring buffers grow invisibly. After 
this condition selects pass-through, generic serializers such as `sum_foreach` 
also allocate nested temporary states from the lifetime `_agg_arena_pool`, so 
repeated blocks keep increasing retained memory until close. Account 
state-owned heap and use a resettable pass-through arena, with tests for both 
families.



##########
be/src/exec/operator/streaming_aggregation_operator.cpp:
##########
@@ -996,6 +997,20 @@ Status StreamingAggOperatorX::init(const TPlanNode& tnode, 
RuntimeState* state)
     return Status::OK();
 }
 
+size_t StreamingAggOperatorX::_memory_limit(RuntimeState* state) const {
+    constexpr size_t low_memory_mode_limit = 1024 * 1024;
+    if (_low_memory_mode.load(std::memory_order_relaxed)) {
+        return low_memory_mode_limit;

Review Comment:
   [P1] Never let low-memory mode raise a tighter cap. This early return 
replaces rather than constrains the live query/task and explicit spill bounds. 
For `enable_spill=true` and `spill_streaming_agg_mem_limit=512 KiB`, entering 
low-memory mode increases the allowed retention to 1 MiB; the live per-task 
query share can also legitimately be below 1 MiB. Compute the normal effective 
limit first and apply the low-memory value with `min`, then cover both sub-MiB 
cases.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


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

Reply via email to