Coverage Report

Created: 2026-08-06 18:45

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