/**
* 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 com.pinterest.terrapin.base;
import com.twitter.ostrich.stats.Stats;
import com.twitter.util.Duration;
import com.twitter.util.Function0;
import com.twitter.util.Future;
import com.twitter.util.JavaTimer;
import com.twitter.util.Timer;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;
import static junit.framework.Assert.assertEquals;
import static junit.framework.TestCase.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
/**
* Tests for FutureUtil.
*/
public class FutureUtilTest {
private static final String EXCEPTION_MSG = "Future failed.";
private static final String STATS_PREFIX = "test";
private static final String BACKUP_FIRED_COUNTER = "test-backup-fired";
private static final String WON_COUNTER = "test-won";
private static final String LOST_COUNTER = "test-lost";
private boolean isBackupFutureInvoked;
private static Timer timer;
@Before
public void setup() {
this.isBackupFutureInvoked = false;
Stats.removeCounter(BACKUP_FIRED_COUNTER);
Stats.removeCounter(WON_COUNTER);
Stats.removeCounter(LOST_COUNTER);
}
@BeforeClass
public static void setupClass() {
timer = new JavaTimer();
}
private Future<Integer> getFuture(final long futureExecutionTimeMs,
final Integer futureValue,
final boolean isFutureSuccessful) {
return timer.doLater(
Duration.fromMilliseconds(futureExecutionTimeMs),
new Function0<Integer>() {
public Integer apply() {
if (isFutureSuccessful) {
return futureValue;
} else {
throw new RuntimeException(EXCEPTION_MSG);
}
}
});
}
private Function0<Future<Integer>> getBackupFutureFunctionWithSuccess(
final long futureExecutionTimeMs,
final Integer futureValue) {
return new Function0<Future<Integer>>() {
public Future<Integer> apply() {
isBackupFutureInvoked = true;
return getFuture(futureExecutionTimeMs, futureValue, true);
}
};
}
private Function0<Future<Integer>> getBackupFutureFunctionWithFailure(
final long futureExecutionTimeMs,
final Integer futureValue) {
return new Function0<Future<Integer>>() {
public Future<Integer> apply() {
isBackupFutureInvoked = true;
return getFuture(futureExecutionTimeMs, futureValue, false);
}
};
}
private void checkStats(int backupFired, int won, int lost) {
assertEquals(backupFired, Stats.getCounter(BACKUP_FIRED_COUNTER).apply());
assertEquals(won, Stats.getCounter(WON_COUNTER).apply());
assertEquals(lost, Stats.getCounter(LOST_COUNTER).apply());
}
/*
* Following 2 test cases deal with the case when the original future either succeeds
* or fails before the timeout for speculative execution.
*/
@Test
public void testNoBackupFutureExecutedSuccess() {
// First future succeeds soon enough.
Future<Integer> originalFuture = getFuture(50, 1, true);
Function0<Future<Integer>> backupFunctionFuture = getBackupFutureFunctionWithSuccess(500, 2);
Future<Integer> speculativeFuture = FutureUtil.getSpeculativeFuture(
originalFuture, backupFunctionFuture, 100, STATS_PREFIX);
assertEquals(1, speculativeFuture.get().intValue());
assertFalse(isBackupFutureInvoked);
checkStats(0, 0, 0);
}
@Test
public void testNoBackupFutureExecutedEarlyFailureForTimeBasedPolicy() {
// First future fails soon enough.
Future<Integer> originalFuture = getFuture(50, 1, false);
Function0<Future<Integer>> backupFunctionFuture = getBackupFutureFunctionWithSuccess(500, 2);
Future<Integer> speculativeFuture = FutureUtil.getSpeculativeFuture(
originalFuture,
backupFunctionFuture,
100,
STATS_PREFIX,
BackupFutureRetryPolicy.TIMEOUT_RETRY_POLICY);
Exception e = null;
try {
speculativeFuture.get();
} catch (RuntimeException re) {
e = re;
}
assertNotNull(e);
assertEquals(EXCEPTION_MSG, e.getMessage());
assertFalse(isBackupFutureInvoked);
checkStats(0, 0, 0);
}
@Test
public void testBackupFutureExecutedEarlyFailure() {
// First future fails before timeout and the backup future gets executed
Future<Integer> originalFuture = getFuture(50, 1, false);
Function0<Future<Integer>> backupFunctionFuture = getBackupFutureFunctionWithSuccess(500, 2);
Future<Integer> speculativeFuture = FutureUtil.getSpeculativeFuture(
originalFuture,
backupFunctionFuture,
100,
STATS_PREFIX,
BackupFutureRetryPolicy.UN_CONDITIONAL_RETRY_POLICY);
speculativeFuture.get();
assertTrue(isBackupFutureInvoked);
checkStats(1, 0, 1);
}
/**
* Following 2 test cases deal with the case when both futures succeed.
*/
@Test
public void testBothFutureSuccessAndOriginalFinishesFirst() {
Future<Integer> originalFuture = getFuture(200, 1, true);
Function0<Future<Integer>> backupFunctionFuture = getBackupFutureFunctionWithSuccess(1000, 2);
Future<Integer> speculativeFuture = FutureUtil.getSpeculativeFuture(
originalFuture, backupFunctionFuture, 100, STATS_PREFIX);
assertEquals(1, speculativeFuture.get().intValue());
assertTrue(isBackupFutureInvoked);
checkStats(1, 1, 0);
}
@Test
public void testBothFutureSuccessAndBackupFinishesFirst() {
Future<Integer> originalFuture = getFuture(1000, 1, true);
Function0<Future<Integer>> backupFunctionFuture = getBackupFutureFunctionWithSuccess(200, 2);
Future<Integer> speculativeFuture = FutureUtil.getSpeculativeFuture(
originalFuture, backupFunctionFuture, 100, STATS_PREFIX);
int value = speculativeFuture.get().intValue();
assertTrue(isBackupFutureInvoked);
assertEquals(2, value);
checkStats(1, 0, 1);
}
/**
* Following 2 test cases deal with the case when the first future that finishes throws an
* exception.
*/
@Test
public void testOriginalFailsAndBackupFinishesSecond() {
Future<Integer> originalFuture = getFuture(300, 1, false);
Function0<Future<Integer>> backupFunctionFuture = getBackupFutureFunctionWithSuccess(800, 2);
Future<Integer> speculativeFuture = FutureUtil.getSpeculativeFuture(
originalFuture, backupFunctionFuture, 100, STATS_PREFIX);
assertEquals(2, speculativeFuture.get().intValue());
assertTrue(isBackupFutureInvoked);
checkStats(1, 0, 1);
}
@Test
public void testBackupFailsAndOriginalFinishesSecond() {
Future<Integer> originalFuture = getFuture(1000, 1, true);
Function0<Future<Integer>> backupFunctionFuture = getBackupFutureFunctionWithFailure(400, 2);
Future<Integer> speculativeFuture = FutureUtil.getSpeculativeFuture(
originalFuture, backupFunctionFuture, 100, STATS_PREFIX);
assertEquals(1, speculativeFuture.get().intValue());
assertTrue(isBackupFutureInvoked);
checkStats(1, 1, 0);
}
/*
* Following 2 test cases deal with the case when the first future to return succeeds while the
* future that runs late errors out.
*/
@Test
public void testOriginalFailsAndBackupFinishesFirst() {
Future<Integer> originalFuture = getFuture(500, 1, false);
Function0<Future<Integer>> backupFunctionFuture = getBackupFutureFunctionWithSuccess(10, 2);
Future<Integer> speculativeFuture = FutureUtil.getSpeculativeFuture(
originalFuture, backupFunctionFuture, 100, STATS_PREFIX);
assertEquals(2, speculativeFuture.get().intValue());
assertTrue(isBackupFutureInvoked);
checkStats(1, 0, 1);
}
@Test
public void testBackupFailsAndOriginalFinishesFirst() {
Future<Integer> originalFuture = getFuture(300, 1, true);
Function0<Future<Integer>> backupFunctionFuture = getBackupFutureFunctionWithFailure(800, 2);
Future<Integer> speculativeFuture = FutureUtil.getSpeculativeFuture(
originalFuture, backupFunctionFuture, 100, STATS_PREFIX);
assertEquals(1, speculativeFuture.get().intValue());
assertTrue(isBackupFutureInvoked);
checkStats(1, 1, 0);
}
/*
* Following test case deals with the case when both futures error out.
*/
@Test
public void testBothFutureFailure() {
Future<Integer> originalFuture = getFuture(200, 1, false);
Function0<Future<Integer>> backupFunctionFuture = getBackupFutureFunctionWithFailure(200, 2);
Future<Integer> speculativeFuture = FutureUtil.getSpeculativeFuture(
originalFuture, backupFunctionFuture, 100, STATS_PREFIX);
Exception e = null;
try {
speculativeFuture.get();
} catch (RuntimeException re) {
e = re;
}
assertNotNull(e);
assertEquals(EXCEPTION_MSG, e.getMessage());
assertTrue(isBackupFutureInvoked);
checkStats(1, 0, 0);
}
}