/* * RHQ Management Platform * Copyright (C) 2005-2014 Red Hat, Inc. * All rights reserved. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation version 2 of the License. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA */ package org.rhq.core.pc.util; import java.lang.reflect.InvocationHandler; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.lang.reflect.Proxy; import java.util.Arrays; import java.util.HashSet; import java.util.Set; import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.rhq.core.clientapi.agent.PluginContainerException; import org.rhq.core.domain.resource.ResourceType; import org.rhq.core.pc.PluginContainer; import org.rhq.core.pc.inventory.ResourceContainer; import org.rhq.core.pc.inventory.TimeoutException; import org.rhq.core.pc.plugin.BlacklistedException; import org.rhq.core.pc.plugin.PluginComponentFactory; import org.rhq.core.pluginapi.inventory.ClassLoaderFacet; import org.rhq.core.pluginapi.inventory.ResourceDiscoveryComponent; /** * Factory that can build discovery component proxies. These proxies wrap * timeouts around discovery component method invocations. * * Note that if a discovery component invocation times out, the resource type * will be blacklisted by this factory. Any further attempt to retrieve a proxy * for that resource type's discovery component will fail. * * @author John Mazzitelli * @author Ian Springer */ public class DiscoveryComponentProxyFactory { private static final Log log = LogFactory.getLog(DiscoveryComponentProxyFactory.class); private static final String DAEMON_THREAD_POOL_NAME = "ResourceDiscoveryComponent.invoker.daemon"; private ExecutorService daemonThreadPool = null; private final Set<ResourceType> blacklist = new HashSet<ResourceType>(); private final PluginComponentFactory pluginComponentFactory; private static boolean blacklistDisable; static { try { blacklistDisable = Boolean.valueOf(System.getProperty("rhq.agent.blacklist.disable", "false")); } catch (Throwable t) { blacklistDisable = false; } // always catch here, always let the class load, use a default if the sysprop is invalid } public DiscoveryComponentProxyFactory(PluginComponentFactory pluginComponentFactory) { this.pluginComponentFactory = pluginComponentFactory; } /** * Same as {@link #getDiscoveryComponentProxy(org.rhq.core.domain.resource.ResourceType, org.rhq.core.pluginapi.inventory.ResourceDiscoveryComponent, long, org.rhq.core.pc.inventory.ResourceContainer)} except * this lets you provide the interface of the discovery component you want to talk to. For example, * use this to talk to the {@link ClassLoaderFacet} of a discovery component. */ @SuppressWarnings("unchecked") public <T> T getDiscoveryComponentProxy(ResourceType type, ResourceDiscoveryComponent component, long timeout, Class<T> componentInterface, ResourceContainer parentResourceContainer) throws PluginContainerException, BlacklistedException { if (isResourceTypeBlacklisted(type)) { throw new BlacklistedException("Discovery component for resource type [" + type + "] has been blacklisted"); } try { ClassLoader pluginClassLoader = pluginComponentFactory.getDiscoveryComponentClassLoader( parentResourceContainer, type.getPlugin()); // This is the handler that will actually invoke the method calls. ResourceDiscoveryComponentInvocationHandler handler = new ResourceDiscoveryComponentInvocationHandler(type, component, timeout, pluginClassLoader, componentInterface); // This is the proxy that will look like the discovery component object that the caller will use. T proxy = (T) Proxy.newProxyInstance(pluginClassLoader, new Class<?>[] { componentInterface }, handler); return proxy; } catch (Throwable t) { throw new PluginContainerException("Cannot get discovery component proxy for [" + component + "]", t); } } /** * Given a discovery component instance, this returns that component wrapped in a proxy that provides the ability * for invocations to that component to timeout after a certain time limit expires. This allows the plugin container * to make calls into the plugin discovery component and not deadlock if that plugin misbehaves and never returns * (or takes too long to return). * * * @param type the resource type that is to be discovered by the given discovery component * @param component the discovery component to be wrapped in a timer proxy * @param timeout the time, in milliseconds, that invocations can take to invoke discovery component methods * * @param parentResourceContainer * @return the discovery component wrapped in a proxy that should be used to make calls to the component * * @throws PluginContainerException if this method failed to create the proxy * @throws BlacklistedException if the resource type's discovery component has been blacklisted and * not allowed to be invoked anymore */ @SuppressWarnings("unchecked") public ResourceDiscoveryComponent getDiscoveryComponentProxy(ResourceType type, ResourceDiscoveryComponent component, long timeout, ResourceContainer parentResourceContainer) throws PluginContainerException, BlacklistedException { return getDiscoveryComponentProxy(type, component, timeout, ResourceDiscoveryComponent.class, parentResourceContainer); } public void initialize() { LoggingThreadFactory daemonFactory = new LoggingThreadFactory(DAEMON_THREAD_POOL_NAME, true); daemonThreadPool = Executors.newCachedThreadPool(daemonFactory); } public void shutdown() { if (daemonThreadPool != null) { PluginContainer.shutdownExecutorService(daemonThreadPool, true); daemonThreadPool = null; } } public HashSet<ResourceType> getResourceTypeBlacklist() { synchronized (this.blacklist) { return new HashSet<ResourceType>(this.blacklist); // return a copy, not the real set } } public void clearResourceTypeBlacklist() { synchronized (this.blacklist) { this.blacklist.clear(); } } public boolean isResourceTypeBlacklisted(ResourceType type) { if (blacklistDisable) return false; synchronized (this.blacklist) { return this.blacklist.contains(type); } } public void addResourceTypeToBlacklist(ResourceType type) { if (blacklistDisable) return; synchronized (this.blacklist) { this.blacklist.add(type); } log.warn("The discovery component for resource type [" + type + "] has been blacklisted"); } private ExecutorService getThreadPool() { return daemonThreadPool; } /** * This is a {@link ResourceDiscoveryComponent} proxy that invokes discovery component methods in pooled threads. * It can interrupt the invocation thread and throw a {@link TimeoutException} if its execution time exceeds a * specified timeout. */ @SuppressWarnings("unchecked") private class ResourceDiscoveryComponentInvocationHandler implements InvocationHandler { private final ResourceDiscoveryComponent component; private final long timeout; private final ResourceType resourceType; private final ClassLoader pluginClassLoader; private final Class<?> componentInterface; public ResourceDiscoveryComponentInvocationHandler(ResourceType type, ResourceDiscoveryComponent component, long timeout, ClassLoader pluginClassLoader, Class<?> componentInterface) { if (timeout <= 0) { throw new IllegalArgumentException("timeout value is not positive."); } if (component == null) { throw new IllegalArgumentException("component is null"); } this.resourceType = type; this.component = component; this.timeout = timeout; this.pluginClassLoader = pluginClassLoader; this.componentInterface = componentInterface; } public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { if (isResourceTypeBlacklisted(this.resourceType)) { throw new RuntimeException("Discovery component for resource type [" + this.resourceType + "] has been blacklisted and can no longer be invoked."); } if (componentInterface.isAssignableFrom(method.getDeclaringClass())) { return invokeInNewThread(method, args); } else { // toString(), etc. return invokeInCurrentThread(method, args); } } private Object invokeInNewThread(Method method, Object[] args) throws Throwable { ExecutorService threadPool = getThreadPool(); ComponentInvocationThread invocationThread = new ComponentInvocationThread(this.component, method, args, this.pluginClassLoader); Future<?> future = threadPool.submit(invocationThread); try { return future.get(this.timeout, TimeUnit.MILLISECONDS); } catch (InterruptedException e) { if (log.isDebugEnabled()) { log.debug("Thread [" + Thread.currentThread().getName() + "] was interrupted."); } future.cancel(true); // this is a daemon thread, let's try to cancel it Thread.currentThread().interrupt(); throw new RuntimeException(invokedMethodString(method, args, "was interrupted.")); } catch (ExecutionException e) { if (log.isDebugEnabled()) { log.debug(invokedMethodString(method, args, "failed."), e); } throw e.getCause(); } catch (java.util.concurrent.TimeoutException e) { addResourceTypeToBlacklist(this.resourceType); String msg = invokedMethodString(method, args, "timed out. Invocation thread will be interrupted."); Thread thread = invocationThread.getThread(); Exception cause; if (thread != null) { StackTraceElement[] stackTrace = thread.getStackTrace(); cause = new Exception(thread + " with id [" + thread.getId() + "] is hung. This exception contains its stack trace."); cause.setStackTrace(stackTrace); } else { cause = null; } TimeoutException timeoutException = new TimeoutException(msg, cause); future.cancel(true); throw timeoutException; } } private Object invokeInCurrentThread(Method method, Object[] args) throws Throwable { // This method is triggered when the component calls itself - do not timeout. // We already have a timed call on the call stack - no need to do it again ClassLoader originalContextClassLoader = Thread.currentThread().getContextClassLoader(); try { Thread.currentThread().setContextClassLoader(this.pluginClassLoader); return method.invoke(this.component, args); } catch (InvocationTargetException ite) { throw (ite.getCause() != null) ? ite.getCause() : ite; } finally { Thread.currentThread().setContextClassLoader(originalContextClassLoader); } } private String invokedMethodString(Method method, Object[] methodArgs, String extraMsg) { String name = this.component.getClass().getName() + '.' + method.getName() + "()"; String args = ((methodArgs != null) ? Arrays.asList(methodArgs).toString() : ""); return "Call to [" + name + "] with args [" + args + "] " + extraMsg; } } @SuppressWarnings("unchecked") private class ComponentInvocationThread implements Callable { private final ResourceDiscoveryComponent component; private final Method method; private final Object[] args; private final ClassLoader pluginClassLoader; private Thread thread; ComponentInvocationThread(ResourceDiscoveryComponent component, Method method, Object[] args, ClassLoader pluginClassLoader) { this.component = component; this.method = method; this.args = args; this.pluginClassLoader = pluginClassLoader; } public Object call() throws Exception { ClassLoader originalContextClassLoader = Thread.currentThread().getContextClassLoader(); try { this.thread = Thread.currentThread(); this.thread.setContextClassLoader(this.pluginClassLoader); // This is the actual call into the discovery component. Object results = this.method.invoke(this.component, this.args); return results; } catch (InvocationTargetException ite) { Throwable cause = ite.getCause(); throw new Exception("Discovery component invocation failed.", cause); } catch (Throwable t) { throw new Exception("Failed to invoke discovery component", t); } finally { this.thread.setContextClassLoader(originalContextClassLoader); this.thread = null; } } public Thread getThread() { return this.thread; } } }