Repository navigation
Conversation
FrankChen021
left a comment
There was a problem hiding this comment.
🟢 Approval recommended
The opt-in key path preserves the legacy result-level cache key when disabled and separates entries by datasource cache key and condensed interval bounds when enabled. I also inspected surrounding result-cache wiring, datasource cacheability contracts, interval normalization, and the added tests.
Reviewed 4 of 4 changed files.
This is an automated review by Codex GPT-5.6-Luna(max)
| // and intervals to the key gives queries that differ only by interval or datasource their own entries, instead of | ||
| // one shared entry that each of them overwrites. | ||
| final CacheKeyBuilder builder = new CacheKeyBuilder(RESULT_LEVEL_CACHE_KEY_WITH_DATASOURCE_AND_INTERVALS) | ||
| .appendByteArray(queryCacheKey); |
There was a problem hiding this comment.
nit: Wouldn't there be a situation where appending could lead the key to overflow?
There was a problem hiding this comment.
This shouldn't be a problem:
- Integer overflow: CacheKeyBuilder tracks the key size as an int, so overflowing it would take a key over 2 GiB. This is unlikely.
- Backend key-length limits: Distributed cache like memcached store the SHA-1 hex of the key (i.e. MemcachedCache.computeKeyHash), so their 250-character limit never applies. Caffeine and the map cache have no limit.
- Added size from this change: the flag adds the datasource key (its name plus about 3 bytes) and 18 bytes per interval. The existing query key already holds the filters, dimensions and aggregators, which are usually much bigger.
| .appendByteArray(queryCacheKey); | ||
| final byte[] dataSourceCacheKey = query.getDataSource().getCacheKey(); | ||
| if (dataSourceCacheKey != null) { | ||
| builder.appendByteArray(dataSourceCacheKey); |
There was a problem hiding this comment.
Do we need to length-prefix or add a seperator between queryCacheKey and dataSourceCacheKey byte arrays to avoid collisions?
Though appendByteArray() adds 0x01 as a type marker, that byte can also appear in the data. For example, [AA] + [BB, 01, CC] and [AA, 01, BB] + [CC] both encode as 01 AA 01 BB 01 CC.
public CacheKeyBuilder appendByteArray(byte[] input)
{
appendItem(BYTE_ARRAY_KEY, input);
return this;
}
...
private void appendItem(byte typeKey, byte[] input)
{
final Item item = new Item(typeKey, input);
items.add(item);
size += item.byteSize();
}
There was a problem hiding this comment.
- A collision is very unlikely. One query's key would have to equal another query's key followed by bytes shaped exactly like a datasource cache key. Although is possible as you pointed out.
- Despite that, the ETag still catches it. Each cache value starts with an ETag, a SHA-1 of the segment IDs, the clipped query intervals and the query cache key (CachingClusteredClient.computeResultLevelCachingEtag, around line 821). Segment IDs include the datasource and interval, so a collision between different datasources or intervals fails the ETag check. It then behaves like today's default: a miss, and the entry gets overwritten.
- It's a simple fix so let me push a change
CacheKeyBuilder.appendByteArray writes no length, so two adjacent byte arrays don't mark where one ends and the next begins. Prefix the query key and the datasource key with their lengths so the encoding is unambiguous. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Description
The Broker's result-level cache key comes only from the query's shape (
CacheStrategy#computeResultLevelCacheKey). It doesn't include the datasource or the query intervals. Those only feed the ETag stored at the start of the cached value (intervals were added there in #7325). So queries that differ only by interval or datasource map to one cache entry. Each reads the other's entry, rejects it because the ETag doesn't match, and overwrites it, so alternating queries (a, b, a, b) never hit.As far as I know, there's no reason not to do this. In #9280 (comment), @gianm suggested changing ResultLevelCachingQueryRunner so the datasource and interval are part of the cache key rather than the cached value. That would let it cache results for queries that are the same except for their interval. The comment gives no reason against the change; it wasn't done only because he didn't plan to work on it himself at the time.
Added
druid.broker.cache.includeDatasourceAndIntervalsInResultLevelCacheKeyThe default is
false, which leaves the key unchanged. Whentrue,ResultLevelCachingQueryRunnerbuilds the key from:DataSource#getCacheKey), when it isn't null,CacheUtil#computeSegmentCacheKey.The ETag in the cached value, and the check on read, are unchanged. The flag only decides which entry a query reads and writes.
Design notes:
Release note
Added
druid.broker.cache.includeDatasourceAndIntervalsInResultLevelCacheKey(defaultfalse). When enabled, the Broker's result-level cache key includes the query's datasource and intervals, so queries that differ only by interval or datasource get their own cache entries instead of overwriting one shared entry.Key changed/added classes in this PR
CacheConfigResultLevelCachingQueryRunnerResultLevelCachingQueryRunnerTestThis PR has: