
com.hazelcast.internal.util.ExceptionUtil Maven / Gradle / Ivy
/*
* Copyright (c) 2008-2024, Hazelcast, Inc. All Rights Reserved.
*
* 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.hazelcast.internal.util;
import com.hazelcast.core.HazelcastException;
import com.hazelcast.instance.impl.OutOfMemoryErrorDispatcher;
import com.hazelcast.logging.ILogger;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.WrongMethodTypeException;
import java.lang.reflect.InvocationTargetException;
import java.util.Collection;
import java.util.concurrent.ExecutionException;
import java.util.function.BiConsumer;
import java.util.function.BiFunction;
import static java.lang.invoke.MethodType.methodType;
/**
* Contains various exception related utility methods.
*/
public final class ExceptionUtil {
static final String EXCEPTION_SEPARATOR = "------ submitted from ------";
private static final BiFunction HAZELCAST_EXCEPTION_WRAPPER = (throwable, message) -> {
if (message != null) {
return new HazelcastException(message, throwable);
} else {
return new HazelcastException(throwable);
}
};
private ExceptionUtil() {
}
/**
* Converts a Throwable stacktrace to a String.
*
* @param cause the Throwable
* @return the String.
*/
public static String toString(Throwable cause) {
StringWriter sw = new StringWriter();
PrintWriter pw = new PrintWriter(sw);
cause.printStackTrace(pw);
return sw.toString();
}
public static RuntimeException peel(final Throwable t) {
return (RuntimeException) peel(t, null, null, HAZELCAST_EXCEPTION_WRAPPER);
}
/**
* Processes {@code Throwable t} so that the returned {@code Throwable}'s type matches {@code allowedType} or
* {@code RuntimeException}. Processing may include unwrapping {@code t}'s cause hierarchy, wrapping it in a
* {@code HazelcastException} or just returning the same instance {@code t} if it is already an instance of
* {@code RuntimeException}.
*
* @param t {@code Throwable} to be peeled
* @param allowedType the type expected to be returned; when {@code null}, this method returns instances
* of {@code RuntimeException}
* @param message if not {@code null}, used as the message in the {@code HazelcastException} that
* may wrap the peeled {@code Throwable}
* @param expected type of {@code Throwable}
* @return the peeled {@code Throwable}
*/
public static Throwable peel(final Throwable t, Class allowedType, String message) {
return peel(t, allowedType, message, HAZELCAST_EXCEPTION_WRAPPER);
}
/**
* Processes {@code Throwable t} so that the returned {@code Throwable}'s type matches {@code allowedType},
* {@code RuntimeException} or any {@code Throwable} returned by `exceptionWrapper`
* Processing may include unwrapping {@code t}'s cause hierarchy, wrapping it in a exception
* created by using exceptionWrapper or just returning the same instance {@code t}
* if it is already an instance of {@code RuntimeException}.
*
* @param t {@code Throwable} to be peeled
* @param allowedType the type expected to be returned; when {@code null}, this method returns instances
* of {@code RuntimeException} or
* @param message if not {@code null}, used as the message in {@code RuntimeException} that
* may wrap the peeled {@code Throwable}
* @param exceptionWrapper wraps the peeled code using this exceptionWrapper
* @param Type of the wrapper exception in exceptionWrapper
* @param allowed type of {@code Throwable}
* @return the peeled {@code Throwable}
*/
public static Throwable peel(final Throwable t, Class allowedType,
String message, BiFunction exceptionWrapper) {
if (t instanceof RuntimeException) {
return t;
}
if (t instanceof ExecutionException || t instanceof InvocationTargetException) {
final Throwable cause = t.getCause();
if (cause != null) {
return peel(cause, allowedType, message, exceptionWrapper);
} else {
return exceptionWrapper.apply(t, message);
}
}
if (allowedType != null && allowedType.isAssignableFrom(t.getClass())) {
return t;
}
return exceptionWrapper.apply(t, message);
}
public static RuntimeException rethrow(Throwable t) {
rethrowIfError(t);
throw peel(t);
}
public static RuntimeException rethrow(Throwable t, BiFunction exceptionWrapper) {
rethrowIfError(t);
throw (RuntimeException) peel(t, null, null, exceptionWrapper);
}
public static RuntimeException rethrow(final Throwable t, Class allowedType) throws T {
rethrowIfError(t);
throw (T) peel(t, allowedType, null);
}
/**
* This rethrow the exception providing an allowed Exception in first priority, even it is a Runtime exception
*/
public static RuntimeException rethrowAllowedTypeFirst(final Throwable t,
Class allowedType) throws T {
rethrowIfError(t);
if (allowedType.isAssignableFrom(t.getClass())) {
throw (T) t;
} else {
throw peel(t);
}
}
public static void rethrowIfError(final Throwable t) {
if (t instanceof Error error) {
if (t instanceof OutOfMemoryError outOfMemoryError) {
OutOfMemoryErrorDispatcher.onOutOfMemory(outOfMemoryError);
}
throw error;
}
}
public static RuntimeException rethrowAllowInterrupted(final Throwable t) throws InterruptedException {
return rethrow(t, InterruptedException.class);
}
@SuppressWarnings("unchecked")
@Nonnull
public static RuntimeException sneakyThrow(@Nonnull Throwable t) throws T {
throw (T) t;
}
/**
* Utility to make sure exceptions inside
* {@link java.util.concurrent.CompletionStage#whenComplete(BiConsumer)} are not swallowed.
* Exceptions will be caught and logged using the supplied logger.
*/
@Nonnull
public static BiConsumer withTryCatch(
@Nonnull ILogger logger, @Nonnull BiConsumer consumer
) {
return withTryCatch(logger, "Exception during callback", consumer);
}
/**
* Utility to make sure exceptions inside
* {@link java.util.concurrent.CompletionStage#whenComplete(BiConsumer)} are not swallowed.
* Exceptions will be caught and logged using the supplied logger and message.
*/
@Nonnull
public static BiConsumer withTryCatch(
@Nonnull ILogger logger, @Nonnull String message, @Nonnull BiConsumer consumer
) {
return (r, t) -> {
try {
consumer.accept(r, t);
} catch (Throwable e) {
logger.severe(message, e);
}
};
}
/**
* Tries to create the exception with appropriate constructor in the following order.
* In all cases the cause is set (via constructor or via {@code initCause})
* new Throwable(String message, Throwable cause)
* new Throwable(Throwable cause)
* new Throwable(String message)
* new Throwable()
* software.amazon.awssdk.core.exception.SdkException.builder().message(String).cause(Throwable).build()
*
* @param exceptionClass class of the exception
* @param message message to be pass to constructor of the exception
* @param cause cause to be set to the exception
* @return {@code null} if can not find a constructor as
* described above, otherwise returns newly constructed exception
*/
@SuppressWarnings("checkstyle:npathcomplexity")
public static T tryCreateExceptionWithMessageAndCause(Class exceptionClass, String message,
@Nullable Throwable cause) {
for (ConstructorMethod method : ConstructorMethod.values()) {
try {
return method.cloneException(exceptionClass, message, cause);
} catch (ClassCastException | WrongMethodTypeException | IllegalAccessException
| SecurityException | NoSuchMethodException | ClassNotFoundException ignored) {
} catch (Throwable t) {
throw new RuntimeException("Exception creation failed ", t);
}
}
return null;
}
/**
* Supplies constructor method types to {@link #tryCreateExceptionWithMessageAndCause}.
*
* Keep order of enums as is.
*/
private enum ConstructorMethod {
// new Throwable(String message, Throwable cause)
MT_INIT_STRING_THROWABLE() {
@Override
T cloneException(Class exceptionClass, String message,
@Nullable Throwable cause) throws Throwable {
MethodHandle constructor = MethodHandles.publicLookup()
.findConstructor(exceptionClass, methodType(void.class, String.class, Throwable.class));
return (T) constructor.invoke(message, cause);
}
},
// new Throwable(Throwable cause)
MT_INIT_THROWABLE() {
@Override
T cloneException(Class exceptionClass, String message,
@Nullable Throwable cause) throws Throwable {
MethodHandle constructor = MethodHandles.publicLookup()
.findConstructor(exceptionClass, methodType(void.class, Throwable.class));
return (T) constructor.invoke(cause);
}
},
// new Throwable(String message)
MT_INIT_STRING() {
@Override
T cloneException(Class exceptionClass, String message,
@Nullable Throwable cause) throws Throwable {
MethodHandle constructor = MethodHandles.publicLookup()
.findConstructor(exceptionClass, methodType(void.class, String.class));
T cloned = (T) constructor.invoke(message);
try {
cloned.initCause(cause);
} catch (IllegalStateException ignored) {
// Cause can be already set by the exception. It can be set to null as well.
// So doing null check is not the solution here. See https://github.com/hazelcast/hazelcast/issues/21414
}
return cloned;
}
},
// new Throwable()
MT_INIT() {
@Override
T cloneException(Class exceptionClass, String message,
@Nullable Throwable cause) throws Throwable {
MethodHandle constructor = MethodHandles.publicLookup()
.findConstructor(exceptionClass, methodType(void.class));
T cloned = (T) constructor.invoke();
try {
cloned.initCause(cause);
} catch (IllegalStateException ignored) {
// Cause can be already set by the exception. It can be set to null as well.
// So doing null check is not the solution here. See https://github.com/hazelcast/hazelcast/issues/21414
}
return cloned;
}
},
// software.amazon.awssdk.core.exception.SdkException.builder().message(String).cause(Throwable).build()
MT_BUILD_STRING_THROWABLE() {
@Override
T cloneException(Class exceptionClass, String message,
@Nullable Throwable cause) throws Throwable {
Class> builderClass = Class.forName(exceptionClass.getName() + "$Builder");
Object builder = exceptionClass.getMethod("builder").invoke(null);
builderClass.getMethod("message", String.class).invoke(builder, message);
builderClass.getMethod("cause", Throwable.class).invoke(builder, cause);
return (T) builderClass.getMethod("build").invoke(builder);
}
};
/**
* @param exceptionClass class of the exception
* @param message message to be passed to constructor of the exception
* @param cause cause to be set to the exception
*/
abstract T cloneException(Class exceptionClass, String message,
@Nullable Throwable cause) throws Throwable;
}
/**
* @param original exception to be cloned with fixed stack trace
* @param type of the original exception
* @return a cloned exception with the current
* stacktrace is added on top of the original exceptions
* stack-trace the cloned exception has the same
* cause and the message as the original exception
*/
@Nonnull
public static T cloneExceptionWithFixedAsyncStackTrace(@Nonnull T original) {
StackTraceElement[] fixedStackTrace = getFixedStackTrace(original, Thread.currentThread().getStackTrace());
Class extends Throwable> exceptionClass = original.getClass();
Throwable clone = tryCreateExceptionWithMessageAndCause(exceptionClass,
original.getMessage(), original.getCause());
if (clone != null) {
clone.setStackTrace(fixedStackTrace);
return (T) clone;
}
return original;
}
private static StackTraceElement[] getFixedStackTrace(Throwable throwable,
StackTraceElement[] localSideStackTrace) {
StackTraceElement[] remoteStackTrace = throwable.getStackTrace();
StackTraceElement[] newStackTrace = new StackTraceElement[localSideStackTrace.length + remoteStackTrace.length];
System.arraycopy(remoteStackTrace, 0, newStackTrace, 0, remoteStackTrace.length);
newStackTrace[remoteStackTrace.length] = new StackTraceElement(EXCEPTION_SEPARATOR, "", "", -1);
System.arraycopy(localSideStackTrace, 1, newStackTrace, remoteStackTrace.length + 1, localSideStackTrace.length - 1);
return newStackTrace;
}
/**
* If there's any Throwable instance in the values, throw it.
*/
@SafeVarargs
public static void rethrowFromCollection(Collection> values, Class extends Throwable> ... ignored) throws Throwable {
outerLoop:
for (Object value : values) {
if (value instanceof Throwable throwable) {
for (Class extends Throwable> ignoredClass : ignored) {
if (ignoredClass.isAssignableFrom(value.getClass())) {
continue outerLoop;
}
}
throw throwable;
}
}
}
}