Coverage Report

Created: 2026-10-15 20:44

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
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