/*
* Copyright (C) 2011 The Guava Authors
*
* 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.common.util.concurrent;
import static com.google.common.truth.Truth.assertThat;
import com.google.common.collect.Iterables;
import com.google.common.collect.Range;
import com.google.common.collect.Sets;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.concurrent.CancellationException;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.CyclicBarrier;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executor;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.locks.LockSupport;
import junit.framework.AssertionFailedError;
import junit.framework.TestCase;
/**
* Tests for {@link AbstractFuture}.
*
* @author Brian Stoler
*/
public class AbstractFutureTest extends TestCase {
public void testSuccess() throws ExecutionException, InterruptedException {
final Object value = new Object();
assertSame(value, new AbstractFuture<Object>() {
{
set(value);
}
}.get());
}
public void testException() throws InterruptedException {
final Throwable failure = new Throwable();
AbstractFuture<String> future = new AbstractFuture<String>() {
{
setException(failure);
}
};
ExecutionException ee1 = getExpectingExecutionException(future);
ExecutionException ee2 = getExpectingExecutionException(future);
// Ensure we get a unique execution exception on each get
assertNotSame(ee1, ee2);
assertSame(failure, ee1.getCause());
assertSame(failure, ee2.getCause());
checkStackTrace(ee1);
checkStackTrace(ee2);
}
public void testCancel_notDoneNoInterrupt() throws Exception {
InterruptibleFuture future = new InterruptibleFuture();
assertTrue(future.cancel(false));
assertTrue(future.isCancelled());
assertTrue(future.isDone());
assertFalse(future.wasInterrupted());
assertFalse(future.interruptTaskWasCalled);
try {
future.get();
fail("Expected CancellationException");
} catch (CancellationException e) {
// See AbstractFutureCancellationCauseTest for how to set causes
assertNull(e.getCause());
}
}
public void testCancel_notDoneInterrupt() throws Exception {
InterruptibleFuture future = new InterruptibleFuture();
assertTrue(future.cancel(true));
assertTrue(future.isCancelled());
assertTrue(future.isDone());
assertTrue(future.wasInterrupted());
assertTrue(future.interruptTaskWasCalled);
try {
future.get();
fail("Expected CancellationException");
} catch (CancellationException e) {
// See AbstractFutureCancellationCauseTest for how to set causes
assertNull(e.getCause());
}
}
public void testCancel_done() throws Exception {
AbstractFuture<String> future = new AbstractFuture<String>() {
{
set("foo");
}
};
assertFalse(future.cancel(true));
assertFalse(future.isCancelled());
assertTrue(future.isDone());
}
public void testGetWithTimeoutDoneFuture() throws Exception {
AbstractFuture<String> future = new AbstractFuture<String>() {
{
set("foo");
}
};
assertEquals("foo", future.get(0, TimeUnit.SECONDS));
}
public void testEvilFuture_setFuture() throws Exception {
final RuntimeException exception = new RuntimeException("you didn't say the magic word!");
AbstractFuture<String> evilFuture = new AbstractFuture<String>() {
@Override public void addListener(Runnable r, Executor e) {
throw exception;
}
};
AbstractFuture<String> normalFuture = new AbstractFuture<String>() {};
normalFuture.setFuture(evilFuture);
assertTrue(normalFuture.isDone());
try {
normalFuture.get();
fail();
} catch (ExecutionException e) {
assertSame(exception, e.getCause());
}
}
public void testRemoveWaiter_interruption() throws Exception {
final AbstractFuture<String> future = new AbstractFuture<String>() {};
WaiterThread waiter1 = new WaiterThread(future);
waiter1.start();
waiter1.awaitWaiting();
WaiterThread waiter2 = new WaiterThread(future);
waiter2.start();
waiter2.awaitWaiting();
// The waiter queue should be waiter2->waiter1
// This should wake up waiter1 and cause the waiter1 node to be removed.
waiter1.interrupt();
waiter1.join();
waiter2.awaitWaiting(); // should still be blocked
LockSupport.unpark(waiter2); // spurious wakeup
waiter2.awaitWaiting(); // should eventually re-park
future.set(null);
waiter2.join();
}
public void testRemoveWaiter_polling() throws Exception {
final AbstractFuture<String> future = new AbstractFuture<String>() {};
WaiterThread waiter = new WaiterThread(future);
waiter.start();
waiter.awaitWaiting();
PollingThread poller = new PollingThread(future);
poller.start();
PollingThread poller2 = new PollingThread(future);
poller2.start();
PollingThread poller3 = new PollingThread(future);
poller3.start();
poller.awaitInLoop();
poller2.awaitInLoop();
poller3.awaitInLoop();
// The waiter queue should be {poller x 3}->waiter1
waiter.interrupt();
// This should wake up waiter1 and cause the waiter1 node to be removed.
waiter.join();
future.set(null);
poller.join();
}
public void testCompletionFinishesWithDone() {
ExecutorService executor = Executors.newFixedThreadPool(10);
for (int i = 0; i < 50000; i++) {
final AbstractFuture<String> future = new AbstractFuture<String>() {};
final AtomicReference<String> errorMessage = Atomics.newReference();
executor.execute(new Runnable() {
@Override
public void run() {
future.set("success");
if (!future.isDone()) {
errorMessage.set("Set call exited before future was complete.");
}
}
});
executor.execute(new Runnable() {
@Override
public void run() {
future.setException(new IllegalArgumentException("failure"));
if (!future.isDone()) {
errorMessage.set("SetException call exited before future was complete.");
}
}
});
executor.execute(new Runnable() {
@Override
public void run() {
future.cancel(true);
if (!future.isDone()) {
errorMessage.set("Cancel call exited before future was complete.");
}
}
});
try {
future.get();
} catch (Throwable t) {
// Ignore, we just wanted to block.
}
String error = errorMessage.get();
assertNull(error, error);
}
executor.shutdown();
}
/**
* He did the bash, he did the future bash
* The future bash, it was a concurrency smash
* He did the bash, it caught on in a flash
* He did the bash, he did the future bash
*/
public void testFutureBash() {
final CyclicBarrier barrier = new CyclicBarrier(
6 // for the setter threads
+ 50 // for the listeners
+ 50 // for the blocking get threads,
+ 1); // for the main thread
final ExecutorService executor = Executors.newFixedThreadPool(barrier.getParties());
final AtomicReference<AbstractFuture<String>> currentFuture = Atomics.newReference();
final AtomicInteger numSuccessfulSetCalls = new AtomicInteger();
Callable<Void> completeSucessFullyRunnable = new Callable<Void>() {
@Override public Void call() {
if (currentFuture.get().set("set")) {
numSuccessfulSetCalls.incrementAndGet();
}
awaitUnchecked(barrier);
return null;
}
};
Callable<Void> completeExceptionallyRunnable = new Callable<Void>() {
Exception failureCause = new Exception("setException");
@Override public Void call() {
if (currentFuture.get().setException(failureCause)) {
numSuccessfulSetCalls.incrementAndGet();
}
awaitUnchecked(barrier);
return null;
}
};
Callable<Void> cancelRunnable = new Callable<Void>() {
@Override public Void call() {
if (currentFuture.get().cancel(true)) {
numSuccessfulSetCalls.incrementAndGet();
}
awaitUnchecked(barrier);
return null;
}
};
Callable<Void> setFutureCompleteSucessFullyRunnable = new Callable<Void>() {
ListenableFuture<String> future = Futures.immediateFuture("setFuture");
@Override public Void call() {
if (currentFuture.get().setFuture(future)) {
numSuccessfulSetCalls.incrementAndGet();
}
awaitUnchecked(barrier);
return null;
}
};
Callable<Void> setFutureCompleteExceptionallyRunnable = new Callable<Void>() {
ListenableFuture<String> future = Futures.immediateFailedFuture(new Exception("setFuture"));
@Override public Void call() {
if (currentFuture.get().setFuture(future)) {
numSuccessfulSetCalls.incrementAndGet();
}
awaitUnchecked(barrier);
return null;
}
};
Callable<Void> setFutureCancelRunnable = new Callable<Void>() {
ListenableFuture<String> future = Futures.immediateCancelledFuture();
@Override public Void call() {
if (currentFuture.get().setFuture(future)) {
numSuccessfulSetCalls.incrementAndGet();
}
awaitUnchecked(barrier);
return null;
}
};
final Set<Object> finalResults = Collections.synchronizedSet(Sets.newIdentityHashSet());
Runnable collectResultsRunnable = new Runnable() {
@Override public void run() {
try {
String result = Uninterruptibles.getUninterruptibly(currentFuture.get());
finalResults.add(result);
} catch (ExecutionException e) {
finalResults.add(e.getCause());
} catch (CancellationException e) {
finalResults.add(e.getCause());
} finally {
awaitUnchecked(barrier);
}
}
};
Runnable collectResultsTimedGetRunnable = new Runnable() {
@Override public void run() {
Future<String> future = currentFuture.get();
while (true) {
try {
String result = Uninterruptibles.getUninterruptibly(future, 0, TimeUnit.SECONDS);
finalResults.add(result);
break;
} catch (ExecutionException e) {
finalResults.add(e.getCause());
break;
} catch (CancellationException e) {
finalResults.add(e.getCause());
break;
} catch (TimeoutException e) {
// loop
}
}
awaitUnchecked(barrier);
}
};
List<Callable<?>> allTasks = new ArrayList<Callable<?>>();
allTasks.add(completeSucessFullyRunnable);
allTasks.add(completeExceptionallyRunnable);
allTasks.add(cancelRunnable);
allTasks.add(setFutureCompleteSucessFullyRunnable);
allTasks.add(setFutureCompleteExceptionallyRunnable);
allTasks.add(setFutureCancelRunnable);
for (int k = 0; k < 50; k++) {
// For each listener we add a task that submits it to the executor directly for the blocking
// get usecase and another task that adds it as a listener to the future to exercise both
// racing addListener calls and addListener calls completing after the future completes.
final Runnable listener = k % 2 == 0
? collectResultsRunnable
: collectResultsTimedGetRunnable;
allTasks.add(Executors.callable(listener));
allTasks.add(new Callable<Void>() {
@Override public Void call() throws Exception {
currentFuture.get().addListener(listener, executor);
return null;
}
});
}
assertEquals(allTasks.size() + 1, barrier.getParties());
for (int i = 0; i < 1000; i++) {
Collections.shuffle(allTasks);
final AbstractFuture<String> future = new AbstractFuture<String>() {};
currentFuture.set(future);
for (Callable<?> task : allTasks) {
@SuppressWarnings("unused") // go/futurereturn-lsc
Future<?> possiblyIgnoredError = executor.submit(task);
}
awaitUnchecked(barrier);
assertThat(future.isDone()).isTrue();
// inspect state and ensure it is correct!
// asserts that all get calling threads received the same value
Object result = Iterables.getOnlyElement(finalResults);
if (result instanceof CancellationException) {
assertTrue(future.isCancelled());
if (future.wasInterrupted()) {
// We were cancelled, it is possible that setFuture could have succeeded to.
assertThat(numSuccessfulSetCalls.get()).isIn(Range.closed(1, 2));
} else {
assertThat(numSuccessfulSetCalls.get()).isEqualTo(1);
}
} else {
assertThat(numSuccessfulSetCalls.get()).isEqualTo(1);
}
// reset for next iteration
numSuccessfulSetCalls.set(0);
finalResults.clear();
}
executor.shutdown();
}
// setFuture and cancel() interact in more complicated ways than the other setters.
public void testSetFutureCancelBash() {
final int size = 50;
final CyclicBarrier barrier = new CyclicBarrier(
2 // for the setter threads
+ size // for the listeners
+ size // for the get threads,
+ 1); // for the main thread
final ExecutorService executor = Executors.newFixedThreadPool(barrier.getParties());
final AtomicReference<AbstractFuture<String>> currentFuture = Atomics.newReference();
final AtomicReference<AbstractFuture<String>> setFutureFuture = Atomics.newReference();
final AtomicBoolean setFutureSetSucess = new AtomicBoolean();
final AtomicBoolean setFutureCompletionSucess = new AtomicBoolean();
final AtomicBoolean cancellationSucess = new AtomicBoolean();
Runnable cancelRunnable = new Runnable() {
@Override public void run() {
cancellationSucess.set(currentFuture.get().cancel(true));
awaitUnchecked(barrier);
}
};
Runnable setFutureCompleteSucessFullyRunnable = new Runnable() {
@Override public void run() {
AbstractFuture<String> future = setFutureFuture.get();
setFutureSetSucess.set(currentFuture.get().setFuture(future));
setFutureCompletionSucess.set(future.set("hello-async-world"));
awaitUnchecked(barrier);
}
};
final Set<Object> finalResults = Collections.synchronizedSet(Sets.newIdentityHashSet());
Runnable collectResultsRunnable = new Runnable() {
@Override public void run() {
try {
String result = Uninterruptibles.getUninterruptibly(currentFuture.get());
finalResults.add(result);
} catch (ExecutionException e) {
finalResults.add(e.getCause());
} catch (CancellationException e) {
finalResults.add(CancellationException.class);
} finally {
awaitUnchecked(barrier);
}
}
};
Runnable collectResultsTimedGetRunnable = new Runnable() {
@Override public void run() {
Future<String> future = currentFuture.get();
while (true) {
try {
String result = Uninterruptibles.getUninterruptibly(future, 0, TimeUnit.SECONDS);
finalResults.add(result);
break;
} catch (ExecutionException e) {
finalResults.add(e.getCause());
break;
} catch (CancellationException e) {
finalResults.add(CancellationException.class);
break;
} catch (TimeoutException e) {
// loop
}
}
awaitUnchecked(barrier);
}
};
List<Runnable> allTasks = new ArrayList<Runnable>();
allTasks.add(cancelRunnable);
allTasks.add(setFutureCompleteSucessFullyRunnable);
for (int k = 0; k < size; k++) {
// For each listener we add a task that submits it to the executor directly for the blocking
// get usecase and another task that adds it as a listener to the future to exercise both
// racing addListener calls and addListener calls completing after the future completes.
final Runnable listener = k % 2 == 0
? collectResultsRunnable
: collectResultsTimedGetRunnable;
allTasks.add(listener);
allTasks.add(new Runnable() {
@Override public void run() {
currentFuture.get().addListener(listener, executor);
}
});
}
assertEquals(allTasks.size() + 1, barrier.getParties()); // sanity check
for (int i = 0; i < 1000; i++) {
Collections.shuffle(allTasks);
final AbstractFuture<String> future = new AbstractFuture<String>() {};
final AbstractFuture<String> setFuture = new AbstractFuture<String>() {};
currentFuture.set(future);
setFutureFuture.set(setFuture);
for (Runnable task : allTasks) {
executor.execute(task);
}
awaitUnchecked(barrier);
assertThat(future.isDone()).isTrue();
// inspect state and ensure it is correct!
// asserts that all get calling threads received the same value
Object result = Iterables.getOnlyElement(finalResults);
if (result == CancellationException.class) {
assertTrue(future.isCancelled());
assertTrue(cancellationSucess.get());
// cancellation can interleave in 3 ways
// 1. prior to setFuture
// 2. after setFuture before set() on the future assigned
// 3. after setFuture and set() are called but before the listener completes.
if (!setFutureSetSucess.get() || !setFutureCompletionSucess.get()) {
// If setFuture fails or set on the future fails then it must be because that future was
// cancelled
assertTrue(setFuture.isCancelled());
assertTrue(setFuture.wasInterrupted()); // we only call cancel(true)
}
} else {
// set on the future completed
assertFalse(cancellationSucess.get());
assertTrue(setFutureSetSucess.get());
assertTrue(setFutureCompletionSucess.get());
}
// reset for next iteration
setFutureSetSucess.set(false);
setFutureCompletionSucess.set(false);
cancellationSucess.set(false);
finalResults.clear();
}
executor.shutdown();
}
// Test to ensure that when calling setFuture with a done future only setFuture or cancel can
// return true.
public void testSetFutureCancelBash_withDoneFuture() {
final CyclicBarrier barrier = new CyclicBarrier(
2 // for the setter threads
+ 1 // for the blocking get thread,
+ 1); // for the main thread
final ExecutorService executor = Executors.newFixedThreadPool(barrier.getParties());
final AtomicReference<AbstractFuture<String>> currentFuture = Atomics.newReference();
final AtomicBoolean setFutureSuccess = new AtomicBoolean();
final AtomicBoolean cancellationSucess = new AtomicBoolean();
Callable<Void> cancelRunnable = new Callable<Void>() {
@Override public Void call() {
cancellationSucess.set(currentFuture.get().cancel(true));
awaitUnchecked(barrier);
return null;
}
};
Callable<Void> setFutureCompleteSucessFullyRunnable = new Callable<Void>() {
final ListenableFuture<String> future = Futures.immediateFuture("hello");
@Override public Void call() {
setFutureSuccess.set(currentFuture.get().setFuture(future));
awaitUnchecked(barrier);
return null;
}
};
final Set<Object> finalResults = Collections.synchronizedSet(Sets.newIdentityHashSet());
final Runnable collectResultsRunnable = new Runnable() {
@Override public void run() {
try {
String result = Uninterruptibles.getUninterruptibly(currentFuture.get());
finalResults.add(result);
} catch (ExecutionException e) {
finalResults.add(e.getCause());
} catch (CancellationException e) {
finalResults.add(CancellationException.class);
} finally {
awaitUnchecked(barrier);
}
}
};
List<Callable<?>> allTasks = new ArrayList<Callable<?>>();
allTasks.add(cancelRunnable);
allTasks.add(setFutureCompleteSucessFullyRunnable);
allTasks.add(Executors.callable(collectResultsRunnable));
assertEquals(allTasks.size() + 1, barrier.getParties()); // sanity check
for (int i = 0; i < 1000; i++) {
Collections.shuffle(allTasks);
final AbstractFuture<String> future = new AbstractFuture<String>() {};
currentFuture.set(future);
for (Callable<?> task : allTasks) {
@SuppressWarnings("unused") // go/futurereturn-lsc
Future<?> possiblyIgnoredError = executor.submit(task);
}
awaitUnchecked(barrier);
assertThat(future.isDone()).isTrue();
// inspect state and ensure it is correct!
// asserts that all get calling threads received the same value
Object result = Iterables.getOnlyElement(finalResults);
if (result == CancellationException.class) {
assertTrue(future.isCancelled());
assertTrue(cancellationSucess.get());
assertFalse(setFutureSuccess.get());
} else {
assertTrue(setFutureSuccess.get());
assertFalse(cancellationSucess.get());
}
// reset for next iteration
setFutureSuccess.set(false);
cancellationSucess.set(false);
finalResults.clear();
}
executor.shutdown();
}
// In a previous implementation this would cause a stack overflow after ~2000 futures chained
// together. Now it should only be limited by available memory (and time)
public void testSetFuture_stackOverflow() {
SettableFuture<String> orig = SettableFuture.create();
SettableFuture<String> prev = orig;
for (int i = 0; i < 100000; i++) {
SettableFuture<String> curr = SettableFuture.create();
prev.setFuture(curr);
prev = curr;
}
// prev represents the 'innermost' future
prev.set("done");
assertTrue(orig.isDone());
}
public void testCancel_stackOverflow() {
SettableFuture<String> orig = SettableFuture.create();
SettableFuture<String> prev = orig;
for (int i = 0; i < 100000; i++) {
SettableFuture<String> curr = SettableFuture.create();
prev.setFuture(curr);
prev = curr;
}
// orig is the 'outermost future', this should propagate fully down the stack of futures.
orig.cancel(true);
assertTrue(orig.isCancelled());
assertTrue(prev.isCancelled());
assertTrue(prev.wasInterrupted());
}
private static void awaitUnchecked(final CyclicBarrier barrier) {
try {
barrier.await();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
private void checkStackTrace(ExecutionException e) {
// Our call site for get() should be in the trace.
int index = findStackFrame(
e, getClass().getName(), "getExpectingExecutionException");
assertThat(index).isNotEqualTo(0);
// Above our method should be the call to get(). Don't assert on the class
// because it could be some superclass.
assertThat(e.getStackTrace()[index - 1].getMethodName()).isEqualTo("get");
}
private static int findStackFrame(
ExecutionException e, String clazz, String method) {
StackTraceElement[] elements = e.getStackTrace();
for (int i = 0; i < elements.length; i++) {
StackTraceElement element = elements[i];
if (element.getClassName().equals(clazz)
&& element.getMethodName().equals(method)) {
return i;
}
}
AssertionFailedError failure =
new AssertionFailedError("Expected element " + clazz + "." + method
+ " not found in stack trace");
failure.initCause(e);
throw failure;
}
private ExecutionException getExpectingExecutionException(
AbstractFuture<String> future) throws InterruptedException {
try {
String got = future.get();
fail("Expected exception but got " + got);
} catch (ExecutionException e) {
return e;
}
// unreachable, but compiler doesn't know that fail() always throws
return null;
}
private static final class WaiterThread extends Thread {
private final AbstractFuture<?> future;
private WaiterThread(AbstractFuture<?> future) {
this.future = future;
}
@Override public void run() {
try {
future.get();
} catch (Exception e) {
// nothing
}
}
void awaitWaiting() {
while (LockSupport.getBlocker(this) != future) {
if (getState() == State.TERMINATED) {
throw new RuntimeException("Thread exited");
}
Thread.yield();
}
}
}
static final class TimedWaiterThread extends Thread {
private final AbstractFuture<?> future;
private final long timeout;
private final TimeUnit unit;
TimedWaiterThread(AbstractFuture<?> future, long timeout, TimeUnit unit) {
this.future = future;
this.timeout = timeout;
this.unit = unit;
}
@Override public void run() {
try {
future.get(timeout, unit);
} catch (Exception e) {
// nothing
}
}
void awaitWaiting() {
while (LockSupport.getBlocker(this) != future) {
if (getState() == State.TERMINATED) {
throw new RuntimeException("Thread exited");
}
Thread.yield();
}
}
}
private final class PollingThread extends Thread {
private final AbstractFuture<?> future;
private final CountDownLatch completedIteration = new CountDownLatch(10);
private PollingThread(AbstractFuture<?> future) {
this.future = future;
}
@Override public void run() {
while (true) {
try {
future.get(0, TimeUnit.SECONDS);
return;
} catch (InterruptedException e) {
return;
} catch (ExecutionException e) {
return;
} catch (TimeoutException e) {
// do nothing
} finally {
completedIteration.countDown();
}
}
}
void awaitInLoop() {
Uninterruptibles.awaitUninterruptibly(completedIteration);
}
}
private static final class InterruptibleFuture
extends AbstractFuture<String> {
boolean interruptTaskWasCalled;
@Override protected void interruptTask() {
assertFalse(interruptTaskWasCalled);
interruptTaskWasCalled = true;
}
}
}