be/src/runtime/query_cache/query_cache.cpp
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 | | #include "runtime/query_cache/query_cache.h" |
19 | | |
20 | | #include <limits> |
21 | | |
22 | | #include "cloud/config.h" |
23 | | #include "common/logging.h" |
24 | | #include "common/metrics/doris_metrics.h" |
25 | | #include "cpp/sync_point.h" |
26 | | #include "storage/olap_common.h" |
27 | | #include "storage/rowset/rowset.h" |
28 | | #include "storage/rowset/rowset_reader.h" |
29 | | #include "storage/tablet/base_tablet.h" |
30 | | #include "storage/tablet/tablet_meta.h" |
31 | | |
32 | | namespace doris { |
33 | | |
34 | 10 | std::vector<int>* QueryCacheHandle::get_cache_slot_orders() { |
35 | 10 | DCHECK(_handle); |
36 | 10 | auto result_ptr = reinterpret_cast<LRUHandle*>(_handle)->value; |
37 | 10 | return &((QueryCache::CacheValue*)(result_ptr))->slot_orders; |
38 | 10 | } |
39 | | |
40 | 13 | CacheResult* QueryCacheHandle::get_cache_result() { |
41 | 13 | DCHECK(_handle); |
42 | 13 | auto result_ptr = reinterpret_cast<LRUHandle*>(_handle)->value; |
43 | 13 | return &((QueryCache::CacheValue*)(result_ptr))->result; |
44 | 13 | } |
45 | | |
46 | 66 | int64_t QueryCacheHandle::get_cache_version() { |
47 | 66 | DCHECK(_handle); |
48 | 66 | auto result_ptr = reinterpret_cast<LRUHandle*>(_handle)->value; |
49 | 66 | return ((QueryCache::CacheValue*)(result_ptr))->version; |
50 | 66 | } |
51 | | |
52 | 28 | int64_t QueryCacheHandle::get_cache_delta_count() { |
53 | 28 | DCHECK(_handle); |
54 | 28 | auto result_ptr = reinterpret_cast<LRUHandle*>(_handle)->value; |
55 | 28 | return ((QueryCache::CacheValue*)(result_ptr))->delta_count; |
56 | 28 | } |
57 | | |
58 | 8 | int64_t QueryCacheHandle::get_cache_total_bytes() { |
59 | 8 | DCHECK(_handle); |
60 | 8 | auto result_ptr = reinterpret_cast<LRUHandle*>(_handle)->value; |
61 | 8 | return ((QueryCache::CacheValue*)(result_ptr))->total_bytes; |
62 | 8 | } |
63 | | |
64 | 8 | int64_t QueryCacheHandle::get_cache_total_rows() { |
65 | 8 | DCHECK(_handle); |
66 | 8 | auto result_ptr = reinterpret_cast<LRUHandle*>(_handle)->value; |
67 | 8 | return ((QueryCache::CacheValue*)(result_ptr))->total_rows; |
68 | 8 | } |
69 | | |
70 | | void QueryCache::insert(const CacheKey& key, int64_t version, CacheResult& res, |
71 | | const std::vector<int>& slot_orders, int64_t cache_size, |
72 | 39 | int64_t delta_count) { |
73 | 39 | SCOPED_SWITCH_THREAD_MEM_TRACKER_LIMITER(ExecEnv::GetInstance()->query_cache_mem_tracker()); |
74 | 39 | CacheResult cache_result; |
75 | 44 | for (auto& block_data : res) { |
76 | 44 | cache_result.emplace_back(Block::create_unique())->swap(block_data->clone_empty()); |
77 | 44 | ScopedMutableBlock scoped_mutable_block(cache_result.back().get()); |
78 | 44 | auto& mutable_block = scoped_mutable_block.mutable_block(); |
79 | 44 | auto st = mutable_block.merge(*block_data); |
80 | 44 | DORIS_CHECK(st.ok()); |
81 | 44 | } |
82 | 39 | auto cache_value_ptr = std::make_unique<QueryCache::CacheValue>( |
83 | 39 | version, std::move(cache_result), slot_orders, delta_count, cache_size); |
84 | | |
85 | 39 | QueryCacheHandle(this, LRUCachePolicy::insert(key, (void*)cache_value_ptr.release(), cache_size, |
86 | 39 | cache_size, CachePriority::NORMAL)); |
87 | 39 | DorisMetrics::instance()->query_cache_write_back_total->increment(1); |
88 | 39 | } |
89 | | |
90 | 9 | bool QueryCache::lookup(const CacheKey& key, int64_t version, doris::QueryCacheHandle* handle) { |
91 | 9 | SCOPED_SWITCH_THREAD_MEM_TRACKER_LIMITER(ExecEnv::GetInstance()->query_cache_mem_tracker()); |
92 | 9 | auto* lru_handle = LRUCachePolicy::lookup(key); |
93 | 9 | if (lru_handle) { |
94 | 9 | QueryCacheHandle tmp_handle(this, lru_handle); |
95 | 9 | if (tmp_handle.get_cache_version() == version) { |
96 | 5 | *handle = std::move(tmp_handle); |
97 | 5 | return true; |
98 | 5 | } |
99 | 9 | } |
100 | 4 | return false; |
101 | 9 | } |
102 | | |
103 | 43 | bool QueryCache::lookup_any_version(const CacheKey& key, QueryCacheHandle* handle) { |
104 | 43 | SCOPED_SWITCH_THREAD_MEM_TRACKER_LIMITER(ExecEnv::GetInstance()->query_cache_mem_tracker()); |
105 | 43 | auto* lru_handle = LRUCachePolicy::lookup(key); |
106 | 43 | if (lru_handle) { |
107 | 37 | *handle = QueryCacheHandle(this, lru_handle); |
108 | 37 | return true; |
109 | 37 | } |
110 | 6 | return false; |
111 | 43 | } |
112 | | |
113 | 42 | QueryCacheInstanceDecision::~QueryCacheInstanceDecision() = default; |
114 | | |
115 | | std::unique_ptr<TabletReadSource> QueryCacheInstanceDecision::take_delta_read_source( |
116 | 7 | int64_t tablet_id) { |
117 | 7 | std::lock_guard<std::mutex> lock(_take_lock); |
118 | 7 | auto it = _delta_read_sources.find(tablet_id); |
119 | 7 | if (it == _delta_read_sources.end()) { |
120 | 3 | return nullptr; |
121 | 3 | } |
122 | 4 | auto res = std::move(it->second); |
123 | 4 | _delta_read_sources.erase(it); |
124 | 4 | return res; |
125 | 7 | } |
126 | | |
127 | | std::shared_ptr<QueryCacheInstanceDecision> QueryCacheRuntime::get_or_make_decision( |
128 | 43 | const std::vector<TScanRangeParams>& scan_ranges) { |
129 | 43 | std::string cache_key; |
130 | 43 | int64_t version = 0; |
131 | 43 | Status st = QueryCache::build_cache_key(scan_ranges, _param, &cache_key, &version); |
132 | 43 | if (!st.ok()) { |
133 | | // No reliable cache key for this instance (e.g. FE could not align the |
134 | | // instance to a single partition so tablets carry different versions). |
135 | | // Degrade to an uncached scan instead of failing the query: both the |
136 | | // scan operator and the cache source operator observe the shared MISS |
137 | | // decision with key_valid=false, so nothing is looked up and nothing |
138 | | // is written back. This is an expected plan shape, so log only once |
139 | | // per fragment instead of twice per instance. |
140 | 2 | std::lock_guard<std::mutex> lock(_lock); |
141 | 2 | if (_invalid_decision == nullptr) { |
142 | 1 | LOG(WARNING) << "query cache degrades to uncached scan, node_id=" << _param.node_id |
143 | 1 | << ", reason: " << st.to_string(); |
144 | 1 | _invalid_decision = std::make_shared<QueryCacheInstanceDecision>(); |
145 | 1 | } |
146 | 2 | return _invalid_decision; |
147 | 2 | } |
148 | | |
149 | 41 | { |
150 | 41 | std::lock_guard<std::mutex> lock(_lock); |
151 | 41 | auto it = _decisions.find(cache_key); |
152 | 41 | if (it != _decisions.end()) { |
153 | 6 | return it->second; |
154 | 6 | } |
155 | 41 | } |
156 | | |
157 | | // Build the candidate outside the lock: a stale-entry decision walks |
158 | | // tablet metadata under the header lock and, on merge-on-write tables, |
159 | | // scans the delete bitmap, so its cost grows with the tablets per |
160 | | // instance and the bitmap size. This runtime is fragment-wide, so a |
161 | | // single lock-scoped build would serialize even unrelated instances' |
162 | | // keys behind one slow decision. Racing callers of the same instance |
163 | | // build duplicate candidates; the loser adopts the winner's decision |
164 | | // below and its own candidate releases with the shared_ptr (the entry |
165 | | // pin and any captured delta read sources go with it), which only costs |
166 | | // a redundant metadata pass. The metrics are settled once, by the |
167 | | // winner, after publication. |
168 | 35 | auto decision = std::make_shared<QueryCacheInstanceDecision>(); |
169 | 35 | decision->key_valid = true; |
170 | 35 | decision->cache_key = cache_key; |
171 | 35 | decision->current_version = version; |
172 | 35 | _make_decision(scan_ranges, decision.get()); |
173 | 35 | TEST_SYNC_POINT("QueryCacheRuntime::get_or_make_decision.before_publish"); |
174 | | |
175 | 35 | std::lock_guard<std::mutex> lock(_lock); |
176 | 35 | auto [it, inserted] = _decisions.emplace(std::move(cache_key), decision); |
177 | 35 | if (inserted) { |
178 | 32 | if (decision->mode == QueryCacheInstanceDecision::Mode::INCREMENTAL) { |
179 | 6 | DorisMetrics::instance()->query_cache_stale_hit_total->increment(1); |
180 | 26 | } else if (decision->mode == QueryCacheInstanceDecision::Mode::MISS && |
181 | 26 | !decision->incremental_fallback_reason.empty()) { |
182 | | // Only count real fallbacks: an empty reason means incremental |
183 | | // merge was not enabled for this query in the first place. |
184 | 15 | DorisMetrics::instance()->query_cache_incremental_fallback_total->increment(1); |
185 | 15 | } |
186 | 32 | } |
187 | 35 | return it->second; |
188 | 41 | } |
189 | | |
190 | | void QueryCacheRuntime::_make_decision(const std::vector<TScanRangeParams>& scan_ranges, |
191 | 35 | QueryCacheInstanceDecision* decision) { |
192 | 35 | decision->mode = QueryCacheInstanceDecision::Mode::MISS; |
193 | 35 | if (_binlog_scan) { |
194 | | // A row-binlog scan reads a different data stream under the same plan |
195 | | // digest: it must neither serve cached blocks nor write its output |
196 | | // back (that would poison the cache for normal queries). |
197 | 1 | decision->key_valid = false; |
198 | 1 | return; |
199 | 1 | } |
200 | 34 | if (_param.force_refresh_query_cache) { |
201 | 1 | return; |
202 | 1 | } |
203 | | |
204 | 33 | QueryCacheHandle handle; |
205 | 33 | if (!_cache->lookup_any_version(decision->cache_key, &handle)) { |
206 | 5 | return; |
207 | 5 | } |
208 | | // Pin the entry: as long as this decision object lives, the entry cannot be |
209 | | // evicted, so every operator consuming this decision sees the same blocks. |
210 | 28 | decision->handle = std::move(handle); |
211 | | |
212 | 28 | if (decision->handle.get_cache_version() == decision->current_version) { |
213 | 3 | decision->mode = QueryCacheInstanceDecision::Mode::HIT; |
214 | 3 | decision->cached_delta_count = decision->handle.get_cache_delta_count(); |
215 | 3 | return; |
216 | 3 | } |
217 | | |
218 | 25 | if (_try_prepare_incremental(scan_ranges, decision)) { |
219 | 7 | decision->mode = QueryCacheInstanceDecision::Mode::INCREMENTAL; |
220 | 7 | return; |
221 | 7 | } |
222 | | |
223 | | // Stale entry not reusable: drop the pin and fall back to a full scan. |
224 | | // The stale-hit/fallback metrics are settled by the caller once the |
225 | | // winning candidate is published, so a losing racer cannot double-count. |
226 | 18 | decision->_delta_read_sources.clear(); |
227 | 18 | decision->handle = QueryCacheHandle(); |
228 | 18 | decision->mode = QueryCacheInstanceDecision::Mode::MISS; |
229 | 18 | } |
230 | | |
231 | | bool QueryCacheRuntime::_try_prepare_incremental(const std::vector<TScanRangeParams>& scan_ranges, |
232 | 25 | QueryCacheInstanceDecision* decision) { |
233 | 25 | if (!(_param.__isset.allow_incremental && _param.allow_incremental)) { |
234 | | // Not a fallback: incremental merge is simply not enabled for this |
235 | | // query, so leave incremental_fallback_reason empty. |
236 | 2 | return false; |
237 | 2 | } |
238 | | // Cloud tablets capture rowsets through a different (partly asynchronous) |
239 | | // path; incremental merge only supports local storage for now. |
240 | 23 | if (config::is_cloud_mode()) { |
241 | 1 | decision->incremental_fallback_reason = "cloud mode"; |
242 | 1 | return false; |
243 | 1 | } |
244 | | |
245 | 22 | int64_t cached_version = decision->handle.get_cache_version(); |
246 | 22 | if (cached_version >= decision->current_version) { |
247 | | // The entry is newer than what this replica is asked to read (e.g. the |
248 | | // entry was filled from another replica with a newer visible version). |
249 | | // A full scan of the requested version is the only safe answer. |
250 | 1 | decision->incremental_fallback_reason = "cached entry is newer"; |
251 | 1 | return false; |
252 | 1 | } |
253 | 21 | int64_t cached_delta_count = decision->handle.get_cache_delta_count(); |
254 | 21 | if (cached_delta_count >= config::query_cache_max_incremental_merge_count) { |
255 | | // Force a full recompute to compact the entry: every incremental merge |
256 | | // appends the delta blocks to the entry, so both the entry and the |
257 | | // upstream merge get more fragmented as deltas accumulate. |
258 | 1 | decision->incremental_fallback_reason = "delta count reached compaction threshold"; |
259 | 1 | return false; |
260 | 1 | } |
261 | | |
262 | 20 | for (const auto& scan_range : scan_ranges) { |
263 | 20 | int64_t tablet_id = scan_range.scan_range.palo_scan_range.tablet_id; |
264 | 20 | if (!_capture_tablet_delta(tablet_id, cached_version, decision)) { |
265 | 13 | return false; |
266 | 13 | } |
267 | 20 | } |
268 | | |
269 | | // If the cached entry alone already exceeds the entry limits, the merged |
270 | | // entry (which can only be larger) could never be written back. Still scan |
271 | | // only the delta, but tell the cache source upfront so it does not clone |
272 | | // blocks for a write back that would be discarded anyway. Such an entry |
273 | | // stays stale until compaction merges its delta away (then the capture |
274 | | // above fails and a full recompute takes over). |
275 | 7 | if (decision->handle.get_cache_total_bytes() > _param.entry_max_bytes || |
276 | 7 | decision->handle.get_cache_total_rows() > _param.entry_max_rows) { |
277 | 1 | decision->write_back_feasible = false; |
278 | 1 | } |
279 | | |
280 | 7 | decision->cached_version = cached_version; |
281 | 7 | decision->cached_delta_count = cached_delta_count; |
282 | 7 | return true; |
283 | 20 | } |
284 | | |
285 | | bool QueryCacheRuntime::_capture_tablet_delta(int64_t tablet_id, int64_t cached_version, |
286 | 20 | QueryCacheInstanceDecision* decision) { |
287 | 20 | auto tablet_res = ExecEnv::get_tablet(tablet_id); |
288 | 20 | if (!tablet_res) { |
289 | 2 | decision->incremental_fallback_reason = "tablet not found"; |
290 | 2 | return false; |
291 | 2 | } |
292 | 18 | BaseTabletSPtr tablet = std::move(tablet_res.value()); |
293 | | // Defensive re-check of what FE promised with allow_incremental: |
294 | | // "cached snapshot + delta rowsets == new snapshot" holds unconditionally |
295 | | // for append-only DUP_KEYS data, and for merge-on-write UNIQUE data as |
296 | | // long as the delta did not rewrite any row that predates the cached |
297 | | // version (verified below once the delta rowsets are known). It never |
298 | | // holds for merge-on-read UNIQUE data (duplicates are resolved by merging |
299 | | // across rowsets at read time, so a delta-only scan cannot stand alone) |
300 | | // nor for AGG tables (rows merge in the storage layer likewise). |
301 | 18 | const bool merge_on_write = tablet->keys_type() == KeysType::UNIQUE_KEYS && |
302 | 18 | tablet->enable_unique_key_merge_on_write(); |
303 | 18 | if (tablet->keys_type() != KeysType::DUP_KEYS && !merge_on_write) { |
304 | 4 | decision->incremental_fallback_reason = "keys type not append-only"; |
305 | 4 | return false; |
306 | 4 | } |
307 | | |
308 | | // quiet: on a version-graph miss (the delta merged away by compaction, an |
309 | | // expected per-query situation) this option makes the capture API neither |
310 | | // log nor error; it returns whatever PREFIX of the path it walked before |
311 | | // the miss (empty when the window start itself is gone), so emptiness AND |
312 | | // window coverage are both checked below as the failure signals. |
313 | 14 | auto source_res = tablet->capture_read_source({cached_version + 1, decision->current_version}, |
314 | 14 | {.quiet = true}); |
315 | 14 | if (!source_res) { |
316 | | // Non-pathfinding failures (e.g. a rowset object missing for a found |
317 | | // path) still surface as errors even under quiet. |
318 | 1 | decision->incremental_fallback_reason = "delta versions not capturable"; |
319 | 1 | return false; |
320 | 1 | } |
321 | 13 | auto read_source = std::make_unique<TabletReadSource>(std::move(source_res.value())); |
322 | 13 | if (read_source->rs_splits.empty()) { |
323 | | // A non-empty version window always lies on a consistent path with at |
324 | | // least one rowset; an empty capture therefore means the delta |
325 | | // versions are gone, never "no new data". Treating it as INCREMENTAL |
326 | | // would silently drop the delta rows from the result. |
327 | 1 | decision->incremental_fallback_reason = "delta versions not capturable"; |
328 | 1 | return false; |
329 | 1 | } |
330 | | // A quiet capture broken mid-window (e.g. a replica whose local view ends |
331 | | // short of current_version) yields a partial prefix, and treating it as |
332 | | // INCREMENTAL would silently drop the tail rows and poison the entry on |
333 | | // write-back. The path is contiguous by construction, so checking both |
334 | | // endpoints pins the whole (cached_version, current_version] window. |
335 | 12 | if (read_source->rs_splits.front().rs_reader->rowset()->start_version() != cached_version + 1 || |
336 | 12 | read_source->rs_splits.back().rs_reader->rowset()->end_version() != |
337 | 12 | decision->current_version) { |
338 | 1 | decision->incremental_fallback_reason = "delta versions not capturable"; |
339 | 1 | return false; |
340 | 1 | } |
341 | 11 | read_source->fill_delete_predicates(); |
342 | 11 | if (!read_source->delete_predicates.empty()) { |
343 | | // A delete in the delta logically removes rows that are already folded |
344 | | // into the cached partial result; that cannot be undone by merging, so |
345 | | // fall back to a full recompute. |
346 | 1 | decision->incremental_fallback_reason = "delta contains delete predicates"; |
347 | 1 | return false; |
348 | 1 | } |
349 | | // Pin the cached side for as long as the classification below reads the |
350 | | // delete bitmap. The markers it looks for sit on the rowsets the cached |
351 | | // snapshot was built from, and the capture above pins only the delta |
352 | | // side, so nothing else keeps that evidence alive: once a compaction |
353 | | // spanning both sides retires such a rowset, an overwritten row has no |
354 | | // counterpart in the output and its marker is therefore not relocated |
355 | | // (BaseTablet::calc_compaction_output_rowset_delete_bitmap skips a source |
356 | | // row whose id does not convert), leaving the marker only on the retired |
357 | | // input, which the stale sweep then hands to the unused-rowset GC. |
358 | | // Classification would see no rewrite and merge the cached rows with |
359 | | // their replacements: a wrong entry, stored at the current version. |
360 | | // Holding the rowsets stops that, in start_delete_unused_rowset(): it |
361 | | // collects a retired rowset only once nothing else references it, and |
362 | | // only what it collects gets remove_rowset_delete_bitmap(), which drops |
363 | | // every version of that rowset's bitmap. The other remover, the key |
364 | | // ranges that the stale sweep queues onto the pre-window rowsets, is |
365 | | // held off by the delta capture instead: that queue is released only |
366 | | // once the swept path's own rowsets are unreferenced. |
367 | 10 | std::vector<RowsetSharedPtr> cached_side_pin; |
368 | 10 | if (merge_on_write) { |
369 | 6 | { |
370 | 6 | std::shared_lock rdlock(tablet->get_header_lock()); |
371 | 6 | auto pin_res = tablet->capture_consistent_rowsets_unlocked({0, cached_version}, |
372 | 6 | {.quiet = true}); |
373 | 6 | if (pin_res) { |
374 | 6 | cached_side_pin = std::move(pin_res.value().rowsets); |
375 | 6 | } |
376 | 6 | } |
377 | | // quiet yields the prefix it walked, so cover the whole cached range |
378 | | // before trusting the classification: an unpinned tail is evidence we |
379 | | // cannot vouch for. |
380 | 6 | if (cached_side_pin.empty() || cached_side_pin.front()->start_version() != 0 || |
381 | 6 | cached_side_pin.back()->end_version() != cached_version) { |
382 | 1 | decision->incremental_fallback_reason = "cached versions not pinnable"; |
383 | 1 | return false; |
384 | 1 | } |
385 | | // The pin is only worth anything while the classification runs, so |
386 | | // let a test observe it exactly there rather than infer it. |
387 | 6 | TEST_SYNC_POINT_CALLBACK("QueryCacheRuntime::_capture_tablet_delta.cached_side_pinned", |
388 | 5 | &cached_side_pin); |
389 | 5 | } |
390 | 9 | if (merge_on_write && |
391 | 9 | _delta_rewrites_history(*tablet, *read_source, cached_version, decision->current_version)) { |
392 | | // A load in the delta window marked rows of the cached snapshot as |
393 | | // deleted through the delete bitmap (an upsert, a partial update or a |
394 | | // delete sign hit a pre-existing key). Those rows are already folded |
395 | | // into the cached partial result and cannot be subtracted, so only a |
396 | | // full recompute is safe. The full run re-bases the entry at the new |
397 | | // version, so an occasional backfill costs exactly one full scan and |
398 | | // the next pure-append hour is incremental again. |
399 | 2 | decision->incremental_fallback_reason = "delta rewrites history rows"; |
400 | 2 | return false; |
401 | 2 | } |
402 | 7 | decision->_delta_read_sources[tablet_id] = std::move(read_source); |
403 | 7 | return true; |
404 | 9 | } |
405 | | |
406 | | bool QueryCacheRuntime::_delta_rewrites_history(BaseTablet& tablet, |
407 | | const TabletReadSource& delta_source, |
408 | 5 | int64_t cached_version, int64_t current_version) { |
409 | 5 | RowsetIdUnorderedSet delta_rowsets; |
410 | 6 | for (const auto& split : delta_source.rs_splits) { |
411 | 6 | delta_rowsets.insert(split.rs_reader->rowset()->rowset_id()); |
412 | 6 | } |
413 | | // The tablet delete bitmap is shared and it does change at versions that |
414 | | // are already visible, so what keeps this scan safe is not immutability |
415 | | // but the fallback it feeds. Loads add their markers before the version |
416 | | // becomes visible. Compaction relocates existing markers onto its output |
417 | | // rowset keeping their original version (BaseTablet::calc_compaction_ |
418 | | // output_rowset_delete_bitmap); markers that matter here sit on a rowset |
419 | | // at or below the cached version, and an output that swallowed such a |
420 | | // rowset starts no later than it does, so the relocated marker lands |
421 | | // outside the delta and is read as a rewrite: conservative, never a miss. |
422 | | // (An output whose inputs all sit inside the window does belong to the |
423 | | // delta, and markers on it are delta-internal, which is what we want.) The one |
424 | | // rewrite that moves markers out of the window, the aggregation that |
425 | | // re-stamps a stale range onto its end version (BaseTablet::agg_delete_ |
426 | | // bitmap_for_stale_rowsets, run by delete_expired_stale_rowset), only |
427 | | // fires once the merged versions are swept, and the capture this scan |
428 | | // pairs with anchors on the window endpoints, so a window whose path is |
429 | | // gone is rejected before we get here. Capture and scan are not atomic, |
430 | | // so a sweep landing exactly in between (with the aggregated keys also |
431 | | // already recycled) can still hide a rewrite: a known narrow race the |
432 | | // fallback does not cover, shared with the baseline exact-hit path. |
433 | | // Pending entries use DeleteBitmap::TEMP_VERSION_COMMON (= 0) and stay |
434 | | // below the window as well. |
435 | | // |
436 | | // The map is ordered by (rowset, segment, version), and most entries of |
437 | | // a group lie OUTSIDE the delta window (dedup history below it, pending |
438 | | // entries at 0, concurrent newer loads above it), so on a long-lived |
439 | | // merge-on-write tablet the map is much larger than the window. Hop from |
440 | | // group to group with lower_bound/upper_bound instead of visiting every |
441 | | // entry: the shared-lock hold, which excludes bitmap writers such as |
442 | | // load publish, is then bounded by the in-window entries plus one |
443 | | // logarithmic hop per group instead of the full map size. |
444 | | // Compare versions in the uint64 key domain (both window bounds are |
445 | | // non-negative), so a hypothetical out-of-int64-range stored version can |
446 | | // only take the above-window hop, whose target is strictly past the |
447 | | // whole group: forward progress holds for arbitrary map contents, like |
448 | | // the plain walk this replaces. |
449 | 5 | const auto window_begin = static_cast<uint64_t>(cached_version) + 1; |
450 | 5 | const auto window_end = static_cast<uint64_t>(current_version); |
451 | 5 | const DeleteBitmap& delete_bitmap = tablet.tablet_meta()->delete_bitmap(); |
452 | 5 | std::shared_lock rdlock(delete_bitmap.lock); |
453 | 5 | const auto& bitmap_map = delete_bitmap.delete_bitmap; |
454 | 5 | auto it = bitmap_map.begin(); |
455 | 11 | while (it != bitmap_map.end()) { |
456 | 8 | const auto& [rowset_id, segment_id, version_key] = it->first; |
457 | 8 | if (version_key < window_begin) { |
458 | 1 | it = bitmap_map.lower_bound({rowset_id, segment_id, window_begin}); |
459 | 1 | continue; |
460 | 1 | } |
461 | 7 | if (version_key > window_end) { |
462 | 3 | it = bitmap_map.upper_bound( |
463 | 3 | {rowset_id, segment_id, std::numeric_limits<uint64_t>::max()}); |
464 | 3 | continue; |
465 | 3 | } |
466 | 4 | if (!it->second.isEmpty() && !delta_rowsets.contains(rowset_id)) { |
467 | 2 | return true; |
468 | 2 | } |
469 | 2 | ++it; |
470 | 2 | } |
471 | 3 | return false; |
472 | 5 | } |
473 | | |
474 | | } // namespace doris |