/*
* 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.communications.command.server;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArrayList;
import javax.management.InstanceNotFoundException;
import javax.management.MBeanServer;
import javax.management.MBeanServerInvocationHandler;
import javax.management.ObjectName;
import mazz.i18n.Logger;
import org.jboss.remoting.InvocationRequest;
import org.jboss.remoting.ServerInvocationHandler;
import org.jboss.remoting.ServerInvoker;
import org.jboss.remoting.callback.InvokerCallbackHandler;
import org.jboss.remoting.stream.StreamInvocationHandler;
import org.rhq.enterprise.communications.command.Command;
import org.rhq.enterprise.communications.command.CommandResponse;
import org.rhq.enterprise.communications.command.CommandType;
import org.rhq.enterprise.communications.command.impl.generic.GenericCommandResponse;
import org.rhq.enterprise.communications.command.impl.identify.IdentifyCommand;
import org.rhq.enterprise.communications.command.impl.remotepojo.RemotePojoInvocationCommand;
import org.rhq.enterprise.communications.command.server.CommandProcessorMetrics.UnsuccessfulReason;
import org.rhq.enterprise.communications.i18n.CommI18NFactory;
import org.rhq.enterprise.communications.i18n.CommI18NResourceKeys;
import org.rhq.enterprise.communications.util.NotPermittedException;
import org.rhq.enterprise.communications.util.NotProcessedException;
/**
* Handles invoked {@link Command commands} from remote clients.
*
* <p>This server invocation handler will delegate the actual execution of the commands to
* {@link CommandServiceMBean command services} located in the same
* {@link org.jboss.remoting.ServerInvocationHandler#setMBeanServer(MBeanServer) MBeanServer} as this handler.</p>
*
* <p>When this handler is given an {@link org.jboss.remoting.InvocationRequest invocation request}, it determines the
* <code>ObjectName</code> of the command service to delegate to by:</p>
*
* <ol>
* <li>Using the handler's {@link org.jboss.remoting.InvocationRequest#getSubsystem() subsystem} as the name of the
* command service's subsystem</li>
* <li>Passing the subsystem and {@link Command#getCommandType() command type} to the
* {@link CommandServiceDirectoryMBean directory} which looks up the command service that provides the command and
* returns its name</li>
* </ol>
*
* <p>This handler will delegate the command to that service's execute method and will return its return value as-is
* back to this invocation handler's client.</p>
*
* @author John Mazzitelli
*/
public class CommandProcessor implements StreamInvocationHandler {
/**
* Logger
*/
private static final Logger LOG = CommI18NFactory.getLogger(CommandProcessor.class);
/**
* the MBeanServer where this handler is located
*/
private MBeanServer m_mBeanServer;
/**
* a proxy to the directory service
*/
private CommandServiceDirectoryMBean m_directoryService;
/**
* The object to authenticate all incoming commands. Maybe <code>null</code>, in which case all commands will be
* allowed to be processed.
*/
private CommandAuthenticator m_authenticator;
/**
* These are notified whenever a new command is received.
*/
private final List<CommandListener> m_commandListeners;
/**
* Where all the statistics are stored.
*/
private final CommandProcessorMetrics m_metrics;
/**
* Constructor for {@link CommandProcessor}.
*/
public CommandProcessor() {
m_mBeanServer = null;
m_directoryService = null;
m_authenticator = null;
m_commandListeners = new CopyOnWriteArrayList<CommandListener>();
m_metrics = new CommandProcessorMetrics();
}
/**
* @see org.jboss.remoting.ServerInvocationHandler#setMBeanServer(javax.management.MBeanServer)
*/
public void setMBeanServer(MBeanServer mbs) {
m_mBeanServer = mbs;
}
/**
* @see org.jboss.remoting.ServerInvocationHandler#setInvoker(org.jboss.remoting.ServerInvoker)
*/
public void setInvoker(ServerInvoker invoker) {
return; // we don't care what invoker is being used
}
/**
* Sets the object that will perform the security checks necessary to authenticate incoming commands. If <code>
* null</code>, no security checks will be performed and all commands will be considered authenticated.
*
* @param authenticator the object to perform authentication checks on all incoming commands
*/
public void setCommandAuthenticator(CommandAuthenticator authenticator) {
m_authenticator = authenticator;
LOG.debug(CommI18NResourceKeys.COMMAND_PROCESSOR_AUTHENTICATOR_SET, authenticator);
}
/**
* Adds the given listener to this object's list of command listeners. This listener will be called each and every
* time a new command has been received by this object.
*
* @param listener
*/
public void addCommandListener(CommandListener listener) {
m_commandListeners.add(listener);
}
/**
* Removes the given listener from this object's list of command listeners. This listener will no longer be called
* when commands are received by this object.
*
* @param listener
*/
public void removeCommandListener(CommandListener listener) {
m_commandListeners.remove(listener);
}
/**
* Returns the metrics object which contains all statistics for the command processor.
*
* @return the object containing all the metric data - this is the live object and will be updated
* as more data is collected
*/
public CommandProcessorMetrics getCommandProcessorMetrics() {
return m_metrics;
}
/**
* Invokes the {@link Command} that is found in the {@link InvocationRequest#getParameter() invocation parameter}.
* Note that the {@link InvocationRequest#getSubsystem() subsystem} being invoked must be the subsystem where the
* command service to be invoked is registered.
*
* @see ServerInvocationHandler#invoke(org.jboss.remoting.InvocationRequest)
*/
public Object invoke(InvocationRequest invocation) throws Throwable {
return handleIncomingInvocationRequest(null, invocation);
}
/**
* Handles incoming stream data from a client request that used the JBoss/Remoting streaming API.
*
* @see StreamInvocationHandler#handleStream(InputStream, InvocationRequest)
*/
public Object handleStream(InputStream in, InvocationRequest invocation) throws Throwable {
return handleIncomingInvocationRequest(in, invocation);
}
/**
* @see ServerInvocationHandler#addListener(InvokerCallbackHandler)
*/
public void addListener(InvokerCallbackHandler callbackHandler) {
// TODO not yet implemented - unsure if we want to support callbacks
}
/**
* @see ServerInvocationHandler#removeListener(InvokerCallbackHandler)
*/
public void removeListener(InvokerCallbackHandler callbackHandler) {
// TODO not yet implemented - unsure if we want to support callbacks
}
/**
* This handles incoming invocation requests - this is the common code that both {@link #invoke(InvocationRequest)}
* and {@link #handleStream(InputStream, InvocationRequest)} will execute.
*
* <p>The design of this method is that it will always return a {@link CommandResponse}, no matter what the results.
* This way, if the client gets an exception or doesn't get a {@link CommandResponse} back, it can be assured that
* the exception was caused by a connection error or an error within the low-level comm layer.</p>
*
* @param in the input stream if this is a streaming request utilizing the JBoss/Remoting stream API (may
* be <code>null</code> which means it isn't a streaming request)
* @param invocation the incoming request
*
* @return the response
*
* @see #invoke(InvocationRequest)
* @see #handleStream(InputStream, InvocationRequest)
*/
private Object handleIncomingInvocationRequest(InputStream in, InvocationRequest invocation) {
Command cmd = null;
CommandResponse ret_response = null;
long elapsed = 0L; // will be the time in ms that it took to invoked the command service if we did invoke it
try {
// get the subsystem - find the command service in this subsystem that will execute our command
String subsystem = invocation.getSubsystem();
// get the Command the client wants to execute
cmd = (Command) invocation.getParameter();
IncomingCommandTrace.start(cmd);
if (cmd != null) {
notifyListenersOfReceivedCommand(cmd);
// make sure the command is authenticated; if it is not, return immediately without further processing the command
if (m_authenticator != null) {
if (!m_authenticator.isAuthenticated(cmd)) {
// We don't want to flood the logs with authentication errors if the command is
// the identify command since that is expected to fail when attempting to auto-detect
// servers that we aren't yet authorized to talk to yet. So we only log a warn
// and we only increment our getNumberFailedCommands counter if its not an identify command.
if ((cmd.getCommandType() == null)
|| !cmd.getCommandType().getName().equals(IdentifyCommand.COMMAND_TYPE.getName())) {
LOG.warn(CommI18NResourceKeys.COMMAND_PROCESSOR_FAILED_AUTHENTICATION, cmd);
m_metrics.numberFailedCommands++;
}
String err = LOG
.getMsgString(CommI18NResourceKeys.COMMAND_PROCESSOR_FAILED_AUTHENTICATION, cmd);
ret_response = new GenericCommandResponse(null, false, null, new AuthenticationException(err));
notifyListenersOfProcessedCommand(cmd, ret_response);
return ret_response;
}
}
// get the command's type
CommandType cmdType = cmd.getCommandType();
// ask the directory what command service supports the command we want to execute
CommandServiceDirectoryEntry entry = null;
ObjectName cmdServiceName = null;
entry = getCommandServiceDirectory().getCommandTypeProvider(subsystem, cmdType);
if (entry != null) {
cmdServiceName = entry.getCommandServiceName();
}
if (cmdServiceName != null) {
// now delegate the execution of the command to the command service
CommandServiceMBean executor;
executor = (CommandServiceMBean) MBeanServerInvocationHandler.newProxyInstance(m_mBeanServer,
cmdServiceName, CommandServiceMBean.class, false);
LOG.debug(CommI18NResourceKeys.COMMAND_PROCESSOR_EXECUTING, cmd);
long start = System.currentTimeMillis();
ret_response = executor.execute(cmd, in, null);
elapsed = System.currentTimeMillis() - start;
LOG.debug(CommI18NResourceKeys.COMMAND_PROCESSOR_EXECUTED, ret_response);
} else {
throw new InstanceNotFoundException(LOG.getMsgString(
CommI18NResourceKeys.COMMAND_PROCESSOR_UNSUPPORTED_COMMAND_TYPE, subsystem, cmdType));
}
} else {
LOG.warn(CommI18NResourceKeys.COMMAND_PROCESSOR_MISSING_COMMAND);
ret_response = new GenericCommandResponse(null, false, null, new Exception(LOG
.getMsgString(CommI18NResourceKeys.COMMAND_PROCESSOR_MISSING_COMMAND)));
}
} catch (Throwable t) {
ret_response = new GenericCommandResponse(cmd, false, null, t);
} finally {
IncomingCommandTrace.finish(cmd, ret_response);
// as per JBoss/Remoting docs, you must ensure you close the input stream
if (in != null) {
try {
in.close();
} catch (Throwable t) {
}
}
}
// finish our processing
try {
if (ret_response == null) {
ret_response = new GenericCommandResponse(cmd, false, null, new IllegalStateException(
"results are null"));
}
updateMetrics(cmd, ret_response, elapsed);
notifyListenersOfProcessedCommand(cmd, ret_response);
} catch (Throwable t) {
// some incredibly rare throwable just happened, just log it but return anyway
LOG.warn(t, CommI18NResourceKeys.COMMAND_PROCESSOR_POST_PROCESSING_FAILURE, cmd);
}
return ret_response;
}
/**
* Stores the metric data.
*
* @param command the command that was executed (might be null in error conditions)
* @param response the response that resulted in the command execution
* @param elapsed the amount of milliseconds that it took to execute the command and get the response
*/
private void updateMetrics(Command cmd, CommandResponse response, long elapsed) {
boolean success = response.isSuccessful();
CommandProcessorMetrics.UnsuccessfulReason unsuccessfulReason = null;
// now that we processed the command, update the appropriate metrics
m_metrics.writeLock();
try {
if (success) {
long num = ++m_metrics.numberSuccessfulCommands;
// calculate the running average - num is the current command count
// this may not be accurate if we execute this code concurrently,
// but its good enough for our simple monitoring needs
long currentAvg = m_metrics.averageExecutionTime;
currentAvg = (((num - 1) * currentAvg) + elapsed) / num;
m_metrics.averageExecutionTime = currentAvg;
} else {
if (response.getException() instanceof NotPermittedException) {
m_metrics.numberDroppedCommands++;
unsuccessfulReason = UnsuccessfulReason.DROPPED;
} else if (response.getException() instanceof NotProcessedException) {
m_metrics.numberNotProcessedCommands++;
unsuccessfulReason = UnsuccessfulReason.NOT_PROCESSED;
} else {
m_metrics.numberFailedCommands++;
unsuccessfulReason = UnsuccessfulReason.FAILED;
}
}
// cmd might be null under odd, error edge cases, have to just be protective here.
if (cmd != null) {
CommandType cmdType = cmd.getCommandType();
m_metrics.addCallTimeData(cmdType.getName(), elapsed, unsuccessfulReason);
if (cmd instanceof RemotePojoInvocationCommand) {
// add additional metrics for the individual pojo method that was invoked
RemotePojoInvocationCommand pojoCmd = (RemotePojoInvocationCommand) cmd;
String ifaceName = pojoCmd.getTargetInterfaceName();
ifaceName = ifaceName.substring(ifaceName.lastIndexOf('.') + 1);
String methodName = pojoCmd.getNameBasedInvocation().getMethodName();
m_metrics.addCallTimeData(ifaceName + '.' + methodName, elapsed, unsuccessfulReason);
}
}
} finally {
m_metrics.writeUnlock();
}
return;
}
private void notifyListenersOfReceivedCommand(Command command) {
for (CommandListener listener : m_commandListeners) {
// notice that we bubble up NotPermittedException or NotProcessedException and abort the command, but any other exception we just log and move on
try {
listener.receivedCommand(command);
} catch (NotPermittedException npe) {
throw npe;
} catch (NotProcessedException npe) {
throw npe;
} catch (Throwable t) {
LOG.warn(t, CommI18NResourceKeys.COMMAND_PROCESSOR_LISTENER_ERROR_RECEIVED, t);
}
}
}
/**
* This will inform all command listeners that a command has finished and has the given response.
*
* <p>Note that this method ensures that it will not throw any exceptions.</p>
*
* @param command the command that was processed
* @param response the result of the command processing
*/
private void notifyListenersOfProcessedCommand(Command command, CommandResponse response) {
for (CommandListener listener : m_commandListeners) {
try {
// notice that on any exception we just log and move on
listener.processedCommand(command, response);
} catch (Throwable t) {
LOG.warn(t, CommI18NResourceKeys.COMMAND_PROCESSOR_LISTENER_ERROR_PROCESSED, t);
}
}
}
/**
* Returns a proxy to the {@link CommandServiceDirectoryMBean command service directory}. This will provide a
* directory that returns the names of command services that support command types.
*
* @return proxy to the command service directory
*
* @throws Exception if failed to get the proxy
* @throws InstanceNotFoundException the directory service is not registered
*/
private CommandServiceDirectoryMBean getCommandServiceDirectory() throws Exception {
if (m_directoryService == null) {
// find the directory - there should be only one and we don't care what JMX domain it is in
// in the future, we may want to have this directory in the same JMX domain as this invocation handler's subsystem
ObjectName query = new ObjectName("*:" + KeyProperty.TYPE + "=" + KeyProperty.TYPE_DIRECTORY);
Set names = m_mBeanServer.queryNames(query, null);
if (names != null) {
// we don't care if there happens to be more than one (there really should not be but...), use the first one
ObjectName directoryName = (ObjectName) names.iterator().next();
m_directoryService = (CommandServiceDirectoryMBean) MBeanServerInvocationHandler.newProxyInstance(
m_mBeanServer, directoryName, CommandServiceDirectoryMBean.class, false);
} else {
throw new InstanceNotFoundException(LOG.getMsgString(
CommI18NResourceKeys.COMMAND_PROCESSOR_NO_DIRECTORY, query));
}
}
return m_directoryService;
}
}