/* * Copyright 2013 The Netty Project * * The Netty Project licenses this file to you under the Apache License, * version 2.0 (the "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at: * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * License for the specific language governing permissions and limitations * under the License. */ package io.netty.util.concurrent; import io.netty.util.internal.logging.InternalLogger; import io.netty.util.internal.logging.InternalLoggerFactory; import org.junit.BeforeClass; import org.junit.Test; import java.util.HashMap; import java.util.Map; import java.util.concurrent.BlockingQueue; import java.util.concurrent.CancellationException; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import java.util.concurrent.atomic.AtomicInteger; import static java.lang.Math.max; import static org.hamcrest.Matchers.lessThan; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertSame; import static org.junit.Assert.assertThat; import static org.junit.Assert.assertTrue; @SuppressWarnings("unchecked") public class DefaultPromiseTest { private static final InternalLogger logger = InternalLoggerFactory.getInstance(DefaultPromiseTest.class); private static int stackOverflowDepth; @BeforeClass public static void beforeClass() { try { findStackOverflowDepth(); throw new IllegalStateException("Expected StackOverflowError but didn't get it?!"); } catch (StackOverflowError e) { logger.debug("StackOverflowError depth: {}", stackOverflowDepth); } } private static void findStackOverflowDepth() { ++stackOverflowDepth; findStackOverflowDepth(); } private static int stackOverflowTestDepth() { return max(stackOverflowDepth << 1, stackOverflowDepth); } @Test(expected = CancellationException.class) public void testCancellationExceptionIsThrownWhenBlockingGet() throws InterruptedException, ExecutionException { final Promise<Void> promise = new DefaultPromise<Void>(ImmediateEventExecutor.INSTANCE); promise.cancel(false); promise.get(); } @Test(expected = CancellationException.class) public void testCancellationExceptionIsThrownWhenBlockingGetWithTimeout() throws InterruptedException, ExecutionException, TimeoutException { final Promise<Void> promise = new DefaultPromise<Void>(ImmediateEventExecutor.INSTANCE); promise.cancel(false); promise.get(1, TimeUnit.SECONDS); } @Test public void testStackOverflowWithImmediateEventExecutorA() throws Exception { testStackOverFlowChainedFuturesA(stackOverflowTestDepth(), ImmediateEventExecutor.INSTANCE, true); testStackOverFlowChainedFuturesA(stackOverflowTestDepth(), ImmediateEventExecutor.INSTANCE, false); } @Test public void testNoStackOverflowWithDefaultEventExecutorA() throws Exception { ExecutorService executorService = Executors.newSingleThreadExecutor(); try { EventExecutor executor = new DefaultEventExecutor(executorService); try { testStackOverFlowChainedFuturesA(stackOverflowTestDepth(), executor, true); testStackOverFlowChainedFuturesA(stackOverflowTestDepth(), executor, false); } finally { executor.shutdownGracefully(0, 0, TimeUnit.MILLISECONDS); } } finally { executorService.shutdown(); } } @Test public void testNoStackOverflowWithImmediateEventExecutorB() throws Exception { testStackOverFlowChainedFuturesB(stackOverflowTestDepth(), ImmediateEventExecutor.INSTANCE, true); testStackOverFlowChainedFuturesB(stackOverflowTestDepth(), ImmediateEventExecutor.INSTANCE, false); } @Test public void testNoStackOverflowWithDefaultEventExecutorB() throws Exception { ExecutorService executorService = Executors.newSingleThreadExecutor(); try { EventExecutor executor = new DefaultEventExecutor(executorService); try { testStackOverFlowChainedFuturesB(stackOverflowTestDepth(), executor, true); testStackOverFlowChainedFuturesB(stackOverflowTestDepth(), executor, false); } finally { executor.shutdownGracefully(0, 0, TimeUnit.MILLISECONDS); } } finally { executorService.shutdown(); } } @Test public void testListenerNotifyOrder() throws Exception { EventExecutor executor = new TestEventExecutor(); try { final BlockingQueue<FutureListener<Void>> listeners = new LinkedBlockingQueue<FutureListener<Void>>(); int runs = 100000; for (int i = 0; i < runs; i++) { final Promise<Void> promise = new DefaultPromise<Void>(executor); final FutureListener<Void> listener1 = new FutureListener<Void>() { @Override public void operationComplete(Future<Void> future) throws Exception { listeners.add(this); } }; final FutureListener<Void> listener2 = new FutureListener<Void>() { @Override public void operationComplete(Future<Void> future) throws Exception { listeners.add(this); } }; final FutureListener<Void> listener4 = new FutureListener<Void>() { @Override public void operationComplete(Future<Void> future) throws Exception { listeners.add(this); } }; final FutureListener<Void> listener3 = new FutureListener<Void>() { @Override public void operationComplete(Future<Void> future) throws Exception { listeners.add(this); future.addListener(listener4); } }; GlobalEventExecutor.INSTANCE.execute(new Runnable() { @Override public void run() { promise.setSuccess(null); } }); promise.addListener(listener1).addListener(listener2).addListener(listener3); assertSame("Fail 1 during run " + i + " / " + runs, listener1, listeners.take()); assertSame("Fail 2 during run " + i + " / " + runs, listener2, listeners.take()); assertSame("Fail 3 during run " + i + " / " + runs, listener3, listeners.take()); assertSame("Fail 4 during run " + i + " / " + runs, listener4, listeners.take()); assertTrue("Fail during run " + i + " / " + runs, listeners.isEmpty()); } } finally { executor.shutdownGracefully(0, 0, TimeUnit.SECONDS).sync(); } } @Test public void testListenerNotifyLater() throws Exception { // Testing first execution path in DefaultPromise testListenerNotifyLater(1); // Testing second execution path in DefaultPromise testListenerNotifyLater(2); } @Test(timeout = 2000) public void testPromiseListenerAddWhenCompleteFailure() throws Exception { testPromiseListenerAddWhenComplete(fakeException()); } @Test(timeout = 2000) public void testPromiseListenerAddWhenCompleteSuccess() throws Exception { testPromiseListenerAddWhenComplete(null); } @Test(timeout = 2000) public void testLateListenerIsOrderedCorrectlySuccess() throws InterruptedException { testLateListenerIsOrderedCorrectly(null); } @Test(timeout = 2000) public void testLateListenerIsOrderedCorrectlyFailure() throws InterruptedException { testLateListenerIsOrderedCorrectly(fakeException()); } @Test public void testSignalRace() { final long wait = TimeUnit.NANOSECONDS.convert(10, TimeUnit.SECONDS); EventExecutor executor = null; try { executor = new TestEventExecutor(); final int numberOfAttempts = 4096; final Map<Thread, DefaultPromise<Void>> promises = new HashMap<Thread, DefaultPromise<Void>>(); for (int i = 0; i < numberOfAttempts; i++) { final DefaultPromise<Void> promise = new DefaultPromise<Void>(executor); final Thread thread = new Thread(new Runnable() { @Override public void run() { promise.setSuccess(null); } }); promises.put(thread, promise); } for (final Map.Entry<Thread, DefaultPromise<Void>> promise : promises.entrySet()) { promise.getKey().start(); final long start = System.nanoTime(); promise.getValue().awaitUninterruptibly(wait, TimeUnit.NANOSECONDS); assertThat(System.nanoTime() - start, lessThan(wait)); } } finally { if (executor != null) { executor.shutdownGracefully(); } } } private void testStackOverFlowChainedFuturesA(int promiseChainLength, final EventExecutor executor, boolean runTestInExecutorThread) throws InterruptedException { final Promise<Void>[] p = new DefaultPromise[promiseChainLength]; final CountDownLatch latch = new CountDownLatch(promiseChainLength); if (runTestInExecutorThread) { executor.execute(new Runnable() { @Override public void run() { testStackOverFlowChainedFuturesA(executor, p, latch); } }); } else { testStackOverFlowChainedFuturesA(executor, p, latch); } assertTrue(latch.await(2, TimeUnit.SECONDS)); for (int i = 0; i < p.length; ++i) { assertTrue("index " + i, p[i].isSuccess()); } } private void testStackOverFlowChainedFuturesA(EventExecutor executor, final Promise<Void>[] p, final CountDownLatch latch) { for (int i = 0; i < p.length; i ++) { final int finalI = i; p[i] = new DefaultPromise<Void>(executor); p[i].addListener(new FutureListener<Void>() { @Override public void operationComplete(Future<Void> future) throws Exception { if (finalI + 1 < p.length) { p[finalI + 1].setSuccess(null); } latch.countDown(); } }); } p[0].setSuccess(null); } private void testStackOverFlowChainedFuturesB(int promiseChainLength, final EventExecutor executor, boolean runTestInExecutorThread) throws InterruptedException { final Promise<Void>[] p = new DefaultPromise[promiseChainLength]; final CountDownLatch latch = new CountDownLatch(promiseChainLength); if (runTestInExecutorThread) { executor.execute(new Runnable() { @Override public void run() { testStackOverFlowChainedFuturesA(executor, p, latch); } }); } else { testStackOverFlowChainedFuturesA(executor, p, latch); } assertTrue(latch.await(2, TimeUnit.SECONDS)); for (int i = 0; i < p.length; ++i) { assertTrue("index " + i, p[i].isSuccess()); } } private void testStackOverFlowChainedFuturesB(EventExecutor executor, final Promise<Void>[] p, final CountDownLatch latch) { for (int i = 0; i < p.length; i ++) { final int finalI = i; p[i] = new DefaultPromise<Void>(executor); p[i].addListener(new FutureListener<Void>() { @Override public void operationComplete(Future<Void> future) throws Exception { future.addListener(new FutureListener<Void>() { @Override public void operationComplete(Future<Void> future) throws Exception { if (finalI + 1 < p.length) { p[finalI + 1].setSuccess(null); } latch.countDown(); } }); } }); } p[0].setSuccess(null); } /** * This test is mean to simulate the following sequence of events, which all take place on the I/O thread: * <ol> * <li>A write is done</li> * <li>The write operation completes, and the promise state is changed to done</li> * <li>A listener is added to the return from the write. The {@link FutureListener#operationComplete(Future)} * updates state which must be invoked before the response to the previous write is read.</li> * <li>The write operation</li> * </ol> */ private static void testLateListenerIsOrderedCorrectly(Throwable cause) throws InterruptedException { final EventExecutor executor = new TestEventExecutor(); try { final AtomicInteger state = new AtomicInteger(); final CountDownLatch latch1 = new CountDownLatch(1); final CountDownLatch latch2 = new CountDownLatch(2); final Promise<Void> promise = new DefaultPromise<Void>(executor); // Add a listener before completion so "lateListener" is used next time we add a listener. promise.addListener(new FutureListener<Void>() { @Override public void operationComplete(Future<Void> future) throws Exception { assertTrue(state.compareAndSet(0, 1)); } }); // Simulate write operation completing, which will execute listeners in another thread. if (cause == null) { promise.setSuccess(null); } else { promise.setFailure(cause); } // Add a "late listener" promise.addListener(new FutureListener<Void>() { @Override public void operationComplete(Future<Void> future) throws Exception { assertTrue(state.compareAndSet(1, 2)); latch1.countDown(); } }); // Wait for the listeners and late listeners to be completed. latch1.await(); assertEquals(2, state.get()); // This is the important listener. A late listener that is added after all late listeners // have completed, and needs to update state before a read operation (on the same executor). executor.execute(new Runnable() { @Override public void run() { promise.addListener(new FutureListener<Void>() { @Override public void operationComplete(Future<Void> future) throws Exception { assertTrue(state.compareAndSet(2, 3)); latch2.countDown(); } }); } }); // Simulate a read operation being queued up in the executor. executor.execute(new Runnable() { @Override public void run() { // This is the key, we depend upon the state being set in the next listener. assertEquals(3, state.get()); latch2.countDown(); } }); latch2.await(); } finally { executor.shutdownGracefully(0, 0, TimeUnit.SECONDS).sync(); } } private static void testPromiseListenerAddWhenComplete(Throwable cause) throws InterruptedException { final CountDownLatch latch = new CountDownLatch(1); final Promise<Void> promise = new DefaultPromise<Void>(ImmediateEventExecutor.INSTANCE); promise.addListener(new FutureListener<Void>() { @Override public void operationComplete(Future<Void> future) throws Exception { promise.addListener(new FutureListener<Void>() { @Override public void operationComplete(Future<Void> future) throws Exception { latch.countDown(); } }); } }); if (cause == null) { promise.setSuccess(null); } else { promise.setFailure(cause); } latch.await(); } private static void testListenerNotifyLater(final int numListenersBefore) throws Exception { EventExecutor executor = new TestEventExecutor(); int expectedCount = numListenersBefore + 2; final CountDownLatch latch = new CountDownLatch(expectedCount); final FutureListener<Void> listener = new FutureListener<Void>() { @Override public void operationComplete(Future<Void> future) throws Exception { latch.countDown(); } }; final Promise<Void> promise = new DefaultPromise<Void>(executor); executor.execute(new Runnable() { @Override public void run() { for (int i = 0; i < numListenersBefore; i++) { promise.addListener(listener); } promise.setSuccess(null); GlobalEventExecutor.INSTANCE.execute(new Runnable() { @Override public void run() { promise.addListener(listener); } }); promise.addListener(listener); } }); assertTrue("Should have notified " + expectedCount + " listeners", latch.await(5, TimeUnit.SECONDS)); executor.shutdownGracefully().sync(); } private static final class TestEventExecutor extends SingleThreadEventExecutor { TestEventExecutor() { super(null, Executors.defaultThreadFactory(), true); } @Override protected void run() { for (;;) { Runnable task = takeTask(); if (task != null) { task.run(); updateLastExecutionTime(); } if (confirmShutdown()) { break; } } } } private static RuntimeException fakeException() { return new RuntimeException("fake exception"); } }