blob: ed353c08ad73509442f20535a1c2affe86a82e4c [file]
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
#pragma once
#include <butil/macros.h>
#include <gen_cpp/PaloInternalService_types.h>
#include <gen_cpp/QueryCache_types.h>
#include <glog/logging.h>
#include <stddef.h>
#include <stdint.h>
#include <atomic>
#include <map>
#include <memory>
#include <mutex>
#include <roaring/roaring.hh>
#include <string>
#include <unordered_map>
#include <vector>
#include "common/config.h"
#include "common/status.h"
#include "core/block/block.h"
#include "io/fs/file_system.h"
#include "io/fs/path.h"
#include "runtime/exec_env.h"
#include "runtime/memory/lru_cache_policy.h"
#include "runtime/memory/mem_tracker.h"
#include "util/lru_cache.h"
#include "util/slice.h"
#include "util/time.h"
namespace doris {
class BaseTablet;
struct TabletReadSource;
using CacheResult = std::vector<BlockUPtr>;
// A handle for mid-result from query lru cache.
// The handle will automatically release the cache entry when it is destroyed.
// So the caller need to make sure the handle is valid in lifecycle.
class QueryCacheHandle {
public:
QueryCacheHandle() = default;
QueryCacheHandle(LRUCachePolicy* cache, Cache::Handle* handle)
: _cache(cache), _handle(handle) {}
~QueryCacheHandle() {
if (_handle != nullptr) {
CHECK(_cache != nullptr);
{
SCOPED_SWITCH_THREAD_MEM_TRACKER_LIMITER(
ExecEnv::GetInstance()->query_cache_mem_tracker());
_cache->release(_handle);
}
}
}
QueryCacheHandle(QueryCacheHandle&& other) noexcept {
std::swap(_cache, other._cache);
std::swap(_handle, other._handle);
}
QueryCacheHandle& operator=(QueryCacheHandle&& other) noexcept {
std::swap(_cache, other._cache);
std::swap(_handle, other._handle);
return *this;
}
bool valid() const { return _handle != nullptr; }
std::vector<int>* get_cache_slot_orders();
CacheResult* get_cache_result();
int64_t get_cache_version();
// How many incremental merges have been accumulated on this entry since the
// last full recompute. See QueryCacheRuntime for the compaction policy.
int64_t get_cache_delta_count();
int64_t get_cache_total_bytes();
int64_t get_cache_total_rows();
private:
LRUCachePolicy* _cache = nullptr;
Cache::Handle* _handle = nullptr;
// Don't allow copy and assign
DISALLOW_COPY_AND_ASSIGN(QueryCacheHandle);
};
class QueryCache : public LRUCachePolicy {
public:
using LRUCachePolicy::insert;
struct CacheValue : public LRUCacheValueBase {
int64_t version;
CacheResult result;
std::vector<int> slot_orders;
// Number of incremental merges accumulated on this entry since the last
// full recompute. 0 means the entry was produced by a full scan.
int64_t delta_count;
// Size of this entry, used to decide upfront whether an incremental
// merge could ever be written back under the entry_max_bytes/rows
// limits (a merged entry can only be larger than the cached one).
int64_t total_bytes;
int64_t total_rows;
CacheValue(int64_t v, CacheResult&& r, const std::vector<int>& so, int64_t dc = 0,
int64_t bytes = 0)
: LRUCacheValueBase(),
version(v),
result(std::move(r)),
slot_orders(so),
delta_count(dc),
total_bytes(bytes) {
total_rows = 0;
for (const auto& block : result) {
total_rows += block->rows();
}
}
};
// Create global instance of this class
static QueryCache* create_global_cache(size_t capacity, uint32_t num_shards = 16) {
auto* res = new QueryCache(capacity, num_shards);
return res;
}
static Status build_cache_key(const std::vector<TScanRangeParams>& scan_ranges,
const TQueryCacheParam& cache_param, std::string* cache_key,
int64_t* version) {
if (scan_ranges.empty()) {
return Status::InternalError("scan_ranges is empty, plan error");
}
std::string digest;
try {
digest = cache_param.digest;
} catch (const std::exception&) {
return Status::InternalError("digest is invalid, plan error");
}
if (digest.empty()) {
return Status::InternalError("digest is empty, plan error");
}
if (cache_param.tablet_to_range.empty()) {
return Status::InternalError("tablet_to_range is empty, plan error");
}
std::vector<int64_t> tablet_ids;
tablet_ids.reserve(scan_ranges.size());
for (const auto& scan_range : scan_ranges) {
auto tablet_id = scan_range.scan_range.palo_scan_range.tablet_id;
tablet_ids.push_back(tablet_id);
}
std::sort(tablet_ids.begin(), tablet_ids.end());
int64_t first_version = -1;
std::string first_tablet_range;
for (size_t i = 0; i < tablet_ids.size(); ++i) {
auto tablet_id = tablet_ids[i];
auto find_tablet = cache_param.tablet_to_range.find(tablet_id);
if (find_tablet == cache_param.tablet_to_range.end()) {
return Status::InternalError("Not find tablet in partition_to_tablets, plan error");
}
auto scan_range_iter =
std::find_if(scan_ranges.begin(), scan_ranges.end(),
[&tablet_id](const TScanRangeParams& range) {
return range.scan_range.palo_scan_range.tablet_id == tablet_id;
});
int64_t current_version = -1;
std::from_chars(scan_range_iter->scan_range.palo_scan_range.version.data(),
scan_range_iter->scan_range.palo_scan_range.version.data() +
scan_range_iter->scan_range.palo_scan_range.version.size(),
current_version);
if (i == 0) {
first_version = current_version;
first_tablet_range = find_tablet->second;
} else {
if (current_version != first_version) {
return Status::InternalError(
"All tablets in one instance must have the same version, plan error");
}
if (find_tablet->second != first_tablet_range) {
return Status::InternalError(
"All tablets in one instance must have the same tablet_to_range, plan "
"error");
}
}
}
*version = first_version;
*cache_key = digest;
for (auto tablet_id : tablet_ids) {
*cache_key += std::string(reinterpret_cast<char*>(&tablet_id), sizeof(tablet_id));
}
*cache_key += first_tablet_range;
return Status::OK();
}
// Return global instance.
// Client should call create_global_cache before.
static QueryCache* instance() { return ExecEnv::GetInstance()->get_query_cache(); }
QueryCache() = delete;
QueryCache(size_t capacity, uint32_t num_shards)
: LRUCachePolicy(CachePolicy::CacheType::QUERY_CACHE, capacity, LRUCacheType::SIZE,
3600 * 24, /*num_shards*/ num_shards,
/*element_count_capacity*/ 0, /*enable_prune*/ true,
/*is_lru_k*/ true) {}
// Ensure Block memory freed during eviction is tracked under query cache, not Orphan.
int64_t adjust_capacity_weighted(double adjust_weighted) override {
SCOPED_SWITCH_THREAD_MEM_TRACKER_LIMITER(ExecEnv::GetInstance()->query_cache_mem_tracker());
return LRUCachePolicy::adjust_capacity_weighted(adjust_weighted);
}
int64_t reset_initial_capacity(double adjust_weighted) override {
SCOPED_SWITCH_THREAD_MEM_TRACKER_LIMITER(ExecEnv::GetInstance()->query_cache_mem_tracker());
return LRUCachePolicy::reset_initial_capacity(adjust_weighted);
}
void prune_stale() override {
SCOPED_SWITCH_THREAD_MEM_TRACKER_LIMITER(ExecEnv::GetInstance()->query_cache_mem_tracker());
LRUCachePolicy::prune_stale();
}
bool lookup(const CacheKey& key, int64_t version, QueryCacheHandle* handle);
// Look up the entry by key regardless of its version. The caller decides
// whether the entry is an exact hit (cached version == expected version) or
// a stale entry usable for incremental merge. Returns false if the key is
// not in the cache at all.
bool lookup_any_version(const CacheKey& key, QueryCacheHandle* handle);
void insert(const CacheKey& key, int64_t version, CacheResult& result,
const std::vector<int>& solt_orders, int64_t cache_size, int64_t delta_count = 0);
};
// The per-fragment-instance decision of how the query cache participates in the
// execution, made exactly once (see QueryCacheRuntime) and consumed by both the
// olap scan operator and the cache source operator, so the two operators can
// never disagree (e.g. scan skips scanning because the entry looked fresh while
// cache source misses because the entry got evicted in between -- which would
// silently produce an empty result and poison the cache with it).
struct QueryCacheInstanceDecision {
enum class Mode {
// Run the full scan and (if the key is valid) write the result back.
MISS,
// The cached entry matches the current version: emit cached blocks,
// skip scanning entirely, do not write back.
HIT,
// A stale entry is reusable: scan only the delta rowsets in
// (cached_version, current_version], emit the cached blocks and the
// delta partial result side by side (the upstream merge aggregation
// combines them), then write the merged entry back.
INCREMENTAL,
};
~QueryCacheInstanceDecision();
// Take the pre-captured delta read source of one tablet. Returns nullptr if
// absent (already taken or never captured). Only meaningful in INCREMENTAL
// mode; each tablet's read source can be consumed exactly once.
std::unique_ptr<TabletReadSource> take_delta_read_source(int64_t tablet_id);
Mode mode = Mode::MISS;
// False when build_cache_key failed (e.g. tablets in this instance carry
// different versions because FE could not align instances to partitions).
// In that case the query degrades to an uncached scan: no lookup, no write
// back, but the query itself still succeeds.
bool key_valid = false;
// False when the merged entry could never satisfy entry_max_bytes/rows
// because the reused cached entry alone already exceeds them: the query
// still scans only the delta (INCREMENTAL), but skips cloning blocks for a
// write back that would be discarded anyway.
bool write_back_feasible = true;
// Why a stale entry was not reused incrementally (empty when it was, or
// when incremental merge is not enabled for this query). For the query
// profile only.
std::string incremental_fallback_reason;
std::string cache_key;
// The version this query is reading (from the scan ranges).
int64_t current_version = 0;
// Only set in INCREMENTAL mode: the version of the reused stale entry.
int64_t cached_version = 0;
// Only set in HIT/INCREMENTAL mode: delta merges accumulated on the entry.
int64_t cached_delta_count = 0;
// Pins the cache entry in HIT/INCREMENTAL mode so it cannot be evicted (and
// its blocks cannot be freed) while this query is using it. Note the pin
// lives until the fragment is torn down; when the merged entry replaces
// this one under the same key, both stay in memory for that window and the
// LRU usage accounting only sees the new one (the mem tracker still sees
// both) -- bounded by (in-flight incremental queries) x entry size.
QueryCacheHandle handle;
private:
friend class QueryCacheRuntime;
std::mutex _take_lock;
// INCREMENTAL mode: read sources of (cached_version, current_version]
// captured at decision time, keyed by tablet id. Captured eagerly so that a
// capture failure (e.g. the delta versions were merged away by compaction)
// downgrades the decision to MISS *before* any operator acts on it; if the
// scan discovered the failure only at prepare time, the cache source might
// already have decided to emit the stale blocks.
std::unordered_map<int64_t, std::unique_ptr<TabletReadSource>> _delta_read_sources;
};
// Fragment-level query cache context shared by the olap scan operator and the
// cache source operator of the same fragment. Both operators obtain the cache
// decision of their instance through get_or_make_decision(); the first caller
// makes the decision and the other one observes the same object, whatever the
// operator local-state init order is.
class QueryCacheRuntime {
public:
// `cache` is injectable for tests; production callers pass nullptr and the
// global instance is used.
explicit QueryCacheRuntime(const TQueryCacheParam& param, QueryCache* cache = nullptr)
: _param(param), _cache(cache != nullptr ? cache : QueryCache::instance()) {}
QueryCache* cache() const { return _cache; }
// Row-binlog scans read a different data stream and must not serve or fill
// the query cache. Called while building the operator tree (single
// threaded, before any local state init), so no locking is needed.
void disable_for_binlog_scan() { _binlog_scan = true; }
// Idempotent: the first call for a given instance (identified by the cache
// key derived from its scan ranges) makes the decision, later calls return
// the same decision object. Never returns nullptr.
std::shared_ptr<QueryCacheInstanceDecision> get_or_make_decision(
const std::vector<TScanRangeParams>& scan_ranges);
#ifdef BE_TEST
// Tests inject a hand-crafted decision (e.g. INCREMENTAL) for an instance,
// since a real storage engine is unavailable to capture delta read sources.
void inject_decision_for_test(const std::string& cache_key,
std::shared_ptr<QueryCacheInstanceDecision> decision) {
std::lock_guard<std::mutex> lock(_lock);
_decisions[cache_key] = std::move(decision);
}
#endif
private:
void _make_decision(const std::vector<TScanRangeParams>& scan_ranges,
QueryCacheInstanceDecision* decision);
// Try to turn a stale entry into an INCREMENTAL decision. Returns true on
// success; on any failure the caller keeps the decision as MISS (full
// recompute), which is always safe.
bool _try_prepare_incremental(const std::vector<TScanRangeParams>& scan_ranges,
QueryCacheInstanceDecision* decision);
// Validate one tablet for incremental merge and capture its delta read
// source of (cached_version, current_version]. On any failure records the
// fallback reason in the decision and returns false.
bool _capture_tablet_delta(int64_t tablet_id, int64_t cached_version,
QueryCacheInstanceDecision* decision);
// Merge-on-write only: true if any delete-bitmap entry stamped with a
// version inside (cached_version, current_version] targets a rowset
// OUTSIDE the captured delta set, i.e. the delta window rewrote rows that
// are already folded into the cached partial result (an upsert, a partial
// update or a delete sign hit a key that predates the cached version).
// Entries targeting the delta rowsets themselves are harmless: the delta
// scan reads those rowsets with the delete bitmap applied.
static bool _delta_rewrites_history(BaseTablet& tablet, const TabletReadSource& delta_source,
int64_t cached_version, int64_t current_version);
TQueryCacheParam _param;
QueryCache* _cache = nullptr;
bool _binlog_scan = false;
std::mutex _lock;
std::map<std::string, std::shared_ptr<QueryCacheInstanceDecision>> _decisions;
// Shared by every instance whose cache key cannot be built (see
// get_or_make_decision): one immutable MISS decision, one log line.
std::shared_ptr<QueryCacheInstanceDecision> _invalid_decision;
};
} // namespace doris