/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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 org.apache.flink.runtime.concurrent;
import org.apache.flink.runtime.concurrent.impl.FlinkCompletableFuture;
import org.apache.flink.runtime.concurrent.impl.FlinkFuture;
import org.apache.flink.util.TestLogger;
import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.junit.Test;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicInteger;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
/**
* Tests for Flink's future implementation.
*/
public class FlinkFutureTest extends TestLogger {
private static ExecutorService executor;
@BeforeClass
public static void setup() {
executor = Executors.newSingleThreadExecutor();
}
@AfterClass
public static void teardown() {
executor.shutdown();
}
@Test(timeout = 10000L)
public void testFutureApplyAsync() throws Exception {
int expectedValue = 42;
CompletableFuture<Integer> initialFuture = new FlinkCompletableFuture<>();
Future<String> appliedFuture = initialFuture.thenApplyAsync(new ApplyFunction<Integer, String>() {
@Override
public String apply(Integer value) {
return String.valueOf(value);
}
}, executor);
initialFuture.complete(expectedValue);
assertEquals(String.valueOf(expectedValue), appliedFuture.get());
}
@Test(expected = TimeoutException.class)
public void testFutureGetTimeout() throws InterruptedException, ExecutionException, TimeoutException {
CompletableFuture<Integer> future = new FlinkCompletableFuture<>();
future.get(10, TimeUnit.MILLISECONDS);
fail("Get should have thrown a timeout exception.");
}
@Test(expected = TestException.class)
public void testExceptionalCompletion() throws Throwable {
CompletableFuture<Integer> initialFuture = new FlinkCompletableFuture<>();
initialFuture.completeExceptionally(new TestException("Test exception"));
try {
initialFuture.get();
fail("Get should have thrown an exception.");
} catch (ExecutionException e) {
throw e.getCause();
}
}
/**
* Tests that an exception is propagated through an apply function.
*/
@Test(expected = TestException.class)
public void testExceptionPropagation() throws Throwable {
CompletableFuture<Integer> initialFuture = new FlinkCompletableFuture<>();
Future<String> mappedFuture = initialFuture.thenApplyAsync(new ApplyFunction<Integer, String>() {
@Override
public String apply(Integer value) {
throw new TestException("Test exception");
}
}, executor);
Future<String> mapped2Future = mappedFuture.thenApplyAsync(new ApplyFunction<String, String>() {
@Override
public String apply(String value) {
return "foobar";
}
}, executor);
initialFuture.complete(42);
try {
mapped2Future.get();
fail("Get should have thrown an exception.");
} catch (ExecutionException e) {
throw e.getCause();
}
}
@Test(timeout = 10000L)
public void testExceptionallyAsync() throws ExecutionException, InterruptedException {
CompletableFuture<Integer> initialFuture = new FlinkCompletableFuture<>();
String exceptionMessage = "Foobar";
Future<String> recovered = initialFuture.exceptionallyAsync(new ApplyFunction<Throwable, String>() {
@Override
public String apply(Throwable value) {
return value.getMessage();
}
}, executor);
initialFuture.completeExceptionally(new TestException(exceptionMessage));
String actualMessage = recovered.get();
assertEquals(exceptionMessage, actualMessage);
}
@Test(timeout = 10000L)
public void testComposeAsync() throws ExecutionException, InterruptedException {
CompletableFuture<Integer> initialFuture = new FlinkCompletableFuture<>();
final int expectedValue = 42;
Future<Integer> composedFuture = initialFuture.thenComposeAsync(new ApplyFunction<Integer, Future<Integer>>() {
@Override
public Future<Integer> apply(Integer value) {
return FlinkFuture.supplyAsync(new Callable<Integer>() {
@Override
public Integer call() throws Exception {
return expectedValue;
}
}, executor);
}
}, executor);
initialFuture.complete(42);
int actualValue = composedFuture.get();
assertEquals(expectedValue, actualValue);
}
@Test(timeout = 10000L)
public void testCombineAsync() throws ExecutionException, InterruptedException {
CompletableFuture<Integer> leftFuture = new FlinkCompletableFuture<>();
CompletableFuture<String> rightFuture = new FlinkCompletableFuture<>();
final int expectedLeftValue = 42;
final String expectedRightValue = "foobar";
Future<String> resultFuture = leftFuture.thenCombineAsync(rightFuture, new BiFunction<Integer, String, String>() {
@Override
public String apply(Integer integer, String s) {
return s + integer;
}
}, executor);
leftFuture.complete(expectedLeftValue);
rightFuture.complete(expectedRightValue);
String result = resultFuture.get();
assertEquals(expectedRightValue + expectedLeftValue, result);
}
@Test(timeout = 10000L)
public void testCombineAsyncLeftFailure() throws InterruptedException {
CompletableFuture<Integer> leftFuture = new FlinkCompletableFuture<>();
CompletableFuture<String> rightFuture = new FlinkCompletableFuture<>();
final String expectedRightValue = "foobar";
final TestException testException = new TestException("barfoo");
Future<String> resultFuture = leftFuture.thenCombineAsync(rightFuture, new BiFunction<Integer, String, String>() {
@Override
public String apply(Integer integer, String s) {
return s + integer;
}
}, executor);
leftFuture.completeExceptionally(testException);
rightFuture.complete(expectedRightValue);
try {
resultFuture.get();
fail("We should have caught an ExecutionException.");
} catch (ExecutionException e) {
assertEquals(testException, e.getCause());
}
}
@Test(timeout = 10000L)
public void testCombineAsyncRightFailure() throws ExecutionException, InterruptedException {
CompletableFuture<Integer> leftFuture = new FlinkCompletableFuture<>();
CompletableFuture<String> rightFuture = new FlinkCompletableFuture<>();
final int expectedLeftValue = 42;
final TestException testException = new TestException("barfoo");
Future<String> resultFuture = leftFuture.thenCombineAsync(rightFuture, new BiFunction<Integer, String, String>() {
@Override
public String apply(Integer integer, String s) {
return s + integer;
}
}, executor);
leftFuture.complete(expectedLeftValue);
rightFuture.completeExceptionally(testException);
try {
resultFuture.get();
fail("We should have caught an ExecutionException.");
} catch (ExecutionException e) {
assertEquals(testException, e.getCause());
}
}
@Test
public void testGetNow() throws ExecutionException {
CompletableFuture<Integer> initialFuture = new FlinkCompletableFuture<>();
final int absentValue = 41;
assertEquals(new Integer(absentValue), initialFuture.getNow(absentValue));
}
@Test(timeout = 10000L)
public void testAcceptAsync() throws ExecutionException, InterruptedException {
CompletableFuture<Integer> initialFuture = new FlinkCompletableFuture<>();
final AtomicInteger atomicInteger = new AtomicInteger(0);
int expectedValue = 42;
Future<Void> result = initialFuture.thenAcceptAsync(new AcceptFunction<Integer>() {
@Override
public void accept(Integer value) {
atomicInteger.set(value);
}
}, executor);
initialFuture.complete(expectedValue);
result.get();
assertEquals(expectedValue, atomicInteger.get());
}
@Test(timeout = 10000L)
public void testHandleAsync() throws ExecutionException, InterruptedException {
CompletableFuture<Integer> initialFuture = new FlinkCompletableFuture<>();
int expectedValue = 43;
Future<String> result = initialFuture.handleAsync(new BiFunction<Integer, Throwable, String>() {
@Override
public String apply(Integer integer, Throwable throwable) {
if (integer != null) {
return String.valueOf(integer);
} else {
return throwable.getMessage();
}
}
}, executor);
initialFuture.complete(expectedValue);
assertEquals(String.valueOf(expectedValue), result.get());
}
@Test(timeout = 10000L)
public void testHandleAsyncException() throws ExecutionException, InterruptedException {
CompletableFuture<Integer> initialFuture = new FlinkCompletableFuture<>();
String exceptionMessage = "foobar";
Future<String> result = initialFuture.handleAsync(new BiFunction<Integer, Throwable, String>() {
@Override
public String apply(Integer integer, Throwable throwable) {
if (integer != null) {
return String.valueOf(integer);
} else {
return throwable.getMessage();
}
}
}, executor);
initialFuture.completeExceptionally(new TestException(exceptionMessage));
assertEquals(exceptionMessage, result.get());
}
@Test(timeout = 10000L)
public void testMultipleCompleteOperations() throws ExecutionException, InterruptedException {
CompletableFuture<Integer> initialFuture = new FlinkCompletableFuture<>();
int expectedValue = 42;
assertTrue(initialFuture.complete(expectedValue));
assertFalse(initialFuture.complete(1337));
assertFalse(initialFuture.completeExceptionally(new TestException("foobar")));
assertEquals(new Integer(expectedValue), initialFuture.get());
}
@Test
public void testApply() throws ExecutionException, InterruptedException {
int expectedValue = 42;
CompletableFuture<Integer> initialFuture = new FlinkCompletableFuture<>();
Future<String> appliedFuture = initialFuture.thenApply(new ApplyFunction<Integer, String>() {
@Override
public String apply(Integer value) {
return String.valueOf(value);
}
});
initialFuture.complete(expectedValue);
assertEquals(String.valueOf(expectedValue), appliedFuture.get());
}
@Test
public void testAccept() throws ExecutionException, InterruptedException {
int expectedValue = 42;
Future<Integer> initialFuture = FlinkCompletableFuture.completed(expectedValue);
final AtomicInteger atomicInteger = new AtomicInteger(0);
Future<Void> result = initialFuture.thenAccept(new AcceptFunction<Integer>() {
@Override
public void accept(Integer value) {
atomicInteger.set(value);
}
});
result.get();
assertEquals(expectedValue, atomicInteger.get());
}
@Test
public void testExceptionally() throws ExecutionException, InterruptedException {
String exceptionMessage = "Foobar";
Future<Integer> initialFuture = FlinkCompletableFuture
.completedExceptionally(new TestException(exceptionMessage));
Future<String> recovered = initialFuture.exceptionally(new ApplyFunction<Throwable, String>() {
@Override
public String apply(Throwable value) {
return value.getMessage();
}
});
String actualMessage = recovered.get();
assertEquals(exceptionMessage, actualMessage);
}
@Test
public void testHandle() throws ExecutionException, InterruptedException {
int expectedValue = 43;
Future<Integer> initialFuture = FlinkCompletableFuture.completed(expectedValue);
Future<String> result = initialFuture.handle(new BiFunction<Integer, Throwable, String>() {
@Override
public String apply(Integer integer, Throwable throwable) {
if (integer != null) {
return String.valueOf(integer);
} else {
return throwable.getMessage();
}
}
});
assertEquals(String.valueOf(expectedValue), result.get());
}
@Test
public void testCompose() throws ExecutionException, InterruptedException {
CompletableFuture<Integer> initialFuture = new FlinkCompletableFuture<>();
final int expectedValue = 42;
Future<Integer> composedFuture = initialFuture.thenCompose(new ApplyFunction<Integer, Future<Integer>>() {
@Override
public Future<Integer> apply(Integer value) {
return FlinkFuture.supplyAsync(new Callable<Integer>() {
@Override
public Integer call() throws Exception {
return expectedValue;
}
}, executor);
}
});
initialFuture.complete(42);
int actualValue = composedFuture.get();
assertEquals(expectedValue, actualValue);
}
@Test
public void testCombine() throws ExecutionException, InterruptedException {
int expectedLeftValue = 1;
int expectedRightValue = 2;
Future<Integer> left = FlinkCompletableFuture.completed(expectedLeftValue);
Future<Integer> right = FlinkCompletableFuture.completed(expectedRightValue);
Future<Integer> sum = left.thenCombine(right, new BiFunction<Integer, Integer, Integer>() {
@Override
public Integer apply(Integer left, Integer right) {
return left + right;
}
});
int result = sum.get();
assertEquals(expectedLeftValue + expectedRightValue, result);
}
/**
* Tests that multiple functions can be called on complete futures.
*/
@Test(timeout = 10000L)
public void testMultipleFunctionsOnCompleteFuture() throws Exception {
final FlinkCompletableFuture<String> future = FlinkCompletableFuture.completed("test");
Future<String> result1 = future.handleAsync(new BiFunction<String, Throwable, String>() {
@Override
public String apply(String s, Throwable throwable) {
return s != null ? s : throwable.getMessage();
}
}, executor);
Future<Void> result2 = future.thenAcceptAsync(new AcceptFunction<String>() {
@Override
public void accept(String value) {}
}, executor);
assertEquals("test", result1.get());
assertNull(result2.get());
}
/**
* Tests that multiple functions can be called on incomplete futures.
*/
@Test(timeout = 10000L)
public void testMultipleFunctionsOnIncompleteFuture() throws Exception {
final FlinkCompletableFuture<String> future = new FlinkCompletableFuture<>();
Future<String> result1 = future.handleAsync(new BiFunction<String, Throwable, String>() {
@Override
public String apply(String s, Throwable throwable) {
return s != null ? s : throwable.getMessage();
}
}, executor);
Future<Void> result2 = future.thenAcceptAsync(new AcceptFunction<String>() {
@Override
public void accept(String value) {}
}, executor);
future.complete("value");
assertEquals("value", result1.get());
assertNull(result2.get());
}
/**
* Tests that multiple functions can be called on complete futures.
*/
@Test(timeout = 10000)
public void testMultipleFunctionsExceptional() throws Exception {
final FlinkCompletableFuture<String> future = new FlinkCompletableFuture<>();
Future<String> result1 = future.handleAsync(new BiFunction<String, Throwable, String>() {
@Override
public String apply(String s, Throwable throwable) {
return s != null ? s : throwable.getMessage();
}
}, executor);
Future<Void> result2 = future.thenAcceptAsync(new AcceptFunction<String>() {
@Override
public void accept(String value) {}
}, executor);
future.completeExceptionally(new TestException("test"));
assertEquals("test", result1.get());
try {
result2.get();
fail("We should have caught an ExecutionException.");
} catch (ExecutionException e) {
assertTrue(e.getCause() instanceof TestException);
}
}
/**
* Tests that a chain of dependent futures will be completed exceptionally if the initial future
* is completed exceptionally.
*/
@Test(timeout = 10000)
public void testChainedFutureExceptionalCompletion() throws ExecutionException, InterruptedException {
final FlinkCompletableFuture<String> future = new FlinkCompletableFuture<>();
Future<String> apply = future.thenApplyAsync(new ApplyFunction<String, String>() {
@Override
public String apply(String value) {
return value;
}
}, executor);
Future<Throwable> applyException = apply.exceptionallyAsync(new ApplyFunction<Throwable, Throwable>() {
@Override
public Throwable apply(Throwable value) {
return value;
}
}, executor);
Future<Void> accept1 = future.thenAcceptAsync(new AcceptFunction<String>() {
@Override
public void accept(String value) {
// noop
}
}, executor);
Future<Throwable> accept1Exception = accept1.exceptionallyAsync(new ApplyFunction<Throwable, Throwable>() {
@Override
public Throwable apply(Throwable value) {
return value;
}
}, executor);
Future<Void> accept2 = future.thenAcceptAsync(new AcceptFunction<String>() {
@Override
public void accept(String value) {
// noop
}
}, executor);
Future<Throwable> accept2Exception = accept2.exceptionallyAsync(new ApplyFunction<Throwable, Throwable>() {
@Override
public Throwable apply(Throwable value) {
return value;
}
}, executor);
TestException testException = new TestException("test");
// fail the initial future
future.completeExceptionally(testException);
assertEquals(testException, applyException.get());
assertEquals(testException, accept1Exception.get());
assertEquals(testException, accept2Exception.get());
}
private static class TestException extends RuntimeException {
private static final long serialVersionUID = -1274022962838535130L;
public TestException(String message) {
super(message);
}
}
}