/* * RHQ Management Platform * Copyright (C) 2005-2013 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.system; import static java.util.concurrent.TimeUnit.MINUTES; import static java.util.concurrent.TimeUnit.SECONDS; import java.lang.management.ManagementFactory; import java.lang.management.ThreadInfo; import java.lang.reflect.InvocationHandler; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ThreadFactory; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.locks.ReentrantLock; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.hyperic.sigar.Sigar; /** * An {@link InvocationHandler} for a {@link org.hyperic.sigar.SigarProxy}. * <p>A single instance of this class will be created by the {@link SigarAccess} class.</p> * This class holds a shared {@link Sigar} instance and serializes calls. If a thread waits more than * 'sharedSigarLockMaxWait' seconds, it will be given a new Sigar instance, which will be destroyed at the end of the * call. * <p>Every 5 minutes, a background task checks that 'localSigarInstancesWarningThreshold' has not been exceeded. It it * has, a warning message will be logged, optionally with a thread dump. * <p>This class is configurable with System properties: * <ul> * <li><strong>sharedSigarLockMaxWait</strong>: maximum time in seconds a thread will wait for the shared Sigar lock * acquistion; defaults to 2 seconds</li> * <li><strong>localSigarInstancesWarningThreshold</strong>: threshold of currently living Sigar instances at which * the background task will print warning messages; defaults to 50</li> * <li><strong>maxLocalSigarInstances</strong>: maximum number of local Sigar instances which can be created, zero * and negative values being interpreted as 'no limit'; defaults to 50</li> * <li><strong>threadDumpOnlocalSigarInstancesWarningThreshold</strong>: if set to true (case insensitive), the * background task will also log a thread dump when <strong>localSigarInstancesWarningThreshold</strong> is met</li> * </ul> * </p> * * @author Thomas Segismont */ class SigarAccessHandler implements InvocationHandler { private static final Log LOG = LogFactory.getLog(SigarAccessHandler.class); private static final String LINE_SEPARATOR = System.getProperty("line.separator"); private static final int SHARED_SIGAR_LOCK_MAX_WAIT = Integer.getInteger("sharedSigarLockMaxWait", 2); private static final int LOCAL_SIGAR_INSTANCES_WARNING_THRESHOLD = Integer.getInteger( "localSigarInstancesWarningThreshold", 50); private static final int MAX_LOCAL_SIGAR_INSTANCES = Integer.getInteger("maxLocalSigarInstances", 50); private static final boolean LIMIT_LOCAL_SIGAR_INSTANCES = MAX_LOCAL_SIGAR_INSTANCES > 0; private static final boolean THREAD_DUMP_ON_SIGAR_INSTANCES_THRESHOLD = Boolean .getBoolean("threadDumpOnlocalSigarInstancesWarningThreshold"); private final SigarFactory sigarFactory; private final ReentrantLock sharedSigarLock; private final ReentrantLock localSigarLock; private final ScheduledExecutorService scheduledExecutorService; private volatile int localSigarInstancesCount; private Sigar sharedSigar; private volatile boolean closed; SigarAccessHandler() { this(new DefaultSigarFactory()); } SigarAccessHandler(SigarFactory sigarFactory) { this.sigarFactory = sigarFactory; sharedSigarLock = new ReentrantLock(); localSigarLock = new ReentrantLock(); scheduledExecutorService = Executors.newSingleThreadScheduledExecutor(new ThreadFactory() { private ThreadFactory defaultThreadFactory = Executors.defaultThreadFactory(); private AtomicInteger threadCounter = new AtomicInteger(0); @Override public Thread newThread(Runnable runnable) { Thread thread = defaultThreadFactory.newThread(runnable); thread.setName("SigarAccessHandler-" + threadCounter.incrementAndGet()); // With daemon threads, there is no need to call #shutdown on the executor to let the JVM go down thread.setDaemon(true); return thread; } }); scheduledExecutorService.scheduleWithFixedDelay(new ThresholdChecker(), 1, 5, MINUTES); localSigarInstancesCount = 0; closed = false; } public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { // its possible in the time between this handler's creation and now, someone disabled the native layer. // throw a runtime exception if the native system was disabled if (SystemInfoFactory.isNativeSystemInfoDisabled()) { throw new SystemInfoException("Native system has been disabled"); } // Acquire lock for shared Sigar instance. Wait 'sharedSigarLockMaxWait' seconds at most boolean acquiredLock = sharedSigarLock.tryLock(SHARED_SIGAR_LOCK_MAX_WAIT, SECONDS); if (acquiredLock) { try { if (closed) { throw new SystemInfoException("SigarAccess has been closed"); } if (sharedSigar == null) { this.sharedSigar = sigarFactory.createSigarInstance(); } return method.invoke(sharedSigar, args); } catch (InvocationTargetException e) { throw e.getTargetException(); } finally { sharedSigarLock.unlock(); } } else { Sigar localSigar = createLocalSigarInstance(); try { return method.invoke(localSigar, args); } catch (InvocationTargetException e) { throw e.getTargetException(); } finally { closeLocalSigarInstance(localSigar); } } } private Sigar createLocalSigarInstance() { localSigarLock.lock(); try { if (LIMIT_LOCAL_SIGAR_INSTANCES && localSigarInstancesCount >= MAX_LOCAL_SIGAR_INSTANCES) { throw new RuntimeException("Too many Sigar instances created"); } Sigar sigarInstance = sigarFactory.createSigarInstance(); localSigarInstancesCount++; return sigarInstance; } finally { localSigarLock.unlock(); } } private void closeLocalSigarInstance(Sigar sigar) { localSigarLock.lock(); try { sigar.close(); localSigarInstancesCount--; } finally { localSigarLock.unlock(); } } void close() { if (sharedSigar != null) { sharedSigarLock.lock(); try { closed = true; sharedSigar.close(); sharedSigar = null; } finally { sharedSigarLock.unlock(); } } scheduledExecutorService.shutdownNow(); } int localSigarInstancesCount() { return localSigarInstancesCount; } interface SigarFactory { Sigar createSigarInstance(); } private static class DefaultSigarFactory implements SigarFactory { @Override public Sigar createSigarInstance() { return new Sigar(); } } private class ThresholdChecker implements Runnable { @Override public void run() { int currentCount = localSigarInstancesCount; if (currentCount > LOCAL_SIGAR_INSTANCES_WARNING_THRESHOLD) { StringBuilder sb = new StringBuilder(); sb.append("There are ").append(currentCount).append(" local Sigar instances currently active. ") .append("This may indicate that a call to the shared Sigar instance did not complete."); if (THREAD_DUMP_ON_SIGAR_INSTANCES_THRESHOLD) { sb.append(LINE_SEPARATOR); threadDump(sb); } LOG.warn(sb.toString()); } } private void threadDump(StringBuilder sb) { ThreadInfo[] threadInfos = ManagementFactory.getThreadMXBean().dumpAllThreads(true, true); for (ThreadInfo threadInfo : threadInfos) { sb.append(threadInfo); sb.append(LINE_SEPARATOR); } } } }