be/src/io/cache/partial_block_writeback_manager.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 <chrono> |
22 | | #include <condition_variable> |
23 | | #include <cstddef> |
24 | | #include <cstdint> |
25 | | #include <list> |
26 | | #include <memory> |
27 | | #include <mutex> |
28 | | #include <optional> |
29 | | #include <unordered_map> |
30 | | #include <vector> |
31 | | |
32 | | #include "common/status.h" |
33 | | #include "io/cache/async_cache_write_manager.h" |
34 | | #include "io/fs/file_range_coalescer.h" |
35 | | #include "io/fs/file_range_read_scheduler.h" |
36 | | #include "io/fs/file_reader.h" |
37 | | #include "util/slice.h" |
38 | | |
39 | | namespace doris { |
40 | | |
41 | | class Thread; |
42 | | class ThreadPool; |
43 | | class ThreadPoolToken; |
44 | | |
45 | | } // namespace doris |
46 | | |
47 | | namespace doris::io { |
48 | | |
49 | | class InflightWriteBufferIndex; |
50 | | |
51 | | struct PartialBlockWritebackOptions { |
52 | | /// Allocation and persistence unit; also bounds each pending task's owned buffer. |
53 | | size_t block_size {1}; |
54 | | /// Number of long-running workers coordinating block completion. |
55 | | size_t worker_count {1}; |
56 | | /// Maximum pool threads for multi-range blocks; single-range reads run on block workers. |
57 | | int remote_read_thread_count {1}; |
58 | | /// Minimum queued time for merging fragments, measured from first admission; zero disables it. |
59 | | int32_t merge_delay_ms {2000}; |
60 | | /// BE-wide byte limit for queued and active full-block buffers. |
61 | | size_t max_pending_bytes {1}; |
62 | | /// Limits used to combine uncovered intervals within one block into source reads. |
63 | | FileRangeCoalesceOptions hole_fill_coalesce; |
64 | | |
65 | | Status validate() const; |
66 | | }; |
67 | | |
68 | | enum class PartialBlockSubmitResult : uint8_t { |
69 | | /// A new per-block task owns a copy of the fragment and is waiting for a worker. |
70 | | QUEUED, |
71 | | /// The fragment was copied into an existing queued task for the same block. |
72 | | MERGED, |
73 | | /// The block is already being filled; the fragment was not copied because that worker reads |
74 | | /// every byte not present when it took the task. |
75 | | ACTIVE_DEDUPLICATED, |
76 | | /// An inflight buffer, downloaded cache block, or existing downloader already covers the block. |
77 | | CACHE_BLOCK_PRESENT, |
78 | | /// The cache invalidation fence changed before the fragment could be accepted. |
79 | | STALE_EPOCH, |
80 | | /// A tracked full-block buffer could not be allocated for a new task. |
81 | | BUFFER_ALLOCATION_FAILED, |
82 | | /// The manager is unavailable or all pending capacity is occupied by active tasks. |
83 | | REJECTED, |
84 | | }; |
85 | | |
86 | | /// One foreground fragment of a partial File Cache block. `data` is copied before try_submit() |
87 | | /// returns. The target write manager and inflight index must outlive every accepted task; |
88 | | /// source_reader is retained by the task. `write_epoch` is captured before the foreground read. |
89 | | struct PartialBlockWritebackRequest { |
90 | | AsyncCacheWriteManager* write_manager {nullptr}; |
91 | | InflightWriteBufferIndex* inflight_index {nullptr}; |
92 | | FileReaderSPtr source_reader; |
93 | | UInt128Wrapper cache_hash; |
94 | | size_t block_offset {0}; |
95 | | size_t block_valid_size {0}; |
96 | | size_t fragment_offset {0}; |
97 | | Slice data; |
98 | | CacheAdmissionContext admission_ctx; |
99 | | AsyncCacheWriteEpoch write_epoch; |
100 | | FileRangeReadIOContext io_context; |
101 | | |
102 | | /// Assert request invariants using the block size from validated manager options. |
103 | | void sanity_check(size_t block_size) const; |
104 | | }; |
105 | | |
106 | | /// BE-level bounded fragment queue, deadline dispatcher, block workers, and shared remote-read |
107 | | /// pool. Query threads probe existing blocks, admit memory, and copy one fragment. Dispatch is |
108 | | /// limited to the worker count, so waiting fragments remain mergeable until execution has room. |
109 | | class PartialBlockWritebackManager { |
110 | | public: |
111 | | ~PartialBlockWritebackManager(); |
112 | | |
113 | | PartialBlockWritebackManager(const PartialBlockWritebackManager&) = delete; |
114 | | PartialBlockWritebackManager& operator=(const PartialBlockWritebackManager&) = delete; |
115 | | |
116 | | /// Validate options, create the worker and remote-read pools, and start block worker loops. |
117 | | static Status create(const PartialBlockWritebackOptions& options, |
118 | | std::unique_ptr<PartialBlockWritebackManager>* output_manager); |
119 | | |
120 | | /// Best-effort submission that probes existing cache metadata before copying a fragment. |
121 | | /// QUEUED and MERGED retain a copy of `request.data`; deduplication results mean the block |
122 | | /// is already cached or being filled. Other results retain none of the fragment bytes. |
123 | | PartialBlockSubmitResult try_submit(PartialBlockWritebackRequest request); |
124 | | |
125 | | /// Resize block workers. Shrinking waits for retiring workers to finish all reads for their |
126 | | /// active blocks; queued blocks remain available to retained workers. |
127 | | Status resize_workers(size_t worker_count); |
128 | | /// Resize the shared remote-read pool. Running GETs finish before excess threads retire. |
129 | | Status resize_remote_read_threads(int remote_read_thread_count); |
130 | | /// Apply a nonnegative delay to queued tasks and wake waiting workers. Active tasks continue |
131 | | /// unchanged; merging another fragment preserves the task's original admission time. |
132 | | void set_merge_delay_ms(int32_t merge_delay_ms); |
133 | | /// Return the configured worker target; a resize may still be converging to this value. |
134 | 1.24k | size_t worker_count() const { return _configured_worker_count.load(std::memory_order_acquire); } |
135 | | /// Return block worker loops currently alive. |
136 | 7 | size_t running_worker_count() const { |
137 | 7 | return _running_worker_count.load(std::memory_order_relaxed); |
138 | 7 | } |
139 | | |
140 | | /// Stop admission, discard waiting/undelivered tasks, and wait for executing reads. Idempotent. |
141 | | void shutdown(); |
142 | | /// Return whether a new submission may currently enter admission. |
143 | | bool accepting() const; |
144 | | /// Return unique block tasks owned by the manager, including queued and active tasks. |
145 | | size_t pending_count() const; |
146 | | /// Return full-block buffer capacity owned by all pending tasks. |
147 | | size_t pending_bytes() const; |
148 | | /// Return tasks still waiting for source-read and cache-writer capacity. |
149 | | size_t queued_count() const; |
150 | | /// Return dispatched tasks, including the bounded handoff queue and executing workers. |
151 | | size_t active_count() const; |
152 | | |
153 | | private: |
154 | | class Worker; |
155 | | |
156 | | struct BlockKey { |
157 | | AsyncCacheWriteManager* write_manager {nullptr}; |
158 | | UInt128Wrapper cache_hash; |
159 | | size_t block_offset {0}; |
160 | | |
161 | 4.41k | bool operator==(const BlockKey&) const = default; |
162 | | }; |
163 | | |
164 | | struct BlockKeyHash { |
165 | | size_t operator()(const BlockKey& key) const; |
166 | | }; |
167 | | |
168 | | struct Task; |
169 | | struct SubmitTrace; |
170 | | struct QueueScanTrace; |
171 | | |
172 | | using TaskPtr = std::shared_ptr<Task>; |
173 | | using Queue = std::list<TaskPtr>; |
174 | | |
175 | | enum class EnqueueResult : uint8_t { |
176 | | /// The candidate became the queued task for its block key. |
177 | | QUEUED, |
178 | | /// A current task already owns the key and is returned to the caller. |
179 | | EXISTING, |
180 | | /// Admission stopped or no queued task could be displaced for the candidate. |
181 | | REJECTED, |
182 | | }; |
183 | | |
184 | | explicit PartialBlockWritebackManager(PartialBlockWritebackOptions options); |
185 | | |
186 | | Status _start(); |
187 | | /// Resize workers while `_lifecycle_mutex` is held. |
188 | | Status _resize_workers_locked(size_t worker_count); |
189 | | /// Stop and join workers in `[keep_worker_count, _workers.size())` under the lifecycle lock. |
190 | | void _stop_workers_locked(size_t keep_worker_count); |
191 | | /// Atomically install `candidate`, replace a stale queued task, or return a current same-key |
192 | | /// task. Destruction of a displaced task occurs after the manager lock is released. |
193 | | EnqueueResult _enqueue_or_get_existing(const TaskPtr& candidate, TaskPtr* existing, |
194 | | SubmitTrace& trace); |
195 | | /// Sole owner of deadline/capacity waits; dispatch at most one task per worker execution slot. |
196 | | void _dispatch_loop(); |
197 | | /// Workers only take dispatched tasks; they never scan or wait on merge deadlines. |
198 | | TaskPtr _take_ready_task(const Worker& worker); |
199 | | /// Under `_mutex`, notify dispatch only if a worker slot is free and its next check can advance. |
200 | | void _notify_dispatcher_locked(std::chrono::steady_clock::time_point ready_at); |
201 | | /// Under `_mutex`, scan due entries, discard stale/deduplicated ones, and move one eligible task |
202 | | /// to `_ready_queue`. Stop at the first future deadline; bypass capacity-blocked writers. If |
203 | | /// nothing is dispatched, return the next deadline or capacity retry in `next_wakeup`. |
204 | | bool _dispatch_task_locked(Queue* discarded_tasks, |
205 | | std::chrono::steady_clock::time_point* next_wakeup, |
206 | | QueueScanTrace* trace = nullptr); |
207 | | /// Remove one queued task under `_mutex` and defer its destruction to `discarded_tasks`. |
208 | | void _discard_queued_task_locked(Queue::iterator iterator, Queue* discarded_tasks); |
209 | | /// Plan holes, wait for parallel source reads, and hand the completed buffer to the cache |
210 | | /// writer. Every failure drops this best-effort task after its reads finish. |
211 | | void _process_task(const TaskPtr& task, ThreadPoolToken& token); |
212 | | /// Remove an active task and release its per-cache-writer slot accounting. |
213 | | void _complete_task(const TaskPtr& task); |
214 | | |
215 | | const PartialBlockWritebackOptions _options; |
216 | | const size_t _max_pending_tasks; |
217 | | // `_tasks` owns every queued or active block. `_queue` is ordered by enqueued_at, including |
218 | | // replacement of stale tasks; the shared `_merge_delay` preserves deadline order on updates. |
219 | | Queue _queue; |
220 | | // Dispatched + executing tasks together are bounded by the configured worker count. During |
221 | | // shrink, previously dispatched tasks drain before dispatch can use the smaller limit. |
222 | | Queue _ready_queue; |
223 | | std::unordered_map<BlockKey, TaskPtr, BlockKeyHash> _tasks; |
224 | | // Count active hole-fill tasks against each writer's point-in-time spare capacity. This does |
225 | | // not reserve capacity inside AsyncCacheWriteManager. |
226 | | std::unordered_map<AsyncCacheWriteManager*, size_t> _active_hole_fill_slots_by_writer; |
227 | | bool _accepting {false}; |
228 | | // Protected by `_mutex`, including runtime updates. |
229 | | std::chrono::milliseconds _merge_delay; |
230 | | // Lock order is `_lifecycle_mutex` then `_mutex`; the reverse order is never used. Cache writer |
231 | | // and inflight-index calls made under `_mutex` do not call back into this manager. A Task |
232 | | // fragment mutex is always acquired alone, and removed tasks are destroyed after `_mutex` is |
233 | | // released. |
234 | | mutable std::mutex _mutex; |
235 | | // Workers wait only for handoff; the dispatcher owns all deadline/capacity waits. |
236 | | std::condition_variable _queue_cv; |
237 | | std::condition_variable _dispatch_cv; |
238 | | // Present only during a timed dispatcher wait; later admissions leave that timer undisturbed. |
239 | | std::optional<std::chrono::steady_clock::time_point> _next_wakeup; |
240 | | std::shared_ptr<Thread> _dispatch_thread; |
241 | | // Worker loops wait on remote-read tokens, so they must run in a separate pool. |
242 | | std::unique_ptr<ThreadPool> _worker_pool; |
243 | | std::unique_ptr<ThreadPool> _remote_read_pool; |
244 | | std::atomic<size_t> _configured_worker_count {0}; |
245 | | std::atomic<size_t> _running_worker_count {0}; |
246 | | // Serializes startup, worker resizing, and shutdown. |
247 | | std::mutex _lifecycle_mutex; |
248 | | std::vector<std::shared_ptr<Worker>> _workers; |
249 | | }; |
250 | | |
251 | | } // namespace doris::io |