/* * RHQ Management Platform * Copyright (C) 2005-2008 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., 675 Mass Ave, Cambridge, MA 02139, USA. */ package org.rhq.enterprise.server.remote; import java.io.Serializable; import java.util.HashMap; import java.util.Map; import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.ReadWriteLock; import java.util.concurrent.locks.ReentrantReadWriteLock; /** * Provides metrics for remote invocations from things like the remote CLI. * * @author John Mazzitelli */ public class RemoteSafeInvocationHandlerMetrics implements RemoteSafeInvocationHandlerMetricsMBean { private long numberSuccessfulInvocations = 0L; private long numberFailedInvocations = 0L; private long averageExecutionTime = 0L; private Map<String, Calltime> calltimes = new HashMap<String, Calltime>(); private ReadWriteLock lock = new ReentrantReadWriteLock(); public void clear() { writeLock(); try { numberSuccessfulInvocations = 0L; numberFailedInvocations = 0L; averageExecutionTime = 0L; calltimes.clear(); } finally { writeUnlock(); } } public Map<String, Calltime> getCallTimeData() { // just do a shallow copy - this allows us to avoid exceptions when we want to concurrently // traverse the calltimes and add to them but it avoids unnecesary duplication of MinMaxAvg // objects (thus we are more efficient both in performance and space). Note that MinMaxAvg // is allowed to be concurrently accessed - even though its possible you could try to get // its data while we are writing new min/max/avg/count values. Even though the data won't be // entirely accurate and up-to-date in that case, its good enough for our purposes. readLock(); try { return new HashMap<String, Calltime>(calltimes); } finally { readUnlock(); } } public Map<String, long[]> getCallTimeDataAsPrimitives() { Map<String, Calltime> calltimeData = getCallTimeData(); Map<String, long[]> calltimeDataPrimitives = new HashMap<String, long[]>(calltimeData.size()); for (Map.Entry<String, Calltime> entry : calltimeData.entrySet()) { Calltime calltime = entry.getValue(); calltimeDataPrimitives.put(entry.getKey(), new long[] { calltime.getCount(), calltime.getSuccesses(), calltime.getFailures(), calltime.getMinimum(), calltime.getMaximum(), calltime.getAverage() }); } return calltimeDataPrimitives; } public long getNumberFailedInvocations() { readLock(); try { return numberFailedInvocations; } finally { readUnlock(); } } public long getNumberSuccessfulInvocations() { readLock(); try { return numberSuccessfulInvocations; } finally { readUnlock(); } } public long getNumberTotalInvocations() { readLock(); try { return numberSuccessfulInvocations + numberFailedInvocations; } finally { readUnlock(); } } public long getAverageExecutionTime() { readLock(); try { return averageExecutionTime; } finally { readUnlock(); } } /** * Add a newly collected metric value for a particular type of invocation to * the stored calltime data. This will update the min/max/avg data, but only * if this represents a succesful call (i.e. <code>successful</code> is <code>true</code>). * We do not want to skew the min/max/avg results from failed invocations because * they almost always fail-fast and will have very fast execution times. * * This is packaged-scoped because only the handler should be * adding calltime data to this object. * * @param type the type of invocation whose min/max/avg is to be stored * @param executionTime the time, in milliseconds, that the type invocation took * @param successful <code>true</code> if the invocation was successful */ void addData(String type, long executionTime, boolean successful) { writeLock(); try { if (type == null) { type = "(unknown)"; } Calltime calltime = calltimes.get(type); if (calltime == null) { calltime = new Calltime(); calltimes.put(type, calltime); } calltime.count++; if (successful) { this.numberSuccessfulInvocations++; this.averageExecutionTime = (((this.numberSuccessfulInvocations - 1) * this.averageExecutionTime) + executionTime) / this.numberSuccessfulInvocations; if (executionTime > calltime.max) { calltime.max = executionTime; } if (executionTime < calltime.min) { calltime.min = executionTime; } long successes = calltime.getSuccesses(); calltime.avg = (((successes - 1) * calltime.avg) + executionTime) / successes; } else { this.numberFailedInvocations++; calltime.failures++; } } finally { writeUnlock(); } return; } private boolean writeLock() { // we try to be good stewards of this object by trying to be thread safe // but do not block the processor here for too long - we don't want to // prevent the processor from processing messages for too long just to // synchronize metric data. If we timeout or are interrupted, return immediately. // Note also that we don't want to throw any exceptions to the caller, try our // best to be fault tolerant in here. try { return lock.writeLock().tryLock(10, TimeUnit.SECONDS); } catch (InterruptedException e) { // don't wait any longer, just return immediately } catch (Exception e) { lock = new ReentrantReadWriteLock(); // something really bad happened, let's create a new one to be safe } return false; } private void writeUnlock() { try { lock.writeLock().unlock(); } catch (Exception e) { // Note that we don't want to throw any exceptions to the caller, try our // best to be fault tolerant in here. This exception occurred probably because // the caller didn't have the lock due to a timeout or interrupt in writeLock. } } private boolean readLock() { // we try to be good stewards of this object by trying to be thread safe // but if we can't get the lock, just return and let's read the data // unlocked. Nothing dangerous will happen, at worst we might read // inconsistent metric data for this thread, nothing too serious to worry about. try { return lock.readLock().tryLock(30, TimeUnit.SECONDS); } catch (Exception e) { } return false; } private void readUnlock() { try { lock.readLock().unlock(); } catch (Exception e) { // Note that we don't want to throw any exceptions to the caller, try our // best to be fault tolerant in here. This exception occurred probably because // the caller didn't have the lock due to a timeout or interrupt in readLock. } } /** * Used to store the minimum, maximum and average times (in milliseconds) * for invocations to a particular invocation. The count of the number * of times an invocation was executed is also kept. Note that the min/max/avg * times will only be for successful invocations. */ public class Calltime implements Serializable { private static final long serialVersionUID = 1L; private long count = 0; private long failures = 0; private long min = Long.MAX_VALUE; private long max = Long.MIN_VALUE; private long avg = 0; public long getCount() { return count; } public long getFailures() { return failures; } public long getSuccesses() { return count - (failures); // ok if not thread-safe, good enough for what we need } public long getMinimum() { return min; } public long getMaximum() { return max; } public long getAverage() { return avg; } @Override public String toString() { return "" + count + ':' + failures + ':' + min + ':' + max + ':' + avg; } } }