Conversation
|
@msfroh Can you take a look at this? |
|
@sgup432 -- I was comparing this to the 10.x branch logic. I see that there, we have the top-level Now that you've partitioned the cache at the top layer, I'm wondering if it makes sense to move the Even with your change as written, if two equal queries happen to land on two different segments, the associated |
Even if we reintroduce(it was removed earlier) the uniqueQueries into partition level, I guess we will still have the same problem i.e. same query being held in memory for two different partitions? As the partition number is decided based on the composite key - |
|
If we have to replace the query with its canonical representation, I think it has to done at a global level so that each partition uses the shared canonical query. There might be cases(and I have seen many) where users send very large queries and they end up getting cached. To have a map at a global level might introduce some regression but we can use a The harder part is to remove the query from this map when this particular query is removed from all the segments/partitions. This can be done by either introducing a reference counting based mechanism like |
|
@msfroh Maybe we can take the canonical change as part of a different PR as might be more involved? |
|
Hmm... since eviction is managed on a per-partition basis, I don't think we can reasonably pull Actually, looking at the implementation, I'm not convinced that we are properly accounting for query size in ramBytesUsed, since the cache entries are on the segment/query pairs. But we increment the query size contribution for each entry, even though it's probably the same query instance. I agree that this change fixes the immediate bug, but I'm worried that there are other issues with the new cache implementation. |
Actually you are right. Within a single search,
Each partition does invoke |
I am going to try implement this approach and update this PR. I think this is the cleanest way to solve it IMO. |
|
@msfroh I have changed the logic. As mentioned above, instead of routing based on It has also has a reference count based tracking per query. It is needed as N segments can have the same query now within same partition, and entry from the map is only remove once no segments is referencing the query. While benchmarking the change and analyzing via JFR, I also saw some small performance optimizations opportunities like avoiding the redundant query hashCode calculation. |
|
Benchmarks: Comparing the 16 partition vs 1 partition Worst case for this cache: |
Description
LRUQueryCache was recently refactored (releasing in Lucene 11.0) to partition the cache, and its key structure was changed into a composite one (QueryCacheKey).
I found a bug in QueryCacheKey: it compares queries by value in equals() but hashes them with System.identityHashCode(query), breaking the hashCode/equals contract. For example, we cache new TermQuery("color","red") and then look it up with another equal new TermQuery("color","red"). The two are equal, but their identity hashes differ, so the lookup resolves to the wrong partition (or the wrong bucket within that partition's HashMap) and misses. In practice this neutered the cache.
The PR first fixes that by hashing on query.hashCode() so it matches equals(), and memoizing the composite hash to avoid recomputing it.
As part of the PR review, we also found issues where query bytes were over-counted as we were not tracking the canonical representation of the query. So now we route each query to a single partition (by the query alone rather than (segment, query)) and canonicalizes it there to one shared Query instance, so a query cached across many segments is no longer duplicated. This in turn lets the cache account a query's RAM once per distinct query instead of once per (segment, query) entry, fixing an over-count that inflated ramBytesUsed and caused premature eviction as a result.