Skip to content
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

Replace guava multimap in PCBC with custom impl #1569

Closed
wants to merge 2 commits into from

Conversation

ivankelly
Copy link
Contributor

For a long time PerChannelBookieClient has used guava
LinkedListMultiMap to store conflicting V2 completion keys and
values. This is problematic though. Completion keys are pooled
objects. When a key-value pair is stored in a LinkedListMultiMap, if
it is the first value for that key, a collection is created for the
values, and added to a top-level map using the key, and then the key
and the value are added to the collection. When a second value is
added for the same key, the key and value are simply added to the
collection. The problem occurs when the first key is removed. PBCB
will recycle the key object, but this object is still being used in
the multimap in the top-level map. This causes all sorts of fun like
NullPointerException and IllegalStateException.

Because of this, this patch introduces a very simple multimap
implementation that only stores the key one time (in the collection)
and uses the hashCode of the key to separate the collections into
buckets. It's pretty inefficient, but this code it only hit in the
rare case where a client is trying to read or write the same entry
from the same ledger more than once at the same time.

For a long time PerChannelBookieClient has used guava
LinkedListMultiMap to store conflicting V2 completion keys and
values. This is problematic though. Completion keys are pooled
objects. When a key-value pair is stored in a LinkedListMultiMap, if
it is the first value for that key, a collection is created for the
values, and added to a top-level map using the key, and then the key
and the value are added to the collection. When a second value is
added for the same key, the key and value are simply added to the
collection. The problem occurs when the first key is removed. PBCB
will recycle the key object, but this object is still being used in
the multimap in the top-level map. This causes all sorts of fun like
NullPointerException and IllegalStateException.

Because of this, this patch introduces a very simple multimap
implementation that only stores the key one time (in the collection)
and uses the hashCode of the key to separate the collections into
buckets. It's pretty inefficient, but this code it only hit in the
rare case where a client is trying to read or write the same entry
from the same ledger more than once at the same time.
@ivankelly ivankelly self-assigned this Jul 26, 2018
Copy link
Contributor

@eolivelli eolivelli left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good.
Awesome implementation and good catch!

@@ -21,7 +21,8 @@
import static org.apache.bookkeeper.client.LedgerHandle.INVALID_ENTRY_ID;

import com.google.common.base.Joiner;
import com.google.common.collect.LinkedListMultimap;
import com.google.common.collect.ArrayListMultimap;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure it is used

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

indeed it is not. checkstyle should have picked that up.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it seems it did here. just not locally :/.

@eolivelli
Copy link
Contributor

There are a few checkstyle issues.

@eolivelli
Copy link
Contributor

rerun bookkeeper-server tls tests

while (completionObjectsV2Conflicts.get(key).size() > 0) {
errorOut(key, rc);
}
Optional<CompletionKey> multikey = completionObjectsV2Conflicts.getAnyKey();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this 'getAnyKey()' sounds a bit weird to me, therefore inside the implementation we allocating a bunch of objects (using stream API).
I have an alternative proposal:

completionObjectsV2Conflicts.forEachValue( k -> errorOut(k, rc))

the iteration will be internal to the implementation, we will save method calls and allocations and overall the code will look better

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

errorOut modifies the collection, so forEachValue will give you a ConcurrentModificationException.

Regarding allocations, this isn't in a critical path, so I didn't take any care to avoid them, but escape analysis should get rid of most of them in any case.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, I missed the internals of errorOut.

Okay for me

Copy link
Contributor

@eolivelli eolivelli left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1

@sijie
Copy link
Member

sijie commented Aug 15, 2018

This change is needed to be cherry-picked to release/4.7.2.

The problem has been observed using 4.7.1.

09:58:15.041 [BookKeeperClientScheduler-OrderedScheduler-0-0] ERROR org.apache.bookkeeper.common.util.SafeRunnable - Unexpected throwable caught
java.util.NoSuchElementException: null
        at com.google.common.collect.LinkedListMultimap.checkElement(LinkedListMultimap.java:313) ~[com.google.guava-guava-21.0.jar:?]
        at com.google.common.collect.LinkedListMultimap.access$300(LinkedListMultimap.java:104) ~[com.google.guava-guava-21.0.jar:?]
        at com.google.common.collect.LinkedListMultimap$NodeIterator.previous(LinkedListMultimap.java:391) ~[com.google.guava-guava-21.0.jar:?]
        at com.google.common.collect.LinkedListMultimap$NodeIterator.<init>(LinkedListMultimap.java:332) ~[com.google.guava-guava-21.0.jar:?]
        at com.google.common.collect.LinkedListMultimap$1ValuesImpl.listIterator(LinkedListMultimap.java:760) ~[com.google.guava-guava-21.0.jar:?]
        at java.util.AbstractList.listIterator(AbstractList.java:299) ~[?:1.8.0_181]
        at java.util.AbstractSequentialList.iterator(AbstractSequentialList.java:239) ~[?:1.8.0_181]
        at org.apache.bookkeeper.proto.PerChannelBookieClient.checkTimeoutOnPendingOperations(PerChannelBookieClient.java:803) ~[org.apache.bookkeeper-bookkeeper-server-4.7.1.jar:4.7.1]
        at org.apache.bookkeeper.proto.DefaultPerChannelBookieClientPool.checkTimeoutOnPendingOperations(DefaultPerChannelBookieClientPool.java:100) ~[org.apache.bookkeeper-bookkeeper-server-4.7.1.jar:4.7.1]
        at org.apache.bookkeeper.proto.BookieClient.monitorPendingOperations(BookieClient.java:453) ~[org.apache.bookkeeper-bookkeeper-server-4.7.1.jar:4.7.1]
        at org.apache.bookkeeper.proto.BookieClient.lambda$new$0(BookieClient.java:118) ~[org.apache.bookkeeper-bookkeeper-server-4.7.1.jar:4.7.1]
        at org.apache.bookkeeper.util.SafeRunnable$1.safeRun(SafeRunnable.java:43) ~[org.apache.bookkeeper-bookkeeper-server-4.7.1.jar:4.7.1]
        at org.apache.bookkeeper.common.util.SafeRunnable.run(SafeRunnable.java:36) [org.apache.bookkeeper-bookkeeper-common-4.7.1.jar:4.7.1]
        at com.google.common.util.concurrent.MoreExecutors$ScheduledListeningDecorator$NeverSuccessfulListenableFutureTask.run(MoreExecutors.java:587) [com.google.guava-guava-21.0.jar:?]
        at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:511) [?:1.8.0_181]
        at java.util.concurrent.FutureTask.runAndReset(FutureTask.java:308) [?:1.8.0_181]
        at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.access$301(ScheduledThreadPoolExecutor.java:180) [?:1.8.0_181]
        at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(ScheduledThreadPoolExecutor.java:294) [?:1.8.0_181]
        at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149) [?:1.8.0_181]
        at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624) [?:1.8.0_181]
        at io.netty.util.concurrent.FastThreadLocalRunnable.run(FastThreadLocalRunnable.java:30) [io.netty-netty-all-4.1.22.Final.jar:4.1.22.Final]
        at java.lang.Thread.run(Thread.java:748) [?:1.8.0_181]

/cc @ivankelly @merlimat

@merlimat
Copy link
Contributor

Yes, this should go into 4.7.2

sijie pushed a commit to sijie/bookkeeper that referenced this pull request Aug 21, 2018
For a long time PerChannelBookieClient has used guava
LinkedListMultiMap to store conflicting V2 completion keys and
values. This is problematic though. Completion keys are pooled
objects. When a key-value pair is stored in a LinkedListMultiMap, if
it is the first value for that key, a collection is created for the
values, and added to a top-level map using the key, and then the key
and the value are added to the collection. When a second value is
added for the same key, the key and value are simply added to the
collection. The problem occurs when the first key is removed. PBCB
will recycle the key object, but this object is still being used in
the multimap in the top-level map. This causes all sorts of fun like
NullPointerException and IllegalStateException.

Because of this, this patch introduces a very simple multimap
implementation that only stores the key one time (in the collection)
and uses the hashCode of the key to separate the collections into
buckets. It's pretty inefficient, but this code it only hit in the
rare case where a client is trying to read or write the same entry
from the same ledger more than once at the same time.

Author: Ivan Kelly <ivan@ivankelly.net>

Reviewers: Enrico Olivelli <eolivelli@gmail.com>

This closes apache#1569 from ivankelly/conc-test-flake
sijie pushed a commit that referenced this pull request Aug 22, 2018
Descriptions of the changes in this PR:

(cherry-pick #1569)

For a long time PerChannelBookieClient has used guava
LinkedListMultiMap to store conflicting V2 completion keys and
values. This is problematic though. Completion keys are pooled
objects. When a key-value pair is stored in a LinkedListMultiMap, if
it is the first value for that key, a collection is created for the
values, and added to a top-level map using the key, and then the key
and the value are added to the collection. When a second value is
added for the same key, the key and value are simply added to the
collection. The problem occurs when the first key is removed. PBCB
will recycle the key object, but this object is still being used in
the multimap in the top-level map. This causes all sorts of fun like
NullPointerException and IllegalStateException.

Because of this, this patch introduces a very simple multimap
implementation that only stores the key one time (in the collection)
and uses the hashCode of the key to separate the collections into
buckets. It's pretty inefficient, but this code it only hit in the
rare case where a client is trying to read or write the same entry
from the same ledger more than once at the same time.

Author: Ivan Kelly <ivanivankelly.net>

Reviewers: Enrico Olivelli <eolivelligmail.com>

This closes #1569 from ivankelly/conc-test-flake

Master Issue: #1569

Author: Ivan Kelly <ivan@ivankelly.net>

Reviewers: Ivan Kelly <ivank@apache.org>, Enrico Olivelli <eolivelli@gmail.com>

This closes #1618 from sijie/cherry-pick-pcbc
reddycharan pushed a commit to reddycharan/bookkeeper that referenced this pull request Oct 17, 2018
Descriptions of the changes in this PR:

(cherry-pick apache#1569)

For a long time PerChannelBookieClient has used guava
LinkedListMultiMap to store conflicting V2 completion keys and
values. This is problematic though. Completion keys are pooled
objects. When a key-value pair is stored in a LinkedListMultiMap, if
it is the first value for that key, a collection is created for the
values, and added to a top-level map using the key, and then the key
and the value are added to the collection. When a second value is
added for the same key, the key and value are simply added to the
collection. The problem occurs when the first key is removed. PBCB
will recycle the key object, but this object is still being used in
the multimap in the top-level map. This causes all sorts of fun like
NullPointerException and IllegalStateException.

Because of this, this patch introduces a very simple multimap
implementation that only stores the key one time (in the collection)
and uses the hashCode of the key to separate the collections into
buckets. It's pretty inefficient, but this code it only hit in the
rare case where a client is trying to read or write the same entry
from the same ledger more than once at the same time.

Author: Ivan Kelly <ivanivankelly.net>

Reviewers: Enrico Olivelli <eolivelligmail.com>

This closes apache#1569 from ivankelly/conc-test-flake

Master Issue: apache#1569

Author: Ivan Kelly <ivan@ivankelly.net>

Reviewers: Ivan Kelly <ivank@apache.org>, Enrico Olivelli <eolivelli@gmail.com>

This closes apache#1618 from sijie/cherry-pick-pcbc

(cherry picked from commit 83d3abe)
Signed-off-by: JV Jujjuri <vjujjuri@salesforce.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants