/**
* Copyright (c) 2014-2017 by the respective copyright holders.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*/
package org.eclipse.smarthome.core.common;
import java.security.AccessController;
import java.security.PrivilegedAction;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* {@link SafeMethodCaller} is a helper class, which can be used to call a method safely. Safely means that a separate
* thread is opened, so that a method call can not block the execution of the system. It also catches Errors and wraps
* them into a {@link ExecutionException}, so that the caller does not have to catch {@link Throwable}. This helper
* class is useful when calling third party code like bindings.
*
* @author Dennis Nobel - Initial contribution
*/
public class SafeMethodCaller {
private static final String SAFE_CALL_POOL_NAME = "safeCall";
/**
* Executable Action. See {@link SafeMethodCaller#call(Action)}
*
* @param <V> return type
*/
public interface Action<V> extends Callable<V> {
}
/**
* Executable Action with exception. See {@link SafeMethodCaller#call(ActionWithException)}
*
* @param <V> return type
*/
public interface ActionWithException<V> extends Callable<V> {
}
/**
* Default timeout for actions in milliseconds.
*/
public static int DEFAULT_TIMEOUT = 5000 /* milliseconds */;
/**
* Executes the action in a new thread with a default timeout (see {@link SafeMethodCaller#DEFAULT_TIMEOUT}). If an
* exception occurs while calling the action or the action does not terminate within the timeout this method
* rethrows the exception.
*
* @param action action to be called
* @return result
* @throws TimeoutException if the action does not terminate within the timeout
* @throws ExecutionException if the action throws an Exception or an Error
*/
public static <V> V call(ActionWithException<V> action) throws TimeoutException, ExecutionException {
return call(action, DEFAULT_TIMEOUT);
}
/**
* Executes the action in a new thread with a given timeout. If an exception occurs while calling the action or the
* action does not terminate within the timeout this method rethrows the exception.
*
* @param action action to be called
* @param timeout timeout of the action in milliseconds. If the action takes longer than the defined timeout a
* {@link TimeoutException} is thrown
* @return result
* @throws TimeoutException if the action does not terminate within the timeout
* @throws ExecutionException if the action throws an Exception or an Error
*/
public static <V> V call(ActionWithException<V> action, int timeout) throws TimeoutException, ExecutionException {
try {
return callAsynchronous(action, timeout);
} catch (InterruptedException ex) {
throw new IllegalStateException("Thread was interrupted.", ex);
}
}
/**
* Executes the action in a new thread with a default timeout (see {@link SafeMethodCaller#DEFAULT_TIMEOUT}). If
* an exception occurs while calling the action or the action does not terminate within the timeout this method just
* logs the exception, but does not rethrow it. In case an exception occurred or the action timeout the result will
* always be null.
*
* @param action action to be called
* @return result or null if an exception occurred or the timeout was reached
*/
public static <V> V call(Action<V> action) {
return call(action, DEFAULT_TIMEOUT);
}
/**
* Executes the action in a new thread with a given timeout. If an exception occurs while calling the action or the
* action does not terminate within the timeout this method just logs the exception, but does not rethrow it. In
* case an exception occurred or the action timeout the result will always be null.
*
* @param action action to be called
* @param timeout timeout of the action in milliseconds. If the action takes longer than the defined timeout an
* exception is logged and this method returns null
* @return result or null if an exception occurred or the timeout was reached
*/
public static <V> V call(Action<V> action, int timeout) {
try {
return callAsynchronous(action, timeout);
} catch (ExecutionException ex) {
StackTraceElement stackTraceElement = findCalledMethod(ex, action.getClass());
if (stackTraceElement != null) {
String className = stackTraceElement.getClassName();
String methodName = stackTraceElement.getMethodName();
getLogger().error("Exception occurred while calling '" + methodName + "' at '" + className + "'", ex);
} else {
getLogger().error("Exception occurred while calling action", ex);
}
return null;
} catch (TimeoutException ex) {
getLogger().error(
"Timeout occurred while calling method. Execution took longer than " + timeout + " milliseconds.",
ex);
return null;
} catch (Throwable throwable) {
getLogger().error("Unkown Exception or Error occurred while calling action", throwable);
return null;
}
}
/**
* This method tries to find the method which was called within the action.
*
* @param eex ExecutionException
* @param actionClass action class
* @return stack trace element for the called method or null
*/
private static StackTraceElement findCalledMethod(ExecutionException eex, Class<?> actionClass) {
if (eex.getCause() == null) {
return null;
}
StackTraceElement[] stackTrace = eex.getCause().getStackTrace();
if (stackTrace == null) {
return null;
}
for (int i = 0; i < stackTrace.length; i++) {
StackTraceElement stackTraceElement = stackTrace[i];
if (stackTraceElement.getClassName().equals(actionClass.getName())) {
return stackTrace[i - 1];
}
}
return null;
}
private static class CallableWrapper<V> implements Callable<V> {
private final Callable<V> callable;
private Thread thread;
public CallableWrapper(final Callable<V> callable) {
this.callable = callable;
}
public Thread getThread() {
return thread;
}
@Override
public V call() throws Exception {
thread = Thread.currentThread();
return callable.call();
}
}
private static <V> V callAsynchronous(final Callable<V> callable, int timeout)
throws InterruptedException, ExecutionException, TimeoutException {
if (Thread.currentThread().getName().startsWith(SAFE_CALL_POOL_NAME + "-")) {
getLogger().trace("Already in a SafeMethodCaller context, executing {} directly.", callable);
return executeDirectly(callable);
}
CallableWrapper<V> wrapper = new CallableWrapper<>(callable);
try {
Future<V> future = ThreadPoolManager.getPool(SAFE_CALL_POOL_NAME).submit(wrapper);
return future.get(timeout, TimeUnit.MILLISECONDS);
} catch (TimeoutException e) {
if (wrapper.getThread() != null) {
final Thread thread = wrapper.getThread();
StackTraceElement element = AccessController.doPrivileged(new PrivilegedAction<StackTraceElement>() {
@Override
public StackTraceElement run() {
return thread.getStackTrace()[0];
}
});
getLogger().debug("Timeout of {}ms exceeded, thread {} ({}) in state {} is at {}.{}({}:{}).", timeout,
thread.getName(), thread.getId(), thread.getState().toString(), element.getClassName(),
element.getMethodName(), element.getFileName(), element.getLineNumber());
throw e;
} else {
getLogger().debug("Timeout of {}ms exceeded but the task was still queued.", timeout);
}
return null;
}
}
private static <V> V executeDirectly(final Callable<V> callable) throws ExecutionException {
try {
return callable.call();
} catch (Exception e) {
throw new ExecutionException(e);
}
}
private static Logger getLogger() {
return LoggerFactory.getLogger(SafeMethodCaller.class);
}
}