Coverage Report

Created: 2026-07-16 19:01

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
be/src/runtime/query_cache/query_cache.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 <butil/macros.h>
21
#include <glog/logging.h>
22
#include <stddef.h>
23
#include <stdint.h>
24
25
#include <atomic>
26
#include <map>
27
#include <memory>
28
#include <mutex>
29
#include <roaring/roaring.hh>
30
#include <string>
31
#include <unordered_map>
32
#include <vector>
33
34
#include "common/config.h"
35
#include "common/status.h"
36
#include "core/block/block.h"
37
#include "io/fs/file_system.h"
38
#include "io/fs/path.h"
39
#include "runtime/exec_env.h"
40
#include "runtime/memory/lru_cache_policy.h"
41
#include "runtime/memory/mem_tracker.h"
42
#include "util/lru_cache.h"
43
#include "util/slice.h"
44
#include "util/time.h"
45
46
namespace doris {
47
48
class BaseTablet;
49
struct TabletReadSource;
50
51
using CacheResult = std::vector<BlockUPtr>;
52
// A handle for mid-result from query lru cache.
53
// The handle will automatically release the cache entry when it is destroyed.
54
// So the caller need to make sure the handle is valid in lifecycle.
55
class QueryCacheHandle {
56
public:
57
127
    QueryCacheHandle() = default;
58
    QueryCacheHandle(LRUCachePolicy* cache, Cache::Handle* handle)
59
90
            : _cache(cache), _handle(handle) {}
60
61
218
    ~QueryCacheHandle() {
62
218
        if (_handle != nullptr) {
63
90
            CHECK(_cache != nullptr);
64
90
            {
65
90
                SCOPED_SWITCH_THREAD_MEM_TRACKER_LIMITER(
66
90
                        ExecEnv::GetInstance()->query_cache_mem_tracker());
67
90
                _cache->release(_handle);
68
90
            }
69
90
        }
70
218
    }
71
72
    QueryCacheHandle(QueryCacheHandle&& other) noexcept {
73
        std::swap(_cache, other._cache);
74
        std::swap(_handle, other._handle);
75
    }
76
77
101
    QueryCacheHandle& operator=(QueryCacheHandle&& other) noexcept {
78
101
        std::swap(_cache, other._cache);
79
101
        std::swap(_handle, other._handle);
80
101
        return *this;
81
101
    }
82
83
    bool valid() const { return _handle != nullptr; }
84
85
    std::vector<int>* get_cache_slot_orders();
86
87
    CacheResult* get_cache_result();
88
89
    int64_t get_cache_version();
90
91
    // How many incremental merges have been accumulated on this entry since the
92
    // last full recompute. See QueryCacheRuntime for the compaction policy.
93
    int64_t get_cache_delta_count();
94
95
    int64_t get_cache_total_bytes();
96
97
    int64_t get_cache_total_rows();
98
99
private:
100
    LRUCachePolicy* _cache = nullptr;
101
    Cache::Handle* _handle = nullptr;
102
103
    // Don't allow copy and assign
104
    DISALLOW_COPY_AND_ASSIGN(QueryCacheHandle);
105
};
106
107
class QueryCache : public LRUCachePolicy {
108
public:
109
    using LRUCachePolicy::insert;
110
111
    struct CacheValue : public LRUCacheValueBase {
112
        int64_t version;
113
        CacheResult result;
114
        std::vector<int> slot_orders;
115
        // Number of incremental merges accumulated on this entry since the last
116
        // full recompute. 0 means the entry was produced by a full scan.
117
        int64_t delta_count;
118
        // Size of this entry, used to decide upfront whether an incremental
119
        // merge could ever be written back under the entry_max_bytes/rows
120
        // limits (a merged entry can only be larger than the cached one).
121
        int64_t total_bytes;
122
        int64_t total_rows;
123
124
        CacheValue(int64_t v, CacheResult&& r, const std::vector<int>& so, int64_t dc = 0,
125
                   int64_t bytes = 0)
126
40
                : LRUCacheValueBase(),
127
40
                  version(v),
128
40
                  result(std::move(r)),
129
40
                  slot_orders(so),
130
40
                  delta_count(dc),
131
40
                  total_bytes(bytes) {
132
40
            total_rows = 0;
133
44
            for (const auto& block : result) {
134
44
                total_rows += block->rows();
135
44
            }
136
40
        }
137
    };
138
139
    // Create global instance of this class
140
44
    static QueryCache* create_global_cache(size_t capacity, uint32_t num_shards = 16) {
141
44
        auto* res = new QueryCache(capacity, num_shards);
142
44
        return res;
143
44
    }
144
145
    static Status build_cache_key(const std::vector<TScanRangeParams>& scan_ranges,
146
                                  const TQueryCacheParam& cache_param, std::string* cache_key,
147
106
                                  int64_t* version) {
148
106
        if (scan_ranges.empty()) {
149
1
            return Status::InternalError("scan_ranges is empty, plan error");
150
1
        }
151
152
105
        std::string digest;
153
105
        try {
154
105
            digest = cache_param.digest;
155
105
        } catch (const std::exception&) {
156
0
            return Status::InternalError("digest is invalid, plan error");
157
0
        }
158
105
        if (digest.empty()) {
159
0
            return Status::InternalError("digest is empty, plan error");
160
0
        }
161
162
105
        if (cache_param.tablet_to_range.empty()) {
163
1
            return Status::InternalError("tablet_to_range is empty, plan error");
164
1
        }
165
166
104
        std::vector<int64_t> tablet_ids;
167
104
        tablet_ids.reserve(scan_ranges.size());
168
110
        for (const auto& scan_range : scan_ranges) {
169
110
            auto tablet_id = scan_range.scan_range.palo_scan_range.tablet_id;
170
110
            tablet_ids.push_back(tablet_id);
171
110
        }
172
104
        std::sort(tablet_ids.begin(), tablet_ids.end());
173
174
104
        int64_t first_version = -1;
175
104
        std::string first_tablet_range;
176
208
        for (size_t i = 0; i < tablet_ids.size(); ++i) {
177
110
            auto tablet_id = tablet_ids[i];
178
179
110
            auto find_tablet = cache_param.tablet_to_range.find(tablet_id);
180
110
            if (find_tablet == cache_param.tablet_to_range.end()) {
181
3
                return Status::InternalError("Not find tablet in partition_to_tablets, plan error");
182
3
            }
183
184
107
            auto scan_range_iter =
185
107
                    std::find_if(scan_ranges.begin(), scan_ranges.end(),
186
113
                                 [&tablet_id](const TScanRangeParams& range) {
187
113
                                     return range.scan_range.palo_scan_range.tablet_id == tablet_id;
188
113
                                 });
189
107
            int64_t current_version = -1;
190
107
            std::from_chars(scan_range_iter->scan_range.palo_scan_range.version.data(),
191
107
                            scan_range_iter->scan_range.palo_scan_range.version.data() +
192
107
                                    scan_range_iter->scan_range.palo_scan_range.version.size(),
193
107
                            current_version);
194
195
107
            if (i == 0) {
196
102
                first_version = current_version;
197
102
                first_tablet_range = find_tablet->second;
198
102
            } else {
199
5
                if (current_version != first_version) {
200
1
                    return Status::InternalError(
201
1
                            "All tablets in one instance must have the same version, plan error");
202
1
                }
203
4
                if (find_tablet->second != first_tablet_range) {
204
2
                    return Status::InternalError(
205
2
                            "All tablets in one instance must have the same tablet_to_range, plan "
206
2
                            "error");
207
2
                }
208
4
            }
209
107
        }
210
211
98
        *version = first_version;
212
213
98
        *cache_key = digest;
214
100
        for (auto tablet_id : tablet_ids) {
215
100
            *cache_key += std::string(reinterpret_cast<char*>(&tablet_id), sizeof(tablet_id));
216
100
        }
217
98
        *cache_key += first_tablet_range;
218
219
98
        return Status::OK();
220
104
    }
221
222
    // Return global instance.
223
    // Client should call create_global_cache before.
224
37
    static QueryCache* instance() { return ExecEnv::GetInstance()->get_query_cache(); }
225
226
    QueryCache() = delete;
227
228
    QueryCache(size_t capacity, uint32_t num_shards)
229
44
            : LRUCachePolicy(CachePolicy::CacheType::QUERY_CACHE, capacity, LRUCacheType::SIZE,
230
44
                             3600 * 24, /*num_shards*/ num_shards,
231
44
                             /*element_count_capacity*/ 0, /*enable_prune*/ true,
232
44
                             /*is_lru_k*/ true) {}
233
234
    // Ensure Block memory freed during eviction is tracked under query cache, not Orphan.
235
2.08k
    int64_t adjust_capacity_weighted(double adjust_weighted) override {
236
2.08k
        SCOPED_SWITCH_THREAD_MEM_TRACKER_LIMITER(ExecEnv::GetInstance()->query_cache_mem_tracker());
237
2.08k
        return LRUCachePolicy::adjust_capacity_weighted(adjust_weighted);
238
2.08k
    }
239
240
0
    int64_t reset_initial_capacity(double adjust_weighted) override {
241
0
        SCOPED_SWITCH_THREAD_MEM_TRACKER_LIMITER(ExecEnv::GetInstance()->query_cache_mem_tracker());
242
0
        return LRUCachePolicy::reset_initial_capacity(adjust_weighted);
243
0
    }
244
245
225
    void prune_stale() override {
246
225
        SCOPED_SWITCH_THREAD_MEM_TRACKER_LIMITER(ExecEnv::GetInstance()->query_cache_mem_tracker());
247
225
        LRUCachePolicy::prune_stale();
248
225
    }
249
250
    bool lookup(const CacheKey& key, int64_t version, QueryCacheHandle* handle);
251
252
    // Look up the entry by key regardless of its version. The caller decides
253
    // whether the entry is an exact hit (cached version == expected version) or
254
    // a stale entry usable for incremental merge. Returns false if the key is
255
    // not in the cache at all.
256
    bool lookup_any_version(const CacheKey& key, QueryCacheHandle* handle);
257
258
    void insert(const CacheKey& key, int64_t version, CacheResult& result,
259
                const std::vector<int>& solt_orders, int64_t cache_size, int64_t delta_count = 0);
260
};
261
262
// The per-fragment-instance decision of how the query cache participates in the
263
// execution, made exactly once (see QueryCacheRuntime) and consumed by both the
264
// olap scan operator and the cache source operator, so the two operators can
265
// never disagree (e.g. scan skips scanning because the entry looked fresh while
266
// cache source misses because the entry got evicted in between -- which would
267
// silently produce an empty result and poison the cache with it).
268
struct QueryCacheInstanceDecision {
269
    enum class Mode {
270
        // Run the full scan and (if the key is valid) write the result back.
271
        MISS,
272
        // The cached entry matches the current version: emit cached blocks,
273
        // skip scanning entirely, do not write back.
274
        HIT,
275
        // A stale entry is reusable: scan only the delta rowsets in
276
        // (cached_version, current_version], emit the cached blocks and the
277
        // delta partial result side by side (the upstream merge aggregation
278
        // combines them), then write the merged entry back.
279
        INCREMENTAL,
280
    };
281
282
    ~QueryCacheInstanceDecision();
283
284
    // Take the pre-captured delta read source of one tablet. Returns nullptr if
285
    // absent (already taken or never captured). Only meaningful in INCREMENTAL
286
    // mode; each tablet's read source can be consumed exactly once.
287
    std::unique_ptr<TabletReadSource> take_delta_read_source(int64_t tablet_id);
288
289
    Mode mode = Mode::MISS;
290
    // False when build_cache_key failed (e.g. tablets in this instance carry
291
    // different versions because FE could not align instances to partitions).
292
    // In that case the query degrades to an uncached scan: no lookup, no write
293
    // back, but the query itself still succeeds.
294
    bool key_valid = false;
295
    // False when the merged entry could never satisfy entry_max_bytes/rows
296
    // because the reused cached entry alone already exceeds them: the query
297
    // still scans only the delta (INCREMENTAL), but skips cloning blocks for a
298
    // write back that would be discarded anyway.
299
    bool write_back_feasible = true;
300
    // Why a stale entry was not reused incrementally (empty when it was, or
301
    // when incremental merge is not enabled for this query). For the query
302
    // profile only.
303
    std::string incremental_fallback_reason;
304
    std::string cache_key;
305
    // The version this query is reading (from the scan ranges).
306
    int64_t current_version = 0;
307
    // Only set in INCREMENTAL mode: the version of the reused stale entry.
308
    int64_t cached_version = 0;
309
    // Only set in HIT/INCREMENTAL mode: delta merges accumulated on the entry.
310
    int64_t cached_delta_count = 0;
311
    // Pins the cache entry in HIT/INCREMENTAL mode so it cannot be evicted (and
312
    // its blocks cannot be freed) while this query is using it. Note the pin
313
    // lives until the fragment is torn down; when the merged entry replaces
314
    // this one under the same key, both stay in memory for that window and the
315
    // LRU usage accounting only sees the new one (the mem tracker still sees
316
    // both) -- bounded by (in-flight incremental queries) x entry size.
317
    QueryCacheHandle handle;
318
319
private:
320
    friend class QueryCacheRuntime;
321
    std::mutex _take_lock;
322
    // INCREMENTAL mode: read sources of (cached_version, current_version]
323
    // captured at decision time, keyed by tablet id. Captured eagerly so that a
324
    // capture failure (e.g. the delta versions were merged away by compaction)
325
    // downgrades the decision to MISS *before* any operator acts on it; if the
326
    // scan discovered the failure only at prepare time, the cache source might
327
    // already have decided to emit the stale blocks.
328
    std::unordered_map<int64_t, std::unique_ptr<TabletReadSource>> _delta_read_sources;
329
};
330
331
// Fragment-level query cache context shared by the olap scan operator and the
332
// cache source operator of the same fragment. Both operators obtain the cache
333
// decision of their instance through get_or_make_decision(); the first caller
334
// makes the decision and the other one observes the same object, whatever the
335
// operator local-state init order is.
336
class QueryCacheRuntime {
337
public:
338
    // `cache` is injectable for tests; production callers pass nullptr and the
339
    // global instance is used.
340
    explicit QueryCacheRuntime(const TQueryCacheParam& param, QueryCache* cache = nullptr)
341
46
            : _param(param), _cache(cache != nullptr ? cache : QueryCache::instance()) {}
342
343
25
    QueryCache* cache() const { return _cache; }
344
345
    // Row-binlog scans read a different data stream and must not serve or fill
346
    // the query cache. Called while building the operator tree (single
347
    // threaded, before any local state init), so no locking is needed.
348
1
    void disable_for_binlog_scan() { _binlog_scan = true; }
349
350
    // Idempotent: the first call for a given instance (identified by the cache
351
    // key derived from its scan ranges) makes the decision, later calls return
352
    // the same decision object. Never returns nullptr.
353
    std::shared_ptr<QueryCacheInstanceDecision> get_or_make_decision(
354
            const std::vector<TScanRangeParams>& scan_ranges);
355
356
#ifdef BE_TEST
357
    // Tests inject a hand-crafted decision (e.g. INCREMENTAL) for an instance,
358
    // since a real storage engine is unavailable to capture delta read sources.
359
    void inject_decision_for_test(const std::string& cache_key,
360
                                  std::shared_ptr<QueryCacheInstanceDecision> decision) {
361
        std::lock_guard<std::mutex> lock(_lock);
362
        _decisions[cache_key] = std::move(decision);
363
    }
364
#endif
365
366
private:
367
    void _make_decision(const std::vector<TScanRangeParams>& scan_ranges,
368
                        QueryCacheInstanceDecision* decision);
369
370
    // Try to turn a stale entry into an INCREMENTAL decision. Returns true on
371
    // success; on any failure the caller keeps the decision as MISS (full
372
    // recompute), which is always safe.
373
    bool _try_prepare_incremental(const std::vector<TScanRangeParams>& scan_ranges,
374
                                  QueryCacheInstanceDecision* decision);
375
376
    // Validate one tablet for incremental merge and capture its delta read
377
    // source of (cached_version, current_version]. On any failure records the
378
    // fallback reason in the decision and returns false.
379
    bool _capture_tablet_delta(int64_t tablet_id, int64_t cached_version,
380
                               QueryCacheInstanceDecision* decision);
381
382
    // Merge-on-write only: true if any delete-bitmap entry stamped with a
383
    // version inside (cached_version, current_version] targets a rowset
384
    // OUTSIDE the captured delta set, i.e. the delta window rewrote rows that
385
    // are already folded into the cached partial result (an upsert, a partial
386
    // update or a delete sign hit a key that predates the cached version).
387
    // Entries targeting the delta rowsets themselves are harmless: the delta
388
    // scan reads those rowsets with the delete bitmap applied.
389
    static bool _delta_rewrites_history(BaseTablet& tablet, const TabletReadSource& delta_source,
390
                                        int64_t cached_version, int64_t current_version);
391
392
    TQueryCacheParam _param;
393
    QueryCache* _cache = nullptr;
394
    bool _binlog_scan = false;
395
396
    std::mutex _lock;
397
    std::map<std::string, std::shared_ptr<QueryCacheInstanceDecision>> _decisions;
398
    // Shared by every instance whose cache key cannot be built (see
399
    // get_or_make_decision): one immutable MISS decision, one log line.
400
    std::shared_ptr<QueryCacheInstanceDecision> _invalid_decision;
401
};
402
403
} // namespace doris