Coverage Report

Created: 2026-10-15 04:53

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
be/src/io/cache/file_cache_common.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
// This file is copied from
18
// https://github.com/ClickHouse/ClickHouse/blob/master/src/Interpreters/Cache/FileCache_fwd.h
19
// and modified by Doris
20
21
#pragma once
22
#include <cstdint>
23
#include <list>
24
#include <optional>
25
#include <vector>
26
27
#include "common/config.h"
28
#include "core/uint128.h"
29
#include "io/io_common.h"
30
31
namespace doris::io {
32
33
inline static constexpr size_t REMOTE_FS_OBJECTS_CACHE_DEFAULT_ELEMENTS = 100 * 1024;
34
inline static constexpr size_t FILE_CACHE_MAX_FILE_BLOCK_SIZE = 1 * 1024 * 1024;
35
inline static constexpr size_t DEFAULT_NORMAL_PERCENT = 40;
36
inline static constexpr size_t DEFAULT_DISPOSABLE_PERCENT = 5;
37
inline static constexpr size_t DEFAULT_INDEX_PERCENT = 5;
38
inline static constexpr size_t DEFAULT_TTL_PERCENT = 50;
39
40
using uint128_t = UInt128;
41
42
enum FileCacheType {
43
    INDEX = 2,
44
    NORMAL = 1,
45
    DISPOSABLE = 0,
46
    TTL = 3,
47
};
48
49
43.6M
inline size_t file_cache_type_index(FileCacheType type) {
50
43.6M
    return static_cast<size_t>(type);
51
43.6M
}
52
53
// A block has a class (INDEX, NORMAL or DISPOSABLE) that is fixed when it is written, and it sits
54
// in the TTL queue while its tablet's TTL protects it. TTL is a queue, never a class: a block
55
// recorded as TTL by an older version lost its class there, and NORMAL is what it was put back to.
56
1.10M
inline FileCacheType cache_class_of(FileCacheType type) {
57
1.10M
    return type == FileCacheType::TTL ? FileCacheType::NORMAL : type;
58
1.10M
}
59
60
std::string cache_type_to_surfix(FileCacheType type);
61
FileCacheType surfix_to_cache_type(const std::string& str);
62
63
FileCacheType string_to_cache_type(const std::string& str);
64
std::string cache_type_to_string(FileCacheType type);
65
66
struct UInt128Wrapper {
67
    uint128_t value_;
68
    [[nodiscard]] std::string to_string() const;
69
70
    UInt128Wrapper() = default;
71
484k
    explicit UInt128Wrapper(const uint128_t& value) : value_(value) {}
72
73
903M
    bool operator==(const UInt128Wrapper& other) const { return value_ == other.value_; }
74
75
504k
    uint64_t high() const { return static_cast<uint64_t>(value_ >> 64); }
76
504k
    uint64_t low() const { return static_cast<uint64_t>(value_); }
77
78
    friend std::ostream& operator<<(std::ostream& os, const UInt128Wrapper& wrapper) {
79
        os << "UInt128Wrapper(" << wrapper.high() << ", " << wrapper.low() << ")";
80
        return os;
81
    }
82
};
83
84
struct ReadStatistics {
85
    bool hit_cache = true;
86
    bool from_peer_cache = false;
87
    bool skip_cache = false;
88
    int64_t bytes_read = 0;
89
    int64_t bytes_read_from_local = 0;
90
    int64_t bytes_read_from_remote = 0;
91
    int64_t bytes_read_from_peer = 0;
92
    int64_t remote_physical_read_bytes = 0;
93
    int64_t bytes_write_into_file_cache = 0;
94
    int64_t remote_read_timer = 0;
95
    int64_t peer_read_timer = 0;
96
    int64_t remote_wait_timer = 0; // wait for other downloader
97
    int64_t local_read_timer = 0;
98
    int64_t local_write_timer = 0;
99
    int64_t read_cache_file_directly_timer = 0;
100
    int64_t cache_get_or_set_timer = 0;
101
    int64_t lock_wait_timer = 0;
102
    int64_t get_timer = 0;
103
    int64_t set_timer = 0;
104
    int64_t async_cache_write_submitted = 0;
105
    int64_t async_cache_write_rejected = 0;
106
    int64_t async_cache_write_buffer_alloc_fail = 0;
107
    int64_t async_cache_write_drop_stale_epoch = 0;
108
    int64_t inflight_write_buffer_index_hit = 0;
109
    int64_t inflight_write_buffer_index_miss = 0;
110
    int64_t probe_downloaded_hit = 0;
111
    int64_t probe_downloading_hit = 0;
112
    int64_t probe_miss = 0;
113
    int64_t block_wait_success = 0;
114
    int64_t block_wait_timeout = 0;
115
};
116
117
class BlockFileCache;
118
struct FileBlocksHolder;
119
using FileBlocksHolderPtr = std::unique_ptr<FileBlocksHolder>;
120
121
struct FileCacheAllocatorBuilder {
122
    bool _is_cold_data;
123
    uint64_t _expiration_time;
124
    UInt128Wrapper _cache_hash;
125
    BlockFileCache* _cache; // Only one ref, the lifetime is owned by FileCache
126
    FileBlocksHolderPtr allocate_cache_holder(size_t offset, size_t size, int64_t tablet_id) const;
127
};
128
129
struct KeyHash {
130
905M
    std::size_t operator()(const UInt128Wrapper& w) const {
131
905M
        return util_hash::HashLen16(w.value_.low(), w.value_.high());
132
905M
    }
133
};
134
135
using AccessKeyAndOffset = std::pair<UInt128Wrapper, size_t>;
136
struct KeyAndOffsetHash {
137
2.94M
    std::size_t operator()(const AccessKeyAndOffset& key) const {
138
2.94M
        return KeyHash()(key.first) ^ std::hash<uint64_t>()(key.second);
139
2.94M
    }
140
};
141
142
struct KeyMeta {
143
    // The TTL deadline the block was written with (absolute time). Whether the block is protected
144
    // is decided by its tablet's current policy (TabletCachePolicyRegistry); this is only the
145
    // fallback for a tablet whose policy is not known, and what an older version reads back.
146
    uint64_t expiration_time;
147
    // The queue the block is in right now: its class, or TTL while it is protected.
148
    FileCacheType type;
149
    int64_t tablet_id {0};
150
    // The class of the block, see cache_class_of(). Persisted; never TTL.
151
    FileCacheType cache_class {FileCacheType::NORMAL};
152
};
153
154
struct FileCacheKey {
155
    UInt128Wrapper hash;
156
    size_t offset;
157
    KeyMeta meta;
158
};
159
160
struct FileCacheSettings {
161
    size_t capacity {0};
162
    size_t disposable_queue_size {0};
163
    size_t disposable_queue_elements {0};
164
    size_t index_queue_size {0};
165
    size_t index_queue_elements {0};
166
    size_t query_queue_size {0};
167
    size_t query_queue_elements {0};
168
    size_t ttl_queue_size {0};
169
    size_t ttl_queue_elements {0};
170
    size_t max_file_block_size {0};
171
    size_t max_query_cache_size {0};
172
    std::string storage;
173
174
    // to string
175
    std::string to_string() const;
176
};
177
178
FileCacheSettings get_file_cache_settings(size_t capacity, size_t max_query_cache_size,
179
                                          size_t normal_percent = DEFAULT_NORMAL_PERCENT,
180
                                          size_t disposable_percent = DEFAULT_DISPOSABLE_PERCENT,
181
                                          size_t index_percent = DEFAULT_INDEX_PERCENT,
182
                                          size_t ttl_percent = DEFAULT_TTL_PERCENT,
183
                                          const std::string& storage = "disk");
184
185
struct CacheContext {
186
    // Every reader-side context has to say which tablet the blocks belong to (0 for data outside
187
    // any Doris tablet). Taking it here rather than assigning it afterwards keeps a new call site
188
    // from silently caching blocks under tablet 0.
189
995k
    CacheContext(const IOContext* io_context, int64_t tablet_id_) : tablet_id(tablet_id_) {
190
        // The class only; whether the blocks go to the TTL queue is the cache's decision, see
191
        // TabletCachePolicyRegistry.
192
995k
        expiration_time = io_context->expiration_time;
193
995k
        if (io_context->is_index_data) {
194
156k
            cache_type = FileCacheType::INDEX;
195
839k
        } else if (io_context->is_disposable) {
196
51.9k
            cache_type = FileCacheType::DISPOSABLE;
197
787k
        } else {
198
787k
            cache_type = FileCacheType::NORMAL;
199
787k
        }
200
995k
        query_id = io_context->query_id ? *io_context->query_id : TUniqueId();
201
995k
        is_warmup = io_context->is_warmup;
202
995k
        remote_scan_cache_write_limiter = io_context->remote_scan_cache_write_limiter;
203
995k
        admit_cache_write_by_remote_scan_limiter =
204
995k
                remote_scan_cache_write_limiter != nullptr &&
205
995k
                io_context->reader_type == ReaderType::READER_QUERY &&
206
995k
                (!io_context->is_index_data || io_context->is_inverted_index ||
207
9
                 config::enable_file_cache_query_limit_segment_meta) &&
208
995k
                !io_context->is_warmup;
209
995k
    }
210
70.7k
    CacheContext() = default;
211
    bool operator==(const CacheContext& rhs) const {
212
        return query_id == rhs.query_id && cache_type == rhs.cache_type &&
213
               expiration_time == rhs.expiration_time && is_cold_data == rhs.is_cold_data;
214
    }
215
    TUniqueId query_id {};
216
    FileCacheType cache_type {FileCacheType::NORMAL};
217
    int64_t expiration_time {0};
218
    bool is_cold_data {false};
219
    ReadStatistics* stats {nullptr};
220
    bool is_warmup {false};
221
    int64_t tablet_id {0};
222
    RemoteScanCacheWriteLimiter* remote_scan_cache_write_limiter = nullptr;
223
    bool admit_cache_write_by_remote_scan_limiter {false};
224
};
225
226
template <class Lock>
227
concept IsXLock = std::same_as<Lock, std::lock_guard<std::mutex>> ||
228
                  std::same_as<Lock, std::unique_lock<std::mutex>>;
229
230
class LRUQueue {
231
public:
232
2.85k
    LRUQueue() = default;
233
    LRUQueue(size_t max_size, size_t max_element_size, int64_t hot_data_interval)
234
1.39k
            : max_size(max_size),
235
1.39k
              max_element_size(max_element_size),
236
1.39k
              hot_data_interval(hot_data_interval) {}
237
238
    struct HashFileKeyAndOffset {
239
2.47M
        std::size_t operator()(const std::pair<UInt128Wrapper, size_t>& pair) const {
240
2.47M
            return KeyHash()(pair.first) + pair.second;
241
2.47M
        }
242
    };
243
244
    struct FileKeyAndOffset {
245
        UInt128Wrapper hash;
246
        size_t offset;
247
        size_t size;
248
249
        FileKeyAndOffset(const UInt128Wrapper& hash, size_t offset, size_t size)
250
717k
                : hash(hash), offset(offset), size(size) {}
251
    };
252
253
    using Iterator = typename std::list<FileKeyAndOffset>::iterator;
254
255
778k
    size_t get_max_size() const { return max_size; }
256
740
    size_t get_max_element_size() const { return max_element_size; }
257
258
    template <class T>
259
        requires IsXLock<T>
260
785k
    size_t get_capacity(T& /* cache_lock */) const {
261
785k
        return cache_size;
262
785k
    }
263
264
737
    size_t get_capacity_unsafe() const { return cache_size; }
265
266
769
    size_t get_elements_num_unsafe() const { return queue.size(); }
267
268
887k
    size_t get_elements_num(std::lock_guard<std::mutex>& /* cache_lock */) const {
269
887k
        return queue.size();
270
887k
    }
271
272
    Iterator add(const UInt128Wrapper& hash, size_t offset, size_t size,
273
                 std::lock_guard<std::mutex>& cache_lock);
274
    // Adds at the cold end, for an entry moved in from another queue that is not known to be
275
    // any warmer than what is already there.
276
    Iterator add_front(const UInt128Wrapper& hash, size_t offset, size_t size,
277
                       std::lock_guard<std::mutex>& cache_lock);
278
    template <class T>
279
        requires IsXLock<T>
280
684k
    void remove(Iterator queue_it, T& /* cache_lock */) {
281
684k
        skip_sweep_cursor(queue_it);
282
684k
        cache_size -= queue_it->size;
283
684k
        map.erase(std::make_pair(queue_it->hash, queue_it->offset));
284
684k
        queue.erase(queue_it);
285
684k
    }
286
287
    void move_to_end(Iterator queue_it, std::lock_guard<std::mutex>& cache_lock);
288
289
    void resize(Iterator queue_it, size_t new_size, std::lock_guard<std::mutex>& cache_lock);
290
291
    std::string to_string(std::lock_guard<std::mutex>& cache_lock) const;
292
293
    bool contains(const UInt128Wrapper& hash, size_t offset,
294
                  std::lock_guard<std::mutex>& cache_lock) const;
295
296
1.68M
    Iterator begin() { return queue.begin(); }
297
298
5.41M
    Iterator end() { return queue.end(); }
299
300
    void remove_all(std::lock_guard<std::mutex>& cache_lock);
301
302
    bool pop_front(std::lock_guard<std::mutex>& cache_lock);
303
304
    Iterator get(const UInt128Wrapper& hash, size_t offset,
305
                 std::lock_guard<std::mutex>& /* cache_lock */) const;
306
307
682k
    int64_t get_hot_data_interval() const { return hot_data_interval; }
308
309
0
    void clear(std::lock_guard<std::mutex>& cache_lock) {
310
0
        sweep_cursor.reset();
311
0
        queue.clear();
312
0
        map.clear();
313
0
        cache_size = 0;
314
0
    }
315
316
    // Moves the sweep cursor past `queue_it` if it points there, before the entry leaves its
317
    // place in the queue.
318
2.01M
    void skip_sweep_cursor(Iterator queue_it) {
319
2.01M
        if (sweep_cursor == queue_it) {
320
2.15k
            ++*sweep_cursor;
321
2.15k
        }
322
2.01M
    }
323
324
    size_t levenshtein_distance_from(LRUQueue& base, std::lock_guard<std::mutex>& cache_lock);
325
326
    size_t max_size;
327
    size_t max_element_size;
328
    std::list<FileKeyAndOffset> queue;
329
    std::unordered_map<std::pair<UInt128Wrapper, size_t>, Iterator, HashFileKeyAndOffset> map;
330
    // Where the background queue sweep resumes, kept across releases of the cache lock. An
331
    // entry that leaves its place moves the cursor past itself first, so it never dangles.
332
    std::optional<Iterator> sweep_cursor;
333
    size_t cache_size = 0;
334
    int64_t hot_data_interval {0};
335
};
336
struct FileCacheInfo {
337
    UInt128Wrapper hash {0};
338
    uint64_t expiration_time {0};
339
    uint64_t size {0};
340
    size_t offset {0};
341
    bool is_tmp {false};
342
    FileCacheType cache_type {NORMAL};
343
344
    std::string to_string() const;
345
};
346
347
class InconsistencyType {
348
    uint32_t type;
349
350
public:
351
    enum : uint32_t {
352
        // No anomaly
353
        NONE = 0,
354
        // Missing a block cache metadata in _files
355
        NOT_LOADED = 1 << 0,
356
        // A block cache is missing in storage
357
        MISSING_IN_STORAGE = 1 << 1,
358
        // Size of a block cache recorded in _files is inconsistent with the storage
359
        SIZE_INCONSISTENT = 1 << 2,
360
        // Cache type of a block cache recorded in _files is inconsistent with the storage
361
        CACHE_TYPE_INCONSISTENT = 1 << 3,
362
        // Expiration time of a block cache recorded in _files is inconsistent with the storage
363
        EXPIRATION_TIME_INCONSISTENT = 1 << 4,
364
        // File in storage has a _tmp suffix, but the state of block cache in _files is not set to downloading
365
        TMP_FILE_EXPECT_DOWNLOADING_STATE = 1 << 5
366
    };
367
0
    InconsistencyType(uint32_t t = 0) : type(t) {}
368
0
    operator uint32_t&() { return type; }
369
370
    std::string to_string() const;
371
};
372
373
struct InconsistencyContext {
374
    // The infos in _files of BlockFileCache.
375
    std::vector<FileCacheInfo> infos_in_manager;
376
    std::vector<FileCacheInfo> infos_in_storage;
377
    std::vector<InconsistencyType> types;
378
};
379
380
std::optional<int64_t> get_tablet_id(std::string file_path);
381
382
} // namespace doris::io