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 |