-
Notifications
You must be signed in to change notification settings - Fork 3.6k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[Broker] Fix race condition in invalidating ledger cache entries #10480
[Broker] Fix race condition in invalidating ledger cache entries #10480
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@lhotari I left a few ideas.
I am not saying you are on the wrong way, but I am not sure we are really fixing the problem
@@ -90,7 +90,7 @@ public Value get(Key key) { | |||
try { | |||
value.retain(); | |||
return value; | |||
} catch (Throwable t) { | |||
} catch (IllegalReferenceCountException e) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
probably we should log something here.
this case must not happen
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yes, logging would be useful to get more information. I'm just wondering if it should be done only at debug level since it's not a real problem. It's part of the expected behavior that this could sometimes happen.
@@ -113,7 +113,7 @@ public Value get(Key key) { | |||
try { | |||
value.retain(); | |||
values.add(value); | |||
} catch (Throwable t) { | |||
} catch (IllegalReferenceCountException e) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
probably we should log something here.
this case must not happen
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's expected to happen and it's fine when it happens. It just indicates the entry is being evicted when we're trying to access it. If the retain succeeds, the operation was successful, otherwise the entry is already gone.
removedSize += weighter.getSize(value); | ||
value.release(); | ||
long entrySize = weighter.getSize(value); | ||
if (value.invalidate()) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am afraid we are only hiding the problem.
there must be some coordination about these entries, some clear protocol about who is the owner of the entry.
when we get to this point then we must be sure that the refcount is valid on the value, otherwise it is always an hazard.
I believe that the right protocol is that before
calling weighter.getSize(value);
we should try
to acquire the entry and in case of failure we can ignore the entry.
Value value = entry.getValue();
if (value.tryAcquire()) {
++removedEntries;
removedSize += weighter.getSize(value);
value.release(); // this refers to the value.tryAcquire()
}
when we remove the entry we must have some write lock over the entry, that prevents double releases
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@lhotari Great finding. I agree with the assessment that multiple invalidations are happening at the same time to cause this.
I think the correct fix here would be to ensure that there's only one eviction happening at a given point in time, so that we avoid touching an entry whose ref-count is potentially 0.
For that, we'd need to make sure that, in RangeCache
, removeRange()
,evictLeastAccessedEntries()
and evictLEntriesBeforeTimestamp()
are either called with a mutex (like clear()
is already doing) or at least from the same single thread.
@@ -113,7 +113,7 @@ public Value get(Key key) { | |||
try { | |||
value.retain(); | |||
values.add(value); | |||
} catch (Throwable t) { | |||
} catch (IllegalReferenceCountException e) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's expected to happen and it's fine when it happens. It just indicates the entry is being evicted when we're trying to access it. If the retain succeeds, the operation was successful, otherwise the entry is already gone.
release(); | ||
return true; | ||
} | ||
return false; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think this is the correct approach. If the issue is that we're using an already released buffer, we should fix that instead.
This will avoid decrementing the ref-count more than once, but it will not prevent the 2nd thread from accessing an entry whose ref-count was already to 0.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a safety measure to prevent races in invalidating the entries. I agree that the issues in releasing must be fixed. The benefit of adding a separate method for invalidation would help detect when the problem is caused by invalidating the entry twice. Some logging could be added to detect the issues where there's a race in invalidation which causes a "double release".
Currently, it seems that the problems that we are seeing could occur only when there's a race in invalidation. At a quick glance, there doesn't seem to be other code paths where the entry is released but not retained as part of the same "flow".
Would this justify adding some extra protection against races in invalidation?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This will avoid decrementing the ref-count more than once, but it will not prevent the 2nd thread from accessing an entry whose ref-count was already to 0.
Yes, that's a good point. I'm thinking of a solution where invalidation would be a completely separate operation which triggers when reference count is 1 or gets back to 1. Another protection here would be a change in logic that release operations to change reference count from 1 to 0 would be rejected completely. That would prevent bugs which are caused by release being called too many times. Those issues could be logged and fixed if such bugs exist.
removedSize += weighter.getSize(value); | ||
value.release(); | ||
++removedEntries; | ||
long entrySize = weighter.getSize(value); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
eg: in case of concurrent invalidate, the value is already invalid here
@lhotari I agree with @merlimat on " If the issue is that we're using an already released buffer, we should fix that instead." Other issue is: do we have a repro? |
Actually, I'm not 100% sure that the having invalidations called by multiple thread could lead to the issue. In all the cases the entries are removed from the |
This is true. Therefore, thinking of the changes as a safety measure and way to detect the source of the problem as explained in my previous comment could be the rationale for adding a separate method for invalidation. |
Yes I agree on this, I'll try to dig deeper. :)
Good point. I'll track those release calls.
Pulsar might not be impacted in cases where AbstractCASReferenceCounted base class is used. Here's AbstractCASReferenceCounted release logic: Lines 95 to 110 in 5e446a6
No. I assume that it's a rare issue since there's not many reports about it. It could be possible to achieve a repro at some kind of unit/integration test level using JCStress, but it's hard to estimate the effort to achieve a repro.
No. The bug report is about concurrent calls to |
I've been trying to spot locations where the entry could get released multiple times, but still continued to be used. This looks risky: pulsar/managed-ledger/src/main/java/org/apache/bookkeeper/mledger/impl/EntryCacheImpl.java Lines 191 to 195 in dcaa1d3
Together with pulsar/managed-ledger/src/main/java/org/apache/bookkeeper/mledger/impl/ManagedCursorImpl.java Lines 1232 to 1237 in d2138f7
|
At this point, we can actually get rid of The change pulsar/managed-ledger/src/main/java/org/apache/bookkeeper/mledger/impl/EntryCacheImpl.java Lines 187 to 196 in dcaa1d3
As for the 2nd part (the Cursor.readEntryFailed), that seems ok to me. The |
I'm thinking of replacing it with something that would give extra protection against bugs. Let's see what it evolves into. I'll push changes to this PR once there's something presentable.
About these 2 lines of code together:
The problem here seems to be that invalidateAllEntries will call |
I don't think there's a double release because:
The main traits of the entry cache are:
|
A better fix is #22789 which doesn't contain the problems that were in this PR a few years ago. |
Fixes #10433
Motivation
See #10433 . There's a rare race condition in invalidating ledger cache entries stored in RangeCache .
Modifications
invalidate
method for invalidatingEntryImpl
ledger cache entries. This prevents race conditions in invalidation.