blob: fce85469a61cf441d25086d06935d5321b411bc7 [file] [log] [blame]
/*
* Copyright (C) 2006 The Guava Authors
*
* 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.google.common.util.concurrent;
import static com.google.common.base.Preconditions.checkArgument;
import static java.lang.Thread.currentThread;
import static java.util.Arrays.asList;
import com.google.common.annotations.GwtIncompatible;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Function;
import com.google.common.collect.Ordering;
import com.google.errorprone.annotations.CanIgnoreReturnValue;
import com.google.j2objc.annotations.J2ObjCIncompatible;
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 org.checkerframework.checker.nullness.qual.Nullable;
import org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement;
/** Static methods used to implement {@link Futures#getChecked(Future, Class)}. */
@GwtIncompatible
final class FuturesGetChecked {
@CanIgnoreReturnValue
static <V, X extends Exception> V getChecked(Future<V> future, Class<X> exceptionClass) throws X {
return getChecked(bestGetCheckedTypeValidator(), future, exceptionClass);
}
/** Implementation of {@link Futures#getChecked(Future, Class)}. */
@CanIgnoreReturnValue
@VisibleForTesting
static <V, X extends Exception> V getChecked(
GetCheckedTypeValidator validator, Future<V> future, Class<X> 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)}. */
@CanIgnoreReturnValue
static <V, X extends Exception> V getChecked(
Future<V> future, Class<X> 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.
*
* <p>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<Boolean> isValidClass =
new ClassValue<Boolean>() {
@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<WeakReference> 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<WeakReference<Class<? extends Exception>>> validClasses =
new CopyOnWriteArraySet<>();
@Override
public void validateClass(Class<? extends Exception> exceptionClass) {
for (WeakReference<Class<? extends Exception>> 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<Class<? extends Exception>>(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 <X extends Exception> void wrapAndThrowExceptionOrError(
Throwable cause, Class<X> 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 {
Exception unused = newWithCause(exceptionClass, new Exception());
return true;
} catch (Exception e) {
return false;
}
}
private static <X extends Exception> X newWithCause(Class<X> exceptionClass, Throwable cause) {
// getConstructors() guarantees this as long as we don't modify the array.
@SuppressWarnings({"unchecked", "rawtypes"})
List<Constructor<X>> constructors = (List) Arrays.asList(exceptionClass.getConstructors());
for (Constructor<X> 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 <X extends Exception> List<Constructor<X>> preferringStrings(
List<Constructor<X>> constructors) {
return WITH_STRING_PARAM_FIRST.sortedCopy(constructors);
}
private static final Ordering<Constructor<?>> WITH_STRING_PARAM_FIRST =
Ordering.natural()
.onResultOf(
new Function<Constructor<?>, Boolean>() {
@Override
public Boolean apply(Constructor<?> input) {
return asList(input.getParameterTypes()).contains(String.class);
}
})
.reverse();
private static <X> @Nullable X newFromConstructor(Constructor<X> 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
| InstantiationException
| IllegalAccessException
| 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() {}
}