Coverage Report

Created: 2026-07-07 17:25

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
be/src/exec/spill/spill_repartitioner.cpp
Line
Count
Source
1
// Licensed to the Apache Software Foundation (ASF) under one
2
// or more contributor license agreements.  See the NOTICE file
3
// distributed with this work for additional information
4
// regarding copyright ownership.  The ASF licenses this file
5
// to you under the Apache License, Version 2.0 (the
6
// "License"); you may not use this file except in compliance
7
// with the License.  You may obtain a copy of the License at
8
//
9
//   http://www.apache.org/licenses/LICENSE-2.0
10
//
11
// Unless required by applicable law or agreed to in writing,
12
// software distributed under the License is distributed on an
13
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14
// KIND, either express or implied.  See the License for the
15
// specific language governing permissions and limitations
16
// under the License.
17
18
#include "exec/spill/spill_repartitioner.h"
19
20
#include <glog/logging.h>
21
22
#include <limits>
23
#include <memory>
24
#include <vector>
25
26
#include "core/block/block.h"
27
#include "core/column/column.h"
28
#include "exec/partitioner/partitioner.h"
29
#include "exec/spill/spill_file.h"
30
#include "exec/spill/spill_file_manager.h"
31
#include "exec/spill/spill_file_reader.h"
32
#include "exec/spill/spill_file_writer.h"
33
#include "runtime/exec_env.h"
34
#include "runtime/runtime_profile.h"
35
#include "runtime/runtime_state.h"
36
#include "util/uid_util.h"
37
38
namespace doris {
39
40
void SpillRepartitioner::init(std::unique_ptr<PartitionerBase> partitioner, RuntimeProfile* profile,
41
8
                              int fanout, int repartition_level) {
42
8
    _partitioner = std::move(partitioner);
43
8
    _use_column_index_mode = false;
44
8
    _fanout = fanout;
45
8
    _repartition_level = repartition_level;
46
8
    _operator_profile = profile;
47
8
    _repartition_timer = ADD_TIMER_WITH_LEVEL(profile, "SpillRepartitionTime", 1);
48
8
    _repartition_rows = ADD_COUNTER_WITH_LEVEL(profile, "SpillRepartitionRows", TUnit::UNIT, 1);
49
8
}
50
51
void SpillRepartitioner::init_with_key_columns(std::vector<size_t> key_column_indices,
52
                                               std::vector<DataTypePtr> key_data_types,
53
                                               RuntimeProfile* profile, int fanout,
54
24
                                               int repartition_level) {
55
24
    _key_column_indices = std::move(key_column_indices);
56
24
    _key_data_types = std::move(key_data_types);
57
24
    _use_column_index_mode = true;
58
24
    _partitioner.reset();
59
24
    _fanout = fanout;
60
24
    _repartition_level = repartition_level;
61
24
    _operator_profile = profile;
62
24
    _repartition_timer = ADD_TIMER_WITH_LEVEL(profile, "SpillRepartitionTime", 1);
63
24
    _repartition_rows = ADD_COUNTER_WITH_LEVEL(profile, "SpillRepartitionRows", TUnit::UNIT, 1);
64
24
}
65
66
Status SpillRepartitioner::setup_output(RuntimeState* state,
67
30
                                        std::vector<SpillFileSPtr>& output_spill_files) {
68
30
    DCHECK_EQ(output_spill_files.size(), _fanout);
69
30
    _output_spill_files = &output_spill_files;
70
30
    _output_writers.resize(_fanout);
71
198
    for (int i = 0; i < _fanout; ++i) {
72
168
        RETURN_IF_ERROR(
73
168
                output_spill_files[i]->create_writer(state, _operator_profile, _output_writers[i]));
74
168
    }
75
    // Reset reader state from any previous repartition session
76
30
    _input_reader.reset();
77
30
    _current_input_file.reset();
78
30
    return Status::OK();
79
30
}
80
81
Status SpillRepartitioner::repartition(RuntimeState* state, SpillFileSPtr& input_spill_file,
82
6
                                       bool* done) {
83
6
    DCHECK(_output_spill_files != nullptr) << "setup_output() must be called first";
84
6
    SCOPED_TIMER(_repartition_timer);
85
86
6
    *done = false;
87
6
    size_t accumulated_bytes = 0;
88
89
    // Create or reuse input reader. If the input file changed, create a new reader.
90
6
    if (_current_input_file != input_spill_file) {
91
6
        _current_input_file = input_spill_file;
92
6
        _input_reader = input_spill_file->create_reader(state, _operator_profile);
93
6
        RETURN_IF_ERROR(_input_reader->open());
94
6
    }
95
96
    // Per-partition write buffers to batch small writes
97
6
    std::vector<std::unique_ptr<MutableBlock>> output_buffers(_fanout);
98
99
6
    bool eos = false;
100
22
    while (!eos && !state->is_cancelled()) {
101
16
        Block block;
102
16
        RETURN_IF_ERROR(_input_reader->read(&block, &eos));
103
104
16
        if (block.empty()) {
105
6
            continue;
106
6
        }
107
108
10
        accumulated_bytes += block.allocated_bytes();
109
10
        COUNTER_UPDATE(_repartition_rows, block.rows());
110
111
10
        if (_use_column_index_mode) {
112
2
            RETURN_IF_ERROR(_route_block_by_columns(state, block, output_buffers));
113
8
        } else {
114
8
            RETURN_IF_ERROR(_route_block(state, block, output_buffers));
115
8
        }
116
117
        // Yield after processing MAX_BATCH_BYTES to let pipeline scheduler re-schedule
118
10
        if (accumulated_bytes >= MAX_BATCH_BYTES && !eos) {
119
0
            break;
120
0
        }
121
10
    }
122
123
    // Flush all remaining buffers
124
6
    RETURN_IF_ERROR(_flush_all_buffers(state, output_buffers, /*force=*/true));
125
126
6
    if (eos) {
127
6
        *done = true;
128
        // Reset reader for this input file
129
6
        _input_reader.reset();
130
6
        _current_input_file.reset();
131
6
    }
132
133
6
    return Status::OK();
134
6
}
135
136
Status SpillRepartitioner::repartition(RuntimeState* state, SpillFileReaderSPtr& reader,
137
6
                                       bool* done) {
138
6
    DCHECK(_output_spill_files != nullptr) << "setup_output() must be called first";
139
6
    DCHECK(reader != nullptr) << "reader must not be null";
140
6
    SCOPED_TIMER(_repartition_timer);
141
142
6
    *done = false;
143
6
    size_t accumulated_bytes = 0;
144
145
    // Per-partition write buffers to batch small writes
146
6
    std::vector<std::unique_ptr<MutableBlock>> output_buffers(_fanout);
147
148
6
    bool eos = false;
149
18
    while (!eos && !state->is_cancelled()) {
150
12
        Block block;
151
12
        RETURN_IF_ERROR(reader->read(&block, &eos));
152
153
12
        if (block.empty()) {
154
6
            continue;
155
6
        }
156
157
6
        accumulated_bytes += block.allocated_bytes();
158
6
        COUNTER_UPDATE(_repartition_rows, block.rows());
159
160
6
        if (_use_column_index_mode) {
161
2
            RETURN_IF_ERROR(_route_block_by_columns(state, block, output_buffers));
162
4
        } else {
163
4
            RETURN_IF_ERROR(_route_block(state, block, output_buffers));
164
4
        }
165
166
        // Yield after processing MAX_BATCH_BYTES to let pipeline scheduler re-schedule
167
6
        if (accumulated_bytes >= MAX_BATCH_BYTES && !eos) {
168
0
            break;
169
0
        }
170
6
    }
171
172
    // Flush all remaining buffers
173
6
    RETURN_IF_ERROR(_flush_all_buffers(state, output_buffers, /*force=*/true));
174
175
6
    if (eos) {
176
6
        *done = true;
177
6
        reader.reset();
178
6
    }
179
180
6
    return Status::OK();
181
6
}
182
183
24
Status SpillRepartitioner::route_block(RuntimeState* state, Block& block) {
184
24
    DCHECK(_output_spill_files != nullptr) << "setup_output() must be called first";
185
24
    if (UNLIKELY(_output_spill_files == nullptr)) {
186
0
        return Status::InternalError("SpillRepartitioner::setup_output() must be called first");
187
0
    }
188
24
    SCOPED_TIMER(_repartition_timer);
189
190
24
    if (block.empty()) {
191
2
        return Status::OK();
192
2
    }
193
194
22
    COUNTER_UPDATE(_repartition_rows, block.rows());
195
196
22
    std::vector<std::unique_ptr<MutableBlock>> output_buffers(_fanout);
197
22
    if (_use_column_index_mode) {
198
18
        RETURN_IF_ERROR(_route_block_by_columns(state, block, output_buffers));
199
18
    } else {
200
4
        RETURN_IF_ERROR(_route_block(state, block, output_buffers));
201
4
    }
202
22
    RETURN_IF_ERROR(_flush_all_buffers(state, output_buffers, /*force=*/true));
203
22
    return Status::OK();
204
22
}
205
206
30
Status SpillRepartitioner::finalize() {
207
30
    DCHECK(_output_spill_files != nullptr) << "setup_output() must be called first";
208
30
    if (UNLIKELY(_output_spill_files == nullptr)) {
209
0
        return Status::InternalError("SpillRepartitioner::setup_output() must be called first");
210
0
    }
211
    // Close all writers (Writer::close() automatically updates SpillFile stats)
212
198
    for (int i = 0; i < _fanout; ++i) {
213
168
        if (_output_writers[i]) {
214
168
            RETURN_IF_ERROR(_output_writers[i]->close());
215
168
        }
216
168
    }
217
30
    _output_writers.clear();
218
30
    _output_spill_files = nullptr;
219
30
    _input_reader.reset();
220
30
    _current_input_file.reset();
221
30
    return Status::OK();
222
30
}
223
224
Status SpillRepartitioner::create_output_spill_files(
225
        RuntimeState* state, int node_id, const std::string& label_prefix, int fanout,
226
32
        std::vector<SpillFileSPtr>& output_spill_files) {
227
32
    output_spill_files.resize(fanout);
228
212
    for (int i = 0; i < fanout; ++i) {
229
180
        auto relative_path = fmt::format("{}/{}_sub{}-{}-{}-{}", print_id(state->query_id()),
230
180
                                         label_prefix, i, node_id, state->task_id(),
231
180
                                         ExecEnv::GetInstance()->spill_file_mgr()->next_id());
232
180
        RETURN_IF_ERROR(ExecEnv::GetInstance()->spill_file_mgr()->create_spill_file(
233
180
                relative_path, output_spill_files[i]));
234
180
    }
235
32
    return Status::OK();
236
32
}
237
238
Status SpillRepartitioner::_route_block(
239
        RuntimeState* state, Block& block,
240
16
        std::vector<std::unique_ptr<MutableBlock>>& output_buffers) {
241
    // Compute raw hash values for every row in the block.
242
16
    RETURN_IF_ERROR(_partitioner->do_partitioning(state, &block));
243
16
    const auto& hash_vals = _partitioner->get_channel_ids();
244
16
    const auto rows = block.rows();
245
246
    // Build per-partition row index lists
247
16
    std::vector<std::vector<uint32_t>> partition_row_indexes(_fanout);
248
56
    for (uint32_t i = 0; i < rows; ++i) {
249
40
        auto partition_idx = _map_hash_to_partition(hash_vals[i]);
250
40
        partition_row_indexes[partition_idx].emplace_back(i);
251
40
    }
252
253
    // Scatter rows into per-partition buffers
254
144
    for (int p = 0; p < _fanout; ++p) {
255
128
        if (partition_row_indexes[p].empty()) {
256
88
            continue;
257
88
        }
258
259
        // Lazily initialize the buffer
260
40
        if (!output_buffers[p]) {
261
36
            output_buffers[p] = MutableBlock::create_unique(block.clone_empty());
262
36
        }
263
264
40
        RETURN_IF_ERROR(output_buffers[p]->add_rows(
265
40
                &block, partition_row_indexes[p].data(),
266
40
                partition_row_indexes[p].data() + partition_row_indexes[p].size()));
267
268
        // Flush large buffers immediately to keep memory bounded
269
40
        if (output_buffers[p]->allocated_bytes() >= MAX_BATCH_BYTES) {
270
0
            RETURN_IF_ERROR(_flush_buffer(state, p, output_buffers[p]));
271
0
        }
272
40
    }
273
274
16
    return Status::OK();
275
16
}
276
277
Status SpillRepartitioner::_route_block_by_columns(
278
        RuntimeState* state, Block& block,
279
22
        std::vector<std::unique_ptr<MutableBlock>>& output_buffers) {
280
22
    const auto rows = block.rows();
281
22
    if (rows == 0) {
282
0
        return Status::OK();
283
0
    }
284
285
    // Compute CRC32 hash on key columns
286
22
    std::vector<uint32_t> hash_vals(rows, 0);
287
22
    auto* __restrict hashes = hash_vals.data();
288
44
    for (size_t j = 0; j < _key_column_indices.size(); ++j) {
289
22
        auto col_idx = _key_column_indices[j];
290
22
        DCHECK_LT(col_idx, block.columns());
291
22
        const auto& column = block.get_by_position(col_idx).column;
292
22
        column->update_crcs_with_value(hashes, _key_data_types[j]->get_primitive_type(),
293
22
                                       static_cast<uint32_t>(rows));
294
22
    }
295
296
    // Map hash values to output channels with level-aware mixing.
297
1.28k
    for (size_t i = 0; i < rows; ++i) {
298
1.26k
        hashes[i] = _map_hash_to_partition(hashes[i]);
299
1.26k
    }
300
301
    // Build per-partition row index lists
302
22
    std::vector<std::vector<uint32_t>> partition_row_indexes(_fanout);
303
1.28k
    for (uint32_t i = 0; i < rows; ++i) {
304
1.26k
        partition_row_indexes[hashes[i]].emplace_back(i);
305
1.26k
    }
306
307
    // Scatter rows into per-partition buffers
308
110
    for (int p = 0; p < _fanout; ++p) {
309
88
        if (partition_row_indexes[p].empty()) {
310
0
            continue;
311
0
        }
312
313
88
        if (!output_buffers[p]) {
314
88
            output_buffers[p] = MutableBlock::create_unique(block.clone_empty());
315
88
        }
316
317
88
        RETURN_IF_ERROR(output_buffers[p]->add_rows(
318
88
                &block, partition_row_indexes[p].data(),
319
88
                partition_row_indexes[p].data() + partition_row_indexes[p].size()));
320
321
88
        if (output_buffers[p]->allocated_bytes() >= MAX_BATCH_BYTES) {
322
0
            RETURN_IF_ERROR(_flush_buffer(state, p, output_buffers[p]));
323
0
        }
324
88
    }
325
326
22
    return Status::OK();
327
22
}
328
329
Status SpillRepartitioner::_flush_buffer(RuntimeState* state, int partition_idx,
330
124
                                         std::unique_ptr<MutableBlock>& buffer) {
331
124
    if (!buffer || buffer->rows() == 0) {
332
0
        return Status::OK();
333
0
    }
334
124
    DCHECK(partition_idx < _fanout && _output_writers[partition_idx]);
335
124
    if (UNLIKELY(partition_idx >= _fanout || !_output_writers[partition_idx])) {
336
0
        return Status::InternalError(
337
0
                "SpillRepartitioner output writer is not initialized for partition {}",
338
0
                partition_idx);
339
0
    }
340
124
    auto out_block = buffer->to_block();
341
124
    buffer.reset();
342
124
    return _output_writers[partition_idx]->write_block(state, out_block);
343
124
}
344
345
Status SpillRepartitioner::_flush_all_buffers(
346
        RuntimeState* state, std::vector<std::unique_ptr<MutableBlock>>& output_buffers,
347
34
        bool force) {
348
218
    for (int i = 0; i < _fanout; ++i) {
349
184
        if (!output_buffers[i] || output_buffers[i]->rows() == 0) {
350
60
            continue;
351
60
        }
352
124
        if (force || output_buffers[i]->allocated_bytes() >= MAX_BATCH_BYTES) {
353
124
            RETURN_IF_ERROR(_flush_buffer(state, i, output_buffers[i]));
354
124
        }
355
124
    }
356
34
    return Status::OK();
357
34
}
358
359
1.30k
uint32_t SpillRepartitioner::_map_hash_to_partition(uint32_t hash) const {
360
1.30k
    DCHECK_GT(_fanout, 0);
361
    // Use a level-dependent salt so each repartition level has a different
362
    // projection from hash-space to partition-space.
363
1.30k
    constexpr uint32_t LEVEL_SALT_BASE = 0x9E3779B9U;
364
1.30k
    auto salt = static_cast<uint32_t>(_repartition_level + 1) * LEVEL_SALT_BASE;
365
1.30k
    auto mixed = crc32c_shuffle_mix(hash ^ salt);
366
1.30k
    return ((mixed >> 16) | (mixed << 16)) % static_cast<uint32_t>(_fanout);
367
1.30k
}
368
369
} // namespace doris