/**
* 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.Function;
import com.twitter.util.Function0;
import com.twitter.util.Future;
import com.twitter.util.JavaTimer;
import com.twitter.util.Return;
import com.twitter.util.Throw;
import com.twitter.util.Timer;
import com.twitter.util.Try;
import org.apache.commons.lang3.tuple.ImmutablePair;
import org.apache.commons.lang3.tuple.Pair;
/**
* Utility functions for dealing with Future(s)
*/
public class FutureUtil {
private static final Timer timer = new JavaTimer();
/**
* Run speculative execution for a set of futures. This is especially useful for improving latency
* and surviving failures. The idea is to fire a backup future if the original future does not
* return within a certain time period. The 1st future which returns successfully is returned.
*
* @param originalFuture The original future which has already been issued.
* @param functionBackupFuture A function which can issue the backup future once @waitTimeMillis
* has expired.
* @param waitTimeMillis The time to wait in milliseconds before issuing the backup future.
* @param statsPrefix The stats prefix to be prefixed with recorded stats such as "won", "lost"
* and "backup-fired".
* @param <T>
* @return Returns a future which encapsulates the speculative execution strategy.
*/
public static <T> Future<T> getSpeculativeFuture(
final Future<T> originalFuture,
final Function0<Future<T>> functionBackupFuture,
long waitTimeMillis,
final String statsPrefix) {
return getSpeculativeFuture(
originalFuture,
functionBackupFuture,
waitTimeMillis,
statsPrefix,
BackupFutureRetryPolicy.UN_CONDITIONAL_RETRY_POLICY);
}
/**
* Run speculative execution for a set of futures. This is especially useful for improving latency
* and surviving failures. The provided BackupFutureRetryPolicy will decide whether the backup
* future should be fired or not. The 1st future which returns successfully is returned.
*
* @param originalFuture The original future which has already been issued.
* @param functionBackupFuture A function which can issue the backup future once @waitTimeMillis
* has expired.
* @param waitTimeMillis The time to wait in milliseconds before issuing the backup future.
* @param statsPrefix The stats prefix to be prefixed with recorded stats such as "won", "lost"
* and "backup-fired".
* @param retryPolicy decides whether the backup future should be fired if the original future
* fails
* @param <T>
* @return Returns a future which encapsulates the speculative execution strategy.
*/
public static <T> Future<T> getSpeculativeFuture(
final Future<T> originalFuture,
final Function0<Future<T>> functionBackupFuture,
long waitTimeMillis,
final String statsPrefix,
final BackupFutureRetryPolicy retryPolicy) {
return originalFuture.within(
Duration.fromMilliseconds(waitTimeMillis), timer).
rescue(new Function<Throwable, Future<T>>() {
public Future<T> apply(Throwable t) {
if (retryPolicy.isRetriable(t)) {
final Future<T> backupFuture = functionBackupFuture.apply();
Stats.incr(statsPrefix + "-backup-fired");
// Build information into each future as to whether this is the original
// future or the backup future.
final Future<Pair<Boolean, T>> originalFutureWithInfo = originalFuture.map(
new Function<T, Pair<Boolean, T>>() {
public Pair<Boolean, T> apply(T t) {
return new ImmutablePair(true, t);
}
});
final Future<Pair<Boolean, T>> backupFutureWithInfo = backupFuture.map(
new Function<T, Pair<Boolean, T>>() {
public Pair<Boolean, T> apply(T t) {
return new ImmutablePair(false, t);
}
});
// If there is an exception, the first future throwing the exception will
// return. Instead we want the 1st future which is successful.
Future<Pair<Boolean, T>> origFutureSuccessful = originalFutureWithInfo
.rescue(new Function<Throwable, Future<Pair<Boolean, T>>>() {
public Future<Pair<Boolean, T>> apply(Throwable t) {
// Fall back to back up future which may also fail in which case we bubble
// up the exception.
return backupFutureWithInfo;
}
});
Future<Pair<Boolean, T>> backupFutureSuccessful = backupFutureWithInfo.rescue(
new Function<Throwable, Future<Pair<Boolean, T>>>() {
public Future<Pair<Boolean, T>> apply(Throwable t) {
// Fall back to original Future which may also fail in which case we bubble
// up the exception.
return originalFutureWithInfo;
}
});
return origFutureSuccessful.select(backupFutureSuccessful).map(
new Function<Pair<Boolean, T>, T>() {
public T apply(Pair<Boolean, T> tuple) {
if (tuple.getLeft()) {
Stats.incr(statsPrefix + "-won");
} else {
Stats.incr(statsPrefix + "-lost");
}
return tuple.getRight();
}
}
);
} else {
return Future.exception(t);
}
}
});
}
public static <T> Future<Try<T>> lifeToTry(Future<T> future) {
return future.map(new Function<T, Try<T>>() {
@Override
public Try<T> apply(T o) {
return new Return(o);
}
}).handle(new Function<Throwable, Try<T>>() {
@Override
public Try<T> apply(Throwable throwable) {
return new Throw(throwable);
}
});
}
}