com.google.common.util.concurrent.FuturesGetChecked Maven / Gradle / Ivy
/*
* 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.J2ktIncompatible;
import com.google.common.annotations.VisibleForTesting;
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 javax.annotation.CheckForNull;
import org.checkerframework.checker.nullness.qual.Nullable;
/** Static methods used to implement {@link Futures#getChecked(Future, Class)}. */
@J2ktIncompatible
@GwtIncompatible
@ElementTypesAreNonnullByDefault
final class FuturesGetChecked {
@CanIgnoreReturnValue
@ParametricNullness
static V getChecked(
Future future, Class exceptionClass) throws X {
return getChecked(bestGetCheckedTypeValidator(), future, exceptionClass);
}
/** Implementation of {@link Futures#getChecked(Future, Class)}. */
@CanIgnoreReturnValue
@VisibleForTesting
@ParametricNullness
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)}. */
@CanIgnoreReturnValue
@ParametricNullness
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();
@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 {
@SuppressWarnings("rawtypes") // class literals
Class extends Enum> theClass =
Class.forName(CLASS_VALUE_VALIDATOR_NAME).asSubclass(Enum.class);
return (GetCheckedTypeValidator) theClass.getEnumConstants()[0];
} catch (ClassNotFoundException
| RuntimeException
| Error 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 {
Exception unused = newWithCause(exceptionClass, new Exception());
return true;
} catch (Throwable t) { // sneaky checked exception
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 : preferringStringsThenThrowables(constructors)) {
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> preferringStringsThenThrowables(
List> constructors) {
return WITH_STRING_PARAM_THEN_WITH_THROWABLE_PARAM.sortedCopy(constructors);
}
// TODO: b/296487962 - Consider defining a total order over constructors.
private static final Ordering>> ORDERING_BY_CONSTRUCTOR_PARAMETER_LIST =
Ordering.natural()
.onResultOf((List> params) -> params.contains(String.class))
.compound(
Ordering.natural()
.onResultOf((List> params) -> params.contains(Throwable.class)))
.reverse();
private static final Ordering> WITH_STRING_PARAM_THEN_WITH_THROWABLE_PARAM =
ORDERING_BY_CONSTRUCTOR_PARAMETER_LIST.onResultOf(
constructor -> asList(constructor.getParameterTypes()));
@CheckForNull
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
| 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() {}
}