/* * Copyright (C) 2012 Facebook, 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.facebook.concurrency; import com.facebook.testing.Function; import com.facebook.testing.MockExecutor; import com.facebook.testing.TestUtils; import org.testng.Assert; import org.testng.annotations.BeforeMethod; import org.testng.annotations.Test; import java.util.ConcurrentModificationException; import java.util.concurrent.Executors; import java.util.concurrent.RejectedExecutionException; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; public class TestUnstoppableScheduledExecutorService { private static final Runnable NO_OP = new Runnable() { @Override public void run() { } }; private ScheduledExecutorService executor; private MockExecutor mockExecutor; @BeforeMethod(alwaysRun = true) public void setUp() throws Exception { mockExecutor = new MockExecutor(); executor = new UnstoppableScheduledExecutorService(mockExecutor); } @Test(groups = "fast") public void testShutdown() throws Exception { executor.shutdown(); Assert.assertFalse( mockExecutor.isShutdown(), "mockExecutor should not be shutdown" ); Assert.assertTrue( executor.isShutdown(), "executor should be shut down" ); } @Test(groups = "fast") public void testShutdownNow() throws Exception { Assert.assertTrue( executor.shutdownNow().isEmpty(), "shutdownNow should return empty list" ); Assert.assertFalse( mockExecutor.isShutdown(), "mockExecutor should not be shutdown" ); Assert.assertTrue( executor.isShutdown(), "executor should be shut down" ); } @Test(groups = "fast") public void testAwaitTermination1() throws Exception { ScheduledFuture<?> future = executor.schedule(NO_OP, 10, TimeUnit.SECONDS); Assert.assertFalse( executor.awaitTermination(1, TimeUnit.NANOSECONDS), "executor is terminated" ); executor.shutdown(); Assert.assertTrue(future.isCancelled(), "scheduled task should be cancelled"); } @Test(groups = "fast") public void testAwaitTermination2() throws Exception { Assert.assertFalse( executor.awaitTermination(1, TimeUnit.NANOSECONDS), "executor is terminated" ); AtomicInteger completed = TestUtils.countCompletedRunnables( 10, new Function<Runnable>() { @Override public void execute(Runnable argument) { executor.execute(argument); } } ); executor.shutdown(); mockExecutor.drain(); Assert.assertTrue( executor.awaitTermination(1, TimeUnit.NANOSECONDS), "executor should be terminated" ); Assert.assertEquals(completed.get(), 10); Assert.assertTrue( executor.isTerminated(), "executor should be terminated" ); } @Test(groups = "fast") public void testScheduledTasksCancelledOnShutdown() throws Exception { ScheduledFuture<?> future1 = executor.schedule(NO_OP, 10, TimeUnit.SECONDS); ScheduledFuture<?> future2 = executor.schedule(Executors.callable(NO_OP), 10, TimeUnit.SECONDS); ScheduledFuture<?> future3 = executor.scheduleAtFixedRate(NO_OP, 10, 10, TimeUnit.SECONDS); ScheduledFuture<?> future4 = executor.scheduleWithFixedDelay(NO_OP, 10, 10, TimeUnit.SECONDS); Assert.assertFalse( executor.awaitTermination(1, TimeUnit.NANOSECONDS), "executor is terminated" ); executor.shutdown(); Assert.assertTrue( future1.isCancelled(), "scheduled task1 should be cancelled" ); Assert.assertTrue( future2.isCancelled(), "scheduled task2 should be cancelled" ); Assert.assertTrue( future3.isCancelled(), "scheduled task3 should be cancelled" ); Assert.assertTrue( future4.isCancelled(), "scheduled task4 should be cancelled" ); } @Test(groups = "fast") public void testSubmission() throws Exception { executor.execute(NO_OP); Assert.assertEquals(mockExecutor.getNumPendingTasks(), 1); executor.submit(NO_OP); Assert.assertEquals(mockExecutor.getNumPendingTasks(), 2); executor.submit(NO_OP); Assert.assertEquals(mockExecutor.getNumPendingTasks(), 3); executor.submit(NO_OP, new Object()); Assert.assertEquals(mockExecutor.getNumPendingTasks(), 4); } @Test(groups = "fast") public void testRejectedAfterShutdown() throws Exception { executor.shutdown(); try { executor.submit(NO_OP); Assert.fail("expected exception"); } catch (RejectedExecutionException e) { // success Assert.assertEquals(executor.isShutdown(), true); Assert.assertEquals(mockExecutor.isShutdown(), false); } } @Test(groups = "fast") public void testShutdownWhileExecuting() throws Exception { // a bit hackish this test provokes a race condition: if a scheduled() // task completes while shutdown() is removing pending tasks futures, // we could get a ConcurrentModificationException (without the fix // to do locking anyway) final int numShutdownTasks = 1000; int numExecutionThreads = 10; final AtomicInteger count = new AtomicInteger(0); final AtomicBoolean fail = new AtomicBoolean(false); Runnable shutdownTask = new Runnable() { @Override public void run() { try { // 10 is totally arbitrary here, but it seems to work if (count.incrementAndGet() == (int)((float)numShutdownTasks * .75)) { executor.shutdown(); } } catch (ConcurrentModificationException e) { fail.set(true); } } }; // schedule a ton of tasks that all shutdown the executor (fills up // its underlying hash) for (int i = 0; i < numShutdownTasks; i++) { executor.schedule(shutdownTask, 1, TimeUnit.MILLISECONDS); } // task that just removes the head of the MockExecutor, runs it, // and schedules it again Runnable executorTask = new Runnable() { @Override public void run() { while (true) { Runnable head; synchronized (this) { if (mockExecutor.getNumPendingTasks() > 0) { head = mockExecutor.removeHead(); } else { return; } } head.run(); try { executor.schedule(head, 1, TimeUnit.MILLISECONDS); } catch (RejectedExecutionException e) { // expected } } } }; // number of execution threads to use for draining the executor Thread[] executorThreads = new Thread[10]; for (int i = 0; i < numExecutionThreads; i++) { executorThreads[i] = new Thread(executorTask); executorThreads[i].start(); } for (int i = 0; i < numExecutionThreads; i++) { executorThreads[i].join(); } Assert.assertFalse( fail.get(), "got concurrent modification exception during shutdown" ); } }