Coverage Report

Created: 2026-11-03 19:03

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
be/src/io/cache/async_cache_write_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 <condition_variable>
22
#include <cstddef>
23
#include <cstdint>
24
#include <deque>
25
#include <functional>
26
#include <memory>
27
#include <mutex>
28
#include <vector>
29
30
#include "common/atomic_shared_ptr.h"
31
#include "common/status.h"
32
#include "io/cache/file_cache_common.h"
33
#include "runtime/memory/mem_tracker_limiter.h"
34
#include "util/slice.h"
35
36
namespace doris {
37
class ThreadPool;
38
}
39
40
namespace doris::io {
41
42
class BlockFileCache;
43
class AsyncCacheWriteEpochRegistry;
44
class InflightWriteBufferIndex;
45
46
/// Cache admission attributes captured on the query thread and replayed by a write worker.
47
struct CacheAdmissionContext {
48
    /// Query identity used by per-query cache admission and accounting.
49
    TUniqueId query_id;
50
    FileCacheType cache_type {FileCacheType::NORMAL};
51
    int64_t expiration_time {0};
52
    int64_t tablet_id {0};
53
    bool is_warmup {false};
54
55
    /// Capture only the cache-admission fields that remain valid after the query thread returns.
56
    static CacheAdmissionContext from_cache_context(const CacheContext& context, int64_t tablet_id);
57
58
    /// Recreate the worker-local cache context and attach its temporary statistics sink.
59
    CacheContext to_cache_context(ReadStatistics* stats) const;
60
};
61
62
/// Reference-counted payload whose allocation is charged to the async-write memory tracker.
63
class AsyncCacheWriteBuffer {
64
public:
65
    ~AsyncCacheWriteBuffer();
66
67
1.67k
    char* data() { return _data; }
68
0
    const char* data() const { return _data; }
69
2.51k
    size_t size() const { return _size; }
70
71
private:
72
    friend class AsyncCacheWriteManager;
73
74
    AsyncCacheWriteBuffer(size_t size, std::shared_ptr<MemTrackerLimiter> tracker);
75
76
    char* _data = nullptr;
77
    size_t _size = 0;
78
    std::shared_ptr<MemTrackerLimiter> _tracker;
79
};
80
81
using AsyncCacheWriteBufferPtr = std::shared_ptr<AsyncCacheWriteBuffer>;
82
83
/// One live per-file write generation. All async read plans and writes for the same cache key share
84
/// the current token. A key-scoped remove invalidates that token and lets later reads capture a new
85
/// generation without retaining an epoch entry for every historical cache key.
86
class AsyncCacheWriteEpochToken {
87
public:
88
    ~AsyncCacheWriteEpochToken();
89
90
8
    uint64_t generation() const { return _generation; }
91
1.99k
    bool is_valid() const { return _valid.load(std::memory_order_acquire); }
92
93
private:
94
    friend class AsyncCacheWriteEpochRegistry;
95
96
    AsyncCacheWriteEpochToken(const UInt128Wrapper& cache_hash, uint64_t generation,
97
                              std::weak_ptr<AsyncCacheWriteEpochRegistry> registry);
98
99
    UInt128Wrapper _cache_hash;
100
    uint64_t _generation {0};
101
    std::weak_ptr<AsyncCacheWriteEpochRegistry> _registry;
102
    std::atomic<bool> _valid {true};
103
};
104
105
/// Composite persistence fence shared by an async read plan and its derived write tasks.
106
/// `cache_epoch` invalidates all older writes on one cache disk during a full-cache clear, while
107
/// `key_token` lets a single-file removal invalidate only older writes for that cache key. The
108
/// epoch does not govern inflight reads because a cache hash identifies immutable file content.
109
struct AsyncCacheWriteEpoch {
110
    uint64_t cache_epoch {0};
111
    std::shared_ptr<AsyncCacheWriteEpochToken> key_token;
112
};
113
114
enum class AsyncCacheWriteBlockSubmitResult : uint8_t {
115
    /// The manager accepted ownership of a complete block buffer.
116
    SUBMITTED,
117
    /// The cache invalidation fence changed before submission.
118
    STALE_EPOCH,
119
    /// The copy-based submission could not allocate its tracked block buffer.
120
    BUFFER_ALLOCATION_FAILED,
121
    /// The same cache block already has a buffer indexed by an accepted write task.
122
    ALREADY_INFLIGHT,
123
    /// The manager is unavailable or its admission policy found no queue capacity.
124
    REJECTED,
125
};
126
127
enum class AsyncCacheWriteAdmissionMode : uint8_t {
128
    /// Normal work may replace the oldest queued task when pending capacity is full.
129
    ALLOW_OLDEST_QUEUED_EVICTION,
130
    /// Lower-priority work is accepted only from currently unused pending capacity.
131
    REQUIRE_SPARE_CAPACITY,
132
};
133
134
/// Transient source bytes and immutable metadata for one complete cache-block submission.
135
/// `data` is copied before try_submit_block() returns. `buffer_size` is the fixed cache block
136
/// allocation size; only the physical EOF block may provide a shorter data slice.
137
struct AsyncCacheWriteBlockRequest {
138
    UInt128Wrapper cache_hash;
139
    size_t file_offset {0};
140
    Slice data;
141
    size_t buffer_size {0};
142
    CacheAdmissionContext admission_ctx;
143
    AsyncCacheWriteEpoch write_epoch;
144
    /// Optional index whose lifetime must cover every accepted write task.
145
    InflightWriteBufferIndex* inflight_index {nullptr};
146
};
147
148
/// Owned complete cache-block payload. The buffer must come from allocate_tracked_buffer() on the
149
/// target manager and is transferred without another data copy.
150
struct AsyncCacheWriteOwnedBlockRequest {
151
    UInt128Wrapper cache_hash;
152
    size_t file_offset {0};
153
    size_t write_size {0};
154
    AsyncCacheWriteBufferPtr buffer;
155
    CacheAdmissionContext admission_ctx;
156
    AsyncCacheWriteEpoch write_epoch;
157
    AsyncCacheWriteAdmissionMode admission_mode {
158
            AsyncCacheWriteAdmissionMode::ALLOW_OLDEST_QUEUED_EVICTION};
159
    /// Optional index whose lifetime must cover every accepted write task.
160
    InflightWriteBufferIndex* inflight_index {nullptr};
161
};
162
163
/// One cache-block write. The sole production submitter allocates every `buffer` with exactly
164
/// `file_cache_each_block_size` bytes. `write_size` is the valid prefix starting at `file_offset`;
165
/// only the physical EOF block may use less than the full buffer. `write_epoch` prevents a worker
166
/// from resurrecting data after cache invalidation.
167
struct AsyncCacheWriteTask {
168
    using Finalizer = std::function<void(const AsyncCacheWriteTask&)>;
169
170
    UInt128Wrapper cache_hash;
171
    size_t file_offset {0};
172
    size_t write_size {0};
173
    AsyncCacheWriteBufferPtr buffer;
174
    CacheAdmissionContext admission_ctx;
175
    int64_t submit_ts_us {0};
176
    AsyncCacheWriteEpoch write_epoch;
177
    Finalizer on_finalized;
178
179
    /// Assert the task contract at the manager boundary.
180
    void validate() const;
181
182
    /// Return the full allocation capacity charged to pending-byte accounting.
183
    size_t buffer_size() const;
184
185
    /// Run the optional owner cleanup after this task reaches a terminal state.
186
    void finalize() const;
187
};
188
189
/// Complete per-cache-disk worker and memory settings. The manager receives this value
190
/// explicitly at construction and through update_options(); it never reads global config.
191
struct AsyncCacheWriteManagerOptions {
192
    size_t worker_count {1};
193
    // Accepted queued+active buffer capacity. With fixed block-size buffers, any remainder smaller
194
    // than one block is intentionally unusable.
195
    size_t max_pending_bytes {1};
196
};
197
198
/// Resolve the configured BE-wide pending-byte ownership limit. A positive value is used
199
/// unchanged; -1 selects max(1 GiB, 1% of `be_mem_limit`). Config validation rejects every other
200
/// value.
201
Status resolve_async_file_cache_write_max_pending_bytes(int64_t configured_bytes,
202
                                                        int64_t be_mem_limit,
203
                                                        size_t* resolved_bytes);
204
205
/// Owns the bounded async-write queue and workers for one BlockFileCache (one cache disk).
206
///
207
/// The referenced cache must outlive this manager. Shutdown stops new producers, waits registered
208
/// producers, and drains all accepted tasks before worker resources are released.
209
class AsyncCacheWriteManager {
210
public:
211
    /// @param cache Non-owning target cache; it must outlive this manager.
212
    /// @param options Initial worker and pending-memory limits.
213
    AsyncCacheWriteManager(BlockFileCache* cache, AsyncCacheWriteManagerOptions options);
214
    ~AsyncCacheWriteManager();
215
216
    /// Create the worker pool and schedule the configured long-running workers. Idempotent.
217
    Status start();
218
219
    /// Admit `task` into the memory-bounded FIFO without waiting for disk I/O. Because all tasks
220
    /// have one fixed cache-block buffer capacity, normal admission to a full queue displaces
221
    /// exactly one oldest queued task. REQUIRE_SPARE_CAPACITY instead rejects a full-queue
222
    /// submission so lower-priority work cannot displace normal writes. Active tasks are never
223
    /// displaced. After a runtime limit decrease, normal submissions continue to replace the
224
    /// oldest queued task without increasing pending bytes, even while existing pending bytes
225
    /// exceed the new limit. This call can briefly wait for the queue mutex and finalizes a
226
    /// displaced task before returning.
227
    /// @return true if ownership was transferred to the queue; false when workers have not been
228
    /// started, during shutdown, or on backpressure. A rejected task's finalization callback is
229
    /// not invoked.
230
    bool try_submit(AsyncCacheWriteTask task,
231
                    AsyncCacheWriteAdmissionMode admission_mode =
232
                            AsyncCacheWriteAdmissionMode::ALLOW_OLDEST_QUEUED_EVICTION);
233
234
    /// Copy and submit one complete cache block through the same epoch, inflight-deduplication,
235
    /// memory-accounting, and queue path used by remote cache-miss reads.
236
    /// @return ALREADY_INFLIGHT when another accepted task already owns this block; in that case
237
    /// the request data is not copied.
238
    AsyncCacheWriteBlockSubmitResult try_submit_block(AsyncCacheWriteBlockRequest request);
239
240
    /// Submit an already owned complete cache block without copying its payload. This is the
241
    /// handoff used after background hole filling completes a tracked block buffer.
242
    /// The manager retains the buffer after this call only when SUBMITTED is returned.
243
    AsyncCacheWriteBlockSubmitResult try_submit_owned_block(
244
            AsyncCacheWriteOwnedBlockRequest request);
245
246
    /// Return whether a fixed-size task fits in currently unused pending capacity. The result is a
247
    /// point-in-time scheduling hint: a concurrent foreground submission may consume the capacity
248
    /// before the caller hands off its task.
249
    bool can_accept_without_eviction(size_t buffer_size) const;
250
251
    /// Return how many fixed-size tasks fit in the same point-in-time spare capacity. Background
252
    /// schedulers use this to avoid launching multiple remote reads against one available slot.
253
    size_t available_slots_without_eviction(size_t buffer_size) const;
254
255
    /// Return whether the manager currently accepts submissions. This is a point-in-time hint;
256
    /// submission methods still make the race-safe final decision.
257
    bool accepting() const;
258
259
    /// Check inflight buffers first, then probe one aligned cache block. Skip writeback if the
260
    /// block is already cached or being filled. Each lookup releases its lock before the next;
261
    /// this call must run outside the hole-fill queue mutex because probe may load cache metadata.
262
    bool should_skip_block_writeback(const UInt128Wrapper& cache_hash, size_t offset, size_t size,
263
                                     const CacheAdmissionContext& admission_ctx,
264
                                     InflightWriteBufferIndex* inflight_index) const;
265
266
    /// Allocate `size` payload bytes charged to the manager tracker and return them in `buffer`.
267
    Status allocate_tracked_buffer(size_t size, AsyncCacheWriteBufferPtr* buffer);
268
269
    /// Capture the disk-wide epoch and current live generation for `cache_hash`.
270
    AsyncCacheWriteEpoch current_write_epoch(const UInt128Wrapper& cache_hash);
271
272
    /// Return the current disk-wide epoch used by cache-clear operations.
273
3.50k
    uint64_t current_cache_epoch() const { return _cache_epoch.load(std::memory_order_acquire); }
274
275
    /// Test whether both levels of `epoch` still accept writes.
276
    bool is_current_write_epoch(const AsyncCacheWriteEpoch& epoch) const;
277
278
    /// Test the epoch and record one stale-drop metric when it is no longer current.
279
    bool check_write_epoch(const AsyncCacheWriteEpoch& epoch);
280
281
    /// Invalidate only queued/inflight work captured for `cache_hash` before this call.
282
    void invalidate_pending_writes(const UInt128Wrapper& cache_hash);
283
284
    /// Advance the disk-wide epoch so every previously captured write becomes stale.
285
    /// @return The newly active disk-wide epoch.
286
    uint64_t invalidate_all_pending_writes();
287
288
    /// Return cache keys whose current valid generation is retained by at least one plan or task.
289
    /// Invalidated generations are excluded even while stale tasks finish releasing them.
290
    size_t active_write_epoch_key_count() const;
291
292
    /// Resize the number of active workers. A shrink waits only for retiring worker loops.
293
    /// @param worker_count Positive target worker count for this cache disk.
294
    Status resize_workers(size_t worker_count);
295
296
    /// Replace all mutable manager settings with one coherent snapshot. Configuration adapters
297
    /// call this method explicitly; the manager itself has no dependency on global config.
298
    /// @param options Complete validated settings, including the desired worker count.
299
    /// @return OK after the new snapshot is active; InvalidArgument for invalid limits, or a
300
    /// worker-resize error when the requested concurrency cannot be applied.
301
    Status update_options(const AsyncCacheWriteManagerOptions& options);
302
303
    /// Return the currently active settings as a value snapshot.
304
    AsyncCacheWriteManagerOptions options() const;
305
306
    /// Stop submissions, drain all accepted tasks, and join worker loops. Idempotent.
307
    void shutdown();
308
309
    /// Return accepted tasks that have not yet completed finalization.
310
170
    size_t pending_count() const { return _pending_count.load(std::memory_order_relaxed); }
311
312
    /// Return buffer-capacity bytes owned by queued and active tasks.
313
16
    size_t pending_bytes() const { return _pending_bytes.load(std::memory_order_relaxed); }
314
315
    /// Return accepted tasks still waiting in the FIFO queue, excluding active workers.
316
    size_t queued_count() const;
317
318
    /// Return buffer-capacity bytes still waiting in the FIFO queue.
319
11
    size_t queued_bytes() const { return _queued_bytes.load(std::memory_order_relaxed); }
320
321
    /// Return tasks currently owned by workers.
322
8
    size_t active_task_count() const { return _active_task_count.load(std::memory_order_relaxed); }
323
324
    /// Return buffer-capacity bytes currently owned by workers.
325
7
    size_t active_bytes() const { return _active_bytes.load(std::memory_order_relaxed); }
326
327
    /// Return worker loops that are currently alive.
328
853
    size_t running_worker_count() const {
329
853
        return _running_worker_count.load(std::memory_order_relaxed);
330
853
    }
331
332
    /// Return bytes currently held by tracked task buffers.
333
869
    int64_t buffer_memory_bytes() const { return _mem_tracker->consumption(); }
334
335
    /// Return tasks displaced by full-queue admission.
336
    uint64_t evicted_oldest_count() const;
337
338
    /// Return the current rolling P99 wait to acquire the FIFO mutex.
339
    int64_t queue_lock_wait_p99_us() const;
340
341
    /// Return the current rolling P99 FIFO mutex critical-section duration.
342
    int64_t queue_lock_hold_p99_us() const;
343
344
private:
345
    class Worker;
346
347
    enum class TaskFinalizationReason : uint8_t {
348
        WORKER_FINISHED,
349
        EVICTED_OLDEST,
350
    };
351
352
    /// Owns bvar registration and translates manager events into coherent metric updates.
353
    class Metrics;
354
355
    /// Resize the owned worker set while `_lifecycle_mutex` is held and `_worker_pool` exists.
356
    /// The pool's minimum thread count is kept equal to the long-running Worker task count so a
357
    /// task is never accepted without a backing OS thread.
358
    Status _resize_workers_locked(size_t worker_count);
359
360
    /// Stop and join workers in `[keep_worker_count, _workers.size())` while the lifecycle mutex is
361
    /// held. Stop requests are published under `_queue_mutex` before waking the worker loops.
362
    void _stop_workers_locked(size_t keep_worker_count);
363
364
    /// Process one task already moved from queued to active ownership.
365
    void _process_task(AsyncCacheWriteTask task);
366
367
    /// Move the oldest queued task to active ownership.
368
    bool _try_activate_task(AsyncCacheWriteTask* task);
369
370
    /// Revalidate epoch/cache state and persist the task's still-empty complete blocks.
371
    Status _persist_task(const AsyncCacheWriteTask& task);
372
373
    /// Move one active task to its terminal state outside the queue lock.
374
    void _complete_active_task(AsyncCacheWriteTask task);
375
376
    /// Record the terminal reason and let the task run its owner cleanup without the queue lock.
377
    void _complete_task(AsyncCacheWriteTask task, TaskFinalizationReason reason);
378
379
    BlockFileCache* _cache;
380
    atomic_shared_ptr<const AsyncCacheWriteManagerOptions> _options;
381
    std::deque<AsyncCacheWriteTask> _queue;
382
    mutable std::mutex _queue_mutex;
383
    std::condition_variable _queue_cv;
384
    // Learned from the first submission and protected by `_queue_mutex`.
385
    size_t _task_buffer_size {0};
386
    // Pending covers all accepted tasks, while queued and active are its disjoint ownership states:
387
    //   pending_count = queue.size() + active_task_count
388
    //   pending_bytes = queued_bytes + active_bytes
389
    // Byte state is maintained directly and is authoritative for memory admission. Production
390
    // tasks currently have a fixed cache-block buffer capacity, including a partial EOF write, but
391
    // byte accounting deliberately does not depend on deriving bytes from task counts.
392
    std::atomic<size_t> _pending_count {0};
393
    std::atomic<size_t> _pending_bytes {0};
394
    std::atomic<size_t> _queued_bytes {0};
395
    std::atomic<size_t> _active_task_count {0};
396
    std::atomic<size_t> _active_bytes {0};
397
    std::atomic<size_t> _running_worker_count {0};
398
    std::atomic<size_t> _active_get_or_set_count {0};
399
    std::atomic<size_t> _active_append_count {0};
400
    std::atomic<size_t> _active_finalize_count {0};
401
    std::atomic<bool> _accepting {true};
402
    std::atomic<size_t> _active_submitters {0};
403
    std::atomic<bool> _started {false};
404
    std::atomic<uint64_t> _cache_epoch {1};
405
    std::shared_ptr<AsyncCacheWriteEpochRegistry> _write_epoch_registry;
406
407
    std::shared_ptr<MemTrackerLimiter> _mem_tracker;
408
    std::unique_ptr<Metrics> _metrics;
409
    std::unique_ptr<ThreadPool> _worker_pool;
410
    std::atomic<size_t> _configured_worker_count {0};
411
    // Serializes start, resize, and shutdown, including all changes to `_workers`.
412
    std::mutex _lifecycle_mutex;
413
    // Protected by `_lifecycle_mutex`. Worker stop state is owned by each Worker.
414
    std::vector<std::shared_ptr<Worker>> _workers;
415
};
416
417
} // namespace doris::io