Bug #11037
Record not being evicted from FFCache
100%
Related issues
History
#1 Updated by Andrei Plugaru 7 months ago
While investigating #10247, I found a scenario where an entry is not being evicted from FFcache at the invalidation stage. This happens when there are 2 queries for the same record. Only the entry associated to a query is deleted. This is a testcase to reproduce the issue:
def temp-table table3 field f1 as int field f2 as logical index idx_1 is primary unique f1. create table3. table3.f1 = 1. table3.f2 = false. find first table3 where f1 = 1 and not table3.f2 no-error. find first table3 where f1 < 10 and not table3.f2 no-error. assign table3.f2 = true . find first table3 where f1 = 1 and not table3.f2 no-error. message available(table3). //OE: false; FWD true
Basically, the last query returns the record from FFCache, as the entry associated with that query wasn't evicted.
The root cause is FastFindCache.ReverseLookup.Value.invalidate. Here it is the initial code:
private void invalidate(BitSet dirtyProps, LRUCache<Node, Value> lruCache) { for (Node node = head; node != null; node = node.next) { if (dirtyProps != null && node.key.k2.nonIndexedProps != null && !node.key.k2.nonIndexedProps.intersects(dirtyProps)) { continue; } if (node == head) { head = node.next; } node.remove(); lruCache.remove(node); } } }
The exact problem is that in
node.remove() the next reference is removed, however we still need it for the for loop, so it exits without iterating through the entire list. I managed to fix my testcase with this patch:
However, I will also look in other methods from there in order to understand if this pattern is used elsewhere.
#3 Updated by Andrei Plugaru 7 months ago
FFCache.cache, like how it happens in FFCache.invalidate(int dmoUid, Integer multiplex, boolean remove).I mainly discovered 2 issues:
- Even though the entries from
ReverseLookup.reverseCachewill typically be removed when the Rec ids will not be hard referenced anywhere else, there is a problem forNO_RECORD. This is a class static filed, so it is always referenced, so the associated entries fromReverseLookup.reverseCachewill never be deleted. - There is at least one execution path when records from
ReverseLookup.lruCacheare not deleted. We are only removing entries from there, when single records are invalidated. However, when the records for an entire dmo should be invalidated(like when called fromFFCache.invalidate(int dmoUid, Integer multiplex, boolean remove)), they are not removed. This causes a real functional problem as inReverseLookup.put(RecordIdentifier<String> recID, Map<L2Key, RecordIdentifier<String>> l3Cache, Key key)we check whether an entry already exists, so the entry is not inserted into reverse lookup, even though it is in the main cache from FFcache. This functional problem can be aleviated by performing some checks inReverseLookup.putto understand if the existing entry is valid or not. However, I feel like this is not the right way. The main downside is that we will consume memory for entries which are not valid, maybe leading to problems in this regard. I feel like we need a mechanism for eagerly removing the entries exactly in the moment when the dmo is invalidated, however also providing good performance results. This should also reduce the number of records in this map. I came across #10776, where it was mentioned that callinggetwas slow on the map fromReverseLookup.
#4 Updated by Andrei Plugaru 7 months ago
- Status changed from New to WIP
- Assignee set to Andrei Plugaru
After a discussion yesterday with Alex, we decided to tackle the problem of invalid records in ReverseLookup.lruCache wit this approach: always add the new entry in ReverseLookup.put no matter if the node currently exists or not. The solution would have looked like:
void put(RecordIdentifier<String> recID, Map<L2Key, RecordIdentifier<String>> l3Cache, Key key)
{
Node node = new Node(l3Cache, key);
Value value = reverseCache.get(recID);
if (value == null)
{
value = new Value(recID);
}
value.addNode(node);
reverseCache.put(recID, value);
if (lruCache.containsKey(node))
{
touch(l3Cache, key);
}
else
{
lruCache.put(node, value);
}
}
However, I have put more thought into that and realised this could cause a memory leak. The problem is that we would add the new Node instance to the Value, however this node instance won't be added into the lruCache as we already have an equivalent instance there. This would mean that this node instance won't be evicted from memory as it isn't tracked into the lruCache. We would basically have the Node and the associated Value objects hanging in memory.
My solution, currently is to identify if we have stale data in lruCache(if the current RecId associated to the Node is different then the recId we try to insert). If that is the case, we remove that node(both the entry node -> value from lruCache and also the links to this node in the linked list in Value).
Show
I have tested this solution, together with the one from #11037-1 and the problems in the unit tests from the large customer application are solved. I will put this into a branch soon in order to be properly reviewed.
#6 Updated by Alexandru Lungu 7 months ago
- % Done changed from 0 to 90
- Status changed from Review to WIP
Review of 11037a:
- Why do you need to actually check if
nodeToRemoveis inside the list? Can't you runnodeToRemove.remove()(and other operations related to head and lruCache@) directly? FYI, the list can be very large and iterating it may be very costly, considering that the nodes containKeywith actual arguments, FQL, etc.- If the node is not in the value, then the
remove()is no-op, removal fromlruCacheis done reasonably and setting ofheadwon't be possible.
- If the node is not in the value, then the
- I do not understand the changes in
invalidateand why they are required. They seem to rewrite the for loop in a while loop without any functional change.
#7 Updated by Alexandru Lungu 7 months ago
- Related to Bug #10776: FFC.touch standing out in profiling added
#8 Updated by Andrei Plugaru 6 months ago
Alexandru Lungu wrote:
- Why do you need to actually check if
nodeToRemoveis inside the list? Can't you runnodeToRemove.remove()(and other operations related to head and lruCache@) directly? FYI, the list can be very large and iterating it may be very costly, considering that the nodes containKeywith actual arguments, FQL, etc.
- If the node is not in the value, then the
remove()is no-op, removal fromlruCacheis done reasonably and setting ofheadwon't be possible.
nodeToRemove comes from ReverseLookup.put. There, a Node instance is created, however, only taking into account l3Cache and key. So, it doesn't have set the structures related to the linked list(prev and next). Therefore, only calling nodeToRemove.remove() won't actually remove it from the linked list. So, this is why I needed the exact instance that was already stored. I understand, however, the performance implications of this and I think that your solution from #10776 where we can access the Node based on the FastFindCacheEntry may come in handy.
I do not understand the changes in invalidate and why they are required. They seem to rewrite the for loop in a while loop without any functional change.
There is actually a slight functional change there:)). This is the diff for the change:
private void invalidate(BitSet dirtyProps, LRUCache<Node, Value> lruCache) { - for (Node node = head; node != null; node = node.next) + Node node = head; + + while (node != null) { - if (dirtyProps != null && - node.key.k2.nonIndexedProps != null && - !node.key.k2.nonIndexedProps.intersects(dirtyProps)) + Node nextNode = node.next; + + boolean shouldSkip = (dirtyProps != null + && node.key.k2.nonIndexedProps != null + && !node.key.k2.nonIndexedProps.intersects(dirtyProps)); + + if (shouldSkip) { + node = nextNode; continue; } - + if (node == head) { - head = node.next; - } - - node.remove(); - lruCache.remove(node); - + head = nextNode; + } + + node.remove(); // Safe to disconnect now + lruCache.remove(node); // Remove from cache + + // Advance the loop using the saved reference + node = nextNode; + } + }
The problem was that the next property is accessed after node.remove(); is called. Inside node.remove, next and prev properties are reset. So, node.next will return null. In my changes I save the reference to node.next right at the beginning of the while loop, so I can access it after node.remove is called.
#9 Updated by Alexandru Lungu 5 months ago
- Status changed from WIP to Review
- % Done changed from 90 to 100
Andrei, please embed the changes from 10776 into 11037a and let me review and test it as a whole (according to #10776-9)
#10 Updated by Andrei Plugaru 5 months ago
Alexandru Lungu wrote:
Andrei, please embed the changes from 10776 into 11037a and let me review and test it as a whole (according to #10776-9)
I'm on it, however, I don't think it is really ready for the review. Your changes from #10776 can allow a more performant solution as I said in the 1st paragraph from #11037-8. I will try to implement that.
#12 Updated by Andrei Plugaru 5 months ago
I have committed 11037a/rev. 16327. I removed the part where I was deleting the stale nodes at put operation. I have changed into removing the records from the reverse lookup when the invalidation in the main cache happens. This was possible as after 10776a, I have the FastFindCacheEntry objects there.
I have retested the testcase from #11037-1 and #10776-10 and they are solved.
Alex, please review!
#13 Updated by Alexandru Lungu 4 months ago
- Assignee changed from Andrei Plugaru to Alexandru Lungu