diff --git a/common/src/main/java/io/netty/util/concurrent/DefaultPromise.java b/common/src/main/java/io/netty/util/concurrent/DefaultPromise.java index 98ec6605dd..6facc2fe71 100644 --- a/common/src/main/java/io/netty/util/concurrent/DefaultPromise.java +++ b/common/src/main/java/io/netty/util/concurrent/DefaultPromise.java @@ -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; @@ -41,6 +42,9 @@ public class DefaultPromise implements Promise { 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; @@ -124,10 +128,32 @@ public class DefaultPromise implements Promise { 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 @@ -294,8 +320,7 @@ public class DefaultPromise implements Promise { */ @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(); } diff --git a/common/src/test/java/io/netty/util/concurrent/DefaultPromiseTest.java b/common/src/test/java/io/netty/util/concurrent/DefaultPromiseTest.java index f66da6e04a..4156fd4915 100644 --- a/common/src/test/java/io/netty/util/concurrent/DefaultPromiseTest.java +++ b/common/src/test/java/io/netty/util/concurrent/DefaultPromiseTest.java @@ -38,6 +38,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.*; @@ -71,7 +72,7 @@ public class DefaultPromiseTest { Mockito.when(executor.inEventLoop()).thenReturn(false); Promise promise = new DefaultPromise(executor); - promise.cancel(false); + assertTrue(promise.cancel(false)); Mockito.verify(executor, Mockito.never()).execute(Mockito.any(Runnable.class)); assertTrue(promise.isCancelled()); } @@ -103,7 +104,7 @@ public class DefaultPromiseTest { @Test(expected = CancellationException.class) public void testCancellationExceptionIsThrownWhenBlockingGet() throws InterruptedException, ExecutionException { final Promise promise = new DefaultPromise<>(ImmediateEventExecutor.INSTANCE); - promise.cancel(false); + assertTrue(promise.cancel(false)); promise.get(); } @@ -111,10 +112,18 @@ public class DefaultPromiseTest { public void testCancellationExceptionIsThrownWhenBlockingGetWithTimeout() throws InterruptedException, ExecutionException, TimeoutException { final Promise promise = new DefaultPromise<>(ImmediateEventExecutor.INSTANCE); - promise.cancel(false); + assertTrue(promise.cancel(false)); promise.get(1, TimeUnit.SECONDS); } + @Test + public void testCancellationExceptionIsReturnedAsCause() throws InterruptedException, + ExecutionException, TimeoutException { + final Promise promise = new DefaultPromise<>(ImmediateEventExecutor.INSTANCE); + assertTrue(promise.cancel(false)); + assertThat(promise.cause(), instanceOf(CancellationException.class)); + } + @Test public void testStackOverflowWithImmediateEventExecutorA() throws Exception { testStackOverFlowChainedFuturesA(stackOverflowTestDepth(), ImmediateEventExecutor.INSTANCE, true);