/**
* 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
*/
package org.corfudb.runtime.view;
import com.google.common.collect.*;
import lombok.extern.slf4j.Slf4j;
import org.corfudb.runtime.exceptions.QuorumUnreachableException;
import java.util.Comparator;
import java.util.Set;
import java.util.concurrent.*;
/**
* Factory for custom futures used by the quorum replication.
* Created by Konstantin Spirov on 2/3/2017.
*/
@Slf4j
public class QuorumFuturesFactory {
/**
* Get a thread safe future that will complete only when n/2+1 futures complete or if there is no hope
* (if n/2+1 futures are canceled or have conflicting value).
*
* The future returned does not block explicitly, it aggregates the futures and delegates the blocking.
*
* In case of normal execution, any of the compete futures can be used to return the result.
* In case of termination, the cancel flag will be updated and if any of the futures threw an exception,
* ExecutionException will be thrown, otherwise the future will return null
*
* @param comparator Any comparator consistent with equals that is able to distinguish the results
* @param futures The N futures
* @return The composite future
*/
static <R> CompositeFuture<R> getQuorumFuture(Comparator<R> comparator, CompletableFuture<R>... futures) {
return new CompositeFuture<R>(comparator, futures.length/2+1, futures);
}
/**
* Get a thread safe future that will complete only when n/2+1 futures complete or if there is no hope
* (if n/2+1 futures are canceled or have conflicting value).
*
* The future returned does not block explicitly, it aggregates the futures and delegates the blocking.
*
* In case of normal execution, any of the compete futures can be used to return the result.
* In case of termination, the cancel flag will be updated and if any of the futures threw an exception,
* ExecutionException will be thrown, otherwise the future will return null
*
* @param comparator Any comparator consistent with equals that is able to distinguish the results
* @param futures The N futures
* @param failFastThrowables list of exceptions that will cause the future to complete immediately.
* All fail fast exceptions are directly propagated and thrown outside.
* @return The composite future
*/
public static <R> CompositeFuture<R> getQuorumFuture(Comparator<R> comparator, CompletableFuture<R>[] futures, Class... failFastThrowables) {
return new CompositeFuture(comparator, futures.length/2+1, futures, failFastThrowables);
}
/**
* Get a thread safe future that will complete only when a single futures complete.
*
* The future returned does not block explicitly, it aggregates the futures and delegates the blocking.
*
* In case if some future completes successfully its value will be returned.
* In case of termination, the cancel flag will be updated and if any of the futures threw an exception,
* ExecutionException will be thrown, otherwise the future will return null
*
* @param comparator Any comparator consistent with equals that is able to distinguish the results
* @param futures The N futures
* @return The composite future
*/
static <R> CompositeFuture<R> getFirstWinsFuture(Comparator<R> comparator, CompletableFuture<R>... futures) {
return new CompositeFuture<R>(comparator, 1, futures);
}
public static class CompositeFuture<R> implements Future<R> {
private final Comparator<R> comparator;
private final int quorum;
private final CompletableFuture<R>[] futures;
private final Set<Class> failFastThrowables;
private final Set<Throwable> throwables = ConcurrentHashMap.newKeySet();
private volatile boolean done = false;
private volatile boolean canceled = false;
private volatile boolean conflict = false;
private CompositeFuture(Comparator<R> comparator, int quorum, CompletableFuture<R>[] futures, Class... failFastThrowables) {
this.failFastThrowables = ImmutableSet.copyOf(failFastThrowables);
this.comparator = comparator;
this.quorum = quorum;
this.futures = futures;
}
@Override
public R get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException {
Comparator<Integer> ic = Integer::compareTo;
TreeMultimap<Integer, R> valuesSortedByCount = TreeMultimap.create(ic.reversed(), comparator);
TreeMultimap<R, Integer> indexesByValue = TreeMultimap.create(comparator, ic);
long until = 0;
boolean infinite = (timeout==Long.MAX_VALUE);
if (!infinite) {
until = System.nanoTime() + unit.toNanos(timeout);
}
while (infinite || System.nanoTime() < until) {
int numIncompleteFutures = 0;
CompletableFuture aggregatedFuture = null; // block until some future completes
for (int i = 0; i < futures.length; i++) {
CompletableFuture<R> c = futures[i];
if (!c.isDone()) {
numIncompleteFutures++;
if (aggregatedFuture == null) {
aggregatedFuture = c;
} else {
aggregatedFuture = CompletableFuture.anyOf(aggregatedFuture, c);
}
} else {
if (c.isCancelled()) {
} else if (c.isCompletedExceptionally()) {
try {
futures[i].get(); // this will throw the ExecutionException
} catch (ExecutionException e) {
Throwable t = e.getCause();
throwables.add(t);
if (failFastThrowables.contains(t.getClass())) {
done = canceled = true;
throw e;
}
}
} else {
R value = c.get();
Set<Integer> indexes = indexesByValue.get(value);
if (!indexes.contains(i)) {
valuesSortedByCount.remove(indexes.size(), value);
indexes.add(i);
valuesSortedByCount.put(indexes.size(), value);
}
if (indexesByValue.keySet().size()>1) {
conflict = true;
}
}
}
}
int greatestNumCompleteFutures = valuesSortedByCount.size()==0? 0:
valuesSortedByCount.keySet().iterator().next();
if (greatestNumCompleteFutures>=quorum) { // normal exit, quorum
done = true;
return valuesSortedByCount.entries().iterator().next().getValue();
}
boolean noMoreHope = numIncompleteFutures+greatestNumCompleteFutures < quorum;
if (noMoreHope) {
done = canceled = true;
for (Throwable t: getThrowables()) {
log.debug(t.getMessage(), t);
}
throw new ExecutionException(
new QuorumUnreachableException(greatestNumCompleteFutures, quorum));
}
try {
if (infinite) {
aggregatedFuture.get();
} else {
aggregatedFuture.get(timeout, unit);
}
} catch (ExecutionException t) {
// The exceptions after after constructing the future will be handled on the next loop
}
} // while
throw new TimeoutException();
}
@Override
public R get() throws InterruptedException, ExecutionException {
try {
return get(Long.MAX_VALUE, null);
} catch (TimeoutException e) {
log.error(e.getMessage(), e); // not likely to happen in near future
return null;
}
}
@Override
public boolean cancel(boolean mayInterruptIfRunning) {
for (CompletableFuture f : futures) {
f.cancel(mayInterruptIfRunning);
}
done = canceled = true;
return canceled;
}
/**
* @return true the future was canceled explicitly, or if the future was unable to reach quorum due to
* conflicts, cancaled futures or futures that have thrown exception.
*/
@Override
public boolean isCancelled() {
return canceled;
}
@Override
public boolean isDone() {
return done;
}
/**
* @return true if there were two successful results with different values, otherwise false
*/
public boolean isConflict() {
return conflict;
}
/**
* Returns a set of Throwable classes from all futures that completed in any exceptional fashion.
* @return Read-only set containing the Throwable classes
*/
public Set<Throwable> getThrowables() {
return ImmutableSet.copyOf(throwables);
}
/**
* Checks whether one of the exceptons is throwable from the given type
* @param check - the throwable to search for
* @return true if there is a throwable from the given type, otherwise false
*/
public boolean containsThrowableFrom(Class <? extends Throwable>check) {
for (Throwable t: throwables) {
if (t.getClass().isAssignableFrom(check)) {
return true;
}
}
return false;
}
}
}