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 |