Coverage Report

Created: 2026-07-18 01:24

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
be/src/io/fs/buffered_reader.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 "io/fs/buffered_reader.h"
19
20
#include <bvar/reducer.h>
21
#include <bvar/window.h>
22
#include <string.h>
23
24
#include <algorithm>
25
#include <chrono>
26
#include <cstdint>
27
#include <memory>
28
29
#include "common/cast_set.h"
30
#include "common/compiler_util.h" // IWYU pragma: keep
31
#include "common/config.h"
32
#include "common/status.h"
33
#include "core/custom_allocator.h"
34
#include "runtime/exec_env.h"
35
#include "runtime/file_scan_profile.h"
36
#include "runtime/runtime_profile.h"
37
#include "runtime/thread_context.h"
38
#include "runtime/workload_management/io_throttle.h"
39
#include "util/slice.h"
40
#include "util/threadpool.h"
41
namespace doris {
42
43
namespace io {
44
struct IOContext;
45
46
// add bvar to capture the download bytes per second by buffered reader
47
bvar::Adder<uint64_t> g_bytes_downloaded("buffered_reader", "bytes_downloaded");
48
bvar::PerSecond<bvar::Adder<uint64_t>> g_bytes_downloaded_per_second("buffered_reader",
49
                                                                     "bytes_downloaded_per_second",
50
                                                                     &g_bytes_downloaded, 60);
51
52
Status MergeRangeFileReader::read_at_impl(size_t offset, Slice result, size_t* bytes_read,
53
477
                                          const IOContext* io_ctx) {
54
477
    _statistics.request_io++;
55
477
    *bytes_read = 0;
56
477
    if (result.size == 0) {
57
0
        return Status::OK();
58
0
    }
59
477
    const int range_index = _search_read_range(offset, offset + result.size);
60
477
    if (range_index < 0) {
61
2
        SCOPED_RAW_TIMER(&_statistics.read_time);
62
2
        Status st = _reader->read_at(offset, result, bytes_read, io_ctx);
63
2
        _statistics.merged_io++;
64
2
        _statistics.request_bytes += *bytes_read;
65
2
        _statistics.merged_bytes += *bytes_read;
66
2
        return st;
67
2
    }
68
475
    if (offset + result.size > _random_access_ranges[range_index].end_offset) {
69
        // return _reader->read_at(offset, result, bytes_read, io_ctx);
70
0
        return Status::IOError("Range in RandomAccessReader should be read sequentially");
71
0
    }
72
73
475
    size_t has_read = 0;
74
475
    RangeCachedData& cached_data = _range_cached_data[range_index];
75
475
    cached_data.has_read = true;
76
475
    if (cached_data.contains(offset)) {
77
        // has cached data in box
78
276
        _read_in_box(cached_data, offset, result, &has_read);
79
276
        _statistics.request_bytes += has_read;
80
276
        if (has_read == result.size) {
81
            // all data is read in cache
82
276
            *bytes_read = has_read;
83
276
            return Status::OK();
84
276
        }
85
276
    } else if (!cached_data.empty()) {
86
        // the data in range may be skipped or ignored
87
0
        for (int16_t box_index : cached_data.ref_box) {
88
0
            _dec_box_ref(box_index);
89
0
        }
90
0
        cached_data.reset();
91
0
    }
92
93
199
    size_t to_read = result.size - has_read;
94
199
    if (to_read >= SMALL_IO || to_read >= _remaining) {
95
0
        SCOPED_RAW_TIMER(&_statistics.read_time);
96
0
        size_t read_size = 0;
97
0
        RETURN_IF_ERROR(_reader->read_at(offset + has_read, Slice(result.data + has_read, to_read),
98
0
                                         &read_size, io_ctx));
99
0
        *bytes_read = has_read + read_size;
100
0
        _statistics.merged_io++;
101
0
        _statistics.request_bytes += read_size;
102
0
        _statistics.merged_bytes += read_size;
103
0
        return Status::OK();
104
0
    }
105
106
    // merge small IO
107
199
    size_t merge_start = offset + has_read;
108
199
    const size_t merge_end = merge_start + _merged_read_slice_size;
109
    // <slice_size, is_content>
110
199
    std::vector<std::pair<size_t, bool>> merged_slice;
111
199
    size_t content_size = 0;
112
199
    size_t hollow_size = 0;
113
199
    if (merge_start > _random_access_ranges[range_index].end_offset) {
114
0
        return Status::IOError("Fail to merge small IO");
115
0
    }
116
199
    int merge_index = range_index;
117
519
    while (merge_start < merge_end && merge_index < _random_access_ranges.size()) {
118
340
        size_t content_max = _remaining - content_size;
119
340
        if (content_max == 0) {
120
0
            break;
121
0
        }
122
340
        if (merge_index != range_index && _range_cached_data[merge_index].has_read) {
123
            // don't read or merge twice
124
0
            break;
125
0
        }
126
340
        if (_random_access_ranges[merge_index].end_offset > merge_end) {
127
0
            size_t add_content = std::min(merge_end - merge_start, content_max);
128
0
            content_size += add_content;
129
0
            merge_start += add_content;
130
0
            merged_slice.emplace_back(add_content, true);
131
0
            break;
132
0
        }
133
340
        size_t add_content =
134
340
                std::min(_random_access_ranges[merge_index].end_offset - merge_start, content_max);
135
340
        content_size += add_content;
136
340
        merge_start += add_content;
137
340
        merged_slice.emplace_back(add_content, true);
138
340
        if (merge_start != _random_access_ranges[merge_index].end_offset) {
139
0
            break;
140
0
        }
141
340
        if (merge_index < _random_access_ranges.size() - 1 && merge_start < merge_end) {
142
161
            size_t gap = _random_access_ranges[merge_index + 1].start_offset -
143
161
                         _random_access_ranges[merge_index].end_offset;
144
161
            if ((content_size + hollow_size) > SMALL_IO && gap >= SMALL_IO) {
145
                // too large gap
146
0
                break;
147
0
            }
148
161
            if (gap < merge_end - merge_start && content_size < _remaining &&
149
161
                !_range_cached_data[merge_index + 1].has_read) {
150
141
                hollow_size += gap;
151
141
                merge_start = _random_access_ranges[merge_index + 1].start_offset;
152
141
                merged_slice.emplace_back(gap, false);
153
141
            } else {
154
                // there's no enough memory to read hollow data
155
20
                break;
156
20
            }
157
161
        }
158
320
        merge_index++;
159
320
    }
160
199
    content_size = 0;
161
199
    hollow_size = 0;
162
199
    std::vector<std::pair<double, size_t>> ratio_and_size;
163
    // Calculate the read amplified ratio for each merge operation and the size of the merged data.
164
    // Find the largest size of the merged data whose amplified ratio is less than config::max_amplified_read_ratio
165
481
    for (const std::pair<size_t, bool>& slice : merged_slice) {
166
481
        if (slice.second) {
167
340
            content_size += slice.first;
168
340
            if (slice.first > 0) {
169
340
                ratio_and_size.emplace_back((double)hollow_size / (double)content_size,
170
340
                                            content_size + hollow_size);
171
340
            }
172
340
        } else {
173
141
            hollow_size += slice.first;
174
141
        }
175
481
    }
176
199
    size_t best_merged_size = 0;
177
539
    for (int i = 0; i < ratio_and_size.size(); ++i) {
178
340
        const std::pair<double, size_t>& rs = ratio_and_size[i];
179
340
        size_t equivalent_size = rs.second / (i + 1);
180
340
        if (rs.second > best_merged_size) {
181
340
            if (rs.first <= _max_amplified_ratio ||
182
340
                (_max_amplified_ratio < 1 && equivalent_size <= _equivalent_io_size)) {
183
340
                best_merged_size = rs.second;
184
340
            }
185
340
        }
186
340
    }
187
188
199
    if (best_merged_size == to_read) {
189
        // read directly to avoid copy operation
190
79
        SCOPED_RAW_TIMER(&_statistics.read_time);
191
79
        size_t read_size = 0;
192
79
        RETURN_IF_ERROR(_reader->read_at(offset + has_read, Slice(result.data + has_read, to_read),
193
79
                                         &read_size, io_ctx));
194
79
        *bytes_read = has_read + read_size;
195
79
        _statistics.merged_io++;
196
79
        _statistics.request_bytes += read_size;
197
79
        _statistics.merged_bytes += read_size;
198
79
        return Status::OK();
199
79
    }
200
201
120
    merge_start = offset + has_read;
202
120
    size_t merge_read_size = 0;
203
120
    RETURN_IF_ERROR(
204
120
            _fill_box(range_index, merge_start, best_merged_size, &merge_read_size, io_ctx));
205
120
    if (cached_data.start_offset != merge_start) {
206
0
        return Status::IOError("Wrong start offset in merged IO");
207
0
    }
208
209
    // read from cached data
210
120
    size_t box_read_size = 0;
211
120
    _read_in_box(cached_data, merge_start, Slice(result.data + has_read, to_read), &box_read_size);
212
120
    *bytes_read = has_read + box_read_size;
213
120
    _statistics.request_bytes += box_read_size;
214
120
    if (*bytes_read < result.size && box_read_size < merge_read_size) {
215
0
        return Status::IOError("Can't read enough bytes in merged IO");
216
0
    }
217
120
    return Status::OK();
218
120
}
219
220
477
int MergeRangeFileReader::_search_read_range(size_t start_offset, size_t end_offset) {
221
477
    if (_random_access_ranges.empty()) {
222
0
        return -1;
223
0
    }
224
477
    int left = 0, right = cast_set<int>(_random_access_ranges.size()) - 1;
225
958
    do {
226
958
        int mid = left + (right - left) / 2;
227
958
        const PrefetchRange& range = _random_access_ranges[mid];
228
958
        if (range.start_offset <= start_offset && start_offset < range.end_offset) {
229
475
            if (range.start_offset <= end_offset && end_offset <= range.end_offset) {
230
475
                return mid;
231
475
            } else {
232
0
                return -1;
233
0
            }
234
483
        } else if (range.start_offset > start_offset) {
235
182
            right = mid - 1;
236
301
        } else {
237
301
            left = mid + 1;
238
301
        }
239
958
    } while (left <= right);
240
2
    return -1;
241
477
}
242
243
249
void MergeRangeFileReader::_clean_cached_data(RangeCachedData& cached_data) {
244
249
    if (!cached_data.empty()) {
245
0
        for (int i = 0; i < cached_data.ref_box.size(); ++i) {
246
0
            DCHECK_GT(cached_data.box_end_offset[i], cached_data.box_start_offset[i]);
247
0
            int16_t box_index = cached_data.ref_box[i];
248
0
            DCHECK_GT(_box_ref[box_index], 0);
249
0
            _box_ref[box_index]--;
250
0
        }
251
0
    }
252
249
    cached_data.reset();
253
249
}
254
255
313
void MergeRangeFileReader::_dec_box_ref(int16_t box_index) {
256
313
    if (--_box_ref[box_index] == 0) {
257
188
        _remaining += BOX_SIZE;
258
188
    }
259
313
    if (box_index == _last_box_ref) {
260
106
        _last_box_ref = -1;
261
106
        _last_box_usage = 0;
262
106
    }
263
313
}
264
265
void MergeRangeFileReader::_read_in_box(RangeCachedData& cached_data, size_t offset, Slice result,
266
396
                                        size_t* bytes_read) {
267
396
    SCOPED_RAW_TIMER(&_statistics.copy_time);
268
399
    auto handle_in_box = [&](size_t remaining, char* copy_out) {
269
399
        size_t to_handle = remaining;
270
399
        int cleaned_box = 0;
271
858
        for (int i = 0; i < cached_data.ref_box.size() && remaining > 0; ++i) {
272
459
            int16_t box_index = cached_data.ref_box[i];
273
459
            size_t box_to_handle = std::min(remaining, (size_t)(cached_data.box_end_offset[i] -
274
459
                                                                cached_data.box_start_offset[i]));
275
459
            if (copy_out != nullptr) {
276
456
            }
277
459
            if (copy_out != nullptr) {
278
456
                memcpy(copy_out + to_handle - remaining,
279
456
                       _boxes[box_index].data() + cached_data.box_start_offset[i], box_to_handle);
280
456
            }
281
459
            remaining -= box_to_handle;
282
459
            cached_data.box_start_offset[i] += box_to_handle;
283
459
            if (cached_data.box_start_offset[i] == cached_data.box_end_offset[i]) {
284
313
                cleaned_box++;
285
313
                _dec_box_ref(box_index);
286
313
            }
287
459
        }
288
399
        DCHECK_EQ(remaining, 0);
289
399
        if (cleaned_box > 0) {
290
283
            cached_data.ref_box.erase(cached_data.ref_box.begin(),
291
283
                                      cached_data.ref_box.begin() + cleaned_box);
292
283
            cached_data.box_start_offset.erase(cached_data.box_start_offset.begin(),
293
283
                                               cached_data.box_start_offset.begin() + cleaned_box);
294
283
            cached_data.box_end_offset.erase(cached_data.box_end_offset.begin(),
295
283
                                             cached_data.box_end_offset.begin() + cleaned_box);
296
283
        }
297
399
        cached_data.start_offset += to_handle;
298
399
        if (cached_data.start_offset == cached_data.end_offset) {
299
249
            _clean_cached_data(cached_data);
300
249
        }
301
399
    };
302
303
396
    if (offset > cached_data.start_offset) {
304
        // the data in range may be skipped
305
3
        size_t to_skip = offset - cached_data.start_offset;
306
3
        handle_in_box(to_skip, nullptr);
307
3
    }
308
309
396
    size_t to_read = std::min(cached_data.end_offset - cached_data.start_offset, result.size);
310
396
    handle_in_box(to_read, result.data);
311
396
    *bytes_read = to_read;
312
396
}
313
314
Status MergeRangeFileReader::_fill_box(int range_index, size_t start_offset, size_t to_read,
315
120
                                       size_t* bytes_read, const IOContext* io_ctx) {
316
120
    if (!_read_slice) {
317
105
        _read_slice = std::make_unique<OwnedSlice>(_merged_read_slice_size);
318
105
    }
319
320
120
    *bytes_read = 0;
321
120
    {
322
120
        SCOPED_RAW_TIMER(&_statistics.read_time);
323
120
        RETURN_IF_ERROR(_reader->read_at(start_offset, Slice(_read_slice->data(), to_read),
324
120
                                         bytes_read, io_ctx));
325
120
        _statistics.merged_io++;
326
120
        _statistics.merged_bytes += *bytes_read;
327
120
    }
328
329
120
    SCOPED_RAW_TIMER(&_statistics.copy_time);
330
120
    size_t copy_start = start_offset;
331
120
    const size_t copy_end = start_offset + *bytes_read;
332
    // copy data into small boxes
333
    // tuple(box_index, box_start_offset, file_start_offset, file_end_offset)
334
120
    std::vector<std::tuple<int16_t, uint32_t, size_t, size_t>> filled_boxes;
335
336
325
    auto fill_box = [&](int16_t fill_box_ref, uint32_t box_usage, size_t box_copy_end) {
337
325
        size_t copy_size = std::min(box_copy_end - copy_start, BOX_SIZE - box_usage);
338
325
        memcpy(_boxes[fill_box_ref].data() + box_usage,
339
325
               _read_slice->data() + copy_start - start_offset, copy_size);
340
325
        filled_boxes.emplace_back(fill_box_ref, box_usage, copy_start, copy_start + copy_size);
341
325
        copy_start += copy_size;
342
325
        _last_box_ref = fill_box_ref;
343
325
        _last_box_usage = box_usage + cast_set<int>(copy_size);
344
325
        _box_ref[fill_box_ref]++;
345
325
        if (box_usage == 0) {
346
200
            _remaining -= BOX_SIZE;
347
200
        }
348
325
    };
349
350
120
    for (int fill_range_index = range_index;
351
381
         fill_range_index < _random_access_ranges.size() && copy_start < copy_end;
352
261
         ++fill_range_index) {
353
261
        RangeCachedData& fill_range_cache = _range_cached_data[fill_range_index];
354
261
        DCHECK(fill_range_cache.empty());
355
261
        fill_range_cache.reset();
356
261
        const PrefetchRange& fill_range = _random_access_ranges[fill_range_index];
357
261
        if (fill_range.start_offset > copy_start) {
358
            // don't copy hollow data
359
119
            size_t hollow_size = fill_range.start_offset - copy_start;
360
119
            DCHECK_GT(copy_end - copy_start, hollow_size);
361
119
            copy_start += hollow_size;
362
119
        }
363
364
261
        const size_t range_copy_end = std::min(copy_end, fill_range.end_offset);
365
        // reuse the remaining capacity of last box
366
261
        if (_last_box_ref >= 0 && _last_box_usage < BOX_SIZE) {
367
125
            fill_box(_last_box_ref, _last_box_usage, range_copy_end);
368
125
        }
369
        // reuse the former released box
370
1.57k
        for (int16_t i = 0; i < _boxes.size() && copy_start < range_copy_end; ++i) {
371
1.31k
            if (_box_ref[i] == 0) {
372
6
                fill_box(i, 0, range_copy_end);
373
6
            }
374
1.31k
        }
375
        // apply for new box to copy data
376
455
        while (copy_start < range_copy_end && _boxes.size() < NUM_BOX) {
377
194
            _boxes.emplace_back(BOX_SIZE);
378
194
            _box_ref.emplace_back(0);
379
194
            fill_box(cast_set<int16_t>(_boxes.size()) - 1, 0, range_copy_end);
380
194
        }
381
261
        DCHECK_EQ(copy_start, range_copy_end);
382
383
261
        if (!filled_boxes.empty()) {
384
261
            fill_range_cache.start_offset = std::get<2>(filled_boxes[0]);
385
261
            fill_range_cache.end_offset = std::get<3>(filled_boxes.back());
386
325
            for (auto& tuple : filled_boxes) {
387
325
                fill_range_cache.ref_box.emplace_back(std::get<0>(tuple));
388
325
                fill_range_cache.box_start_offset.emplace_back(std::get<1>(tuple));
389
325
                fill_range_cache.box_end_offset.emplace_back(
390
325
                        std::get<1>(tuple) + std::get<3>(tuple) - std::get<2>(tuple));
391
325
            }
392
261
            filled_boxes.clear();
393
261
        }
394
261
    }
395
120
    return Status::OK();
396
120
}
397
398
// there exists occasions where the buffer is already closed but
399
// some prior tasks are still queued in thread pool, so we have to check whether
400
// the buffer is closed each time the condition variable is notified.
401
23
void PrefetchBuffer::reset_offset(size_t offset) {
402
23
    {
403
23
        std::unique_lock lck {_lock};
404
23
        if (!_prefetched.wait_for(
405
23
                    lck, std::chrono::milliseconds(config::buffered_reader_read_timeout_ms),
406
23
                    [this]() { return _buffer_status != BufferStatus::PENDING; })) {
407
0
            _prefetch_status = Status::TimedOut("time out when reset prefetch buffer");
408
0
            return;
409
0
        }
410
23
        if (UNLIKELY(_buffer_status == BufferStatus::CLOSED)) {
411
0
            _prefetched.notify_all();
412
0
            return;
413
0
        }
414
23
        _buffer_status = BufferStatus::RESET;
415
23
        _offset = offset;
416
23
        _prefetched.notify_all();
417
23
    }
418
23
    if (UNLIKELY(offset >= _file_range.end_offset)) {
419
18
        _len = 0;
420
18
        _exceed = true;
421
18
        return;
422
18
    } else {
423
5
        _exceed = false;
424
5
    }
425
    // Lazy-allocate the backing buffer in the calling (query) thread, which has a
426
    // MemTrackerLimiter attached. The prefetch thread pool threads are "Orphan" threads
427
    // without a tracker, so allocation must not happen there.
428
5
    if (_buf.empty()) {
429
5
        _buf.resize(_size);
430
5
    }
431
5
    _prefetch_status = ExecEnv::GetInstance()->buffered_reader_prefetch_thread_pool()->submit_func(
432
5
            [buffer_ptr = shared_from_this()]() { buffer_ptr->prefetch_buffer(); });
433
5
}
434
435
// only this function would run concurrently in another thread
436
5
void PrefetchBuffer::prefetch_buffer() {
437
5
    {
438
5
        std::unique_lock lck {_lock};
439
5
        if (!_prefetched.wait_for(
440
5
                    lck, std::chrono::milliseconds(config::buffered_reader_read_timeout_ms),
441
5
                    [this]() {
442
5
                        return _buffer_status == BufferStatus::RESET ||
443
5
                               _buffer_status == BufferStatus::CLOSED;
444
5
                    })) {
445
0
            _prefetch_status = Status::TimedOut("time out when invoking prefetch buffer");
446
0
            return;
447
0
        }
448
        // in case buffer is already closed
449
5
        if (UNLIKELY(_buffer_status == BufferStatus::CLOSED)) {
450
0
            _prefetched.notify_all();
451
0
            return;
452
0
        }
453
5
        _buffer_status = BufferStatus::PENDING;
454
5
        _prefetched.notify_all();
455
5
    }
456
457
0
    int read_range_index = search_read_range(_offset);
458
5
    size_t buf_size;
459
5
    if (read_range_index == -1) {
460
5
        buf_size =
461
5
                _file_range.end_offset - _offset > _size ? _size : _file_range.end_offset - _offset;
462
5
    } else {
463
0
        buf_size = merge_small_ranges(_offset, read_range_index);
464
0
    }
465
466
5
    _len = 0;
467
5
    Status s;
468
469
5
    {
470
5
        SCOPED_RAW_TIMER(&_statis.read_time);
471
5
        s = _reader->read_at(_offset, Slice {_buf.data(), buf_size}, &_len, _io_ctx);
472
5
    }
473
5
    if (UNLIKELY(s.ok() && buf_size != _len)) {
474
        // This indicates that the data size returned by S3 object storage is smaller than what we requested,
475
        // which seems to be a violation of the S3 protocol since our request range was valid.
476
        // We currently consider this situation a bug and will treat this task as a failure.
477
0
        s = Status::InternalError("Data size returned by S3 is smaller than requested");
478
0
        LOG(WARNING) << "Data size returned by S3 is smaller than requested" << _reader->path()
479
0
                     << " request bytes " << buf_size << " returned size " << _len;
480
0
    }
481
5
    g_bytes_downloaded << _len;
482
5
    _statis.prefetch_request_io += 1;
483
5
    _statis.prefetch_request_bytes += _len;
484
5
    std::unique_lock lck {_lock};
485
5
    if (!_prefetched.wait_for(lck,
486
5
                              std::chrono::milliseconds(config::buffered_reader_read_timeout_ms),
487
5
                              [this]() { return _buffer_status == BufferStatus::PENDING; })) {
488
0
        _prefetch_status = Status::TimedOut("time out when invoking prefetch buffer");
489
0
        return;
490
0
    }
491
5
    if (!s.ok() && _offset < _reader->size()) {
492
        // We should print the error msg since this buffer might not be accessed by the consumer
493
        // which would result in the status being missed
494
1
        LOG_WARNING("prefetch path {} failed, offset {}, error {}", _reader->path().native(),
495
1
                    _offset, s.to_string());
496
1
        _prefetch_status = std::move(s);
497
1
    }
498
5
    _buffer_status = BufferStatus::PREFETCHED;
499
5
    _prefetched.notify_all();
500
    // eof would come up with len == 0, it would be handled by read_buffer
501
5
}
502
503
5
int PrefetchBuffer::search_read_range(size_t off) const {
504
5
    if (_random_access_ranges == nullptr || _random_access_ranges->empty()) {
505
5
        return -1;
506
5
    }
507
0
    const std::vector<PrefetchRange>& random_access_ranges = *_random_access_ranges;
508
0
    int left = 0, right = cast_set<int>(random_access_ranges.size()) - 1;
509
0
    do {
510
0
        int mid = left + (right - left) / 2;
511
0
        const PrefetchRange& range = random_access_ranges[mid];
512
0
        if (range.start_offset <= off && range.end_offset > off) {
513
0
            return mid;
514
0
        } else if (range.start_offset > off) {
515
0
            right = mid;
516
0
        } else {
517
0
            left = mid + 1;
518
0
        }
519
0
    } while (left < right);
520
0
    if (random_access_ranges[right].start_offset > off) {
521
0
        return right;
522
0
    } else {
523
0
        return -1;
524
0
    }
525
0
}
526
527
0
size_t PrefetchBuffer::merge_small_ranges(size_t off, int range_index) const {
528
0
    if (_random_access_ranges == nullptr || _random_access_ranges->empty()) {
529
0
        return _size;
530
0
    }
531
0
    int64_t remaining = _size;
532
0
    const std::vector<PrefetchRange>& random_access_ranges = *_random_access_ranges;
533
0
    while (remaining > 0 && range_index < random_access_ranges.size()) {
534
0
        const PrefetchRange& range = random_access_ranges[range_index];
535
0
        if (range.start_offset <= off && range.end_offset > off) {
536
0
            remaining -= range.end_offset - off;
537
0
            off = range.end_offset;
538
0
            range_index++;
539
0
        } else if (range.start_offset > off) {
540
            // merge small range
541
0
            size_t hollow = range.start_offset - off;
542
0
            if (hollow < remaining) {
543
0
                remaining -= hollow;
544
0
                off = range.start_offset;
545
0
            } else {
546
0
                break;
547
0
            }
548
0
        } else {
549
0
            DCHECK(false);
550
0
        }
551
0
    }
552
0
    if (remaining < 0 || remaining == _size) {
553
0
        remaining = 0;
554
0
    }
555
0
    return _size - remaining;
556
0
}
557
558
Status PrefetchBuffer::read_buffer(size_t off, const char* out, size_t buf_len,
559
12
                                   size_t* bytes_read) {
560
12
    if (UNLIKELY(off >= _file_range.end_offset)) {
561
        // Reader can read out of [start_offset, end_offset) by synchronous method.
562
0
        return _reader->read_at(off, Slice {out, buf_len}, bytes_read, _io_ctx);
563
0
    }
564
12
    if (_exceed) {
565
0
        reset_offset((off / _size) * _size);
566
0
        return read_buffer(off, out, buf_len, bytes_read);
567
0
    }
568
12
    {
569
12
        std::unique_lock lck {_lock};
570
        // buffer must be prefetched or it's closed
571
12
        if (!_prefetched.wait_for(
572
12
                    lck, std::chrono::milliseconds(config::buffered_reader_read_timeout_ms),
573
21
                    [this]() {
574
21
                        return _buffer_status == BufferStatus::PREFETCHED ||
575
21
                               _buffer_status == BufferStatus::CLOSED;
576
21
                    })) {
577
1
            _prefetch_status = Status::TimedOut("time out when read prefetch buffer");
578
1
            return _prefetch_status;
579
1
        }
580
11
        if (UNLIKELY(BufferStatus::CLOSED == _buffer_status)) {
581
0
            return Status::OK();
582
0
        }
583
11
    }
584
11
    RETURN_IF_ERROR(_prefetch_status);
585
    // there is only parquet would do not sequence read
586
    // it would read the end of the file first
587
11
    if (UNLIKELY(!contains(off))) {
588
0
        reset_offset((off / _size) * _size);
589
0
        return read_buffer(off, out, buf_len, bytes_read);
590
0
    }
591
11
    if (UNLIKELY(0 == _len || _offset + _len < off)) {
592
0
        return Status::OK();
593
0
    }
594
595
11
    {
596
11
        LIMIT_REMOTE_SCAN_IO(bytes_read);
597
        // [0]: maximum len trying to read, [1] maximum length buffer can provide, [2] actual len buffer has
598
11
        size_t read_len = std::min({buf_len, _offset + _size - off, _offset + _len - off});
599
11
        {
600
11
            SCOPED_RAW_TIMER(&_statis.copy_time);
601
11
            memcpy((void*)out, _buf.data() + (off - _offset), read_len);
602
11
        }
603
11
        *bytes_read = read_len;
604
11
        _statis.request_io += 1;
605
11
        _statis.request_bytes += read_len;
606
11
    }
607
11
    if (off + *bytes_read == _offset + _len) {
608
3
        reset_offset(_offset + _whole_buffer_size);
609
3
    }
610
11
    return Status::OK();
611
11
}
612
613
20
void PrefetchBuffer::close() {
614
20
    std::unique_lock lck {_lock};
615
    // in case _reader still tries to write to the buf after we close the buffer
616
20
    if (!_prefetched.wait_for(lck,
617
20
                              std::chrono::milliseconds(config::buffered_reader_read_timeout_ms),
618
21
                              [this]() { return _buffer_status != BufferStatus::PENDING; })) {
619
1
        _prefetch_status = Status::TimedOut("time out when close prefetch buffer");
620
1
        return;
621
1
    }
622
19
    _buffer_status = BufferStatus::CLOSED;
623
19
    _prefetched.notify_all();
624
    // Explicitly release the backing buffer here, in the calling (query) thread which has a
625
    // MemTrackerLimiter. The destructor may run in the thread pool's Orphan thread (when the
626
    // last shared_ptr ref is released after the prefetch lambda completes), so we must not
627
    // rely on ~PODArray() to release memory — that would trigger memory_orphan_check().
628
19
    PODArray<char>().swap(_buf);
629
19
}
630
631
0
void PrefetchBuffer::_collect_profile_before_close() {
632
0
    if (_sync_profile != nullptr) {
633
0
        _sync_profile(*this);
634
0
    }
635
0
}
636
637
// buffered reader
638
PrefetchBufferedReader::PrefetchBufferedReader(RuntimeProfile* profile, io::FileReaderSPtr reader,
639
                                               PrefetchRange file_range,
640
                                               std::shared_ptr<const IOContext> io_ctx,
641
                                               int64_t buffer_size)
642
5
        : _reader(std::move(reader)), _file_range(file_range), _io_ctx_holder(std::move(io_ctx)) {
643
5
    if (_io_ctx_holder == nullptr) {
644
5
        _io_ctx_holder = std::make_shared<IOContext>();
645
5
    }
646
5
    _io_ctx = _io_ctx_holder.get();
647
5
    if (buffer_size == -1L) {
648
5
        buffer_size = config::remote_storage_read_buffer_mb * 1024 * 1024;
649
5
    }
650
5
    _size = _reader->size();
651
5
    _whole_pre_buffer_size = buffer_size;
652
5
    _file_range.end_offset = std::min(_file_range.end_offset, _size);
653
5
    int buffer_num = buffer_size > s_max_pre_buffer_size
654
5
                             ? cast_set<int>(buffer_size) / cast_set<int>(s_max_pre_buffer_size)
655
5
                             : 1;
656
5
    std::function<void(PrefetchBuffer&)> sync_buffer = nullptr;
657
5
    if (profile != nullptr) {
658
0
        const char* prefetch_buffered_reader = "PrefetchBufferedReader";
659
0
        auto* total_time =
660
0
                ADD_CHILD_TIMER(profile, prefetch_buffered_reader,
661
0
                                file_scan_profile::parent_or_root(profile, file_scan_profile::IO));
662
0
        auto copy_time = ADD_CHILD_TIMER(profile, "CopyTime", prefetch_buffered_reader);
663
0
        auto read_time = ADD_CHILD_TIMER(profile, "ReadTime", prefetch_buffered_reader);
664
0
        auto prefetch_request_io =
665
0
                ADD_CHILD_COUNTER(profile, "PreRequestIO", TUnit::UNIT, prefetch_buffered_reader);
666
0
        auto prefetch_request_bytes = ADD_CHILD_COUNTER(profile, "PreRequestBytes", TUnit::BYTES,
667
0
                                                        prefetch_buffered_reader);
668
0
        auto request_io =
669
0
                ADD_CHILD_COUNTER(profile, "RequestIO", TUnit::UNIT, prefetch_buffered_reader);
670
0
        auto request_bytes =
671
0
                ADD_CHILD_COUNTER(profile, "RequestBytes", TUnit::BYTES, prefetch_buffered_reader);
672
0
        sync_buffer = [=](PrefetchBuffer& buf) {
673
0
            COUNTER_UPDATE(total_time, buf._statis.copy_time + buf._statis.read_time);
674
0
            COUNTER_UPDATE(copy_time, buf._statis.copy_time);
675
0
            COUNTER_UPDATE(read_time, buf._statis.read_time);
676
0
            COUNTER_UPDATE(prefetch_request_io, buf._statis.prefetch_request_io);
677
0
            COUNTER_UPDATE(prefetch_request_bytes, buf._statis.prefetch_request_bytes);
678
0
            COUNTER_UPDATE(request_io, buf._statis.request_io);
679
0
            COUNTER_UPDATE(request_bytes, buf._statis.request_bytes);
680
0
        };
681
0
    }
682
    // set the _cur_offset of this reader as same as the inner reader's,
683
    // to make sure the buffer reader will start to read at right position.
684
25
    for (int i = 0; i < buffer_num; i++) {
685
20
        _pre_buffers.emplace_back(std::make_shared<PrefetchBuffer>(
686
20
                _file_range, s_max_pre_buffer_size, _whole_pre_buffer_size, _reader, _io_ctx_holder,
687
20
                sync_buffer));
688
20
    }
689
5
}
690
691
5
PrefetchBufferedReader::~PrefetchBufferedReader() {
692
    /// Better not to call virtual functions in a destructor.
693
5
    static_cast<void>(_close_internal());
694
5
}
695
696
Status PrefetchBufferedReader::read_at_impl(size_t offset, Slice result, size_t* bytes_read,
697
17
                                            const IOContext* io_ctx) {
698
17
    if (!_initialized) {
699
5
        reset_all_buffer(offset);
700
5
        _initialized = true;
701
5
    }
702
17
    if (UNLIKELY(result.get_size() == 0 || offset >= size())) {
703
5
        *bytes_read = 0;
704
5
        return Status::OK();
705
5
    }
706
12
    size_t nbytes = result.get_size();
707
12
    int actual_bytes_read = 0;
708
23
    while (actual_bytes_read < nbytes && offset < size()) {
709
12
        size_t read_num = 0;
710
12
        auto buffer_pos = get_buffer_pos(offset);
711
12
        RETURN_IF_ERROR(
712
12
                _pre_buffers[buffer_pos]->read_buffer(offset, result.get_data() + actual_bytes_read,
713
12
                                                      nbytes - actual_bytes_read, &read_num));
714
11
        actual_bytes_read += read_num;
715
11
        offset += read_num;
716
11
    }
717
11
    *bytes_read = actual_bytes_read;
718
11
    return Status::OK();
719
12
}
720
721
0
Status PrefetchBufferedReader::close() {
722
0
    return _close_internal();
723
0
}
724
725
5
Status PrefetchBufferedReader::_close_internal() {
726
5
    if (!_closed) {
727
5
        _closed = true;
728
5
        std::for_each(_pre_buffers.begin(), _pre_buffers.end(),
729
20
                      [](std::shared_ptr<PrefetchBuffer>& buffer) { buffer->close(); });
730
5
        return _reader->close();
731
5
    }
732
733
0
    return Status::OK();
734
5
}
735
736
0
void PrefetchBufferedReader::_collect_profile_before_close() {
737
0
    std::for_each(_pre_buffers.begin(), _pre_buffers.end(),
738
0
                  [](std::shared_ptr<PrefetchBuffer>& buffer) {
739
0
                      buffer->collect_profile_before_close();
740
0
                  });
741
0
    if (_reader != nullptr) {
742
0
        _reader->collect_profile_before_close();
743
0
    }
744
0
}
745
746
// InMemoryFileReader
747
2
InMemoryFileReader::InMemoryFileReader(io::FileReaderSPtr reader) : _reader(std::move(reader)) {
748
2
    _size = _reader->size();
749
2
}
750
751
2
InMemoryFileReader::~InMemoryFileReader() {
752
2
    static_cast<void>(_close_internal());
753
2
}
754
755
4
Status InMemoryFileReader::close() {
756
4
    return _close_internal();
757
4
}
758
759
6
Status InMemoryFileReader::_close_internal() {
760
6
    if (!_closed) {
761
2
        _closed = true;
762
2
        return _reader->close();
763
2
    }
764
4
    return Status::OK();
765
6
}
766
767
Status InMemoryFileReader::read_at_impl(size_t offset, Slice result, size_t* bytes_read,
768
3
                                        const IOContext* io_ctx) {
769
3
    if (_data == nullptr) {
770
2
        _data = std::make_unique_for_overwrite<char[]>(_size);
771
772
2
        size_t file_size = 0;
773
2
        RETURN_IF_ERROR(_reader->read_at(0, Slice(_data.get(), _size), &file_size, io_ctx));
774
2
        DCHECK_EQ(file_size, _size);
775
2
    }
776
3
    if (UNLIKELY(offset > _size)) {
777
0
        return Status::IOError("Out of bounds access");
778
0
    }
779
3
    *bytes_read = std::min(result.size, _size - offset);
780
3
    memcpy(result.data, _data.get() + offset, *bytes_read);
781
3
    return Status::OK();
782
3
}
783
784
0
void InMemoryFileReader::_collect_profile_before_close() {
785
0
    if (_reader != nullptr) {
786
0
        _reader->collect_profile_before_close();
787
0
    }
788
0
}
789
790
// BufferedFileStreamReader
791
BufferedFileStreamReader::BufferedFileStreamReader(io::FileReaderSPtr file, uint64_t offset,
792
                                                   uint64_t length, size_t max_buf_size)
793
423
        : _file(file),
794
423
          _file_start_offset(offset),
795
423
          _file_end_offset(offset + length),
796
423
          _max_buf_size(max_buf_size) {}
797
798
Status BufferedFileStreamReader::read_bytes(const uint8_t** buf, uint64_t offset,
799
1.22k
                                            const size_t bytes_to_read, const IOContext* io_ctx) {
800
1.22k
    if (offset < _file_start_offset || offset >= _file_end_offset ||
801
1.22k
        offset + bytes_to_read > _file_end_offset) {
802
0
        return Status::IOError(
803
0
                "Out-of-bounds Access: offset={}, bytes_to_read={}, file_start={}, "
804
0
                "file_end={}",
805
0
                offset, bytes_to_read, _file_start_offset, _file_end_offset);
806
0
    }
807
1.22k
    int64_t end_offset = offset + bytes_to_read;
808
1.22k
    if (_buf_start_offset <= offset && _buf_end_offset >= end_offset) {
809
811
        *buf = _buf.get() + offset - _buf_start_offset;
810
811
        return Status::OK();
811
811
    }
812
416
    size_t buf_size = std::max(_max_buf_size, bytes_to_read);
813
416
    if (_buf_size < buf_size) {
814
367
        auto new_buf = make_unique_buffer<uint8_t>(buf_size);
815
367
        if (offset >= _buf_start_offset && offset < _buf_end_offset) {
816
34
            memcpy(new_buf.get(), _buf.get() + offset - _buf_start_offset,
817
34
                   _buf_end_offset - offset);
818
34
        }
819
367
        _buf = std::move(new_buf);
820
367
        _buf_size = buf_size;
821
367
    } else if (offset > _buf_start_offset && offset < _buf_end_offset) {
822
35
        memmove(_buf.get(), _buf.get() + offset - _buf_start_offset, _buf_end_offset - offset);
823
35
    }
824
416
    if (offset < _buf_start_offset || offset >= _buf_end_offset) {
825
347
        _buf_end_offset = offset;
826
347
    }
827
416
    _buf_start_offset = offset;
828
416
    int64_t buf_remaining = _buf_end_offset - _buf_start_offset;
829
416
    int64_t to_read = std::min(_buf_size - buf_remaining, _file_end_offset - _buf_end_offset);
830
416
    int64_t has_read = 0;
831
832
    while (has_read < to_read) {
832
416
        size_t loop_read = 0;
833
416
        Slice result(_buf.get() + buf_remaining + has_read, to_read - has_read);
834
416
        RETURN_IF_ERROR(_file->read_at(_buf_end_offset + has_read, result, &loop_read, io_ctx));
835
416
        if (loop_read == 0) {
836
0
            break;
837
0
        }
838
416
        has_read += loop_read;
839
416
    }
840
416
    if (has_read != to_read) {
841
0
        return Status::Corruption("Try to read {} bytes, but received {} bytes", to_read, has_read);
842
0
    }
843
416
    _buf_end_offset += to_read;
844
416
    *buf = _buf.get();
845
416
    return Status::OK();
846
416
}
847
848
Status BufferedFileStreamReader::read_bytes(Slice& slice, uint64_t offset,
849
589
                                            const IOContext* io_ctx) {
850
589
    return read_bytes((const uint8_t**)&slice.data, offset, slice.size, io_ctx);
851
589
}
852
853
Result<io::FileReaderSPtr> DelegateReader::create_file_reader(
854
        RuntimeProfile* profile, const FileSystemProperties& system_properties,
855
        const FileDescription& file_description, const io::FileReaderOptions& reader_options,
856
93
        AccessMode access_mode, const IOContext* io_ctx, const PrefetchRange file_range) {
857
93
    std::shared_ptr<const IOContext> io_ctx_holder;
858
93
    if (io_ctx != nullptr) {
859
        // Old API: best-effort safety by copying the IOContext onto the heap.
860
51
        io_ctx_holder = std::make_shared<IOContext>(*io_ctx);
861
51
    }
862
93
    return create_file_reader(profile, system_properties, file_description, reader_options,
863
93
                              access_mode, std::move(io_ctx_holder), file_range);
864
93
}
865
866
Result<io::FileReaderSPtr> DelegateReader::create_file_reader(
867
        RuntimeProfile* profile, const FileSystemProperties& system_properties,
868
        const FileDescription& file_description, const io::FileReaderOptions& reader_options,
869
        AccessMode access_mode, std::shared_ptr<const IOContext> io_ctx,
870
467
        const PrefetchRange file_range) {
871
467
    if (io_ctx == nullptr) {
872
324
        io_ctx = std::make_shared<IOContext>();
873
324
    }
874
467
    return FileFactory::create_file_reader(system_properties, file_description, reader_options,
875
467
                                           profile)
876
467
            .transform([&](auto&& reader) -> io::FileReaderSPtr {
877
462
                if (reader->size() < config::in_memory_file_size &&
878
462
                    typeid_cast<io::S3FileReader*>(reader.get())) {
879
0
                    return std::make_shared<InMemoryFileReader>(std::move(reader));
880
0
                }
881
882
462
                if (access_mode == AccessMode::SEQUENTIAL) {
883
0
                    bool is_thread_safe = false;
884
0
                    if (typeid_cast<io::S3FileReader*>(reader.get())) {
885
0
                        is_thread_safe = true;
886
0
                    } else if (auto* cached_reader =
887
0
                                       typeid_cast<io::CachedRemoteFileReader*>(reader.get());
888
0
                               cached_reader &&
889
0
                               typeid_cast<io::S3FileReader*>(cached_reader->get_remote_reader())) {
890
0
                        is_thread_safe = true;
891
0
                    }
892
0
                    if (is_thread_safe) {
893
                        // PrefetchBufferedReader needs thread-safe reader to prefetch data concurrently.
894
0
                        return std::make_shared<io::PrefetchBufferedReader>(
895
0
                                profile, std::move(reader), file_range, io_ctx);
896
0
                    }
897
0
                }
898
899
462
                return reader;
900
462
            });
901
467
}
902
903
Status LinearProbeRangeFinder::get_range_for(int64_t desired_offset,
904
6
                                             io::PrefetchRange& result_range) {
905
9
    while (index < _ranges.size()) {
906
9
        io::PrefetchRange& range = _ranges[index];
907
9
        if (range.end_offset > desired_offset) {
908
6
            if (range.start_offset > desired_offset) [[unlikely]] {
909
0
                return Status::InvalidArgument("Invalid desiredOffset");
910
0
            }
911
6
            result_range = range;
912
6
            return Status::OK();
913
6
        }
914
3
        ++index;
915
3
    }
916
0
    return Status::InvalidArgument("Invalid desiredOffset");
917
6
}
918
919
RangeCacheFileReader::RangeCacheFileReader(RuntimeProfile* profile, io::FileReaderSPtr inner_reader,
920
                                           std::shared_ptr<RangeFinder> range_finder)
921
35
        : _profile(profile),
922
35
          _inner_reader(std::move(inner_reader)),
923
35
          _range_finder(std::move(range_finder)) {
924
35
    _size = _inner_reader->size();
925
35
    uint64_t max_cache_size =
926
35
            std::max((uint64_t)4096, (uint64_t)_range_finder->get_max_range_size());
927
35
    _cache = OwnedSlice(max_cache_size);
928
929
35
    if (_profile != nullptr) {
930
19
        const char* random_profile = "RangeCacheFileReader";
931
19
        _total_time = ADD_CHILD_TIMER_WITH_LEVEL(
932
19
                _profile, random_profile,
933
19
                file_scan_profile::parent_or_root(_profile, file_scan_profile::IO), 1);
934
19
        _request_io =
935
19
                ADD_CHILD_COUNTER_WITH_LEVEL(_profile, "RequestIO", TUnit::UNIT, random_profile, 1);
936
19
        _request_bytes = ADD_CHILD_COUNTER_WITH_LEVEL(_profile, "RequestBytes", TUnit::BYTES,
937
19
                                                      random_profile, 1);
938
19
        _request_time = ADD_CHILD_TIMER_WITH_LEVEL(_profile, "RequestTime", random_profile, 1);
939
19
        _read_to_cache_time =
940
19
                ADD_CHILD_TIMER_WITH_LEVEL(_profile, "ReadToCacheTime", random_profile, 1);
941
19
        _cache_refresh_count = ADD_CHILD_COUNTER_WITH_LEVEL(_profile, "CacheRefreshCount",
942
19
                                                            TUnit::UNIT, random_profile, 1);
943
19
        _read_to_cache_bytes = ADD_CHILD_COUNTER_WITH_LEVEL(_profile, "ReadToCacheBytes",
944
19
                                                            TUnit::BYTES, random_profile, 1);
945
19
    }
946
35
}
947
948
Status RangeCacheFileReader::read_at_impl(size_t offset, Slice result, size_t* bytes_read,
949
6
                                          const IOContext* io_ctx) {
950
6
    auto request_size = result.size;
951
952
6
    _cache_statistics.request_io++;
953
6
    _cache_statistics.request_bytes += request_size;
954
6
    SCOPED_RAW_TIMER(&_cache_statistics.request_time);
955
956
6
    PrefetchRange range;
957
6
    if (_range_finder->get_range_for(offset, range)) [[likely]] {
958
6
        if (_current_start_offset != range.start_offset) { // need read new range to cache.
959
6
            auto range_size = range.end_offset - range.start_offset;
960
961
6
            _cache_statistics.cache_refresh_count++;
962
6
            _cache_statistics.read_to_cache_bytes += range_size;
963
6
            SCOPED_RAW_TIMER(&_cache_statistics.read_to_cache_time);
964
965
6
            Slice cache_slice = {_cache.data(), range_size};
966
6
            RETURN_IF_ERROR(
967
6
                    _inner_reader->read_at(range.start_offset, cache_slice, bytes_read, io_ctx));
968
969
6
            if (*bytes_read != range_size) [[unlikely]] {
970
0
                return Status::InternalError(
971
0
                        "RangeCacheFileReader use inner reader read bytes {} not eq expect size {}",
972
0
                        *bytes_read, range_size);
973
0
            }
974
975
6
            _current_start_offset = range.start_offset;
976
6
        }
977
978
6
        int64_t buffer_offset = offset - _current_start_offset;
979
6
        memcpy(result.data, _cache.data() + buffer_offset, request_size);
980
6
        *bytes_read = request_size;
981
982
6
        return Status::OK();
983
6
    } else {
984
0
        return Status::InternalError("RangeCacheFileReader read  not in Ranges. Offset = {}",
985
0
                                     offset);
986
        //                RETURN_IF_ERROR(_inner_reader->read_at(offset, result , bytes_read, io_ctx));
987
        //                return Status::OK();
988
        // think return error is ok,otherwise it will cover up the error.
989
0
    }
990
6
}
991
992
32
void RangeCacheFileReader::_collect_profile_before_close() {
993
32
    if (_profile != nullptr) {
994
19
        COUNTER_UPDATE(_total_time, _cache_statistics.request_time);
995
19
        COUNTER_UPDATE(_request_io, _cache_statistics.request_io);
996
19
        COUNTER_UPDATE(_request_bytes, _cache_statistics.request_bytes);
997
19
        COUNTER_UPDATE(_request_time, _cache_statistics.request_time);
998
19
        COUNTER_UPDATE(_read_to_cache_time, _cache_statistics.read_to_cache_time);
999
19
        COUNTER_UPDATE(_cache_refresh_count, _cache_statistics.cache_refresh_count);
1000
19
        COUNTER_UPDATE(_read_to_cache_bytes, _cache_statistics.read_to_cache_bytes);
1001
19
        if (_inner_reader != nullptr) {
1002
19
            _inner_reader->collect_profile_before_close();
1003
19
        }
1004
19
    }
1005
32
}
1006
1007
} // namespace io
1008
1009
} // namespace doris