Coverage Report

Created: 2026-10-30 22:12

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