Avoid CancellationException construction in DefaultPromise (#9534)

Motivation

#9152 reverted some static exception reuse optimizations due to the
problem with Throwable#addSuppressed() raised in #9151. This introduced
a performance issue when promises are cancelled at a high frequency due
to the construction cost of CancellationException at the time that
DefaultPromise#cancel() is called.

Modifications

- Reinstate the prior static CANCELLATION_CAUSE_HOLDER but use it just
as a sentinel to indicate cancellation, constructing a new
CancellationException only if/when one needs to be explicitly
returned/thrown
- Subclass CancellationException, overriding fillInStackTrace() to
minimize the construction cost in these cases

Result

Promises are much cheaper to cancel. Fixes #9522.
This commit is contained in:
Nick Hill 2019-09-05 02:07:24 -07:00 committed by Norman Maurer
parent d446765b84
commit 768a825035
2 changed files with 40 additions and 6 deletions

View File

@ -19,6 +19,7 @@ import io.netty.util.internal.InternalThreadLocalMap;
import io.netty.util.internal.PlatformDependent;
import io.netty.util.internal.StringUtil;
import io.netty.util.internal.SystemPropertyUtil;
import io.netty.util.internal.ThrowableUtil;
import io.netty.util.internal.logging.InternalLogger;
import io.netty.util.internal.logging.InternalLoggerFactory;
@ -40,6 +41,9 @@ public class DefaultPromise<V> extends AbstractFuture<V> implements Promise<V> {
AtomicReferenceFieldUpdater.newUpdater(DefaultPromise.class, Object.class, "result");
private static final Object SUCCESS = new Object();
private static final Object UNCANCELLABLE = new Object();
private static final CauseHolder CANCELLATION_CAUSE_HOLDER = new CauseHolder(ThrowableUtil.unknownStackTrace(
new CancellationException(), DefaultPromise.class, "cancel(...)"));
private static final StackTraceElement[] CANCELLATION_STACK = CANCELLATION_CAUSE_HOLDER.cause.getStackTrace();
private volatile Object result;
private final EventExecutor executor;
@ -131,10 +135,32 @@ public class DefaultPromise<V> extends AbstractFuture<V> implements Promise<V> {
return result == null;
}
private static final class LeanCancellationException extends CancellationException {
private static final long serialVersionUID = 2794674970981187807L;
@Override
public Throwable fillInStackTrace() {
setStackTrace(CANCELLATION_STACK);
return this;
}
@Override
public String toString() {
return CancellationException.class.getName();
}
}
@Override
public Throwable cause() {
Object result = this.result;
return (result instanceof CauseHolder) ? ((CauseHolder) result).cause : null;
if (result != CANCELLATION_CAUSE_HOLDER) {
return (result instanceof CauseHolder) ? ((CauseHolder) result).cause : null;
}
CancellationException ce = new LeanCancellationException();
if (RESULT_UPDATER.compareAndSet(this, CANCELLATION_CAUSE_HOLDER, new CauseHolder(ce))) {
return ce;
}
return ((CauseHolder) this.result).cause;
}
@Override
@ -301,8 +327,7 @@ public class DefaultPromise<V> extends AbstractFuture<V> implements Promise<V> {
*/
@Override
public boolean cancel(boolean mayInterruptIfRunning) {
if (RESULT_UPDATER.get(this) == null &&
RESULT_UPDATER.compareAndSet(this, null, new CauseHolder(new CancellationException()))) {
if (RESULT_UPDATER.compareAndSet(this, null, CANCELLATION_CAUSE_HOLDER)) {
if (checkNotifyWaiters()) {
notifyListeners();
}

View File

@ -37,6 +37,7 @@ import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicInteger;
import static java.lang.Math.max;
import static org.hamcrest.Matchers.instanceOf;
import static org.hamcrest.Matchers.lessThan;
import static org.junit.Assert.*;
@ -70,7 +71,7 @@ public class DefaultPromiseTest {
Mockito.when(executor.inEventLoop()).thenReturn(false);
Promise<Void> promise = new DefaultPromise<Void>(executor);
promise.cancel(false);
assertTrue(promise.cancel(false));
Mockito.verify(executor, Mockito.never()).execute(Mockito.any(Runnable.class));
assertTrue(promise.isCancelled());
}
@ -102,7 +103,7 @@ public class DefaultPromiseTest {
@Test(expected = CancellationException.class)
public void testCancellationExceptionIsThrownWhenBlockingGet() throws InterruptedException, ExecutionException {
final Promise<Void> promise = new DefaultPromise<Void>(ImmediateEventExecutor.INSTANCE);
promise.cancel(false);
assertTrue(promise.cancel(false));
promise.get();
}
@ -110,10 +111,18 @@ public class DefaultPromiseTest {
public void testCancellationExceptionIsThrownWhenBlockingGetWithTimeout() throws InterruptedException,
ExecutionException, TimeoutException {
final Promise<Void> promise = new DefaultPromise<Void>(ImmediateEventExecutor.INSTANCE);
promise.cancel(false);
assertTrue(promise.cancel(false));
promise.get(1, TimeUnit.SECONDS);
}
@Test
public void testCancellationExceptionIsReturnedAsCause() throws InterruptedException,
ExecutionException, TimeoutException {
final Promise<Void> promise = new DefaultPromise<Void>(ImmediateEventExecutor.INSTANCE);
assertTrue(promise.cancel(false));
assertThat(promise.cause(), instanceOf(CancellationException.class));
}
@Test
public void testStackOverflowWithImmediateEventExecutorA() throws Exception {
testStackOverFlowChainedFuturesA(stackOverflowTestDepth(), ImmediateEventExecutor.INSTANCE, true);