/*
* 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.AnnotatedRunnable;
import com.facebook.testing.LoopThread;
import com.facebook.testing.MockExecutor;
import com.facebook.testing.TestUtils;
import com.facebook.testing.ThreadHelper;
import org.apache.log4j.Logger;
import org.joda.time.DateTimeUtils;
import org.testng.Assert;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.DataProvider;
import org.testng.annotations.Test;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;
public class TestExecutorServiceFront {
private static final Logger LOG =
Logger.getLogger(TestExecutorServiceFront.class);
private MockExecutor mockExecutor;
private ExecutorServiceFront executorFront;
private ExecutorServiceFront executorFront2;
private AtomicLong count;
private AtomicLong offsetTime;
private Runnable countTask;
private LatchTask latchTask;
private Runnable slowTask;
private static final int NUM_THREADS = 10;
@BeforeMethod(alwaysRun = true)
public void setUp() throws Exception {
count = new AtomicLong(0);
offsetTime = new AtomicLong(0);
countTask = new Runnable() {
@Override
public void run() {
count.incrementAndGet();
}
};
slowTask = new Runnable() {
@Override
public void run() {
try {
DateTimeUtils.setCurrentMillisOffset(
offsetTime.addAndGet(20000)
);
} catch (SecurityException e) {
LOG.warn("security exception on incrementing the system time!", e);
throw new RuntimeException(e);
}
count.incrementAndGet();
}
};
latchTask = LatchTask.createPaused();
mockExecutor = new MockExecutor();
executorFront = new ExecutorServiceFront(
mockExecutor, 10000, TimeUnit.MILLISECONDS
);
executorFront2 = new ExecutorServiceFront(
new LinkedBlockingQueue<Runnable>(),
mockExecutor,
"fuu",
2,
10000,
TimeUnit.MILLISECONDS
);
}
/**
* tests
* 1. multiple submits result in only 1 task being submitted to underlying
* executor
* 2. running that one task drains our own queue
*/
@Test(groups = "fast")
public void testMaxDrainer() throws Exception {
Assert.assertEquals(mockExecutor.getNumPendingTasks(), 0);
executorFront.execute(countTask);
Assert.assertEquals(mockExecutor.getNumPendingTasks(), 1);
executorFront.execute(countTask);
Assert.assertEquals(mockExecutor.getNumPendingTasks(), 1);
mockExecutor.drain();
Assert.assertEquals(mockExecutor.getNumPendingTasks(), 0);
Assert.assertEquals(count.get(), 2);
}
/**
* This test will submit several tasks at least one of which will
* be the latch-task. Another thread will try to drain the backing
* executor. This will hang that thread running a task
*/
@Test(groups = "fast")
public void testConcurrentDrainerAndSubmit() throws Exception {
// thread will drain the executor backing us
Thread drainingThread = new Thread(
new Runnable() {
@Override
public void run() {
mockExecutor.drain();
}
}
);
// submit a task that will hang, and a count task => 1 drainer task
executorFront.execute(latchTask);
executorFront.execute(countTask);
Assert.assertEquals(mockExecutor.getNumPendingTasks(), 1);
// hang at the latch task
drainingThread.start();
Assert.assertEquals(count.get(), 0);
// submit a new task that will no result in a new drainer task
executorFront.execute(countTask);
// let the drainer proceed, and wait for it to complete
latchTask.proceed();
drainingThread.join();
// should have 2 count tasks
Assert.assertEquals(count.get(), 2);
// 0 drainer tasks, add a count task => 1 drainer task
Assert.assertEquals(mockExecutor.getNumPendingTasks(), 0);
executorFront.execute(countTask);
Assert.assertEquals(mockExecutor.getNumPendingTasks(), 1);
}
/**
* This test:
* 1. submit several slow tasks all of which will expire. Check these tasks
* are executed one by one by several drainers.
* 2. submit several fast tasks all of which will not expire. Check all these
* tasks are executed in batch by one drainer.
*/
@Test(groups = "fast")
public void testExpiringSingleDrainer() throws Exception {
int numTask = 3;
// submit several slow tasks
for (int i = 0; i < numTask; i++) {
executorFront.execute(slowTask);
}
Assert.assertEquals(mockExecutor.getNumPendingTasks(), 1);
for (int i = 0; i < numTask; i++) {
// a drainer should run one task each time, then expire
mockExecutor.removeHead().run();
Assert.assertEquals(count.get(), i + 1);
}
//reset the counter
count.set(0);
// submit several fast task
for (int i = 0; i < numTask; i++) {
executorFront.execute(countTask);
}
Assert.assertEquals(mockExecutor.getNumPendingTasks(), 1);
// a drainer should run all tasks
mockExecutor.removeHead().run();
Assert.assertEquals(count.get(), numTask);
// must clear the timer offset
DateTimeUtils.setCurrentMillisOffset(0);
}
/**
* This test will submit several tasks two of which will
* expire. We check the drainer(s) in the pending list
* before and after the expiration.
*/
@Test(groups = "fast")
public void testExpiringDualDrainer() throws Exception {
// submit three tasks
executorFront2.execute(slowTask);
executorFront2.execute(countTask);
executorFront2.execute(slowTask);
Assert.assertEquals(mockExecutor.getNumPendingTasks(), 2);
AnnotatedRunnable drainer2 = mockExecutor.getRunnableList().get(1);
mockExecutor.removeHead().run();
// Drainer1 expires after the 1st task completes,
// and is rescheduled to the end of the pending list.
Assert.assertEquals(mockExecutor.getNumPendingTasks(), 2);
// Drainer2 should be on the head of the pending list
Assert.assertSame(drainer2, mockExecutor.getRunnableList().get(0));
mockExecutor.removeHead().run();
// Drainer2 expires after the 2nd and 3rd task complete.
AnnotatedRunnable drainer1 = mockExecutor.getRunnableList().get(0);
// Drainer1 should be on the head of the pending list
Assert.assertNotSame(drainer1, drainer2);
// should have 2 slow tasks and 1 count tasks
Assert.assertEquals(count.get(), 3);
// must clear the timer offset
DateTimeUtils.setCurrentMillisOffset(0);
}
@DataProvider(name = "getTestExecutors")
public Object[][] getTestExecutors() {
return new Object[][]{
{Executors.newFixedThreadPool(NUM_THREADS)},
{new ExecutorServiceFront(
new LinkedBlockingQueue<Runnable>(),
Executors.newFixedThreadPool(NUM_THREADS),
"fuu",
NUM_THREADS, 1, TimeUnit.MILLISECONDS
)}
};
}
/**
* Tests that tasks that die with RuntimeException don't cause the
* Executor to lose threads.
*
* @param executor The test executor
*/
@Test(groups = "fast", dataProvider = "getTestExecutors")
public void testDyingThreads(Executor executor) throws Exception {
final int numTasks = NUM_THREADS * 2;
final CountDownLatch latch = new CountDownLatch(numTasks);
// kill all the threads
for (int i = 0; i < numTasks; i++) {
executor.execute(
new Runnable() {
@Override
public void run() {
latch.countDown();
throw new RuntimeException("Expected Failure");
}
}
);
}
Assert.assertTrue(latch.await(5, TimeUnit.SECONDS));
// now add more tasks to run
final CountDownLatch latch2 = new CountDownLatch(NUM_THREADS);
// Run tasks to see if they can still run
for (int i = 0; i < NUM_THREADS; i++) {
executor.execute(
new Runnable() {
@Override
public void run() {
latch2.countDown();
}
}
);
}
Assert.assertTrue(latch.await(5, TimeUnit.SECONDS));
}
@Test(groups = "fast")
public void testTimeExpirationWithEmptyQueue() throws Exception {
try {
LinkedBlockingQueue<Runnable> workQueue = new LinkedBlockingQueue<Runnable>();
final ExecutorServiceFront executorServiceFront = new ExecutorServiceFront(
workQueue, mockExecutor, "fuu", 1, 1, TimeUnit.SECONDS
);
DateTimeUtils.setCurrentMillisFixed(0);
executorServiceFront.execute(latchTask);
Assert.assertEquals(mockExecutor.getNumPendingTasks(), 1);
Thread t = TestUtils.runInThread(
new Runnable() {
@Override
public void run() {
AnnotatedRunnable drainer = mockExecutor.removeHead();
drainer.run();
}
},
"drainer"
);
// once the queue is empty, we know the drainer has taken the task out and is blocked on the
// latch
while (!workQueue.isEmpty()) {
Thread.sleep(50);
}
// move time forward, which will cause the Drainer to terminate based on time-slice (not
// empty queue)
DateTimeUtils.setCurrentMillisFixed(1001);
latchTask.proceed();
// wait for drainer to terminate
t.join();
// our time has expired, and there are no tasks in the queue, no tasks should be in the queue
Assert.assertEquals(mockExecutor.getNumPendingTasks(), 0);
} finally {
DateTimeUtils.setCurrentMillisSystem();
}
}
@Test(groups = "fast")
public void testRenameThread() throws Exception {
LinkedBlockingQueue<Runnable> workQueue = new LinkedBlockingQueue<Runnable>();
final ExecutorServiceFront executorServiceFront = new ExecutorServiceFront(
workQueue, mockExecutor, "custom-name", 1
);
executorServiceFront.execute(latchTask);
Assert.assertEquals(mockExecutor.getNumPendingTasks(), 1);
Thread t = createDrainerThread();
// once the queue is empty, we know the drainer has taken the task out and is blocked on the
// latch
while (!workQueue.isEmpty()) {
Thread.sleep(50);
}
Assert.assertEquals(t.getName(), "custom-name-000");
latchTask.proceed();
t.join();
Assert.assertEquals(t.getName(), "original");
}
@Test(groups = "fast")
public void testCreateManyThreads() throws Exception {
LinkedBlockingQueue<Runnable> workQueue = new LinkedBlockingQueue<Runnable>();
ExecutorServiceFront executorServiceFront = new ExecutorServiceFront(
workQueue, mockExecutor, "custom-name", 2
);
LatchTask task1 = LatchTask.createPaused();
LatchTask task2 = LatchTask.createPaused();
// put 2 tasks in the queue, both will hang, causing 2 drainers to be created
executorServiceFront.execute(task1);
executorServiceFront.execute(task2);
ThreadHelper threadHelper = new ThreadHelper();
// ie, we'll want two physical threads
LoopThread drainerThread1 = createDrainerThread(threadHelper);
LoopThread drainerThread2 = createDrainerThread(threadHelper);
// this waits for both tasks to get into a Drainer. Two tasks that pause immediately and
// two drainers => we get 1 task per drainer
while (!workQueue.isEmpty()) {
Thread.sleep(50);
}
// check that the names change according to our ESF
Assert.assertEquals(drainerThread1.getName(), "custom-name-000");
task1.proceed();
task1.await();
Assert.assertEquals(drainerThread2.getName(), "custom-name-001");
task2.proceed();
task2.await();
drainerThread1.join();
drainerThread2.join();
// and that the 'base name' of the borrowed threads is restored
Assert.assertEquals(drainerThread1.getName(), "drainer");
Assert.assertEquals(drainerThread2.getName(), "drainer");
}
private LoopThread createDrainerThread(ThreadHelper threadHelper) {
return threadHelper.repeatInThread(
new Runnable() {
@Override
public void run() {
AnnotatedRunnable drainer = mockExecutor.removeHead();
if (drainer != null) {
drainer.run();
} else {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
},
"drainer"
);
}
private Thread createDrainerThread() {
return TestUtils.runInThread(
new Runnable() {
@Override
public void run() {
AnnotatedRunnable drainer = mockExecutor.removeHead();
drainer.run();
}
},
"original"
);
}
}