com.diffplug.common.util.concurrent.FuturesGetChecked Maven / Gradle / Ivy
Show all versions of durian-concurrent Show documentation
/*
* Original Guava code is copyright (C) 2015 The Guava Authors.
* Modifications from Guava are copyright (C) 2016 DiffPlug.
*
* 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.diffplug.common.util.concurrent;
import static com.diffplug.common.base.Preconditions.checkArgument;
import static java.lang.Thread.currentThread;
import static java.util.Arrays.asList;
import java.lang.ref.WeakReference;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.util.Arrays;
import java.util.List;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArraySet;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.function.Function;
import javax.annotation.Nullable;
import org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement;
import com.google.j2objc.annotations.J2ObjCIncompatible;
import com.diffplug.common.annotations.VisibleForTesting;
import com.diffplug.common.collect.Ordering;
/**
* Static methods used to implement {@link Futures#getChecked(Future, Class)}.
*/
final class FuturesGetChecked {
static V getChecked(Future future, Class exceptionClass) throws X {
return getChecked(bestGetCheckedTypeValidator(), future, exceptionClass);
}
/**
* Implementation of {@link Futures#getChecked(Future, Class)}.
*/
@VisibleForTesting
static V getChecked(
GetCheckedTypeValidator validator, Future future, Class exceptionClass) throws X {
validator.validateClass(exceptionClass);
try {
return future.get();
} catch (InterruptedException e) {
currentThread().interrupt();
throw newWithCause(exceptionClass, e);
} catch (ExecutionException e) {
wrapAndThrowExceptionOrError(e.getCause(), exceptionClass);
throw new AssertionError();
}
}
/**
* Implementation of {@link Futures#getChecked(Future, Class, long, TimeUnit)}.
*/
static V getChecked(
Future future, Class exceptionClass, long timeout, TimeUnit unit) throws X {
// TODO(cpovirk): benchmark a version of this method that accepts a GetCheckedTypeValidator
bestGetCheckedTypeValidator().validateClass(exceptionClass);
try {
return future.get(timeout, unit);
} catch (InterruptedException e) {
currentThread().interrupt();
throw newWithCause(exceptionClass, e);
} catch (TimeoutException e) {
throw newWithCause(exceptionClass, e);
} catch (ExecutionException e) {
wrapAndThrowExceptionOrError(e.getCause(), exceptionClass);
throw new AssertionError();
}
}
@VisibleForTesting
interface GetCheckedTypeValidator {
void validateClass(Class extends Exception> exceptionClass);
}
private static GetCheckedTypeValidator bestGetCheckedTypeValidator() {
return GetCheckedTypeValidatorHolder.BEST_VALIDATOR;
}
@VisibleForTesting
static GetCheckedTypeValidator weakSetValidator() {
return GetCheckedTypeValidatorHolder.WeakSetValidator.INSTANCE;
}
@J2ObjCIncompatible // ClassValue
@VisibleForTesting
static GetCheckedTypeValidator classValueValidator() {
return GetCheckedTypeValidatorHolder.ClassValueValidator.INSTANCE;
}
/**
* Provides a check of whether an exception type is valid for use with
* {@link FuturesGetChecked#getChecked(Future, Class)}, possibly using caching.
*
* Uses reflection to gracefully fall back to when certain implementations aren't available.
*/
@VisibleForTesting
static class GetCheckedTypeValidatorHolder {
static final String CLASS_VALUE_VALIDATOR_NAME = GetCheckedTypeValidatorHolder.class.getName() + "$ClassValueValidator";
static final GetCheckedTypeValidator BEST_VALIDATOR = getBestValidator();
@IgnoreJRERequirement // getChecked falls back to another implementation if necessary
@J2ObjCIncompatible // ClassValue
enum ClassValueValidator implements GetCheckedTypeValidator {
INSTANCE;
/*
* Static final fields are presumed to be fastest, based on our experience with
* UnsignedBytesBenchmark. TODO(cpovirk): benchmark this
*/
private static final ClassValue isValidClass = new ClassValue() {
@Override
protected Boolean computeValue(Class> type) {
checkExceptionClassValidity(type.asSubclass(Exception.class));
return true;
}
};
@Override
public void validateClass(Class extends Exception> exceptionClass) {
isValidClass.get(exceptionClass); // throws if invalid; returns safely (and caches) if valid
}
}
enum WeakSetValidator implements GetCheckedTypeValidator {
INSTANCE;
/*
* Static final fields are presumed to be fastest, based on our experience with
* UnsignedBytesBenchmark. TODO(cpovirk): benchmark this
*/
/*
* A CopyOnWriteArraySet is faster than a newSetFromMap of a MapMaker map with
* weakKeys() and concurrencyLevel(1), even up to at least 12 cached exception types.
*/
private static final Set>> validClasses = new CopyOnWriteArraySet>>();
@Override
public void validateClass(Class extends Exception> exceptionClass) {
for (WeakReference> knownGood : validClasses) {
if (exceptionClass.equals(knownGood.get())) {
return;
}
// TODO(cpovirk): if reference has been cleared, remove it?
}
checkExceptionClassValidity(exceptionClass);
/*
* It's very unlikely that any loaded Futures class will see getChecked called with more
* than a handful of exceptions. But it seems prudent to set a cap on how many we'll cache.
* This avoids out-of-control memory consumption, and it keeps the cache from growing so
* large that doing the lookup is noticeably slower than redoing the work would be.
*
* Ideally we'd have a real eviction policy, but until we see a problem in practice, I hope
* that this will suffice. I have not even benchmarked with different size limits.
*/
if (validClasses.size() > 1000) {
validClasses.clear();
}
validClasses.add(new WeakReference>(exceptionClass));
}
}
/**
* Returns the ClassValue-using validator, or falls back to the "weak Set" implementation if
* unable to do so.
*/
static GetCheckedTypeValidator getBestValidator() {
try {
Class> theClass = Class.forName(CLASS_VALUE_VALIDATOR_NAME);
return (GetCheckedTypeValidator) theClass.getEnumConstants()[0];
} catch (Throwable t) { // ensure we really catch *everything*
return weakSetValidator();
}
}
}
// TODO(cpovirk): change parameter order to match other helper methods (Class, Throwable)?
private static void wrapAndThrowExceptionOrError(
Throwable cause, Class exceptionClass) throws X {
if (cause instanceof Error) {
throw new ExecutionError((Error) cause);
}
if (cause instanceof RuntimeException) {
throw new UncheckedExecutionException(cause);
}
throw newWithCause(exceptionClass, cause);
}
/*
* TODO(user): FutureChecker interface for these to be static methods on? If
* so, refer to it in the (static-method) Futures.getChecked documentation
*/
private static boolean hasConstructorUsableByGetChecked(
Class extends Exception> exceptionClass) {
try {
newWithCause(exceptionClass, new Exception());
return true;
} catch (Exception e) {
return false;
}
}
private static X newWithCause(Class exceptionClass, Throwable cause) {
// getConstructors() guarantees this as long as we don't modify the array.
@SuppressWarnings({"unchecked", "rawtypes"})
List> constructors = (List) Arrays.asList(exceptionClass.getConstructors());
for (Constructor constructor : preferringStrings(constructors)) {
@Nullable
X instance = newFromConstructor(constructor, cause);
if (instance != null) {
if (instance.getCause() == null) {
instance.initCause(cause);
}
return instance;
}
}
throw new IllegalArgumentException(
"No appropriate constructor for exception of type "
+ exceptionClass
+ " in response to chained exception",
cause);
}
private static List> preferringStrings(
List> constructors) {
return WITH_STRING_PARAM_FIRST.sortedCopy(constructors);
}
private static final Ordering> WITH_STRING_PARAM_FIRST = Ordering.natural()
.onResultOf(
new Function, Boolean>() {
@Override
public Boolean apply(Constructor> input) {
return asList(input.getParameterTypes()).contains(String.class);
}
})
.reverse();
@Nullable
private static X newFromConstructor(Constructor constructor, Throwable cause) {
Class>[] paramTypes = constructor.getParameterTypes();
Object[] params = new Object[paramTypes.length];
for (int i = 0; i < paramTypes.length; i++) {
Class> paramType = paramTypes[i];
if (paramType.equals(String.class)) {
params[i] = cause.toString();
} else if (paramType.equals(Throwable.class)) {
params[i] = cause;
} else {
return null;
}
}
try {
return constructor.newInstance(params);
} catch (IllegalArgumentException e) {
return null;
} catch (InstantiationException e) {
return null;
} catch (IllegalAccessException e) {
return null;
} catch (InvocationTargetException e) {
return null;
}
}
@VisibleForTesting
static boolean isCheckedException(Class extends Exception> type) {
return !RuntimeException.class.isAssignableFrom(type);
}
@VisibleForTesting
static void checkExceptionClassValidity(Class extends Exception> exceptionClass) {
checkArgument(
isCheckedException(exceptionClass),
"Futures.getChecked exception type (%s) must not be a RuntimeException",
exceptionClass);
checkArgument(
hasConstructorUsableByGetChecked(exceptionClass),
"Futures.getChecked exception type (%s) must be an accessible class with an accessible "
+ "constructor whose parameters (if any) must be of type String and/or Throwable",
exceptionClass);
}
private FuturesGetChecked() {}
}