package org.threadly.concurrent; import static org.junit.Assert.*; import static org.threadly.TestConstants.*; import java.util.ArrayList; import java.util.Iterator; import java.util.List; import java.util.concurrent.RejectedExecutionException; import java.util.concurrent.TimeoutException; import org.junit.Test; import org.threadly.BlockingTestRunnable; import org.threadly.concurrent.AbstractPriorityScheduler.OneTimeTaskWrapper; import org.threadly.concurrent.future.ListenableFuture; import org.threadly.test.concurrent.AsyncVerifier; import org.threadly.test.concurrent.TestCondition; import org.threadly.test.concurrent.TestRunnable; import org.threadly.util.Clock; @SuppressWarnings("javadoc") public class PrioritySchedulerTest extends AbstractPrioritySchedulerTest { @Override protected AbstractPrioritySchedulerFactory getAbstractPrioritySchedulerFactory() { return getPrioritySchedulerFactory(); } protected PrioritySchedulerServiceFactory getPrioritySchedulerFactory() { return new PrioritySchedulerFactory(); } @Override protected boolean isSingleThreaded() { return false; } @Test @SuppressWarnings("unused") public void constructorFail() { try { new PriorityScheduler(0, TaskPriority.High, 1, null); fail("Exception should have thrown"); } catch (IllegalArgumentException e) { // expected } try { new PriorityScheduler(1, TaskPriority.High, -1, null); fail("Exception should have thrown"); } catch (IllegalArgumentException e) { // expected } } @Test public void constructorNullFactoryTest() { PriorityScheduler ps = new PriorityScheduler(1, TaskPriority.High, 1, null); // should be set with default assertNotNull(ps.workerPool.threadFactory); } @Test public void getAndSetPoolSizeTest() { PrioritySchedulerServiceFactory factory = getPrioritySchedulerFactory(); int corePoolSize = 1; PriorityScheduler scheduler = factory.makePriorityScheduler(corePoolSize); try { assertEquals(corePoolSize, scheduler.getMaxPoolSize()); corePoolSize = 10; scheduler.setPoolSize(corePoolSize); assertEquals(corePoolSize, scheduler.getMaxPoolSize()); } finally { factory.shutdown(); } } @Test public void setPoolSizeSmallerTest() { PrioritySchedulerServiceFactory factory = getPrioritySchedulerFactory(); PriorityScheduler scheduler = factory.makePriorityScheduler(10); try { scheduler.prestartAllThreads(); scheduler.setPoolSize(1); assertEquals(1, scheduler.getMaxPoolSize()); } finally { factory.shutdown(); } } @Test public void setPoolSizeFail() { PrioritySchedulerServiceFactory factory = getPrioritySchedulerFactory(); // first construct a valid scheduler PriorityScheduler scheduler = factory.makePriorityScheduler(1); try { // verify no negative values try { scheduler.setPoolSize(-1); fail("Exception should have been thrown"); } catch (IllegalArgumentException expected) { // ignored } } finally { factory.shutdown(); } } @Test public void increasePoolSizeWithWaitingTaskTest() { PrioritySchedulerServiceFactory factory = getPrioritySchedulerFactory(); PriorityScheduler scheduler = factory.makePriorityScheduler(1); BlockingTestRunnable btr = new BlockingTestRunnable(); try { scheduler.execute(btr); btr.blockTillStarted(); // all these runnables should be blocked List<TestRunnable> executedRunnables = executeTestRunnables(scheduler, 0); // this should allow the waiting test runnables to quickly execute scheduler.setPoolSize(Math.max(2, TEST_QTY / 2)); Iterator<TestRunnable> it = executedRunnables.iterator(); while (it.hasNext()) { it.next().blockTillStarted(); // will throw exception if not ran } } finally { btr.unblock(); factory.shutdown(); } } @Test public void getCurrentPoolSizeTest() { PrioritySchedulerServiceFactory factory = getPrioritySchedulerFactory(); PriorityScheduler scheduler = factory.makePriorityScheduler(1); try { // verify nothing at the start assertEquals(0, scheduler.getCurrentPoolSize()); TestRunnable tr = new TestRunnable(); scheduler.execute(tr); tr.blockTillStarted(); // wait for execution assertEquals(1, scheduler.getCurrentPoolSize()); } finally { factory.shutdown(); } } @Test public void interruptedDuringRunTest() throws InterruptedException, TimeoutException { final long taskRunTime = 1000 * 10; PrioritySchedulerServiceFactory factory = getPrioritySchedulerFactory(); try { final PriorityScheduler scheduler = factory.makePriorityScheduler(1); final AsyncVerifier interruptSentAV = new AsyncVerifier(); TestRunnable tr = new TestRunnable() { @Override public void handleRunFinish() { long startTime = System.currentTimeMillis(); Thread currentThread = Thread.currentThread(); while (System.currentTimeMillis() - startTime < taskRunTime && ! currentThread.isInterrupted()) { // spin } interruptSentAV.assertTrue(currentThread.isInterrupted()); interruptSentAV.signalComplete(); } }; ListenableFuture<?> future = scheduler.submit(tr); tr.blockTillStarted(); // should interrupt assertTrue(future.cancel(true)); interruptSentAV.waitForTest(); // verify thread was interrupted as expected // verify worker was returned to pool new TestCondition(() -> scheduler.workerPool.idleWorker.get() != null).blockTillTrue(); // verify pool size is still correct assertEquals(1, scheduler.getCurrentPoolSize()); // verify interrupted status has been cleared final AsyncVerifier interruptClearedAV = new AsyncVerifier(); scheduler.execute(new Runnable() { @Override public void run() { interruptClearedAV.assertFalse(Thread.currentThread().isInterrupted()); interruptClearedAV.signalComplete(); } }); // block till we have verified that the interrupted status has been reset interruptClearedAV.waitForTest(); } finally { factory.shutdown(); } } @Test public void isShutdownTest() { PrioritySchedulerServiceFactory factory = getPrioritySchedulerFactory(); try { PriorityScheduler scheduler = factory.makePriorityScheduler(1); assertFalse(scheduler.isShutdown()); scheduler.shutdown(); assertTrue(scheduler.isShutdown()); scheduler = factory.makePriorityScheduler(1); scheduler.shutdownNow(); assertTrue(scheduler.isShutdown()); } finally { factory.shutdown(); } } @Test public void shutdownTest() { PrioritySchedulerServiceFactory factory = getPrioritySchedulerFactory(); try { PriorityScheduler scheduler = factory.makePriorityScheduler(1); /* adding a run time to have greater chances that runnable * will be waiting to execute after shutdown call. */ TestRunnable lastRunnable = executeTestRunnables(scheduler, 5).get(TEST_QTY - 1); scheduler.shutdown(); // runnable should finish lastRunnable.blockTillFinished(); } finally { factory.shutdown(); } } @Test public void shutdownRecurringTest() { PrioritySchedulerServiceFactory factory = getPrioritySchedulerFactory(); try { final PriorityScheduler scheduler = factory.makePriorityScheduler(1); TestRunnable tr = new TestRunnable(); scheduler.scheduleWithFixedDelay(tr, 0, 0); tr.blockTillStarted(); scheduler.shutdown(); new TestCondition(() -> scheduler.workerPool.isShutdownFinished() && scheduler.getCurrentPoolSize() == 0).blockTillTrue(); } finally { factory.shutdown(); } } @Test public void shutdownFail() { PrioritySchedulerServiceFactory factory = getPrioritySchedulerFactory(); try { PriorityScheduler scheduler = factory.makePriorityScheduler(1); scheduler.shutdown(); try { scheduler.execute(DoNothingRunnable.instance()); fail("Execption should have been thrown"); } catch (RejectedExecutionException e) { // expected } try { scheduler.schedule(DoNothingRunnable.instance(), 1000, null); fail("Execption should have been thrown"); } catch (RejectedExecutionException e) { // expected } try { scheduler.scheduleWithFixedDelay(DoNothingRunnable.instance(), 100, 100); fail("Execption should have been thrown"); } catch (RejectedExecutionException e) { // expected } } finally { factory.shutdown(); } } @Test public void shutdownNowTest() { PrioritySchedulerServiceFactory factory = getPrioritySchedulerFactory(); BlockingTestRunnable btr = new BlockingTestRunnable(); try { final PriorityScheduler scheduler = factory.makePriorityScheduler(1); // execute one runnable which will not complete scheduler.execute(btr); btr.blockTillStarted(); final List<TestRunnable> expectedRunnables = new ArrayList<>(TEST_QTY); for (int i = 0; i < TEST_QTY; i++) { TestRunnable tr = new TestRunnable(); expectedRunnables.add(tr); scheduler.execute(tr, i % 2 == 0 ? TaskPriority.High : TaskPriority.Low); } List<Runnable> canceledRunnables = scheduler.shutdownNow(); // unblock now so that others can run (if the unit test fails) btr.unblock(); assertNotNull(canceledRunnables); assertTrue(canceledRunnables.containsAll(expectedRunnables)); assertTrue(expectedRunnables.containsAll(canceledRunnables)); Iterator<TestRunnable> it = expectedRunnables.iterator(); while (it.hasNext()) { assertEquals(0, it.next().getRunCount()); } } finally { btr.unblock(); factory.shutdown(); } } @Test public void shutdownNowFail() { PrioritySchedulerServiceFactory factory = getPrioritySchedulerFactory(); try { PriorityScheduler scheduler = factory.makePriorityScheduler(1); scheduler.shutdownNow(); try { scheduler.execute(DoNothingRunnable.instance()); fail("Execption should have been thrown"); } catch (RejectedExecutionException e) { // expected } try { scheduler.schedule(DoNothingRunnable.instance(), 1000, null); fail("Execption should have been thrown"); } catch (RejectedExecutionException e) { // expected } try { scheduler.scheduleWithFixedDelay(DoNothingRunnable.instance(), 100, 100); fail("Execption should have been thrown"); } catch (RejectedExecutionException e) { // expected } } finally { factory.shutdown(); } } @Test public void shutdownNowIgnoreCanceledFuturesTest() { PrioritySchedulerServiceFactory factory = getPrioritySchedulerFactory(); try { PriorityScheduler scheduler = factory.makePriorityScheduler(1); Runnable nonCanceledRunnable = new TestRunnable(); scheduler.submitScheduled(nonCanceledRunnable, 1000 * 60 * 60); ListenableFuture<?> future = scheduler.submitScheduled(DoNothingRunnable.instance(), 1000 * 60 * 60); future.cancel(false); List<Runnable> result = scheduler.shutdownNow(); assertEquals(1, result.size()); // only canceled task removed } finally { factory.shutdown(); } } @Test public void awaitTerminationTest() throws InterruptedException { PrioritySchedulerServiceFactory factory = getPrioritySchedulerFactory(); try { PriorityScheduler scheduler = factory.makePriorityScheduler(1); TestRunnable tr = new TestRunnable(DELAY_TIME * 2); long start = Clock.accurateForwardProgressingMillis(); scheduler.execute(tr); tr.blockTillStarted(); scheduler.shutdown(); scheduler.awaitTermination(); long stop = Clock.accurateForwardProgressingMillis(); assertTrue(stop - start >= (DELAY_TIME * 2) - 10); } finally { factory.shutdown(); } } @Test public void awaitTerminationTimeoutTest() throws InterruptedException { PrioritySchedulerServiceFactory factory = getPrioritySchedulerFactory(); try { PriorityScheduler scheduler = factory.makePriorityScheduler(1); TestRunnable tr = new TestRunnable(DELAY_TIME * 2); long start = Clock.accurateForwardProgressingMillis(); scheduler.execute(tr); tr.blockTillStarted(); scheduler.shutdown(); assertTrue(scheduler.awaitTermination(DELAY_TIME * 10)); long stop = Clock.accurateForwardProgressingMillis(); assertTrue(stop - start >= (DELAY_TIME * 2) - 10); } finally { factory.shutdown(); } } @Test public void awaitTerminationTimeoutExcededTest() throws InterruptedException { PrioritySchedulerServiceFactory factory = getPrioritySchedulerFactory(); try { PriorityScheduler scheduler = factory.makePriorityScheduler(1); TestRunnable tr = new TestRunnable(DELAY_TIME * 100); scheduler.execute(tr); tr.blockTillStarted(); scheduler.shutdown(); long start = Clock.accurateForwardProgressingMillis(); assertFalse(scheduler.awaitTermination(DELAY_TIME)); long stop = Clock.accurateForwardProgressingMillis(); assertTrue(stop - start >= DELAY_TIME); } finally { factory.shutdown(); } } @Test public void addToQueueTest() { PrioritySchedulerServiceFactory factory = getPrioritySchedulerFactory(); long taskDelay = 1000 * 10; // make it long to prevent it from getting consumed from the queue PriorityScheduler scheduler = factory.makePriorityScheduler(1); try { scheduler.addToScheduleQueue(scheduler.taskQueueManager.highPriorityQueueSet, new OneTimeTaskWrapper(new TestRunnable(), null, Clock.lastKnownForwardProgressingMillis() + taskDelay)); assertEquals(1, scheduler.taskQueueManager.highPriorityQueueSet.scheduleQueue.size()); assertEquals(0, scheduler.taskQueueManager.lowPriorityQueueSet.scheduleQueue.size()); scheduler.addToScheduleQueue(scheduler.taskQueueManager.lowPriorityQueueSet, new OneTimeTaskWrapper(new TestRunnable(), null, Clock.lastKnownForwardProgressingMillis() + taskDelay)); assertEquals(1, scheduler.taskQueueManager.highPriorityQueueSet.scheduleQueue.size()); assertEquals(1, scheduler.taskQueueManager.lowPriorityQueueSet.scheduleQueue.size()); } finally { factory.shutdown(); } } @Test public void scheduleLaterThenSoonerTest() { // This test is focused around the scheduling defect fixed in 4.4.1 // The condition hit was where we would park for one scheduled task, then a future task // would not get executed in time because the first parked thread was not woken up PrioritySchedulerServiceFactory factory = getPrioritySchedulerFactory(); final PriorityScheduler scheduler = factory.makePriorityScheduler(2); try { // schedule one task a ways out scheduler.schedule(DoNothingRunnable.instance(), 1000 * 60 * 10); // ensure first thread has blocked new TestCondition(() -> scheduler.workerPool.idleWorker.get() != null).blockTillTrue(); // start second thread scheduler.prestartAllThreads(); // ensure second thread has blocked new TestCondition(() -> scheduler.workerPool.idleWorker.get().nextIdleWorker != null) .blockTillTrue(); // schedule soon to run task TestRunnable tr = new TestRunnable(); scheduler.schedule(tr, 10); tr.blockTillStarted(); } finally { factory.shutdown(); } } public interface PrioritySchedulerServiceFactory extends AbstractPrioritySchedulerFactory { public PriorityScheduler makePriorityScheduler(int poolSize, TaskPriority defaultPriority, long maxWaitForLowPriority); public PriorityScheduler makePriorityScheduler(int poolSize); } public static class PrioritySchedulerFactory implements PrioritySchedulerServiceFactory { private final List<PriorityScheduler> executors; public PrioritySchedulerFactory() { executors = new ArrayList<>(2); } @Override public SubmitterScheduler makeSubmitterScheduler(int poolSize, boolean prestartIfAvailable) { return makeSchedulerService(poolSize, prestartIfAvailable); } @Override public SubmitterExecutor makeSubmitterExecutor(int poolSize, boolean prestartIfAvailable) { return makeSchedulerService(poolSize, prestartIfAvailable); } @Override public SchedulerService makeSchedulerService(int poolSize, boolean prestartIfAvailable) { PriorityScheduler result = makePriorityScheduler(poolSize); if (prestartIfAvailable) { result.prestartAllThreads(); } return result; } @Override public AbstractPriorityScheduler makeAbstractPriorityScheduler(int poolSize, TaskPriority defaultPriority, long maxWaitForLowPriority) { return makePriorityScheduler(poolSize, defaultPriority, maxWaitForLowPriority); } @Override public AbstractPriorityScheduler makeAbstractPriorityScheduler(int poolSize) { return makePriorityScheduler(poolSize); } @Override public PriorityScheduler makePriorityScheduler(int poolSize, TaskPriority defaultPriority, long maxWaitForLowPriority) { PriorityScheduler result = new StrictPriorityScheduler(poolSize, defaultPriority, maxWaitForLowPriority); executors.add(result); return result; } @Override public PriorityScheduler makePriorityScheduler(int poolSize) { PriorityScheduler result = new StrictPriorityScheduler(poolSize); executors.add(result); return result; } @Override public void shutdown() { Iterator<PriorityScheduler> it = executors.iterator(); while (it.hasNext()) { it.next().shutdownNow(); it.remove(); } } } }