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 |