Coverage Report

Created: 2026-10-15 15:22

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
be/src/io/fs/file_range_read_scheduler.h
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
#pragma once
19
20
#include <atomic>
21
#include <condition_variable>
22
#include <cstddef>
23
#include <cstdint>
24
#include <memory>
25
#include <mutex>
26
#include <unordered_set>
27
#include <vector>
28
29
#include "common/status.h"
30
#include "io/fs/file_range_coalescer.h"
31
#include "io/fs/file_reader.h"
32
#include "io/io_common.h"
33
#include "util/slice.h"
34
35
namespace doris {
36
37
class MemTrackerLimiter;
38
class ThreadPool;
39
40
namespace io {
41
42
struct ReadAheadStatistics;
43
44
enum class FileRangeReadRejectReason : uint8_t {
45
    /// At least one executor task was accepted. Other handles may fail to submit or read.
46
    NONE,
47
    /// A reader, context, range, or aggregate byte count failed request validation.
48
    INVALID_REQUEST,
49
    /// The query context had already been cancelled before admission.
50
    QUERY_CANCELLED,
51
    /// Reserving the complete batch would exceed its query-scoped byte budget.
52
    QUERY_BYTE_LIMIT,
53
    /// Reserving the complete batch would exceed the BE-wide byte budget.
54
    GLOBAL_BYTE_LIMIT,
55
    /// At least one range buffer could not be allocated; no read handles are returned.
56
    ALLOCATION_FAILED,
57
    /// The executor rejected every task in the batch.
58
    EXECUTOR_REJECTED,
59
    /// The scheduler stopped accepting work before the batch was submitted.
60
    SHUTTING_DOWN,
61
};
62
63
struct FileRangeReadSchedulerOptions {
64
    /// Maximum bytes retained by live range handles from one query.
65
    size_t max_bytes_per_query {1};
66
    /// Maximum bytes retained by all live range handles in this BE.
67
    size_t max_bytes_per_be {1};
68
69
    Status validate() const;
70
};
71
72
/// Byte reservation shared by range buffers. It limits retained memory, not executor concurrency.
73
class FileRangeReadBudget {
74
public:
75
    explicit FileRangeReadBudget(size_t max_bytes);
76
77
    size_t resident_bytes() const;
78
79
private:
80
    friend class FileRangeReadScheduler;
81
    friend class FileRangeReadReservation;
82
83
    bool _can_reserve_unlocked(size_t bytes) const;
84
    void _reserve_unlocked(size_t bytes);
85
    void _release(size_t bytes);
86
87
    mutable std::mutex _mutex;
88
    const size_t _max_bytes;
89
    size_t _resident_bytes {0};
90
};
91
92
class FileRangeRead;
93
94
/// Query-scoped cancellation and admission state shared by every segment read.
95
class FileRangeReadContext {
96
public:
97
    explicit FileRangeReadContext(size_t max_bytes);
98
99
    /// Cancel all registered reads. Running source IO finishes normally and publishes CANCELLED.
100
    void cancel();
101
139
    bool cancelled() const { return _cancelled.load(std::memory_order_acquire); }
102
10
    size_t resident_bytes() const { return _budget.resident_bytes(); }
103
104
private:
105
    friend class FileRangeReadScheduler;
106
    friend class FileRangeReadReservation;
107
108
    void _register_reads(const std::vector<std::shared_ptr<FileRangeRead>>& reads);
109
110
    FileRangeReadBudget _budget;
111
    std::atomic<bool> _cancelled {false};
112
    mutable std::mutex _reads_mutex;
113
    std::vector<std::weak_ptr<FileRangeRead>> _reads;
114
};
115
116
/// Move-only reservation that releases both query and BE byte budgets on destruction.
117
class FileRangeReadReservation {
118
public:
119
    FileRangeReadReservation(const FileRangeReadReservation&) = delete;
120
    FileRangeReadReservation& operator=(const FileRangeReadReservation&) = delete;
121
    FileRangeReadReservation(FileRangeReadReservation&& other) noexcept;
122
    FileRangeReadReservation& operator=(FileRangeReadReservation&& other) noexcept;
123
    ~FileRangeReadReservation();
124
125
private:
126
    friend class FileRangeReadScheduler;
127
128
    FileRangeReadReservation(std::shared_ptr<FileRangeReadContext> context,
129
                             std::shared_ptr<FileRangeReadBudget> global_budget,
130
                             size_t bytes) noexcept;
131
    void _reset();
132
133
    std::shared_ptr<FileRangeReadContext> _context;
134
    std::shared_ptr<FileRangeReadBudget> _global_budget;
135
    size_t _bytes {0};
136
};
137
138
struct FileRangeReadStats {
139
    size_t bytes_read {0};
140
    /// Full successful source reads, including a read whose buffer is subsequently cancelled.
141
    size_t successful_source_bytes {0};
142
    FileCacheStatistics file_cache;
143
    FileReaderStats file_reader;
144
    int64_t queue_wait_ns {0};
145
    int64_t read_ns {0};
146
};
147
148
/// One scheduled range and its owned buffer. The byte reservation follows this handle's lifetime.
149
class FileRangeRead {
150
public:
151
    enum class State : uint8_t {
152
        /// Admitted and allocated, but its executor task has not started.
153
        QUEUED,
154
        /// The executor task owns the handle and may be blocked in source IO.
155
        RUNNING,
156
        /// The entire requested range is available through data() or slice().
157
        READY,
158
        /// Submission, source IO, or exact-length validation failed.
159
        FAILED,
160
        /// Cancellation won before execution or was observed after source IO returned.
161
        CANCELLED,
162
    };
163
164
    ~FileRangeRead();
165
166
    /// Wait for a terminal state and return its status. Optionally report whether the buffer was
167
    /// already READY on entry, using the same lock as the wait.
168
    Status wait(bool* ready_on_entry = nullptr);
169
    /// Request cancellation without interrupting an already running source read.
170
    void request_cancel();
171
    State state() const;
172
211
    const FileRange& range() const { return _range; }
173
47
    uint64_t trace_id() const { return _trace_id; }
174
    /// Return the full buffer. The handle must be READY and owns the returned memory.
175
    Slice data() const;
176
    /// Return a checked subrange of a READY buffer.
177
    Slice slice(size_t buffer_offset, size_t size) const;
178
    FileRangeReadStats stats() const;
179
180
private:
181
    friend class FileRangeReadScheduler;
182
183
    FileRangeRead(FileRange range, std::shared_ptr<MemTrackerLimiter> tracker,
184
                  FileRangeReadReservation reservation,
185
                  std::shared_ptr<ReadAheadStatistics> statistics);
186
    static Status create(FileRange range, std::shared_ptr<MemTrackerLimiter> tracker,
187
                         FileRangeReadReservation reservation,
188
                         std::shared_ptr<FileRangeRead>* output,
189
                         std::shared_ptr<ReadAheadStatistics> statistics = nullptr);
190
191
    static bool _is_terminal(State state);
192
    bool _mark_running();
193
    bool _is_cancel_requested() const;
194
    void _publish_ready(FileRangeReadStats stats);
195
    void _publish_failed(Status status, FileRangeReadStats stats);
196
    void _publish_submit_failure(Status status);
197
    void _publish_cancelled(FileRangeReadStats stats = {});
198
    void _publish_from_running(State state, Status status, FileRangeReadStats stats);
199
    const FileRange _range;
200
    const uint64_t _trace_id;
201
    char* _data {nullptr};
202
    const std::shared_ptr<MemTrackerLimiter> _tracker;
203
    FileRangeReadReservation _reservation;
204
    const std::shared_ptr<ReadAheadStatistics> _statistics;
205
    mutable std::mutex _mutex;
206
    std::condition_variable _cv;
207
    State _state {State::QUEUED};
208
    Status _status;
209
    FileRangeReadStats _stats;
210
    bool _cancel_requested {false};
211
};
212
213
/// Owns every pointer embedded in IOContext that an asynchronous read keeps after submission.
214
struct FileRangeReadIOContext {
215
    IOContext io_context;
216
    std::shared_ptr<TUniqueId> query_id;
217
    std::shared_ptr<ReadAheadStatistics> statistics;
218
219
    /// Copy fields safe for asynchronous use, own query_id, and detach caller-owned statistics.
220
    static FileRangeReadIOContext from_caller(const IOContext& source);
221
};
222
223
struct FileRangeReadSubmitResult {
224
    std::vector<std::shared_ptr<FileRangeRead>> reads;
225
    FileRangeReadRejectReason reject_reason {FileRangeReadRejectReason::NONE};
226
    Status status;
227
228
    static FileRangeReadSubmitResult rejected(FileRangeReadRejectReason reason,
229
                                              Status status = Status::OK());
230
55
    bool accepted() const {
231
55
        return reject_reason == FileRangeReadRejectReason::NONE && status.ok();
232
55
    }
233
};
234
235
/// BE-global admission and execution scheduler for already-planned file ranges. It does not
236
/// understand pages, range coalescing, cache-block completion, or writeback policy.
237
class FileRangeReadScheduler {
238
public:
239
    ~FileRangeReadScheduler();
240
241
    FileRangeReadScheduler(const FileRangeReadScheduler&) = delete;
242
    FileRangeReadScheduler& operator=(const FileRangeReadScheduler&) = delete;
243
244
    /// `executor` must remain running and outlive the scheduler. Its thread count controls range
245
    /// read concurrency; the scheduler never shuts down the shared executor.
246
    static Status create(const FileRangeReadSchedulerOptions& options, ThreadPool* executor,
247
                         std::unique_ptr<FileRangeReadScheduler>* output);
248
249
    /// Create a query-scoped cancellation and byte-budget context.
250
    std::shared_ptr<FileRangeReadContext> create_context() const;
251
252
    /// Atomically admits and allocates the complete batch, then submits one executor task per
253
    /// range. A rejected batch returns no read handles and leaves both budgets unchanged. If the
254
    /// executor accepts only part of a batch, the returned handles expose each rejected range as a
255
    /// failed read.
256
    FileRangeReadSubmitResult try_submit(const std::vector<FileRange>& ranges,
257
                                         FileReaderSPtr reader, FileRangeReadIOContext io_context,
258
                                         const std::shared_ptr<FileRangeReadContext>& context);
259
260
    /// Stop admission, request cancellation, and wait for all accepted executor tasks. Idempotent.
261
    void shutdown();
262
    bool accepting() const;
263
6
    std::shared_ptr<FileRangeReadBudget> global_budget() const { return _global_budget; }
264
265
private:
266
    struct ReadTask {
267
        FileReaderSPtr reader;
268
        std::shared_ptr<FileRangeRead> read;
269
        std::shared_ptr<FileRangeReadContext> context;
270
        FileRangeReadIOContext io_context;
271
        int64_t submit_ns {0};
272
    };
273
274
    FileRangeReadScheduler(FileRangeReadSchedulerOptions options, ThreadPool* executor);
275
276
    /// Validate the complete request and compute its total buffer capacity.
277
    Status _validate_request(const std::vector<FileRange>& ranges, const FileReaderSPtr& reader,
278
                             const std::shared_ptr<FileRangeReadContext>& context,
279
                             size_t* total_bytes) const;
280
    /// Atomically reserve query and BE bytes for every range in the batch.
281
    FileRangeReadRejectReason _reserve_batch(const std::shared_ptr<FileRangeReadContext>& context,
282
                                             const std::vector<FileRange>& ranges,
283
                                             size_t total_bytes,
284
                                             std::vector<FileRangeReadReservation>* reservations);
285
    /// Submit one task per range while retaining accepted handles for shutdown coordination.
286
    Status _submit_tasks(std::vector<ReadTask> tasks, FileRangeReadRejectReason* reject_reason);
287
    void _task_finished(const std::shared_ptr<FileRangeRead>& read);
288
    /// Perform one exact source read and publish one terminal handle state.
289
    void _run_task(ReadTask task) const;
290
291
    const FileRangeReadSchedulerOptions _options;
292
    const std::shared_ptr<FileRangeReadBudget> _global_budget;
293
    const std::shared_ptr<MemTrackerLimiter> _mem_tracker;
294
    ThreadPool* const _executor;
295
296
    mutable std::mutex _mutex;
297
    std::condition_variable _tasks_cv;
298
    // The shared executor owns task ordering. This set only keeps accepted reads alive and allows
299
    // shutdown to cancel them; its size is not a concurrency limit.
300
    std::unordered_set<std::shared_ptr<FileRangeRead>> _inflight_reads;
301
    bool _accepting {true};
302
};
303
304
} // namespace io
305
} // namespace doris