/* * Copyright 2012 LinkedIn, Inc * * Licensed 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 com.linkedin.parseq; import com.linkedin.parseq.promise.Promise; import com.linkedin.parseq.promise.Promises; import com.linkedin.parseq.promise.SettablePromise; import org.testng.annotations.AfterMethod; import org.testng.annotations.BeforeMethod; import org.testng.annotations.Test; import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.RejectedExecutionException; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; import static com.linkedin.parseq.TestUtil.withDisabledLogging; import static org.testng.AssertJUnit.assertEquals; import static org.testng.AssertJUnit.assertFalse; import static org.testng.AssertJUnit.assertTrue; /** * @author Chris Pettitt */ public class TestEngine { private Engine _engine; private ScheduledExecutorService _scheduler; @BeforeMethod public void setUp() throws Exception { final int numCores = Runtime.getRuntime().availableProcessors(); _scheduler = Executors.newScheduledThreadPool(numCores + 1); _engine = new EngineBuilder().setTaskExecutor(_scheduler).setTimerScheduler(_scheduler).build(); } @AfterMethod public void tearDown() throws Exception { _engine.shutdown(); _engine.awaitTermination(50, TimeUnit.MILLISECONDS); _engine = null; _scheduler.shutdownNow(); _scheduler = null; } @Test public void testShutdownWithNoTasks() throws InterruptedException { _engine.shutdown(); assertTrue(_engine.isShutdown()); assertTrue(_engine.awaitTermination(50, TimeUnit.MILLISECONDS)); assertTrue(_engine.isTerminated()); assertTrue(_engine.isShutdown()); } @Test public void testShutdownThenRunTask() throws InterruptedException { _engine.shutdown(); final Task<String> task = Task.value("task executed"); _engine.run(task); // Task should be cancelled immediately assertTrue(task.await(50, TimeUnit.MILLISECONDS)); assertTrue(task.isFailed()); } @Test public void testShutdownWithRunningTask() throws InterruptedException { final CountDownLatch finishLatch = new CountDownLatch(1); final String taskValue = "task executed"; final Task<String> task = new BaseTask<String>() { @Override protected Promise<? extends String> run(final Context context) throws Exception { finishLatch.await(); return Promises.value(taskValue); } }; _engine.run(task); _engine.shutdown(); // shutdown should not complete until after our task is done assertFalse(_engine.awaitTermination(50, TimeUnit.MILLISECONDS)); assertTrue(_engine.isShutdown()); assertFalse(_engine.isTerminated()); finishLatch.countDown(); assertTrue(_engine.awaitTermination(50, TimeUnit.MILLISECONDS)); assertTrue(_engine.isShutdown()); assertTrue(_engine.isTerminated()); // Task should finish shortly assertTrue(task.await(50, TimeUnit.MILLISECONDS)); assertEquals(taskValue, task.get()); } @Test public void testShutdownWithRunningAndSuccessorTask() throws InterruptedException { final CountDownLatch finishLatch = new CountDownLatch(1); final String predValue = "task executed"; final String sucValue = "task executed"; final Task<String> predTask = new BaseTask<String>() { @Override protected Promise<? extends String> run(final Context context) throws Exception { finishLatch.await(); return Promises.value(predValue); } }; final Task<String> sucTask = Task.value(sucValue); final Task<String> seq = predTask.andThen(sucTask); _engine.run(seq); _engine.shutdown(); // shutdown should not complete until after our task is done assertFalse(_engine.awaitTermination(50, TimeUnit.MILLISECONDS)); assertTrue(_engine.isShutdown()); assertFalse(_engine.isTerminated()); finishLatch.countDown(); assertTrue(_engine.awaitTermination(50, TimeUnit.MILLISECONDS)); assertTrue(_engine.isShutdown()); assertTrue(_engine.isTerminated()); // Tasks should finish shortly assertTrue(predTask.await(50, TimeUnit.MILLISECONDS)); assertEquals(predValue, predTask.get()); assertTrue(sucTask.await(50, TimeUnit.MILLISECONDS)); assertEquals(sucValue, sucTask.get()); } @Test public void testShutdownWithSideEffectTask() throws InterruptedException { final CountDownLatch finishLatch = new CountDownLatch(1); final String mainValue = "main task executed"; final String sideEffectValue = "side-effect task executed"; Task<String> sideEffect = Task.async(context -> { finishLatch.await(); return Promises.value(sideEffectValue); }); Task<String> task = Task.value(mainValue).withSideEffect(v -> sideEffect); _engine.run(task); _engine.shutdown(); assertFalse(_engine.awaitTermination(50, TimeUnit.MILLISECONDS)); assertTrue(_engine.isShutdown()); assertFalse(_engine.isTerminated()); finishLatch.countDown(); assertTrue(_engine.awaitTermination(50, TimeUnit.MILLISECONDS)); assertTrue(_engine.isShutdown()); assertTrue(_engine.isTerminated()); assertEquals(mainValue, task.get()); assertEquals(sideEffectValue, sideEffect.get()); } @Test public void testShutdownWithSideEffectTask2() throws InterruptedException { final SettablePromise<String> sideEffectPromise = Promises.settable(); final String mainValue = "main task executed"; final String sideEffectValue = "side-effect task executed"; Task<String> sideEffect = Task.async(context -> sideEffectPromise); Task<String> task = Task.value(mainValue).withSideEffect(v -> sideEffect); _engine.run(task); _engine.shutdown(); assertFalse(_engine.awaitTermination(50, TimeUnit.MILLISECONDS)); assertTrue(_engine.isShutdown()); assertFalse(_engine.isTerminated()); sideEffectPromise.done(sideEffectValue); assertTrue(_engine.awaitTermination(50, TimeUnit.MILLISECONDS)); assertTrue(_engine.isShutdown()); assertTrue(_engine.isTerminated()); assertEquals(mainValue, task.get()); assertEquals(sideEffectValue, sideEffect.get()); } @Test public void testFailPlanExecution() throws InterruptedException { // This test ensures that if execution of a plan's serial executor loop // fails, e.g. in the case that the underlying executor is saturated, that // we fail the plan. To simplify this test, we constructor our own executor // instead of using the default executor set up for test. final ExecutorService executorService = new ThreadPoolExecutor(1, 1, 0, TimeUnit.SECONDS, new ArrayBlockingQueue<Runnable>(1), new ThreadPoolExecutor.AbortPolicy()); final ScheduledExecutorService scheduledExecutorService = Executors.newSingleThreadScheduledExecutor(); try { final Engine engine = new EngineBuilder().setTaskExecutor(executorService).setTimerScheduler(scheduledExecutorService).build(); // First we submit two tasks that will never finish. This saturates the // underlying executor by using its only thread and saturating its // single slot queue. engine.run(neverEndingBlockingTask()); engine.run(neverEndingBlockingTask()); // Now we submit another task. The execution loop for this task will fail // during submit to the underlying executor. We expect that it will be // cancelled. final Task<?> task = neverEndingBlockingTask(); withDisabledLogging(new Runnable() { @Override public void run() { engine.run(task); try { assertTrue(task.await(5, TimeUnit.SECONDS)); } catch (InterruptedException e) { // Ignore. } } }); assertTrue(task.isFailed()); assertTrue( "Expected underlying exception to be instance of RejectedExecutionException, but was: " + task.getError().getCause().getCause(), task.getError().getCause().getCause() instanceof RejectedExecutionException); engine.shutdown(); } finally { scheduledExecutorService.shutdownNow(); executorService.shutdownNow(); } } /** * A task that blocks forever when it is executed, tying up whatever thread * executes it. */ private Task<?> neverEndingBlockingTask() { return new BaseTask<Object>() { @Override protected Promise<?> run(Context context) throws Throwable { new CountDownLatch(1).await(); return Promises.value("A value that should never be seen!"); } }; } }