package com.linkedin.parseq.internal; import static org.testng.Assert.assertEquals; import static org.testng.AssertJUnit.assertFalse; import static org.testng.AssertJUnit.assertTrue; import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutorService; import java.util.concurrent.RejectedExecutionException; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; import org.testng.annotations.AfterMethod; import org.testng.annotations.BeforeMethod; import org.testng.annotations.DataProvider; import org.testng.annotations.Test; import com.linkedin.parseq.internal.SerialExecutor.DeactivationListener; public class TestSerialExecutor { private ExecutorService _executorService; private CapturingExceptionHandler _rejectionHandler; private SerialExecutor _serialExecutor; private CapturingActivityListener _capturingDeactivationListener; @DataProvider(name = "draining") public Object[][] testDataProvider() { return new Object[][] { { true }, { false } }; } @BeforeMethod public void setUp(Object[] testArgs) { _executorService = new ThreadPoolExecutor(1, 1, 0, TimeUnit.SECONDS, new ArrayBlockingQueue<>(1), new ThreadPoolExecutor.AbortPolicy()); _rejectionHandler = new CapturingExceptionHandler(); _capturingDeactivationListener = new CapturingActivityListener(); _serialExecutor = new SerialExecutor(_executorService, _rejectionHandler, _capturingDeactivationListener, new FIFOPriorityQueue<>(), (boolean)testArgs[0]); } @AfterMethod public void tearDown() throws InterruptedException { _serialExecutor = null; _rejectionHandler = null; _executorService.shutdownNow(); _executorService.awaitTermination(1, TimeUnit.SECONDS); _executorService = null; } @Test(dataProvider = "draining") public void testExecuteOneStepPlan(boolean draining) throws InterruptedException { final LatchedRunnable runnable = new LatchedRunnable(); _serialExecutor.execute(runnable); assertTrue(runnable.await(5, TimeUnit.SECONDS)); assertFalse(_rejectionHandler.wasExecuted()); assertTrue(_capturingDeactivationListener.await(5, TimeUnit.SECONDS)); assertEquals(_capturingDeactivationListener.getDeactivatedCount(), 1); } @Test(dataProvider = "draining") public void testExecuteTwoStepPlan(boolean draining) throws InterruptedException { final LatchedRunnable inner = new LatchedRunnable(); final Runnable outer = new Runnable() { @Override public void run() { _serialExecutor.execute(inner); } }; _executorService.execute(outer); assertTrue(inner.await(5, TimeUnit.SECONDS)); assertFalse(_rejectionHandler.wasExecuted()); assertTrue(_capturingDeactivationListener.await(5, TimeUnit.SECONDS)); assertEquals(_capturingDeactivationListener.getDeactivatedCount(), 1); } @Test(dataProvider = "draining") public void testRejectOnFirstExecute(boolean draining) throws InterruptedException { // First fill up the underlying executor service so that a subsequent // submission of an execution loop by the serial executor will fail. _executorService.execute(new NeverEndingRunnable()); assertFalse(_rejectionHandler.wasExecuted()); _executorService.execute(new NeverEndingRunnable()); assertFalse(_rejectionHandler.wasExecuted()); // Now submit our task to serial executor. The underlying executor should // throw RejectedExecutionException and the rejectionRunnable should run. _serialExecutor.execute(new NeverEndingRunnable()); assertTrue(_rejectionHandler.await(5, TimeUnit.SECONDS)); assertTrue( "Expected " + _rejectionHandler.getLastError() + " to be instance of " + RejectedExecutionException.class.getName(), _rejectionHandler.getLastError() instanceof RejectedExecutionException); } @Test(dataProvider = "draining") public void testRejectOnLoop(boolean draining) throws InterruptedException { final CountDownLatch innerLatch = new CountDownLatch(1); final CountDownLatch outerLatch = new CountDownLatch(1); final LatchedRunnable inner = new LatchedRunnable(); final LatchedRunnable outer = new LatchedRunnable() { @Override public void run() { try { outerLatch.countDown(); innerLatch.await(); _serialExecutor.execute(inner); } catch (InterruptedException e) { // Shouldn't happen } super.run(); } }; // First we submit the outer task. This task will wait until for a count // down on the inner latch. _serialExecutor.execute(outer); outerLatch.await(); // Now we submit another runnable to the underlying executor. This should // get queued up. _executorService.execute(new NeverEndingRunnable()); // Now the inner task will be submitted. This will eventually result in // the re-execution of the serial executor loop, but that re-submission will // fail because the thread is tied up by the current loop and the queue is // saturated with the previously submitted runnable. innerLatch.countDown(); if (!draining) { assertTrue(_rejectionHandler.await(5, TimeUnit.SECONDS)); assertTrue( "Expected " + _rejectionHandler.getLastError() + " to be instance of " + RejectedExecutionException.class.getName(), _rejectionHandler.getLastError() instanceof RejectedExecutionException); } else { assertFalse(_rejectionHandler.wasExecuted()); } } @Test(dataProvider = "draining") public void testThrowingRunnable(boolean draining) throws InterruptedException { _serialExecutor.execute(new ThrowingRunnable()); assertTrue(_rejectionHandler.await(5, TimeUnit.SECONDS)); assertTrue( "Expected " + _rejectionHandler.getLastError() + " to be instance of " + RuntimeException.class.getName(), _rejectionHandler.getLastError() instanceof RuntimeException); } private static class NeverEndingRunnable implements PrioritizableRunnable { @Override public void run() { try { new CountDownLatch(1).await(); } catch (InterruptedException e) { // This is our shutdown mechanism. } } } private static class ThrowingRunnable implements PrioritizableRunnable { @Override public void run() { throw new RuntimeException(); } } private static class CapturingActivityListener implements DeactivationListener { private AtomicInteger _deactivatedCount = new AtomicInteger(); private final CountDownLatch _latch = new CountDownLatch(1); @Override public void deactivated() { _deactivatedCount.incrementAndGet(); _latch.countDown(); } public int getDeactivatedCount() { return _deactivatedCount.get(); } public boolean await(long timeout, TimeUnit unit) throws InterruptedException { return _latch.await(timeout, unit); } } private static class CapturingExceptionHandler implements UncaughtExceptionHandler { private final CountDownLatch _latch = new CountDownLatch(1); private volatile Throwable _lastError; @Override public void uncaughtException(Throwable error) { _lastError = error; _latch.countDown(); } public boolean await(long timeout, TimeUnit unit) throws InterruptedException { return _latch.await(timeout, unit); } public boolean wasExecuted() { return _latch.getCount() == 0; } public Throwable getLastError() { return _lastError; } } private static class LatchedRunnable implements PrioritizableRunnable { private final CountDownLatch _latch = new CountDownLatch(1); @Override public void run() { _latch.countDown(); } public boolean await(long timeout, TimeUnit unit) throws InterruptedException { return _latch.await(timeout, unit); } } }