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


##########
be/src/exec/scan/olap_scanner.cpp:
##########
@@ -312,6 +900,11 @@ Status OlapScanner::_open_impl(RuntimeState* state) {
     RETURN_IF_ERROR(Scanner::_open_impl(state));
     SCOPED_TIMER(_local_state->cast<OlapScanLocalState>()._reader_init_timer);
 
+    RETURN_IF_ERROR(_prepare_seq_map_candidate_keys());

Review Comment:
   [P1] Do not execute the multi-block candidate scan as one Scanner::open() 
task. The scheduler normally reads one output block and reschedules, then 
publishes realtime row/byte counters; this prepass can consume candidate blocks 
up to the full-scan cutoff before open() returns, so a large tablet can 
monopolize the scan worker and delay workload-group IO accounting for the 
entire prepass. The cumulative cost guards bound total work but do not restore 
fairness or interim accounting, and the started max_run_time_watch is never 
consulted. Please make candidate collection resumable under the scanner task 
budget, or cap synchronous work tightly and fall back while publishing deltas 
between blocks.



##########
be/src/exec/scan/olap_scanner.cpp:
##########
@@ -163,6 +178,579 @@ static bool has_file_cache_statistics(const 
io::FileCacheStatistics& stats) {
            stats.inverted_index_serial_read_rounds != 0;
 }
 
+std::vector<RowSetSplits> OlapScanner::_clone_rowset_splits() const {
+    std::vector<RowSetSplits> cloned;
+    cloned.reserve(_tablet_reader_params.rs_splits.size());
+    for (const auto& split : _tablet_reader_params.rs_splits) {
+        RowSetSplits copy(split.rs_reader->clone());
+        copy.segment_offsets = split.segment_offsets;
+        copy.segment_row_ranges = split.segment_row_ranges;
+        cloned.emplace_back(std::move(copy));
+    }
+    return cloned;
+}
+
+std::string OlapScanner::_encode_candidate_key(const OlapTuple& key) {
+    std::string encoded;
+    for (size_t i = 0; i < key.size(); ++i) {
+        const auto& field = key.get_field(i);
+        const auto type = static_cast<int32_t>(field.get_type());
+        encoded.append(reinterpret_cast<const char*>(&type), sizeof(type));
+        if (field.is_null()) {
+            continue;
+        }
+        const auto value = field.as_string_view();
+        const auto size = static_cast<uint64_t>(value.size());
+        encoded.append(reinterpret_cast<const char*>(&size), sizeof(size));
+        encoded.append(value);
+    }
+    return encoded;
+}
+
+OlapScanner::CandidateMemoryBudget OlapScanner::_split_candidate_memory_budget(
+        size_t reservation_bytes) {
+    if (reservation_bytes <= MIN_SEQ_MAP_CANDIDATE_WORKSPACE_BYTES) {
+        return {};
+    }
+    const size_t workspace_bytes =
+            std::clamp(reservation_bytes / 5, 
MIN_SEQ_MAP_CANDIDATE_WORKSPACE_BYTES,
+                       MAX_SEQ_MAP_CANDIDATE_WORKSPACE_BYTES);
+    const size_t key_bytes =
+            std::min(MAX_SEQ_MAP_CANDIDATE_KEY_BYTES, reservation_bytes - 
workspace_bytes);
+    return {
+            .reservation_bytes = key_bytes + workspace_bytes,
+            .key_bytes = key_bytes,
+            .workspace_bytes = workspace_bytes,
+    };
+}
+
+OlapScanner::CandidateMemoryBudget OlapScanner::_candidate_memory_budget() 
const {
+    const auto tracker = _state->query_mem_tracker();
+    if (tracker->limit() < 0) {
+        return 
_split_candidate_memory_budget(MAX_SEQ_MAP_CANDIDATE_RESERVATION_BYTES);
+    }
+    if (tracker->consumption() >= tracker->limit()) {
+        return {};
+    }
+    const auto remaining = static_cast<size_t>(tracker->limit() - 
tracker->consumption());
+    return _split_candidate_memory_budget(
+            std::min(MAX_SEQ_MAP_CANDIDATE_RESERVATION_BYTES, remaining / 8));
+}
+
+size_t OlapScanner::_estimate_candidate_key_bytes(const std::string& 
encoded_key,
+                                                  size_t key_column_count) {
+    // encoded_key contains the complete variable-length payload. Count it 
once for the map key
+    // and once as a conservative proxy for payload owned by string-like 
Fields.
+    const size_t fixed_bytes = sizeof(CandidateKeyMap::value_type) + 4 * 
sizeof(void*) +
+                               sizeof(RowCursor) + key_column_count * 
sizeof(Field);
+    if (encoded_key.size() > (std::numeric_limits<size_t>::max() - 
fixed_bytes) / 2) {
+        return std::numeric_limits<size_t>::max();
+    }
+    return fixed_bytes + 2 * encoded_key.size();
+}
+
+OlapScanner::CandidateKeyInsertResult 
OlapScanner::_try_add_seq_map_candidate_key(
+        std::string encoded_key, OlapTuple&& key, size_t key_column_count,
+        size_t max_candidate_bytes, size_t reservation_headroom_bytes,
+        CandidateKeyMap* candidate_keys, size_t* candidate_bytes) {
+    DCHECK(candidate_keys != nullptr);
+    DCHECK(candidate_bytes != nullptr);
+    if (candidate_keys->contains(encoded_key)) {
+        return CandidateKeyInsertResult::OK;
+    }
+
+    const size_t key_bytes = _estimate_candidate_key_bytes(encoded_key, 
key_column_count);
+    if (*candidate_bytes > max_candidate_bytes ||
+        key_bytes > max_candidate_bytes - *candidate_bytes) {
+        return CandidateKeyInsertResult::KEY_BYTES_LIMIT;
+    }
+    if (key_bytes > reservation_headroom_bytes) {
+        return CandidateKeyInsertResult::RESERVATION_LIMIT;
+    }
+    candidate_keys->emplace(std::move(encoded_key), std::move(key));
+    *candidate_bytes += key_bytes;
+    return CandidateKeyInsertResult::OK;
+}
+
+size_t OlapScanner::_estimate_candidate_map_bytes(const CandidateKeyMap& 
candidate_keys) const {
+    size_t bytes = 0;
+    const size_t key_column_count = 
_tablet_reader_params.tablet_schema->num_key_columns();
+    for (const auto& entry : candidate_keys) {
+        const size_t key_bytes = _estimate_candidate_key_bytes(entry.first, 
key_column_count);
+        if (key_bytes > std::numeric_limits<size_t>::max() - bytes) {
+            return std::numeric_limits<size_t>::max();
+        }
+        bytes += key_bytes;
+    }
+    return bytes;
+}
+
+static size_t saturating_add_size(size_t lhs, size_t rhs) {
+    return rhs > std::numeric_limits<size_t>::max() - lhs ? 
std::numeric_limits<size_t>::max()
+                                                          : lhs + rhs;
+}
+
+static size_t saturating_multiply_size(size_t lhs, size_t rhs) {
+    return lhs != 0 && rhs > std::numeric_limits<size_t>::max() / lhs
+                   ? std::numeric_limits<size_t>::max()
+                   : lhs * rhs;
+}
+
+bool OlapScanner::CandidateScanCostLimit::exceeded(int64_t 
previous_candidate_scan_rows,
+                                                   int64_t 
current_candidate_scan_rows,
+                                                   size_t candidate_key_count) 
const {
+    if (!enabled || full_scan_rows <= 0 || point_probe_cost_per_key == 0 ||
+        previous_candidate_scan_rows < 0 || current_candidate_scan_rows < 0) {
+        return false;
+    }
+    if (previous_candidate_scan_rows >= full_scan_rows ||
+        current_candidate_scan_rows >= full_scan_rows - 
previous_candidate_scan_rows) {
+        return true;
+    }
+
+    // Compare against the remaining row budget without multiplying candidate 
count by the
+    // weighted lower/upper short-key probe cost.
+    const auto remaining_rows = static_cast<uint64_t>(
+            full_scan_rows - previous_candidate_scan_rows - 
current_candidate_scan_rows);
+    return candidate_key_count > (remaining_rows - 1) / 
point_probe_cost_per_key;
+}
+
+void OlapScanner::_add_seq_map_candidate_cost(uint64_t row_count, size_t 
segment_count,
+                                              CandidateScanCostLimit* 
cost_limit) {
+    DCHECK(cost_limit != nullptr);
+    if (cost_limit->full_scan_rows != std::numeric_limits<int64_t>::max()) {
+        if (row_count > 
static_cast<uint64_t>(std::numeric_limits<int64_t>::max() -
+                                              cost_limit->full_scan_rows)) {
+            cost_limit->full_scan_rows = std::numeric_limits<int64_t>::max();
+        } else {
+            cost_limit->full_scan_rows += static_cast<int64_t>(row_count);
+        }
+    }
+
+    // MOR point lookup uses the short-key path. Each lower/upper ordinal 
lookup can binary-search
+    // up to the rowset row count, which is a conservative upper bound for 
every segment.
+    const size_t binary_search_steps =
+            std::max<size_t>(1, std::bit_width(std::max<uint64_t>(1, 
row_count)));
+    const size_t rowset_probe_cost = saturating_multiply_size(
+            saturating_multiply_size(2, segment_count), binary_search_steps);
+    cost_limit->point_probe_cost_per_key =
+            saturating_add_size(cost_limit->point_probe_cost_per_key, 
rowset_probe_cost);
+}
+
+void OlapScanner::_merge_seq_map_candidate_stats(const OlapReaderStatistics& 
candidate_stats,
+                                                 OlapReaderStatistics* 
total_stats) {
+    DCHECK(total_stats != nullptr);
+    total_stats->seq_map_candidate_scan_rows += candidate_stats.raw_rows_read;
+    total_stats->seq_map_candidate_scan_bytes += 
candidate_stats.uncompressed_bytes_read;
+    total_stats->seq_map_candidate_index_filtered_rows +=
+            candidate_stats.rows_inverted_index_filtered;
+    total_stats->seq_map_candidate_index_downgrades +=
+            candidate_stats.inverted_index_downgrade_count;
+    total_stats->seq_map_candidate_index_lookup_ns += 
candidate_stats.inverted_index_lookup_timer;
+    total_stats->seq_map_candidate_cache_local_bytes +=
+            candidate_stats.file_cache_stats.bytes_read_from_local;
+    total_stats->seq_map_candidate_cache_remote_bytes +=
+            candidate_stats.file_cache_stats.bytes_read_from_remote;
+    total_stats->file_cache_stats.merge_from(candidate_stats.file_cache_stats);
+
+    total_stats->io_ns += candidate_stats.io_ns;
+    total_stats->compressed_bytes_read += 
candidate_stats.compressed_bytes_read;
+    total_stats->decompress_ns += candidate_stats.decompress_ns;
+    total_stats->uncompressed_bytes_read += 
candidate_stats.uncompressed_bytes_read;
+    total_stats->bytes_read += candidate_stats.bytes_read;
+    total_stats->raw_rows_read += candidate_stats.raw_rows_read;
+}
+
+Status OlapScanner::_collect_seq_map_candidate_keys(
+        const std::vector<std::shared_ptr<ColumnPredicate>>& driver_predicates,
+        const std::vector<std::shared_ptr<ColumnPredicate>>& key_predicates,
+        int64_t previous_candidate_scan_rows, bool price_point_lookups, 
int64_t max_candidate_keys,
+        size_t max_candidate_bytes, size_t candidate_workspace_bytes,
+        const CandidateScanCostLimit& cost_limit, CandidateKeyMap* 
candidate_keys,
+        size_t* candidate_bytes, bool* limit_exceeded, bool* bytes_exceeded,
+        bool* reservation_exceeded, bool* cost_exceeded) {
+    DCHECK(candidate_keys != nullptr);
+    DCHECK(candidate_bytes != nullptr);
+    DCHECK(limit_exceeded != nullptr);
+    DCHECK(bytes_exceeded != nullptr);
+    DCHECK(reservation_exceeded != nullptr);
+    DCHECK(cost_exceeded != nullptr);
+    *candidate_bytes = 0;
+    *limit_exceeded = false;
+    *bytes_exceeded = false;
+    *reservation_exceeded = false;
+    *cost_exceeded = false;
+    candidate_keys->clear();
+
+    auto candidate_params = _tablet_reader_params;
+    candidate_params.rs_splits = _clone_rowset_splits();
+    candidate_params.predicates.clear();
+    for (const auto& predicate : key_predicates) {
+        
candidate_params.predicates.emplace_back(predicate->clone(predicate->column_id()));
+    }
+    for (const auto& predicate : driver_predicates) {
+        
candidate_params.predicates.emplace_back(predicate->clone(predicate->column_id()));
+    }
+    candidate_params.function_filters.clear();
+    candidate_params.all_access_paths.clear();
+    candidate_params.predicate_access_paths.clear();
+    candidate_params.output_columns.clear();
+    candidate_params.extra_columns.clear();
+    candidate_params.common_expr_ctxs_push_down.clear();
+    candidate_params.topn_filter_source_node_ids.clear();
+    candidate_params.key_group_cluster_key_idxes.clear();
+    candidate_params.virtual_column_exprs.clear();
+    candidate_params.score_runtime.reset();
+    candidate_params.collection_statistics.reset();
+    candidate_params.ann_topn_runtime.reset();
+    candidate_params.direct_mode = true;
+    candidate_params.aggregation = false;
+    candidate_params.is_seq_map_candidate_scan = true;
+    candidate_params.seq_map_candidate_pruned = false;
+    candidate_params.push_down_agg_type_opt = TPushAggOp::NONE;
+    candidate_params.read_orderby_key = false;
+    candidate_params.read_orderby_key_reverse = false;
+    candidate_params.read_orderby_key_num_prefix_columns = 0;
+    candidate_params.read_orderby_key_limit = 0;
+    candidate_params.condition_cache_digest = 0;
+    candidate_params.general_read_limit = -1;

Review Comment:
   [P1] Keep scanner-level LIMIT queries out of this prepass. This path clears 
general_read_limit and runs from open() before Scanner::get_block() can apply 
_limit/_shared_scan_limit, while admission still prices the unchanged path as a 
tablet-wide scan. A broad indexed LIMIT 1 can therefore build candidates up to 
the key/memory/cost guard on every scanner before the normal reader returns its 
first block, although the unchanged MOR scan can stop there. The existing 
key_range_present guard does not cover an ordinary LIMIT because it leaves 
start_key/end_key empty. Please fall back when a scan limit is active, or use a 
sound limit-aware baseline; do not truncate the candidate set to LIMIT because 
incomplete point keys would create false negatives.



##########
be/src/exec/scan/olap_scanner.cpp:
##########
@@ -163,6 +178,579 @@ static bool has_file_cache_statistics(const 
io::FileCacheStatistics& stats) {
            stats.inverted_index_serial_read_rounds != 0;
 }
 
+std::vector<RowSetSplits> OlapScanner::_clone_rowset_splits() const {
+    std::vector<RowSetSplits> cloned;
+    cloned.reserve(_tablet_reader_params.rs_splits.size());
+    for (const auto& split : _tablet_reader_params.rs_splits) {
+        RowSetSplits copy(split.rs_reader->clone());
+        copy.segment_offsets = split.segment_offsets;
+        copy.segment_row_ranges = split.segment_row_ranges;
+        cloned.emplace_back(std::move(copy));
+    }
+    return cloned;
+}
+
+std::string OlapScanner::_encode_candidate_key(const OlapTuple& key) {
+    std::string encoded;
+    for (size_t i = 0; i < key.size(); ++i) {
+        const auto& field = key.get_field(i);
+        const auto type = static_cast<int32_t>(field.get_type());
+        encoded.append(reinterpret_cast<const char*>(&type), sizeof(type));
+        if (field.is_null()) {
+            continue;
+        }
+        const auto value = field.as_string_view();
+        const auto size = static_cast<uint64_t>(value.size());
+        encoded.append(reinterpret_cast<const char*>(&size), sizeof(size));
+        encoded.append(value);
+    }
+    return encoded;
+}
+
+OlapScanner::CandidateMemoryBudget OlapScanner::_split_candidate_memory_budget(
+        size_t reservation_bytes) {
+    if (reservation_bytes <= MIN_SEQ_MAP_CANDIDATE_WORKSPACE_BYTES) {
+        return {};
+    }
+    const size_t workspace_bytes =
+            std::clamp(reservation_bytes / 5, 
MIN_SEQ_MAP_CANDIDATE_WORKSPACE_BYTES,
+                       MAX_SEQ_MAP_CANDIDATE_WORKSPACE_BYTES);
+    const size_t key_bytes =
+            std::min(MAX_SEQ_MAP_CANDIDATE_KEY_BYTES, reservation_bytes - 
workspace_bytes);
+    return {
+            .reservation_bytes = key_bytes + workspace_bytes,
+            .key_bytes = key_bytes,
+            .workspace_bytes = workspace_bytes,
+    };
+}
+
+OlapScanner::CandidateMemoryBudget OlapScanner::_candidate_memory_budget() 
const {
+    const auto tracker = _state->query_mem_tracker();
+    if (tracker->limit() < 0) {
+        return 
_split_candidate_memory_budget(MAX_SEQ_MAP_CANDIDATE_RESERVATION_BYTES);
+    }
+    if (tracker->consumption() >= tracker->limit()) {
+        return {};
+    }
+    const auto remaining = static_cast<size_t>(tracker->limit() - 
tracker->consumption());
+    return _split_candidate_memory_budget(
+            std::min(MAX_SEQ_MAP_CANDIDATE_RESERVATION_BYTES, remaining / 8));
+}
+
+size_t OlapScanner::_estimate_candidate_key_bytes(const std::string& 
encoded_key,
+                                                  size_t key_column_count) {
+    // encoded_key contains the complete variable-length payload. Count it 
once for the map key
+    // and once as a conservative proxy for payload owned by string-like 
Fields.
+    const size_t fixed_bytes = sizeof(CandidateKeyMap::value_type) + 4 * 
sizeof(void*) +
+                               sizeof(RowCursor) + key_column_count * 
sizeof(Field);
+    if (encoded_key.size() > (std::numeric_limits<size_t>::max() - 
fixed_bytes) / 2) {
+        return std::numeric_limits<size_t>::max();
+    }
+    return fixed_bytes + 2 * encoded_key.size();
+}
+
+OlapScanner::CandidateKeyInsertResult 
OlapScanner::_try_add_seq_map_candidate_key(
+        std::string encoded_key, OlapTuple&& key, size_t key_column_count,
+        size_t max_candidate_bytes, size_t reservation_headroom_bytes,
+        CandidateKeyMap* candidate_keys, size_t* candidate_bytes) {
+    DCHECK(candidate_keys != nullptr);
+    DCHECK(candidate_bytes != nullptr);
+    if (candidate_keys->contains(encoded_key)) {
+        return CandidateKeyInsertResult::OK;
+    }
+
+    const size_t key_bytes = _estimate_candidate_key_bytes(encoded_key, 
key_column_count);
+    if (*candidate_bytes > max_candidate_bytes ||
+        key_bytes > max_candidate_bytes - *candidate_bytes) {
+        return CandidateKeyInsertResult::KEY_BYTES_LIMIT;
+    }
+    if (key_bytes > reservation_headroom_bytes) {
+        return CandidateKeyInsertResult::RESERVATION_LIMIT;
+    }
+    candidate_keys->emplace(std::move(encoded_key), std::move(key));
+    *candidate_bytes += key_bytes;
+    return CandidateKeyInsertResult::OK;
+}
+
+size_t OlapScanner::_estimate_candidate_map_bytes(const CandidateKeyMap& 
candidate_keys) const {
+    size_t bytes = 0;
+    const size_t key_column_count = 
_tablet_reader_params.tablet_schema->num_key_columns();
+    for (const auto& entry : candidate_keys) {
+        const size_t key_bytes = _estimate_candidate_key_bytes(entry.first, 
key_column_count);
+        if (key_bytes > std::numeric_limits<size_t>::max() - bytes) {
+            return std::numeric_limits<size_t>::max();
+        }
+        bytes += key_bytes;
+    }
+    return bytes;
+}
+
+static size_t saturating_add_size(size_t lhs, size_t rhs) {
+    return rhs > std::numeric_limits<size_t>::max() - lhs ? 
std::numeric_limits<size_t>::max()
+                                                          : lhs + rhs;
+}
+
+static size_t saturating_multiply_size(size_t lhs, size_t rhs) {
+    return lhs != 0 && rhs > std::numeric_limits<size_t>::max() / lhs
+                   ? std::numeric_limits<size_t>::max()
+                   : lhs * rhs;
+}
+
+bool OlapScanner::CandidateScanCostLimit::exceeded(int64_t 
previous_candidate_scan_rows,
+                                                   int64_t 
current_candidate_scan_rows,
+                                                   size_t candidate_key_count) 
const {
+    if (!enabled || full_scan_rows <= 0 || point_probe_cost_per_key == 0 ||
+        previous_candidate_scan_rows < 0 || current_candidate_scan_rows < 0) {
+        return false;
+    }
+    if (previous_candidate_scan_rows >= full_scan_rows ||
+        current_candidate_scan_rows >= full_scan_rows - 
previous_candidate_scan_rows) {
+        return true;
+    }
+
+    // Compare against the remaining row budget without multiplying candidate 
count by the
+    // weighted lower/upper short-key probe cost.
+    const auto remaining_rows = static_cast<uint64_t>(
+            full_scan_rows - previous_candidate_scan_rows - 
current_candidate_scan_rows);
+    return candidate_key_count > (remaining_rows - 1) / 
point_probe_cost_per_key;
+}
+
+void OlapScanner::_add_seq_map_candidate_cost(uint64_t row_count, size_t 
segment_count,
+                                              CandidateScanCostLimit* 
cost_limit) {
+    DCHECK(cost_limit != nullptr);
+    if (cost_limit->full_scan_rows != std::numeric_limits<int64_t>::max()) {
+        if (row_count > 
static_cast<uint64_t>(std::numeric_limits<int64_t>::max() -
+                                              cost_limit->full_scan_rows)) {
+            cost_limit->full_scan_rows = std::numeric_limits<int64_t>::max();
+        } else {
+            cost_limit->full_scan_rows += static_cast<int64_t>(row_count);
+        }
+    }
+
+    // MOR point lookup uses the short-key path. Each lower/upper ordinal 
lookup can binary-search
+    // up to the rowset row count, which is a conservative upper bound for 
every segment.
+    const size_t binary_search_steps =
+            std::max<size_t>(1, std::bit_width(std::max<uint64_t>(1, 
row_count)));
+    const size_t rowset_probe_cost = saturating_multiply_size(
+            saturating_multiply_size(2, segment_count), binary_search_steps);
+    cost_limit->point_probe_cost_per_key =
+            saturating_add_size(cost_limit->point_probe_cost_per_key, 
rowset_probe_cost);
+}
+
+void OlapScanner::_merge_seq_map_candidate_stats(const OlapReaderStatistics& 
candidate_stats,
+                                                 OlapReaderStatistics* 
total_stats) {
+    DCHECK(total_stats != nullptr);
+    total_stats->seq_map_candidate_scan_rows += candidate_stats.raw_rows_read;
+    total_stats->seq_map_candidate_scan_bytes += 
candidate_stats.uncompressed_bytes_read;
+    total_stats->seq_map_candidate_index_filtered_rows +=
+            candidate_stats.rows_inverted_index_filtered;
+    total_stats->seq_map_candidate_index_downgrades +=
+            candidate_stats.inverted_index_downgrade_count;
+    total_stats->seq_map_candidate_index_lookup_ns += 
candidate_stats.inverted_index_lookup_timer;
+    total_stats->seq_map_candidate_cache_local_bytes +=
+            candidate_stats.file_cache_stats.bytes_read_from_local;
+    total_stats->seq_map_candidate_cache_remote_bytes +=
+            candidate_stats.file_cache_stats.bytes_read_from_remote;
+    total_stats->file_cache_stats.merge_from(candidate_stats.file_cache_stats);
+
+    total_stats->io_ns += candidate_stats.io_ns;
+    total_stats->compressed_bytes_read += 
candidate_stats.compressed_bytes_read;
+    total_stats->decompress_ns += candidate_stats.decompress_ns;
+    total_stats->uncompressed_bytes_read += 
candidate_stats.uncompressed_bytes_read;
+    total_stats->bytes_read += candidate_stats.bytes_read;
+    total_stats->raw_rows_read += candidate_stats.raw_rows_read;
+}
+
+Status OlapScanner::_collect_seq_map_candidate_keys(
+        const std::vector<std::shared_ptr<ColumnPredicate>>& driver_predicates,
+        const std::vector<std::shared_ptr<ColumnPredicate>>& key_predicates,
+        int64_t previous_candidate_scan_rows, bool price_point_lookups, 
int64_t max_candidate_keys,
+        size_t max_candidate_bytes, size_t candidate_workspace_bytes,
+        const CandidateScanCostLimit& cost_limit, CandidateKeyMap* 
candidate_keys,
+        size_t* candidate_bytes, bool* limit_exceeded, bool* bytes_exceeded,
+        bool* reservation_exceeded, bool* cost_exceeded) {
+    DCHECK(candidate_keys != nullptr);
+    DCHECK(candidate_bytes != nullptr);
+    DCHECK(limit_exceeded != nullptr);
+    DCHECK(bytes_exceeded != nullptr);
+    DCHECK(reservation_exceeded != nullptr);
+    DCHECK(cost_exceeded != nullptr);
+    *candidate_bytes = 0;
+    *limit_exceeded = false;
+    *bytes_exceeded = false;
+    *reservation_exceeded = false;
+    *cost_exceeded = false;
+    candidate_keys->clear();
+
+    auto candidate_params = _tablet_reader_params;
+    candidate_params.rs_splits = _clone_rowset_splits();
+    candidate_params.predicates.clear();
+    for (const auto& predicate : key_predicates) {
+        
candidate_params.predicates.emplace_back(predicate->clone(predicate->column_id()));
+    }
+    for (const auto& predicate : driver_predicates) {
+        
candidate_params.predicates.emplace_back(predicate->clone(predicate->column_id()));
+    }
+    candidate_params.function_filters.clear();
+    candidate_params.all_access_paths.clear();
+    candidate_params.predicate_access_paths.clear();
+    candidate_params.output_columns.clear();
+    candidate_params.extra_columns.clear();
+    candidate_params.common_expr_ctxs_push_down.clear();
+    candidate_params.topn_filter_source_node_ids.clear();
+    candidate_params.key_group_cluster_key_idxes.clear();
+    candidate_params.virtual_column_exprs.clear();
+    candidate_params.score_runtime.reset();
+    candidate_params.collection_statistics.reset();
+    candidate_params.ann_topn_runtime.reset();
+    candidate_params.direct_mode = true;
+    candidate_params.aggregation = false;
+    candidate_params.is_seq_map_candidate_scan = true;
+    candidate_params.seq_map_candidate_pruned = false;
+    candidate_params.push_down_agg_type_opt = TPushAggOp::NONE;
+    candidate_params.read_orderby_key = false;
+    candidate_params.read_orderby_key_reverse = false;
+    candidate_params.read_orderby_key_num_prefix_columns = 0;
+    candidate_params.read_orderby_key_limit = 0;
+    candidate_params.condition_cache_digest = 0;
+    candidate_params.general_read_limit = -1;
+    candidate_params.read_row_binlog = false;
+    candidate_params.binlog_scan_type = TBinlogScanType::NONE;
+    candidate_params.start_tso.reset();
+    candidate_params.end_tso.reset();
+    candidate_params.tso_predicate_column_id.reset();
+
+    std::vector<ColumnId> candidate_columns;
+    
candidate_columns.reserve(_tablet_reader_params.tablet_schema->num_key_columns()
 +
+                              driver_predicates.size());
+    for (uint32_t cid = 0; cid < 
_tablet_reader_params.tablet_schema->num_key_columns(); ++cid) {
+        candidate_columns.push_back(cid);
+    }
+    for (const auto& predicate : driver_predicates) {
+        if (std::find(candidate_columns.begin(), candidate_columns.end(), 
predicate->column_id()) ==
+            candidate_columns.end()) {
+            candidate_columns.push_back(predicate->column_id());
+        }
+    }
+    candidate_params.return_columns = candidate_columns;
+    candidate_params.origin_return_columns = &candidate_columns;
+    candidate_params.tablet_columns_convert_to_null_set = nullptr;
+
+    BlockReader candidate_reader;
+    candidate_reader.set_batch_size(_state->batch_size());
+    candidate_reader.set_preferred_block_size_bytes(candidate_workspace_bytes);
+    Defer account_candidate_stats {[&]() {
+        _merge_seq_map_candidate_stats(candidate_reader.stats(), 
_tablet_reader->mutable_stats());
+    }};
+    RETURN_IF_ERROR(candidate_reader.init(candidate_params));
+
+    Block block = 
candidate_params.tablet_schema->create_block(candidate_columns);
+    const size_t key_column_count = 
candidate_params.tablet_schema->num_key_columns();
+    bool eof = false;
+    while (!eof) {
+        RETURN_IF_ERROR(candidate_reader.next_block_with_aggregation(&block, 
&eof));
+        _tablet_reader->mutable_stats()->seq_map_candidate_rows += 
block.rows();
+        for (size_t row = 0; row < block.rows(); ++row) {
+            OlapTuple key;
+            for (size_t col = 0; col < key_column_count; ++col) {
+                Field field;
+                block.get_by_position(col).column->get(row, field);
+                key.add_field(std::move(field));
+            }
+            auto encoded_key = _encode_candidate_key(key);
+            const int64_t reserved_bytes = 
thread_context()->thread_mem_tracker_mgr->reserved_mem();
+            const size_t reservation_headroom =
+                    reserved_bytes > cast_set<int64_t>(
+                                             
MIN_SEQ_MAP_CANDIDATE_RESERVATION_HEADROOM_BYTES)
+                            ? cast_set<size_t>(reserved_bytes -
+                                               
MIN_SEQ_MAP_CANDIDATE_RESERVATION_HEADROOM_BYTES)
+                            : 0;
+            const auto insert_result = _try_add_seq_map_candidate_key(
+                    std::move(encoded_key), std::move(key), key_column_count, 
max_candidate_bytes,
+                    reservation_headroom, candidate_keys, candidate_bytes);
+            if (insert_result == CandidateKeyInsertResult::KEY_BYTES_LIMIT) {
+                *bytes_exceeded = true;
+                break;
+            }
+            if (insert_result == CandidateKeyInsertResult::RESERVATION_LIMIT) {
+                *reservation_exceeded = true;
+                break;
+            }
+            if (candidate_keys->size() > 
static_cast<size_t>(max_candidate_keys)) {
+                *limit_exceeded = true;
+                break;
+            }
+        }
+        block.clear_column_data();
+        if (*limit_exceeded || *bytes_exceeded || *reservation_exceeded) {
+            break;
+        }
+        const size_t candidate_key_count = price_point_lookups ? 
candidate_keys->size() : 0;
+        if (cost_limit.exceeded(previous_candidate_scan_rows,
+                                candidate_reader.stats().raw_rows_read, 
candidate_key_count)) {
+            *cost_exceeded = true;
+            break;
+        }
+    }
+    return Status::OK();
+}
+
+Status OlapScanner::_materialize_seq_map_point_keys(CandidateKeyMap* 
candidate_keys,
+                                                    size_t retained_bytes,
+                                                    PointKeySetSPtr* 
point_keys) {
+    DCHECK(candidate_keys != nullptr);
+    DCHECK(point_keys != nullptr);
+
+    const auto key_schema =
+            
RowCursor::create_shared_schema(_tablet_reader_params.tablet_schema,
+                                            
_tablet_reader_params.tablet_schema->num_key_columns());
+    auto mutable_point_keys = std::make_shared<PointKeySet>(key_schema);
+    mutable_point_keys->keys.reserve(candidate_keys->size());
+    for (auto& entry : *candidate_keys) {
+        RowCursor point_key;
+        RETURN_IF_ERROR(point_key.init(key_schema, 
std::move(entry.second).release_fields()));
+        mutable_point_keys->keys.emplace_back(std::move(point_key));
+    }
+    std::sort(mutable_point_keys->keys.begin(), mutable_point_keys->keys.end(),
+              [](const RowCursor& lhs, const RowCursor& rhs) {
+                  return compare_row_key(lhs, rhs) < 0;
+              });
+    mutable_point_keys->retained_bytes = retained_bytes;
+    *point_keys = std::move(mutable_point_keys);
+    return Status::OK();
+}
+
+bool OlapScanner::_is_candidate_memory_failure(const Status& status) {
+    return status.is<ErrorCode::MEM_LIMIT_EXCEEDED>() || 
status.is<ErrorCode::MEM_ALLOC_FAILED>() ||
+           status.is<ErrorCode::QUERY_MEMORY_EXCEEDED>() ||
+           status.is<ErrorCode::WORKLOAD_GROUP_MEMORY_EXCEEDED>() ||
+           status.is<ErrorCode::PROCESS_MEMORY_EXCEEDED>();
+}
+
+void OlapScanner::_record_seq_map_candidate_fallback_reason(RuntimeProfile* 
profile,
+                                                            const std::string& 
fallback_reason) {
+    DCHECK(profile != nullptr);
+    DCHECK(!fallback_reason.empty());
+    profile->add_info_string("SeqMapCandidateFallbackReason." + 
fallback_reason, fallback_reason);
+}
+
+Status OlapScanner::_build_seq_map_candidate_keys(
+        const std::vector<std::shared_ptr<ColumnPredicate>>& key_predicates,
+        const std::map<uint32_t, 
std::vector<std::shared_ptr<ColumnPredicate>>>& group_drivers,
+        int64_t max_candidate_keys, const CandidateMemoryBudget& memory_budget,
+        const CandidateScanCostLimit& cost_limit) {
+    auto& params = _tablet_reader_params;
+    auto* stats = _tablet_reader->mutable_stats();
+
+    // Reserve key retention and reader workspace before either candidate map 
starts allocating.
+    auto* mem_tracker_mgr = thread_context()->thread_mem_tracker_mgr.get();
+    auto inherited_reservation = mem_tracker_mgr->take_reserved_memory();
+    Defer restore_inherited_reservation {
+            [&] { 
mem_tracker_mgr->adopt_reserved_memory(std::move(inherited_reservation)); }};
+    auto reserve_status =
+            
mem_tracker_mgr->try_reserve(cast_set<int64_t>(memory_budget.reservation_bytes));
+    if (!reserve_status.ok()) {
+        ++stats->seq_map_candidate_fallbacks;
+        _seq_map_candidate_fallback_reason = 
"candidate_key_memory_reservation";
+        return Status::OK();
+    }
+    DEFER_RELEASE_RESERVED();

Review Comment:
   [P1] Include the downstream point-range bitmaps in the candidate memory 
fallback. This reservation ends when candidate construction returns, but 
BlockReader::init() then builds the merge heap and initialized segments expand 
the same key set into RowRanges and retained sparse _row_bitmap state. With 
100k keys across 100 overlapping segments, that is about 10 million RowRange 
entries (roughly 160 MiB before vector capacity and roaring storage) outside 
the reservation, even though the break-even model can still admit this on a 
billion-row tablet. A memory failure here fails the scan, while the unchanged 
full-range bitmap is compact. Please reserve/estimate through final-reader 
initialization, or recreate the unoptimized reader on its memory-limit statuses.



-- 
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