Merge pull request #48284 from ClickHouse/rs/qc-quota

Query Cache: Allow per-user quotas
This commit is contained in:
Robert Schulze 2023-04-24 11:32:32 +02:00 committed by GitHub
commit ded8eca041
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 298 additions and 53 deletions

View File

@ -88,6 +88,33 @@ If the query was aborted due to an exception or user cancellation, no entry is w
The size of the query cache in bytes, the maximum number of cache entries and the maximum size of individual cache entries (in bytes and in
records) can be configured using different [server configuration options](server-configuration-parameters/settings.md#server_configuration_parameters_query-cache).
It is also possible to limit the cache usage of individual users using [settings profiles](settings/settings-profiles.md) and [settings
constraints](settings/constraints-on-settings.md). More specifically, you can restrict the maximum amount of memory (in bytes) a user may
allocate in the query cache and the the maximum number of stored query results. For that, first provide configurations
[query_cache_max_size_in_bytes](settings/settings.md#query-cache-max-size-in-bytes) and
[query_cache_max_entries](settings/settings.md#query-cache-size-max-items) in a user profile in `users.xml`, then make both settings
readonly:
``` xml
<profiles>
<default>
<!-- The maximum cache size in bytes for user/profile 'default' -->
<query_cache_max_size_in_bytes>10000</query_cache_max_size_in_bytes>
<!-- The maximum number of SELECT query results stored in the cache for user/profile 'default' -->
<query_cache_max_entries>100</query_cache_max_entries>
<!-- Make both settings read-only so the user cannot change them -->
<constraints>
<query_cache_max_size_in_bytes>
<readonly/>
</query_cache_max_size_in_bytes>
<query_cache_max_entries>
<readonly/>
<query_cache_max_entries>
</constraints>
</default>
</profiles>
```
To define how long a query must run at least such that its result can be cached, you can use setting
[query_cache_min_query_duration](settings/settings.md#query-cache-min-query-duration). For example, the result of query

View File

@ -1382,25 +1382,25 @@ If the table does not exist, ClickHouse will create it. If the structure of the
The following settings are available:
- `max_size`: The maximum cache size in bytes. 0 means the query cache is disabled. Default value: `1073741824` (1 GiB).
- `max_size_in_bytes`: The maximum cache size in bytes. 0 means the query cache is disabled. Default value: `1073741824` (1 GiB).
- `max_entries`: The maximum number of `SELECT` query results stored in the cache. Default value: `1024`.
- `max_entry_size`: The maximum size in bytes `SELECT` query results may have to be saved in the cache. Default value: `1048576` (1 MiB).
- `max_entry_rows`: The maximum number of rows `SELECT` query results may have to be saved in the cache. Default value: `30000000` (30 mil).
- `max_entry_size_in_bytes`: The maximum size in bytes `SELECT` query results may have to be saved in the cache. Default value: `1048576` (1 MiB).
- `max_entry_size_in_rows`: The maximum number of rows `SELECT` query results may have to be saved in the cache. Default value: `30000000` (30 mil).
Changed settings take effect immediately.
:::note
Data for the query cache is allocated in DRAM. If memory is scarce, make sure to set a small value for `max_size` or disable the query cache altogether.
Data for the query cache is allocated in DRAM. If memory is scarce, make sure to set a small value for `max_size_in_bytes` or disable the query cache altogether.
:::
**Example**
```xml
<query_cache>
<max_size>1073741824</max_size>
<max_size_in_bytes>1073741824</max_size_in_bytes>
<max_entries>1024</max_entries>
<max_entry_size>1048576</max_entry_size>
<max_entry_rows>30000000</max_entry_rows>
<max_entry_size_in_bytes>1048576</max_entry_size_in_bytes>
<max_entry_size_in_rows>30000000</max_entry_size_in_rows>
</query_cache>
```

View File

@ -1512,6 +1512,26 @@ Possible values:
Default value: `0`.
## query_cache_max_size_in_bytes {#query-cache-max-size-in-bytes}
The maximum amount of memory (in bytes) the current user may allocate in the query cache. 0 means unlimited.
Possible values:
- Positive integer >= 0.
Default value: 0 (no restriction).
## query_cache_max_entries {#query-cache-max-entries}
The maximum number of query results the current user may store in the query cache. 0 means unlimited.
Possible values:
- Positive integer >= 0.
Default value: 0 (no restriction).
## insert_quorum {#settings-insert_quorum}
Enables the quorum writes.

View File

@ -1517,10 +1517,10 @@
<!-- Configuration for the query cache -->
<!-- <query_cache> -->
<!-- <max_size>1073741824</max_size> -->
<!-- <max_size_in_bytes>1073741824</max_size_in_bytes> -->
<!-- <max_entries>1024</max_entries> -->
<!-- <max_entry_size>1048576</max_entry_size> -->
<!-- <max_entry_rows>30000000</max_entry_rows> -->
<!-- <max_entry_size_in_bytes>1048576</max_entry_size_in_bytes> -->
<!-- <max_entry_size_in_rows>30000000</max_entry_size_in_rows> -->
<!-- </query_cache> -->
<!-- Uncomment if enable merge tree metadata cache -->

View File

@ -214,13 +214,19 @@ public:
void setMaxCount(size_t max_count)
{
std::lock_guard lock(mutex);
return cache_policy->setMaxCount(max_count, lock);
cache_policy->setMaxCount(max_count, lock);
}
void setMaxSize(size_t max_size_in_bytes)
{
std::lock_guard lock(mutex);
return cache_policy->setMaxSize(max_size_in_bytes, lock);
cache_policy->setMaxSize(max_size_in_bytes, lock);
}
void setQuotaForUser(const String & user_name, size_t max_size_in_bytes, size_t max_entries)
{
std::lock_guard lock(mutex);
cache_policy->setQuotaForUser(user_name, max_size_in_bytes, max_entries, lock);
}
virtual ~CacheBase() = default;

View File

@ -1,6 +1,7 @@
#pragma once
#include <Common/Exception.h>
#include <Common/ICachePolicyUserQuota.h>
#include <functional>
#include <memory>
@ -38,12 +39,16 @@ public:
MappedPtr mapped;
};
virtual size_t weight(std::lock_guard<std::mutex> & /* cache_lock */) const = 0;
virtual size_t count(std::lock_guard<std::mutex> & /* cache_lock */) const = 0;
virtual size_t maxSize(std::lock_guard<std::mutex>& /* cache_lock */) const = 0;
explicit ICachePolicy(CachePolicyUserQuotaPtr user_quotas_) : user_quotas(std::move(user_quotas_)) {}
virtual ~ICachePolicy() = default;
virtual size_t weight(std::lock_guard<std::mutex> & /*cache_lock*/) const = 0;
virtual size_t count(std::lock_guard<std::mutex> & /*cache_lock*/) const = 0;
virtual size_t maxSize(std::lock_guard<std::mutex>& /*cache_lock*/) const = 0;
virtual void setMaxCount(size_t /*max_count*/, std::lock_guard<std::mutex> & /* cache_lock */) { throw Exception(ErrorCodes::NOT_IMPLEMENTED, "Not implemented for cache policy"); }
virtual void setMaxSize(size_t /*max_size_in_bytes*/, std::lock_guard<std::mutex> & /* cache_lock */) { throw Exception(ErrorCodes::NOT_IMPLEMENTED, "Not implemented for cache policy"); }
virtual void setQuotaForUser(const String & user_name, size_t max_size_in_bytes, size_t max_entries, std::lock_guard<std::mutex> & /*cache_lock*/) { user_quotas->setQuotaForUser(user_name, max_size_in_bytes, max_entries); }
/// HashFunction usually hashes the entire key and the found key will be equal the provided key. In such cases, use get(). It is also
/// possible to store other, non-hashed data in the key. In that case, the found key is potentially different from the provided key.
@ -51,14 +56,15 @@ public:
virtual MappedPtr get(const Key & key, std::lock_guard<std::mutex> & /* cache_lock */) = 0;
virtual std::optional<KeyMapped> getWithKey(const Key &, std::lock_guard<std::mutex> & /*cache_lock*/) = 0;
virtual void set(const Key & key, const MappedPtr & mapped, std::lock_guard<std::mutex> & /* cache_lock */) = 0;
virtual void set(const Key & key, const MappedPtr & mapped, std::lock_guard<std::mutex> & /*cache_lock*/) = 0;
virtual void remove(const Key & key, std::lock_guard<std::mutex> & /* cache_lock */) = 0;
virtual void remove(const Key & key, std::lock_guard<std::mutex> & /*cache_lock*/) = 0;
virtual void reset(std::lock_guard<std::mutex> & /* cache_lock */) = 0;
virtual void reset(std::lock_guard<std::mutex> & /*cache_lock*/) = 0;
virtual std::vector<KeyMapped> dump() const = 0;
virtual ~ICachePolicy() = default;
protected:
CachePolicyUserQuotaPtr user_quotas;
};
}

View File

@ -0,0 +1,43 @@
#pragma once
#include <base/types.h>
namespace DB
{
/// Per-user quotas for usage of shared caches, used by ICachePolicy.
/// Currently allows to limit
/// - the maximum amount of cache memory a user may consume
/// - the maximum number of items a user can store in the cache
/// Note that caches usually also have global limits which restrict these values at cache level. Per-user quotas have no effect if they
/// exceed the global thresholds.
class ICachePolicyUserQuota
{
public:
/// Register or update the user's quota for the given resource.
virtual void setQuotaForUser(const String & user_name, size_t max_size_in_bytes, size_t max_entries) = 0;
/// Update the actual resource usage for the given user.
virtual void increaseActual(const String & user_name, size_t entry_size_in_bytes) = 0;
virtual void decreaseActual(const String & user_name, size_t entry_size_in_bytes) = 0;
/// Is the user allowed to write a new entry into the cache?
virtual bool approveWrite(const String & user_name, size_t entry_size_in_bytes) const = 0;
virtual ~ICachePolicyUserQuota() = default;
};
using CachePolicyUserQuotaPtr = std::unique_ptr<ICachePolicyUserQuota>;
class NoCachePolicyUserQuota : public ICachePolicyUserQuota
{
public:
void setQuotaForUser(const String & /*user_name*/, size_t /*max_size_in_bytes*/, size_t /*max_entries*/) override {}
void increaseActual(const String & /*user_name*/, size_t /*entry_size_in_bytes*/) override {}
void decreaseActual(const String & /*user_name*/, size_t /*entry_size_in_bytes*/) override {}
bool approveWrite(const String & /*user_name*/, size_t /*entry_size_in_bytes*/) const override { return true; }
};
}

View File

@ -27,7 +27,8 @@ public:
* max_count == 0 means no elements size restrictions.
*/
LRUCachePolicy(size_t max_size_in_bytes_, size_t max_count_, OnWeightLossFunction on_weight_loss_function_)
: max_size_in_bytes(std::max(1uz, max_size_in_bytes_))
: Base(std::make_unique<NoCachePolicyUserQuota>())
, max_size_in_bytes(std::max(1uz, max_size_in_bytes_))
, max_count(max_count_)
, on_weight_loss_function(on_weight_loss_function_)
{

View File

@ -31,7 +31,8 @@ public:
*/
/// TODO: construct from special struct with cache policy parameters (also with max_protected_size).
SLRUCachePolicy(size_t max_size_in_bytes_, size_t max_count_, double size_ratio, OnWeightLossFunction on_weight_loss_function_)
: max_protected_size(static_cast<size_t>(max_size_in_bytes_ * std::min(1.0, size_ratio)))
: Base(std::make_unique<NoCachePolicyUserQuota>())
, max_protected_size(static_cast<size_t>(max_size_in_bytes_ * std::min(1.0, size_ratio)))
, max_size_in_bytes(max_size_in_bytes_)
, max_count(max_count_)
, on_weight_loss_function(on_weight_loss_function_)

View File

@ -2,11 +2,80 @@
#include <Common/ICachePolicy.h>
#include <limits>
#include <unordered_map>
namespace DB
{
class PerUserTTLCachePolicyUserQuota : public ICachePolicyUserQuota
{
public:
void setQuotaForUser(const String & user_name, size_t max_size_in_bytes, size_t max_entries) override
{
quotas[user_name] = {max_size_in_bytes, max_entries};
}
void increaseActual(const String & user_name, size_t entry_size_in_bytes) override
{
auto & actual_for_user = actual[user_name];
actual_for_user.size_in_bytes += entry_size_in_bytes;
actual_for_user.num_items += 1;
}
void decreaseActual(const String & user_name, size_t entry_size_in_bytes) override
{
chassert(actual.contains(user_name));
chassert(actual[user_name].size_in_bytes >= entry_size_in_bytes);
actual[user_name].size_in_bytes -= entry_size_in_bytes;
chassert(actual[user_name].num_items >= 1);
actual[user_name].num_items -= 1;
}
bool approveWrite(const String & user_name, size_t entry_size_in_bytes) const override
{
auto it_actual = actual.find(user_name);
Resources actual_for_user{.size_in_bytes = 0, .num_items = 0}; /// assume zero actual resource consumption is user isn't found
if (it_actual != actual.end())
actual_for_user = it_actual->second;
auto it_quota = quotas.find(user_name);
Resources quota_for_user{.size_in_bytes = std::numeric_limits<size_t>::max(), .num_items = std::numeric_limits<size_t>::max()}; /// assume no threshold if no quota is found
if (it_quota != quotas.end())
quota_for_user = it_quota->second;
/// Special case: A quota configured as 0 means no threshold
if (quota_for_user.size_in_bytes == 0)
quota_for_user.size_in_bytes = std::numeric_limits<UInt64>::max();
if (quota_for_user.num_items == 0)
quota_for_user.num_items = std::numeric_limits<UInt64>::max();
/// Check size quota
if (actual_for_user.size_in_bytes + entry_size_in_bytes >= quota_for_user.size_in_bytes)
return false;
/// Check items quota
if (quota_for_user.num_items + 1 >= quota_for_user.num_items)
return false;
return true;
}
struct Resources
{
size_t size_in_bytes = 0;
size_t num_items = 0;
};
/// user name --> cache size quota (in bytes) / number of items quota
std::map<String, Resources> quotas;
/// user name --> actual cache usage (in bytes) / number of items
std::map<String, Resources> actual;
};
/// TTLCachePolicy evicts entries for which IsStaleFunction returns true.
/// The cache size (in bytes and number of entries) can be changed at runtime. It is expected to set both sizes explicitly after construction.
template <typename Key, typename Mapped, typename HashFunction, typename WeightFunction, typename IsStaleFunction>
@ -18,8 +87,9 @@ public:
using typename Base::KeyMapped;
using typename Base::OnWeightLossFunction;
TTLCachePolicy()
: max_size_in_bytes(0)
explicit TTLCachePolicy(CachePolicyUserQuotaPtr quotas_)
: Base(std::move(quotas_))
, max_size_in_bytes(0)
, max_count(0)
{
}
@ -61,8 +131,10 @@ public:
auto it = cache.find(key);
if (it == cache.end())
return;
size_in_bytes -= weight_function(*it->second);
size_t sz = weight_function(*it->second);
Base::user_quotas->decreaseActual(it->first.user_name, sz);
cache.erase(it);
size_in_bytes -= sz;
}
MappedPtr get(const Key & key, std::lock_guard<std::mutex> & /* cache_lock */) override
@ -88,35 +160,47 @@ public:
const size_t entry_size_in_bytes = weight_function(*mapped);
/// Checks against per-cache limits
auto sufficient_space_in_cache = [&]()
{
return (size_in_bytes + entry_size_in_bytes <= max_size_in_bytes) && (cache.size() + 1 <= max_count);
};
if (!sufficient_space_in_cache())
/// Checks against per-user limits
auto sufficient_space_in_cache_for_user = [&]()
{
return Base::user_quotas->approveWrite(key.user_name, entry_size_in_bytes);
};
if (!sufficient_space_in_cache() || !sufficient_space_in_cache_for_user())
{
/// Remove stale entries
for (auto it = cache.begin(); it != cache.end();)
if (is_stale_function(it->first))
{
size_in_bytes -= weight_function(*it->second);
size_t sz = weight_function(*it->second);
Base::user_quotas->decreaseActual(it->first.user_name, sz);
it = cache.erase(it);
size_in_bytes -= sz;
}
else
++it;
}
if (sufficient_space_in_cache())
if (sufficient_space_in_cache() && sufficient_space_in_cache_for_user())
{
/// Insert or replace key
if (auto it = cache.find(key); it != cache.end())
{
size_in_bytes -= weight_function(*it->second);
size_t sz = weight_function(*it->second);
Base::user_quotas->decreaseActual(it->first.user_name, sz);
cache.erase(it); // stupid bug: (*) doesn't replace existing entries (likely due to custom hash function), need to erase explicitly
size_in_bytes -= sz;
}
cache[key] = std::move(mapped); // (*)
size_in_bytes += entry_size_in_bytes;
Base::user_quotas->increaseActual(key.user_name, entry_size_in_bytes);
}
}

View File

@ -565,6 +565,8 @@ class IColumn;
M(Bool, enable_writes_to_query_cache, true, "Enable storing results of SELECT queries in the query cache", 0) \
M(Bool, enable_reads_from_query_cache, true, "Enable reading results of SELECT queries from the query cache", 0) \
M(Bool, query_cache_store_results_of_queries_with_nondeterministic_functions, false, "Store results of queries with non-deterministic functions (e.g. rand(), now()) in the query cache", 0) \
M(UInt64, query_cache_max_size_in_bytes, 0, "The maximum amount of memory (in bytes) the current user may allocate in the query cache. 0 means unlimited. ", 0) \
M(UInt64, query_cache_max_entries, 0, "The maximum number of query results the current user may store in the query cache. 0 means unlimited.", 0) \
M(UInt64, query_cache_min_query_runs, 0, "Minimum number a SELECT query must run before its result is stored in the query cache", 0) \
M(Milliseconds, query_cache_min_query_duration, 0, "Minimum time in milliseconds for a query to run for its result to be stored in the query cache.", 0) \
M(Bool, query_cache_compress_entries, true, "Compress cache entries.", 0) \

View File

@ -123,12 +123,13 @@ ASTPtr removeQueryCacheSettings(ASTPtr ast)
QueryCache::Key::Key(
ASTPtr ast_,
Block header_,
const std::optional<String> & username_,
const String & user_name_, bool is_shared_,
std::chrono::time_point<std::chrono::system_clock> expires_at_,
bool is_compressed_)
: ast(removeQueryCacheSettings(ast_))
, header(header_)
, username(username_)
, user_name(user_name_)
, is_shared(is_shared_)
, expires_at(expires_at_)
, is_compressed(is_compressed_)
{
@ -169,7 +170,8 @@ bool QueryCache::IsStale::operator()(const Key & key) const
return (key.expires_at < std::chrono::system_clock::now());
};
QueryCache::Writer::Writer(Cache & cache_, const Key & key_,
QueryCache::Writer::Writer(
Cache & cache_, const Key & key_,
size_t max_entry_size_in_bytes_, size_t max_entry_size_in_rows_,
std::chrono::milliseconds min_query_runtime_,
bool squash_partial_results_,
@ -307,7 +309,7 @@ QueryCache::Reader::Reader(Cache & cache_, const Key & key, const std::lock_guar
return;
}
if (entry->key.username.has_value() && entry->key.username != key.username)
if (!entry->key.is_shared && entry->key.user_name != key.user_name)
{
LOG_TRACE(&Poco::Logger::get("QueryCache"), "Inaccessible entry found for query {}", key.queryStringFromAst());
return;
@ -368,8 +370,13 @@ QueryCache::Reader QueryCache::createReader(const Key & key)
return Reader(cache, key, lock);
}
QueryCache::Writer QueryCache::createWriter(const Key & key, std::chrono::milliseconds min_query_runtime, bool squash_partial_results, size_t max_block_size)
QueryCache::Writer QueryCache::createWriter(const Key & key, std::chrono::milliseconds min_query_runtime, bool squash_partial_results, size_t max_block_size, size_t max_query_cache_size_in_bytes_quota, size_t max_query_cache_entries_quota)
{
/// Update the per-user cache quotas with the values stored in the query context. This happens per query which writes into the query
/// cache. Obviously, this is overkill but I could find the good place to hook into which is called when the settings profiles in
/// users.xml change.
cache.setQuotaForUser(key.user_name, max_query_cache_size_in_bytes_quota, max_query_cache_entries_quota);
std::lock_guard lock(mutex);
return Writer(cache, key, max_entry_size_in_bytes, max_entry_size_in_rows, min_query_runtime, squash_partial_results, max_block_size);
}
@ -399,7 +406,7 @@ std::vector<QueryCache::Cache::KeyMapped> QueryCache::dump() const
}
QueryCache::QueryCache()
: cache(std::make_unique<TTLCachePolicy<Key, Chunks, KeyHasher, QueryResultWeight, IsStale>>())
: cache(std::make_unique<TTLCachePolicy<Key, Chunks, KeyHasher, QueryResultWeight, IsStale>>(std::make_unique<PerUserTTLCachePolicyUserQuota>()))
{
}
@ -407,14 +414,14 @@ void QueryCache::updateConfiguration(const Poco::Util::AbstractConfiguration & c
{
std::lock_guard lock(mutex);
size_t max_size_in_bytes = config.getUInt64("query_cache.max_size", 1_GiB);
size_t max_size_in_bytes = config.getUInt64("query_cache.max_size_in_bytes", 1_GiB);
cache.setMaxSize(max_size_in_bytes);
size_t max_entries = config.getUInt64("query_cache.max_entries", 1024);
cache.setMaxCount(max_entries);
max_entry_size_in_bytes = config.getUInt64("query_cache.max_entry_size", 1_MiB);
max_entry_size_in_rows = config.getUInt64("query_cache.max_entry_rows", 30'000'000);
max_entry_size_in_bytes = config.getUInt64("query_cache.max_entry_size_in_bytes", 1_MiB);
max_entry_size_in_rows = config.getUInt64("query_cache.max_entry_rows_in_rows", 30'000'000);
}
}

View File

@ -42,10 +42,13 @@ public:
/// Result metadata for constructing the pipe.
const Block header;
/// std::nullopt means that the associated entry can be read by other users. In general, sharing is a bad idea: First, it is
/// unlikely that different users pose the same queries. Second, sharing potentially breaches security. E.g. User A should not be
/// able to bypass row policies on some table by running the same queries as user B for whom no row policies exist.
const std::optional<String> username;
/// The user who executed the query.
const String user_name;
/// If the associated entry can be read by other users. In general, sharing is a bad idea: First, it is unlikely that different
/// users pose the same queries. Second, sharing potentially breaches security. E.g. User A should not be able to bypass row
/// policies on some table by running the same queries as user B for whom no row policies exist.
bool is_shared;
/// When does the entry expire?
const std::chrono::time_point<std::chrono::system_clock> expires_at;
@ -55,7 +58,7 @@ public:
Key(ASTPtr ast_,
Block header_,
const std::optional<String> & username_,
const String & user_name_, bool is_shared_,
std::chrono::time_point<std::chrono::system_clock> expires_at_,
bool is_compressed);
@ -144,7 +147,7 @@ public:
void updateConfiguration(const Poco::Util::AbstractConfiguration & config);
Reader createReader(const Key & key);
Writer createWriter(const Key & key, std::chrono::milliseconds min_query_runtime, bool squash_partial_results, size_t max_block_size);
Writer createWriter(const Key & key, std::chrono::milliseconds min_query_runtime, bool squash_partial_results, size_t max_block_size, size_t max_query_cache_size_in_bytes_quota, size_t max_query_cache_entries_quota);
void reset();

View File

@ -725,9 +725,9 @@ static std::tuple<ASTPtr, BlockIO> executeQueryImpl(
{
QueryCache::Key key(
ast, res.pipeline.getHeader(),
std::make_optional<String>(context->getUserName()),
context->getUserName(), /*dummy for is_shared*/ false,
/*dummy value for expires_at*/ std::chrono::system_clock::from_time_t(1),
/*dummy value for is_compressed*/ true);
/*dummy value for is_compressed*/ false);
QueryCache::Reader reader = query_cache->createReader(key);
if (reader.hasCacheEntryForKey())
{
@ -748,7 +748,7 @@ static std::tuple<ASTPtr, BlockIO> executeQueryImpl(
{
QueryCache::Key key(
ast, res.pipeline.getHeader(),
settings.query_cache_share_between_users ? std::nullopt : std::make_optional<String>(context->getUserName()),
context->getUserName(), settings.query_cache_share_between_users,
std::chrono::system_clock::now() + std::chrono::seconds(settings.query_cache_ttl),
settings.query_cache_compress_entries);
@ -760,7 +760,9 @@ static std::tuple<ASTPtr, BlockIO> executeQueryImpl(
res.pipeline.getHeader(), query_cache, key,
std::chrono::milliseconds(context->getSettings().query_cache_min_query_duration.totalMilliseconds()),
context->getSettings().query_cache_squash_partial_results,
context->getSettings().max_block_size);
context->getSettings().max_block_size,
context->getSettings().query_cache_max_size_in_bytes,
context->getSettings().query_cache_max_entries);
res.pipeline.streamIntoQueryCache(stream_in_query_cache_transform);
}
}

View File

@ -9,9 +9,10 @@ StreamInQueryCacheTransform::StreamInQueryCacheTransform(
const QueryCache::Key & cache_key,
std::chrono::milliseconds min_query_duration,
bool squash_partial_results,
size_t max_block_size)
size_t max_block_size,
size_t max_query_cache_size_in_bytes_quota, size_t max_query_cache_entries_quota)
: ISimpleTransform(header_, header_, false)
, cache_writer(cache->createWriter(cache_key, min_query_duration, squash_partial_results, max_block_size))
, cache_writer(cache->createWriter(cache_key, min_query_duration, squash_partial_results, max_block_size, max_query_cache_size_in_bytes_quota, max_query_cache_entries_quota))
{
}

View File

@ -15,7 +15,8 @@ public:
const QueryCache::Key & cache_key,
std::chrono::milliseconds min_query_duration,
bool squash_partial_results,
size_t max_block_size);
size_t max_block_size,
size_t max_query_cache_size_in_bytes_quota, size_t max_query_cache_entries_quota);
protected:
void transform(Chunk & chunk) override;

View File

@ -36,18 +36,18 @@ void StorageSystemQueryCache::fillData(MutableColumns & res_columns, ContextPtr
std::vector<QueryCache::Cache::KeyMapped> content = query_cache->dump();
const String & username = context->getUserName();
const String & user_name = context->getUserName();
for (const auto & [key, query_result] : content)
{
/// Showing other user's queries is considered a security risk
if (key.username.has_value() && key.username != username)
if (!key.is_shared && key.user_name != user_name)
continue;
res_columns[0]->insert(key.queryStringFromAst()); /// approximates the original query string
res_columns[1]->insert(QueryCache::QueryResultWeight()(*query_result));
res_columns[2]->insert(key.expires_at < std::chrono::system_clock::now());
res_columns[3]->insert(!key.username.has_value());
res_columns[3]->insert(!key.is_shared);
res_columns[4]->insert(key.is_compressed);
res_columns[5]->insert(std::chrono::system_clock::to_time_t(key.expires_at));
res_columns[6]->insert(key.ast->getTreeHash().first);

View File

@ -0,0 +1,10 @@
Run SELECT with quota that current user may use only 1 byte in the query cache 1
Expect no entries in the query cache 0
Run SELECT again but w/o quota 1
Expect one entry in the query cache 1
---
Run SELECT which writes its result in the query cache 1
Run another SELECT with quota that current user may write only 1 entry in the query cache 1
Expect one entry in the query cache 1
Run another SELECT w/o quota 1
Expect two entries in the query cache 2

View File

@ -0,0 +1,31 @@
-- Tags: no-parallel
-- Tag no-parallel: Messes with internal cache
-- Tests per-user quotas of the query cache. Settings 'query_cache_max_size_in_bytes' and 'query_cache_max_entries' are actually supposed to
-- be used in a settings profile, together with a readonly constraint. For simplicity, test both settings stand-alone in a stateless test
-- instead of an integration test - the relevant logic will still be covered by that.
SET allow_experimental_query_cache = true;
SYSTEM DROP QUERY CACHE;
SET query_cache_max_size_in_bytes = 1;
SELECT 'Run SELECT with quota that current user may use only 1 byte in the query cache', 1 SETTINGS use_query_cache = true;
SELECT 'Expect no entries in the query cache', count(*) FROM system.query_cache;
SET query_cache_max_size_in_bytes = DEFAULT;
SELECT 'Run SELECT again but w/o quota', 1 SETTINGS use_query_cache = true;
SELECT 'Expect one entry in the query cache', count(*) FROM system.query_cache;
SELECT '---';
SYSTEM DROP QUERY CACHE;
SELECT 'Run SELECT which writes its result in the query cache', 1 SETTINGS use_query_cache = true;
SET query_cache_max_entries = 1;
SELECT 'Run another SELECT with quota that current user may write only 1 entry in the query cache', 1 SETTINGS use_query_cache = true;
SELECT 'Expect one entry in the query cache', count(*) FROM system.query_cache;
SET query_cache_max_entries = DEFAULT;
SELECT 'Run another SELECT w/o quota', 1 SETTINGS use_query_cache = true;
SELECT 'Expect two entries in the query cache', count(*) FROM system.query_cache;
SYSTEM DROP QUERY CACHE;