package org.radargun.stages;
import java.io.IOException;
import java.lang.management.ManagementFactory;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import javax.management.Attribute;
import javax.management.MBeanServerConnection;
import javax.management.MalformedObjectNameException;
import javax.management.ObjectInstance;
import javax.management.ObjectName;
import javax.management.remote.JMXConnector;
import org.radargun.DistStageAck;
import org.radargun.StageResult;
import org.radargun.config.Property;
import org.radargun.config.Stage;
import org.radargun.state.SlaveState;
import org.radargun.traits.InjectTrait;
import org.radargun.traits.JmxConnectionProvider;
import org.radargun.utils.PrimitiveValue;
/**
*
* See example configurations in the benchmark-xsite-jmx.xml file.
*
* @author Matej Cimbora <mcimbora@redhat.com>
*/
@Stage(doc = "Allows to invoke JMX-exposed methods and attributes.")
public class JMXInvocationStage extends AbstractDistStage {
@Property(optional = false, doc = "Method will be invoked on all ObjectInstances matching given query.")
private String query;
@Property(optional = false, doc = "Name of the method to be invoked / attribute to be queried for.")
private String targetName;
@Property(doc = "Type of action to be performed. Invocation of specified method (INVOKE_METHOD) is performed "
+ "by default. Optionally, query for a specified attribute (via method-parameters) can be performed "
+ "(GET_ATTRIBUTE_VALUE) or setting a specified attribute (via method-parameters) can be performed"
+ "(SET_ATTRIBUTE_VALUE).")
private OperationType operationType = OperationType.INVOKE_METHOD;
@Property(doc = "Method parameters. If specified, the number of parameters must match the number of parameter "
+ "signatures supplied.", complexConverter = PrimitiveValue.ListConverter.class)
private List<PrimitiveValue> methodParameters = new ArrayList<>();
@Property(doc = "Method parameter signatures.")
private String[] methodSignatures = new String[0];
@Property(doc = "Continue method invocations if an exception occurs. Default is false.")
private boolean continueOnFailure = false;
@Property(doc = "Expected result value. If specified, results of method invocations are compared with this value.",
complexConverter = PrimitiveValue.ObjectConverter.class)
private PrimitiveValue expectedSlaveResult;
@Property(doc = "Expected result, calculated as sum/concatenation (with ',' delimeter) of results from individual slaves.",
complexConverter = PrimitiveValue.ObjectConverter.class)
private PrimitiveValue expectedTotalResult;
@InjectTrait
private JmxConnectionProvider jmxConnectionProvider;
@Override
public DistStageAck executeOnSlave() {
if (!isServiceRunning()) {
return successfulResponse();
}
if (methodParameters.size() != methodSignatures.length) {
return errorResponse("Signatures need to be specified for individual method parameters.");
}
MBeanServerConnection connection = null;
if (jmxConnectionProvider != null) {
JMXConnector connector = jmxConnectionProvider.getConnector();
if (connector != null) {
try {
connection = connector.getMBeanServerConnection();
} catch (IOException e) {
return errorResponse("Failed to connect to MBean server.", e);
}
} else {
return errorResponse("Failed to connect to MBean server.");
}
} else {
connection = ManagementFactory.getPlatformMBeanServer();
}
Collection<ObjectInstance> queryResult = null;
try {
queryResult = getQueryResult(connection);
if (queryResult == null || queryResult.isEmpty()) {
return errorResponse(String.format("Specified query '%s' returned no results.", query));
}
log.trace(String.format("Query returned %d results", queryResult.size()));
} catch (Exception e) {
return errorResponse(String.format("Exception while performing query '%s'", query), e);
}
List<Object> values = new ArrayList<>();
if (methodParameters != null) {
for (PrimitiveValue primitiveValue : methodParameters) {
values.add(primitiveValue.getElementValue());
}
}
List<Object> results = new ArrayList<>(queryResult.size());
for (ObjectInstance objectInstance : queryResult) {
try {
Object result = null;
if (operationType == OperationType.INVOKE_METHOD) {
log.trace("Invoking method " + targetName);
result = connection.invoke(objectInstance.getObjectName(), targetName, values.toArray(new Object[methodParameters.size()]), methodSignatures);
} else if (operationType == OperationType.GET_ATTRIBUTE_VALUE) {
log.trace("Getting value of attribute " + targetName);
result = connection.getAttribute(objectInstance.getObjectName(), targetName);
} else if (operationType == OperationType.SET_ATTRIBUTE_VALUE) {
if (methodParameters != null && !methodParameters.isEmpty()) {
log.trace("Setting value of attribute " + targetName + " to "
+ methodParameters.get(0).getElementValue().toString());
Attribute attribute = new Attribute(targetName, methodParameters.get(0).getElementValue());
connection.setAttribute(objectInstance.getObjectName(), attribute);
result = connection.getAttribute(objectInstance.getObjectName(), targetName);
} else {
return errorResponse("New value for attribute was not specified in methodParameters property.");
}
}
if (expectedSlaveResult != null && !expectedSlaveResult.getElementValue().equals(result)) {
return errorResponse(String.format("Method invocation returned incorrect result. Expected '%s', was '%s'.", expectedSlaveResult.getElementValue(), result));
}
results.add(result);
} catch (Exception e) {
if (continueOnFailure) {
continue;
} else {
return errorResponse(String.format("Exception while invoking method '%s'.", targetName), e);
}
}
}
return new JMXInvocationAck(slaveState, results);
}
private Collection<ObjectInstance> getQueryResult(MBeanServerConnection connection) throws MalformedObjectNameException, IOException {
return connection.queryMBeans(new ObjectName(query), null);
}
@Override
public StageResult processAckOnMaster(List<DistStageAck> acks) {
StageResult stageResult = super.processAckOnMaster(acks);
if (stageResult.isError()) {
return stageResult;
}
if (expectedTotalResult != null) {
if (acks == null || acks.isEmpty()) {
log.error("Expected total result has been specified, but no results have been returned from slaves.");
return StageResult.FAIL;
}
Object totalResult = null;
for (DistStageAck dack : acks) {
if (dack instanceof JMXInvocationAck) {
JMXInvocationAck ack = (JMXInvocationAck) dack;
for (Object ackResult : ack.results) {
try {
if (totalResult == null) {
totalResult = ackResult;
} else {
log.trace(String.format("Adding value %s, current total value %s", ackResult.toString(), totalResult.toString()));
if (!totalResult.getClass().equals(ackResult.getClass())) {
throw new IllegalStateException(String.format("Failed to determine total result due to incompatible value." +
" Expected %s, got %s.", totalResult.getClass().getName(), ackResult.getClass().getName()));
}
if (ackResult instanceof Long) {
totalResult = (Long) totalResult + (Long) ackResult;
} else if (ackResult instanceof Integer) {
totalResult = (Integer) totalResult + (Integer) ackResult;
} else if (ackResult instanceof Byte) {
totalResult = (Byte) totalResult + (Byte) ackResult;
} else if (ackResult instanceof Short) {
totalResult = (Short) totalResult + (Short) ackResult;
} else if (ackResult instanceof Boolean) {
totalResult = (Boolean) totalResult && (Boolean) ackResult;
} else if (ackResult instanceof Character) {
totalResult = totalResult + "," + ackResult;
} else if (ackResult instanceof String) {
totalResult = totalResult + "," + ackResult;
} else {
throw new IllegalStateException(String.format("Values of type %s are not supported.", totalResult.getClass().getName()));
}
}
} catch (Exception e) {
log.error("Failed to determine total result. Incompatible value " + ackResult, e);
}
}
}
}
if (totalResult == null) {
log.error("Total result is empty.");
return StageResult.FAIL;
}
if (totalResult.equals(expectedTotalResult.getElementValue())) {
log.trace("Total value is the one expected " + expectedTotalResult.getElementValue());
return StageResult.SUCCESS;
} else {
log.error(String.format("Total value %s is not the one expected %s.",
totalResult.toString(), expectedTotalResult.getElementValue().toString()));
return StageResult.FAIL;
}
} else {
return StageResult.SUCCESS;
}
}
private static enum OperationType {
INVOKE_METHOD, GET_ATTRIBUTE_VALUE, SET_ATTRIBUTE_VALUE
}
private static class JMXInvocationAck extends DistStageAck {
private final List<Object> results;
public JMXInvocationAck(SlaveState slaveState, List<Object> results) {
super(slaveState);
this.results = results;
}
}
}