/*
* 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.modules.plugins.jbossas7;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import org.rhq.core.domain.measurement.AvailabilityType;
import org.rhq.core.domain.measurement.DataType;
import org.rhq.core.domain.measurement.MeasurementReport;
import org.rhq.core.domain.measurement.MeasurementScheduleRequest;
import org.rhq.core.domain.measurement.calltime.CallTimeData;
import org.rhq.core.domain.util.OSGiVersion;
import org.rhq.core.pluginapi.inventory.ResourceComponent;
import org.rhq.core.util.stream.StreamUtil;
import org.rhq.modules.plugins.jbossas7.json.Address;
import org.rhq.modules.plugins.jbossas7.json.ReadAttribute;
import org.rhq.modules.plugins.jbossas7.json.Result;
import org.rhq.modules.plugins.jbossas7.json.WriteAttribute;
/**
* A specialization of the AS component for EJB3 runtime beans. This kind of beans can collect method invocation
* statistics which is what this class handles.
*
* @author Lukas Krejci
* @since 4.10.0
*/
public class Ejb3BeanRuntimeComponent extends BaseComponent<BaseComponent<?>> {
private static final String METHODS_ATTRIBUTE = "methods";
private static final int CALLTIME_METRIC_NAME_PREFIX_LENGTH = "__calltime:".length();
private static final Address RUNTIME_MBEAN_ADDRESS = new Address("core-service=platform-mbean,type=runtime");
private static final String START_TIME_ATTRIBUTE = "start-time";
private static final OSGiVersion FIRST_VERSION_SUPPORTING_METHOD_STATS = new OSGiVersion("7.2.1");
private OSGiVersion asVersion = null;
/**
* cached value, so we don't ask for it anytime a calltime metric is requested
*/
private Boolean ejb3StatisticsEnalbed = null;
private static class StatsRecord implements Serializable {
private static final long serialVersionUID = 1L;
long invocations;
long total;
}
private static class Stats implements Serializable {
private static final long serialVersionUID = 1L;
//we kept the serialVersionUID=1 even though this field was added. This means that the collection will work
//even with the persisted data coming from the previous version of the plugin.
//We need to have special handling in the code that works around the collectionStartTime having 0 value.
long serverStartTime;
long collectionTime;
Map<String, StatsRecord> data;
static Stats fromMap(Map<String, Map<String, Number>> map, String collectedMetric, long collectionTime,
long serverStartTime) {
Stats ret = new Stats();
ret.serverStartTime = serverStartTime;
ret.collectionTime = collectionTime;
ret.data = new HashMap<String, StatsRecord>(map.size());
for(Map.Entry<String, Map<String, Number>> entry : map.entrySet()) {
StatsRecord rec = new StatsRecord();
String methodName = entry.getKey();
rec.invocations = entry.getValue().get("invocations").longValue();
rec.total = entry.getValue().get(collectedMetric).longValue();
ret.data.put(methodName, rec);
}
return ret;
}
}
@Override
public AvailabilityType getAvailability() {
AvailabilityType avail = super.getAvailability();
if (avail == AvailabilityType.DOWN) {
asVersion = null;
}
return avail;
}
@Override
public void getValues(MeasurementReport report, Set<MeasurementScheduleRequest> metrics) throws Exception {
//we'll handling the rest of the metrics using the super method, but we may leave out some of the requests
//if we handle them here. Right now, just use the obtained set. We only create a copy of the (unmodifiable) set
//of requests if necessary.
Set<MeasurementScheduleRequest> metricsToPassDown = metrics;
for(MeasurementScheduleRequest request : metrics) {
if (request.getDataType() == DataType.CALLTIME) {
ensureGlobalEJB3StatisticsEnabled();
//make a copy to pass down to super class if necessary
if (metricsToPassDown == metrics) {
metricsToPassDown = new HashSet<MeasurementScheduleRequest>(metrics);
}
metricsToPassDown.remove(request);
//handle this ourselves
//the name of the metric is actually the name of the stat collected for each method. we then provide
//the calltime data for each method.
Result result = getASConnection().execute(new ReadAttribute(address, METHODS_ATTRIBUTE));
Object value = result.getResult();
if (value instanceof Map) {
@SuppressWarnings("unchecked")
Map<String, Map<String, Number>> allMethodStats = (Map<String, Map<String, Number>>) value;
if (allMethodStats.isEmpty()) {
continue;
}
//first we need to know since when the values were collected
result = getASConnection().execute(new ReadAttribute(RUNTIME_MBEAN_ADDRESS, START_TIME_ATTRIBUTE));
long serverStartTime = (Long) result.getResult();
//now process the calltime value
String requestedMetric = request.getName().substring(CALLTIME_METRIC_NAME_PREFIX_LENGTH);
Stats lastCollection = getLastCallTimeCollection(requestedMetric, allMethodStats, serverStartTime);
Stats thisCollection = Stats.fromMap(allMethodStats, requestedMetric, System.currentTimeMillis(),
serverStartTime);
CallTimeData callTime = new CallTimeData(request);
fillCallTimeData(callTime, thisCollection, lastCollection);
saveCallTimeCollection(requestedMetric, thisCollection);
report.addData(callTime);
} else {
OSGiVersion currentAsVersion = getASVersion();
if (currentAsVersion == null) {
getLog().warn(
"Could not determine the AS version while reporting unexpected result of method"
+ " stats. Request: " + request);
} else if (FIRST_VERSION_SUPPORTING_METHOD_STATS.compareTo(currentAsVersion) <= 0) {
getLog().error(
"Unexpected type of results when querying method stats for measurement request " + request
+ ". Expected map but got " + (value == null ? "null" : value.getClass().getName()));
}
}
}
}
super.getValues(report, metricsToPassDown);
}
private Stats getLastCallTimeCollection(String requestName, Map<String, Map<String, Number>> fallbackValues,
long fallbackStartTime) throws IOException {
File dataFile = new File(context.getResourceDataDirectory(), requestName);
if (!dataFile.exists()) {
return Stats.fromMap(fallbackValues, requestName, System.currentTimeMillis(), fallbackStartTime);
} else {
ObjectInputStream in = null;
try {
in = new ObjectInputStream(new FileInputStream(dataFile));
Stats stats = (Stats) in.readObject();
if (stats.serverStartTime == 0) {
//we might get serverStartTime == 0 if the datafile comes from the old version of the plugin
//in that case just fallback to the old behavior that assumed no server restarts.
//After that we save the new version of the stats with the start time remembered and we will
//switch to the new correct behavior from the next collection.
stats.serverStartTime = fallbackStartTime;
}
return stats;
} catch (IOException e) {
throw new IOException("Couldn't read the stored calltime data from file " + dataFile + ".", e);
} catch (ClassNotFoundException e) {
throw new IllegalStateException("Couldn't find plugin API classes. This is serious!", e);
} finally {
StreamUtil.safeClose(in);
}
}
}
private void saveCallTimeCollection(String requestName, Stats stats) throws IOException {
File dataFile = new File(context.getResourceDataDirectory(), requestName);
ObjectOutputStream out = null;
try {
out = new ObjectOutputStream(new FileOutputStream(dataFile));
out.writeObject(stats);
} catch (IOException e) {
throw new IOException("Couldn't write the last collected calltime data to file " + dataFile + ".", e);
} finally {
StreamUtil.safeClose(out);
}
}
/**
* Given the current and previous stats collected from the AS, this fills in the provided calltime data record with
* the differential values from the last time to this time.
*
* @param callTimeData the calltime record to fill in
* @param stats the current stats collected from the AS
* @param previousStats the previously collected stats
*/
private void fillCallTimeData(CallTimeData callTimeData, Stats stats, Stats previousStats) {
Date startDate = new Date(previousStats.collectionTime);
Date endDate = new Date(stats.collectionTime);
boolean serverRestarted = stats.serverStartTime != previousStats.serverStartTime;
for(Map.Entry<String, StatsRecord> entry : stats.data.entrySet()) {
String methodName = entry.getKey();
StatsRecord thisStatsRecord = entry.getValue();
long invocations;
long total;
if (serverRestarted) {
// If server restarted, we know the counter goes from 0. Note that in that case we still might have
// missed some invocations - the ones that happened between the last collection and the server restart.
// That cannot be avoided though unless the server itself pushes the data to us instead of us pulling
// the data on a schedule.
invocations = thisStatsRecord.invocations;
total = thisStatsRecord.total;
} else {
StatsRecord previousStatsRecord = previousStats.data.get(methodName);
long oldInvocations = previousStatsRecord != null ? previousStatsRecord.invocations : 0;
invocations = thisStatsRecord.invocations - oldInvocations;
long oldTotal = previousStatsRecord != null ? previousStatsRecord.total : 0;
total = thisStatsRecord.total - oldTotal;
}
if (invocations == 0) {
continue;
}
//AS doesn't really give us this info...
double min = (double) total / invocations;
double max = (double) total / invocations;
callTimeData.addAggregatedCallData(methodName, startDate, endDate, min, max, total,
invocations);
}
}
private OSGiVersion getASVersion() {
if (asVersion == null) {
ResourceComponent<?> base = context.getParentResourceComponent();
while (base != null && base instanceof BaseComponent && !(base instanceof BaseServerComponent)) {
base = ((BaseComponent<?>)base).context.getParentResourceComponent();
}
if (base != null && base instanceof BaseServerComponent) {
String version = ((BaseServerComponent<?>)base).getReleaseVersion();
asVersion = new OSGiVersion(version);
}
}
return asVersion;
}
private void ensureGlobalEJB3StatisticsEnabled() {
if (ejb3StatisticsEnalbed != null && ejb3StatisticsEnalbed.booleanValue()) {
return;
}
BaseServerComponent server = getServerComponent();
Address ejbAddress = new Address(server.getServerAddress());
ejbAddress.add("subsystem", "ejb3");
try {
ejb3StatisticsEnalbed = readAttribute(ejbAddress, "enable-statistics", Boolean.class);
if (!Boolean.TRUE.equals(ejb3StatisticsEnalbed)) {
getLog().debug("Enabling global EJB3 statistics");
WriteAttribute op = new WriteAttribute(ejbAddress, "enable-statistics", true);
Result result = getASConnection().execute(op);
if (result.isSuccess()) {
getLog()
.info(
server.context.getResourceDetails()
+ " Global EJB3 statistics is now enabled, because there is a request to collect EJB Calltime metrics.");
} else {
getLog().error("Failed to enable EJB3 statistics : " + result.getFailureDescription());
}
}
} catch (Exception e) {
getLog().error("Failed to read and enable EJB3 statistics", e);
}
}
}