jtuglu1 commented on code in PR #20815:
URL: https://github.com/apache/druid/pull/20815#discussion_r4504247537


##########
docs/configuration/index.md:
##########
@@ -1967,6 +1968,7 @@ You can optionally only configure caching to be enabled 
on the Broker by setting
 |`druid.broker.cache.populateCache`|true, false|Populate the cache on the 
Broker.|false|
 |`druid.broker.cache.useResultLevelCache`|true, false|Enable result level 
caching on the Broker.|false|
 |`druid.broker.cache.populateResultLevelCache`|true, false|Populate the result 
level cache on the Broker.|false|
+|`druid.broker.cache.includeDatasourceAndIntervalsInResultLevelCacheKey`|true, 
false|Include the query's datasource and intervals in the result level cache 
key. By default the key has neither, so queries that differ only by interval or 
datasource share one cache entry and overwrite each other. Enabling this gives 
each of them its own entry, which helps when the same queries repeat over a few 
different intervals. Trade-offs: the cache holds more entries, and a query 
whose interval changes on every run, such as a rolling time window, adds a new 
entry each run that is never read again. Queries whose intervals cover the same 
data but differ elsewhere, such as an end time past the latest data, no longer 
reuse each other's entry. Changing this setting means existing result level 
cache entries are no longer read.|false|

Review Comment:
   I generally dislike adding so many random config flags, but given this could 
substantially negatively impact cache hit rates so seems to me.



##########
server/src/test/java/org/apache/druid/query/ResultLevelCachingQueryRunnerTest.java:
##########
@@ -404,6 +405,97 @@ public void testPopulateCacheThrowsException()
     emitter.verifyValue(DefaultQueryMetrics.QUERY_RESULT_CACHE_HIT, 0);
   }
 
+  @Test

Review Comment:
   non-blocking nit: might be nice to add a test that verifies the case where a 
cache is persistent across a process restart (i.e external cache)? And that 
populating with the flag off -> enable it -> change segments -> disable it 
again w/ assert the old entry is rejected and current results are returned.



##########
server/src/main/java/org/apache/druid/query/ResultLevelCachingQueryRunner.java:
##########
@@ -206,6 +209,29 @@ private byte[] fetchResultsFromResultLevelCache(
     return null;
   }
 
+  private byte[] computeResultLevelCacheKey()
+  {
+    final byte[] queryCacheKey = strategy.computeResultLevelCacheKey(query);
+    if (!cacheConfig.isIncludeDatasourceAndIntervalsInResultLevelCacheKey()) {
+      return queryCacheKey;
+    }
+    // The cached value still starts with the ETag, which is checked before 
the entry is used. Adding the datasource
+    // 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. The byte arrays are 
length-prefixed so that where one ends and the
+    // next begins is unambiguous.
+    final CacheKeyBuilder builder = new 
CacheKeyBuilder(RESULT_LEVEL_CACHE_KEY_WITH_DATASOURCE_AND_INTERVALS)
+        .appendInt(queryCacheKey.length)
+        .appendByteArray(queryCacheKey);
+    final byte[] dataSourceCacheKey = query.getDataSource().getCacheKey();
+    if (dataSourceCacheKey != null) {
+      
builder.appendInt(dataSourceCacheKey.length).appendByteArray(dataSourceCacheKey);
+    }
+    for (Interval interval : query.getIntervals()) {

Review Comment:
   getIntervals() is sorted but might be nice to add a small test to ensure we 
get equal cache keys queries which pass different order intervals :)
   
   and might be worth thinking about appendLongArray() or similar if it exists, 
looks like that might shave off some bytes on the cache key size



##########
server/src/test/java/org/apache/druid/query/ResultLevelCachingQueryRunnerTest.java:
##########
@@ -404,6 +405,97 @@ public void testPopulateCacheThrowsException()
     emitter.verifyValue(DefaultQueryMetrics.QUERY_RESULT_CACHE_HIT, 0);
   }
 
+  @Test
+  public void testDifferentIntervalsShareOneEntryByDefault()
+  {
+    prepareCluster(10);
+    final Query<Result<TimeseriesResultValue>> query1 = 
timeseriesQuery(Intervals.of("2000-01-01T00/PT2H"));
+    final Query<Result<TimeseriesResultValue>> query2 = 
timeseriesQuery(Intervals.of("2000-01-01T01/PT2H"));
+    final CacheConfig config = newCacheConfig(true, true, 
DEFAULT_CACHE_ENTRY_MAX_SIZE);
+
+    createQueryRunner(config, query1).run(QueryPlus.wrap(query1), 
responseContext()).toList();
+    createQueryRunner(config, query2).run(QueryPlus.wrap(query2), 
responseContext()).toList();
+    Assertions.assertEquals(1, cache.getStats().getNumEntries());
+    emitter.flush();
+
+    createQueryRunner(config, query1).run(QueryPlus.wrap(query1), 
responseContext()).toList();
+
+    // query2 overwrote query1's entry, so the last run finds an entry (a hit 
for the cache) but can't use it.
+    Assertions.assertEquals(1, cache.getStats().getNumEntries());
+    Assertions.assertEquals(2, cache.getStats().getNumHits());
+    emitter.verifyEmitted(DefaultQueryMetrics.QUERY_RESULT_CACHE_HIT, 1);
+    emitter.verifyValue(DefaultQueryMetrics.QUERY_RESULT_CACHE_HIT, 0);
+  }
+
+  @Test
+  public void testDifferentIntervalsUseSeparateEntriesWhenFlagEnabled()
+  {
+    prepareCluster(10);
+    final Query<Result<TimeseriesResultValue>> query1 = 
timeseriesQuery(Intervals.of("2000-01-01T00/PT2H"));
+    final Query<Result<TimeseriesResultValue>> query2 = 
timeseriesQuery(Intervals.of("2000-01-01T01/PT2H"));
+    final CacheConfig config = newCacheConfig(true, true, 
DEFAULT_CACHE_ENTRY_MAX_SIZE, true);
+
+    final List<Result<TimeseriesResultValue>> results1 =
+        createQueryRunner(config, query1).run(QueryPlus.wrap(query1), 
responseContext()).toList();
+    final List<Result<TimeseriesResultValue>> results2 =
+        createQueryRunner(config, query2).run(QueryPlus.wrap(query2), 
responseContext()).toList();
+    Assertions.assertEquals(2, cache.getStats().getNumEntries());
+    emitter.flush();
+
+    Assertions.assertEquals(
+        results1,
+        createQueryRunner(config, query1).run(QueryPlus.wrap(query1), 
responseContext()).toList()
+    );
+    emitter.verifyEmitted(DefaultQueryMetrics.QUERY_RESULT_CACHE_HIT, 1);
+    emitter.verifyValue(DefaultQueryMetrics.QUERY_RESULT_CACHE_HIT, 1);
+    emitter.flush();
+
+    Assertions.assertEquals(
+        results2,
+        createQueryRunner(config, query2).run(QueryPlus.wrap(query2), 
responseContext()).toList()
+    );
+    emitter.verifyEmitted(DefaultQueryMetrics.QUERY_RESULT_CACHE_HIT, 1);
+    emitter.verifyValue(DefaultQueryMetrics.QUERY_RESULT_CACHE_HIT, 1);
+    Assertions.assertEquals(2, cache.getStats().getNumEntries());
+  }
+
+  @Test
+  public void testDatasourceAndIntervalsInCacheKeyOnlyWhenFlagEnabled()
+  {
+    cache = Mockito.spy(cache);
+    prepareCluster(10);
+    final Query<Result<TimeseriesResultValue>> query = 
timeseriesQuery(Intervals.of("2000-01-01T00/PT2H"));
+    final Query<Result<TimeseriesResultValue>> sameQuery = 
timeseriesQuery(Intervals.of("2000-01-01T00/PT2H"));
+    final Query<Result<TimeseriesResultValue>> otherInterval = 
timeseriesQuery(Intervals.of("2000-01-01T01/PT2H"));

Review Comment:
   maybe add another overlap case: `"2000-01-01T00/PT2H","2000-01-01T02/PT2H"` 
and `"2000-01-01T00/PT2H","2000-01-01T03/PT2H"`



##########
server/src/test/java/org/apache/druid/query/ResultLevelCachingQueryRunnerTest.java:
##########
@@ -404,6 +405,97 @@ public void testPopulateCacheThrowsException()
     emitter.verifyValue(DefaultQueryMetrics.QUERY_RESULT_CACHE_HIT, 0);
   }
 
+  @Test
+  public void testDifferentIntervalsShareOneEntryByDefault()
+  {
+    prepareCluster(10);
+    final Query<Result<TimeseriesResultValue>> query1 = 
timeseriesQuery(Intervals.of("2000-01-01T00/PT2H"));

Review Comment:
   nit: let's test same start but different end and same end but different 
start?



##########
server/src/main/java/org/apache/druid/query/ResultLevelCachingQueryRunner.java:
##########
@@ -95,7 +98,7 @@ public Sequence<T> run(QueryPlus queryPlus, ResponseContext 
responseContext)
   {
     if (useResultCache || populateResultCache) {
 
-      final byte[] queryCacheKey = strategy.computeResultLevelCacheKey(query);
+      final byte[] queryCacheKey = computeResultLevelCacheKey();
       final Cache.NamedKey cacheKey = 
CacheUtil.computeResultLevelCacheKey(queryCacheKey);
       final byte[] cachedResultSet = 
fetchResultsFromResultLevelCache(cacheKey);

Review Comment:
   something I noticed while looking over 
[this](https://github.com/maytasm/druid/blob/f577780e1c7d044cddda8e629c3e8a1dadbae678/server/src/main/java/org/apache/druid/query/ResultLevelCachingQueryRunner.java#L366)
 code is that looks like there might? be a further way to increase hit rate. 
Consider 3 query instances that map to the same query cache key:
   
   ```
   q1: read cache, calculate E1 from old segments, start execution
   q2: read cache, calculate E2 from new segments, start execution
   q2: finish, write (key, {E2, results2})
   q1: finish, write (key, {E1, results1})
   q3: read cached {E1, results1}
   q3: calculate E2 from current segments
   q3: reject E1, execute query, write (key, {E2, results3})
   ```
   In order to prevent q3 from executing, we might be able to either:
   
   - Do some sort of cache.putIfAbsentOrOlder() which sets the result if it is 
not there yet or the etag represents a segment set that is "fresher" than the 
prior cached result.
   - Alternatively, can maybe think about storing the etag in the cache key 
itself which would blow up the cache size potentially (and you'd need eviction 
policy which is complicated)



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to