// Copyright 2009 Google 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.google.enterprise.connector.instantiator;
import com.google.enterprise.connector.util.testing.AdjustableClock;
import junit.framework.TestCase;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.TimeUnit;
/**
* Unit tests for {@link ThreadPool}
*/
public class ThreadPoolTest extends TestCase {
/**
* A suggested default amount of time to let tasks run before automatic
* cancellation.
*/
public static final int DEFAULT_TASK_LIFE_SECS = 60;
AdjustableClock clock;
@Override
protected void setUp() throws Exception {
clock = new AdjustableClock();
}
// TODO(strellis): Add test of cancel timer popping during submit - after the
// timer is running and before the task is running.
public void testRunOne() throws Exception {
ThreadPool threadPool = new ThreadPool(DEFAULT_TASK_LIFE_SECS, clock);
BlockingQueue<Object> runningQ = new ArrayBlockingQueue<Object>(10);
BlockingQueue<Object> stoppingQ = new ArrayBlockingQueue<Object>(10);
CancelableTask task = new BlockingQueueCancelable(runningQ, stoppingQ);
TaskHandle taskHandle = threadPool.submit(task);
take(1, runningQ);
assertTrue(!taskHandle.isDone());
stoppingQ.put(this);
verifyCompleted(taskHandle);
assert (0 == task.getCancelCount());
assertEquals(0, task.getTimeoutCount());
}
public void testRunMany() throws Exception {
final int count = 103;
ThreadPool threadPool = new ThreadPool(DEFAULT_TASK_LIFE_SECS, clock);
BlockingQueue<Object> taskRunningQ = new ArrayBlockingQueue<Object>(count);
BlockingQueue<Object> taskStoppingQ = new ArrayBlockingQueue<Object>(count);
List<CancelableTask> tasks = new ArrayList<CancelableTask>();
List<TaskHandle> taskHandles = new ArrayList<TaskHandle>();
for (int ix = 0; ix < count; ix++) {
CancelableTask task =
new BlockingQueueCancelable(taskRunningQ, taskStoppingQ);
tasks.add(task);
taskHandles.add(threadPool.submit(task));
}
take(tasks.size(), taskRunningQ);
verifyRunning(taskHandles);
put(tasks.size(), taskStoppingQ);
verifyCompleted(taskHandles);
assertCancelCount(0, tasks);
assertTimeoutCount(0, tasks);
}
public void testCancel() throws Exception {
final int count = 103;
BlockingQueue<Object> taskRunningQ = new ArrayBlockingQueue<Object>(count);
BlockingQueue<Object> taskCanceledQ = new ArrayBlockingQueue<Object>(count);
ThreadPool threadPool = new ThreadPool(DEFAULT_TASK_LIFE_SECS, clock);
List<CancelableTask> tasks = new ArrayList<CancelableTask>();
List<TaskHandle> handles = new ArrayList<TaskHandle>();
for (int ix = 0; ix < count; ix++) {
CancelableTask task =
new VerifyInterruptedCancelable(taskRunningQ, taskCanceledQ);
tasks.add(task);
handles.add(threadPool.submit(task));
}
take(tasks.size(), taskRunningQ);
// Verify no task unblocks without being canceled.
verifyRunning(handles);
assertEquals(0, taskCanceledQ.size());
for (TaskHandle handle : handles) {
handle.cancel();
}
take(tasks.size(), taskCanceledQ);
verifyCompleted(handles);
assertCancelCount(1, tasks);
assertTimeoutCount(0, tasks);
}
public void testTimeoutHung() throws Exception {
BlockingQueue<Object> taskRunningQ = new ArrayBlockingQueue<Object>(10);
ThreadPool threadPool = new ThreadPool(DEFAULT_TASK_LIFE_SECS, clock);
HangingCancelable task = new HangingCancelable(taskRunningQ);
TaskHandle handle = threadPool.submit(task);
take(1, taskRunningQ);
assertFalse(handle.isDone());
assertFalse(task.isExiting());
handle.cancel();
verifyCompleted(handle);
assertEquals(1, task.getCancelCount());
assertFalse(task.isExiting());
}
public void testShutdown() throws Exception {
final int count = 9;
BlockingQueue<Object> taskRunningQ = new ArrayBlockingQueue<Object>(count);
BlockingQueue<Object> taskCanceledQ = new ArrayBlockingQueue<Object>(count);
ThreadPool threadPool = new ThreadPool(DEFAULT_TASK_LIFE_SECS, clock);
List<CancelableTask> tasks = new ArrayList<CancelableTask>();
List<TaskHandle> handles = new ArrayList<TaskHandle>();
for (int ix = 0; ix < count; ix++) {
CancelableTask task =
new VerifyInterruptedCancelable(taskRunningQ, taskCanceledQ);
tasks.add(task);
handles.add(threadPool.submit(task));
}
take(tasks.size(), taskRunningQ);
verifyRunning(handles);
assertTrue(threadPool.shutdown(true, 1000));
take(tasks.size(), taskCanceledQ);
verifyCompleted(handles);
assertCancelCount(0, tasks);
assertTimeoutCount(0, tasks);
}
public void testShutdownWithHung() throws Exception {
BlockingQueue<Object> taskRunningQ = new ArrayBlockingQueue<Object>(10);
ThreadPool threadPool = new ThreadPool(DEFAULT_TASK_LIFE_SECS, clock);
HangingCancelable task = new HangingCancelable(taskRunningQ);
TaskHandle handle = threadPool.submit(task);
take(1, taskRunningQ);
assertFalse(handle.isDone());
assertFalse(threadPool.shutdown(true, 100));
// TODO(strellis): Shutdown seems to interrupt the task but not set its
// isDone state. Since the task is hung, perhaps this is OK. If it causes
// a problem we will need to explicitly cancel the task during shutdown.
assertFalse(task.isExiting());
assertEquals(0, task.getCancelCount());
assertEquals(0, task.getTimeoutCount());
}
public void testSubmitAfterShutdown() throws Exception {
ThreadPool threadPool = new ThreadPool(DEFAULT_TASK_LIFE_SECS, clock);
threadPool.shutdown(true, 10);
BlockingQueue<Object> runningQ = new ArrayBlockingQueue<Object>(10);
BlockingQueue<Object> stoppingQ = new ArrayBlockingQueue<Object>(10);
CancelableTask task = new BlockingQueueCancelable(runningQ, stoppingQ);
TaskHandle handle = threadPool.submit(task);
assertNull(handle);
}
private final static int SHORT_TASK_LIFE_SECS = 1;
public void testTimeToLiveWithHungBatch() throws Exception {
BlockingQueue<Object> taskRunningQ = new ArrayBlockingQueue<Object>(10);
ThreadPool threadPool = new ThreadPool(SHORT_TASK_LIFE_SECS, clock);
HangingCancelable task = new HangingCancelable(taskRunningQ);
TaskHandle taskHandel = threadPool.submit(task);
clock.adjustTime((3 + SHORT_TASK_LIFE_SECS) * 1000L);
take(1, taskRunningQ);
verifyCompleted(taskHandel);
assertFalse(task.isExiting());
assertEquals(1, task.getCancelCount());
assertEquals(1, task.getTimeoutCount());
}
public void testTimeToLiveWithSlowBatch() throws Exception {
final int count = 2;
BlockingQueue<Object> taskRunningQ = new ArrayBlockingQueue<Object>(count);
BlockingQueue<Object> taskCanceledQ = new ArrayBlockingQueue<Object>(count);
ThreadPool threadPool = new ThreadPool(SHORT_TASK_LIFE_SECS, clock);
List<VerifyInterruptedCancelable> tasks =
new ArrayList<VerifyInterruptedCancelable>();
List<TaskHandle> handles = new ArrayList<TaskHandle>();
for (int ix = 0; ix < count; ix++) {
VerifyInterruptedCancelable task =
new VerifyInterruptedCancelable(taskRunningQ, taskCanceledQ);
tasks.add(task);
handles.add(threadPool.submit(task));
}
take(tasks.size(), taskRunningQ);
take(tasks.size(), taskCanceledQ);
verifyCompleted(handles);
assertCancelCount(1, tasks);
assertTimeoutCount(1, tasks);
assertIsExiting(true, tasks);
}
private void assertIsExiting(boolean expect,
List<VerifyInterruptedCancelable> tasks) throws InterruptedException{
for (VerifyInterruptedCancelable task : tasks) {
long timeToGiveUp = clock.getTimeMillis() + 1200;
while (clock.getTimeMillis() < timeToGiveUp) {
if (task.isExiting() == expect) {
return;
}
Thread.sleep(10);
}
assertEquals(expect, task.isExiting());
}
}
private void verifyCompleted(TaskHandle taskHandle)
throws InterruptedException {
long timeToGiveUp = clock.getTimeMillis() + 2100;
while (clock.getTimeMillis() < timeToGiveUp) {
if (taskHandle.isDone()) {
return;
}
Thread.sleep(10);
}
fail("Some background tasks did not complete");
}
private void verifyCompleted(List<TaskHandle> tasks)
throws InterruptedException {
for (TaskHandle task : tasks) {
verifyCompleted(task);
}
}
private void verifyRunning(List<TaskHandle> tasks) {
for (TaskHandle task : tasks) {
assertTrue(!task.isDone());
}
}
/**
* Take a value from a queue. For debugging purposes the timeout can be set
* with the system property "TAKE_WAIT_MILLIS".
* @param count
* @param q
* @throws InterruptedException
*/
private void take(int count, BlockingQueue<?> q)
throws InterruptedException {
take(count, q, 2050);
}
/**
* Take a value from a queue. For debugging purposes the timeout can be set
* with the system property "TAKE_WAIT_MILLIS".
* @param count
* @param q
* @param timeout
* @throws InterruptedException
*/
private void take(int count, BlockingQueue<?> q, long timeout)
throws InterruptedException {
long timeoutMillis = Long.getLong("TAKE_WAIT_MILLIS", timeout);
for (int ix = 0; ix < count; ix++) {
Object result = q.poll(timeoutMillis, TimeUnit.MILLISECONDS);
if (result == null) {
fail("Expected object not written to queue "
+ "- this means a backgound task hung or failed");
}
}
}
private void put(int count, BlockingQueue<Object> q)
throws InterruptedException {
for (int ix = 0; ix < count; ix++) {
q.put(this);
}
}
private void assertCancelCount(int expectCount,
List<? extends CancelableTask> tasks) {
for (CancelableTask task : tasks) {
assertEquals(expectCount, task.getCancelCount());
}
}
private void assertTimeoutCount(int expectCount,
List<? extends CancelableTask> tasks) {
for (CancelableTask task : tasks) {
assertEquals(expectCount, task.getTimeoutCount());
}
}
private static class BlockingQueueCancelable extends CancelableTask {
// Written by me.
private final BlockingQueue<Object> runningQ;
// Written by the test.
private final BlockingQueue<Object> stoppingQ;
BlockingQueueCancelable(BlockingQueue<Object> runningQ,
BlockingQueue<Object> stoppingQ) {
this.runningQ = runningQ;
this.stoppingQ = stoppingQ;
}
public void run() {
try {
runningQ.put(this);
stoppingQ.take();
} catch (InterruptedException ie) {
Thread.currentThread().interrupt();
}
}
}
private static class HangingCancelable extends CancelableTask {
// Cancelable writes to this Q so test can block Cancelable is running.
private final BlockingQueue<Object> taskRunningQ;
private volatile boolean isExiting = false;
// This is an instance variable so findbugs wont issue a diagnostic for
// hang.
public boolean dontChangeMe = true;
HangingCancelable(BlockingQueue<Object> taskRunningQ) {
this.taskRunningQ = taskRunningQ;
}
public void run() {
try {
taskRunningQ.add(this);
synchronized (this) {
while (dontChangeMe) {
try {
wait();
} catch (InterruptedException ie) {
// Ignored so I hang.
}
}
}
} finally {
isExiting = true;
}
}
boolean isExiting() {
return isExiting;
}
}
private static class VerifyInterruptedCancelable extends CancelableTask {
// Cancelable writes to this Q so test can block Cancelable is running.
private final BlockingQueue<Object> taskRunningQ;
// Cancelable writes to this after interrupt so test can verify interrupt
// occurred
private final BlockingQueue<Object> taskCanceledQ;
private volatile boolean isExiting = false;
VerifyInterruptedCancelable(BlockingQueue<Object> taskRunningQ,
BlockingQueue<Object> taskCanceledQ) {
this.taskRunningQ = taskRunningQ;
this.taskCanceledQ = taskCanceledQ;
}
public void run() {
try {
taskRunningQ.add(this);
while (true) {
Thread.sleep(10000);
}
} catch (InterruptedException ie) {
// Expected
} finally {
taskCanceledQ.add(this);
isExiting = true;
}
}
boolean isExiting() {
return isExiting;
}
}
private abstract static class CancelableTask implements TimedCancelable {
private volatile int cancelCount;
private volatile int timeoutCount;
public int getCancelCount() {
return cancelCount;
}
public void cancel() {
cancelCount++;
}
public int getTimeoutCount() {
return timeoutCount;
}
public void timeout(TaskHandle taskHandle) {
timeoutCount++;
taskHandle.cancel();
}
}
}