/*
* RHQ Management Platform
* Copyright (C) 2005-2014 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.enterprise.communications.command.client;
import java.net.MalformedURLException;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock.WriteLock;
import mazz.i18n.Logger;
import org.jboss.remoting.Client;
import org.jboss.remoting.InvokerLocator;
import org.jboss.remoting.ServerInvoker;
import org.rhq.core.util.exception.ThrowableUtil;
import org.rhq.enterprise.communications.command.Command;
import org.rhq.enterprise.communications.command.CommandResponse;
import org.rhq.enterprise.communications.command.impl.generic.GenericCommandResponse;
import org.rhq.enterprise.communications.i18n.CommI18NFactory;
import org.rhq.enterprise.communications.i18n.CommI18NResourceKeys;
import org.rhq.enterprise.communications.util.NotPermittedException;
/**
* Provides basic functionality to all command clients that want to use JBoss/Remoting as the remoting framework.
*
* <p>This superclass provides the hooks by which users of the client can select the
* {@link #setInvokerLocator(String) location of the remote server} and the {@link #setSubsystem(String) subsystem}
* where the command is to be invoked.</p>
*
* <p>Under the covers, a {@link org.jboss.remoting.Client remoting client} is created and maintained by this object.
* There is no need to call {@link #connect()} since it will be done automatically when appropriate; however,
* it is good practice to tell this object to {@link #disconnect()} its client when no longer necessary to
* issue commands to the remote server.</p>
*
* <p>All subclasses should include a no-arg constructor so they can be built dynamically by the command line client.</p>
*
* @author John Mazzitelli
*/
public class JBossRemotingRemoteCommunicator implements RemoteCommunicator {
private static final Logger LOG = CommI18NFactory.getLogger(JBossRemotingRemoteCommunicator.class);
/**
* The default subsystem to use when sending messages via the JBoss/Remoting client.
*/
public static final String DEFAULT_SUBSYSTEM = "RHQ";
/**
* the JBoss/Remoting locator that this client will use to remotely connect to the command server
*/
private volatile InvokerLocator m_invokerLocator;
/**
* The subsystem to target when invoking commands. The subsystem is defined by the JBoss/Remoting API - it specifies
* the actual invoker handler to target. The Command framework uses the subsystem to organize command processors
* into different domains.
*/
private final String m_subsystem;
/**
* The configuration to send to the client - used to configure things like the SSL setup.
*/
private final Map<String, String> m_clientConfiguration;
/**
* the actual JBoss/Remoting client object that will be used to transport the commands to the server
*/
private volatile AtomicReference<Client> m_client = new AtomicReference<Client>();
/**
* Optionally-defined callback that will be called when a failure is detected when sending a message.
*/
private volatile FailureCallback m_failureCallback;
/**
* Optionally-defined callback that will be called when this communicator sends its first command.
*/
private volatile InitializeCallback m_initializeCallback;
/**
* When <code>true</code>, the initialize callback will need to be called prior
* to sending any commands. Used in conjunection with its associated RW lock.
*/
private volatile boolean m_needToCallInitializeCallback;
/**
* RW lock when needing to access its associated atomic boolean flag.
*/
private final ReentrantReadWriteLock m_needToCallInitializeCallbackLock;
/**
* Number of minutes to wait while attempting to acquire a lock before attempting
* to invoke the initialize callback. If this amount of minutes expires before the lock
* is acquired, an error will occur and the initialize callback will have to be attempted later.
*/
private final long m_initializeCallbackLockAcquisitionTimeoutMins;
/**
* Constructor for {@link JBossRemotingRemoteCommunicator} that allows you to indicate the
* {@link InvokerLocator invoker locator} to use by specifying the locator's URI. The subsystem will be set to the
* {@link #DEFAULT_SUBSYSTEM}.
*
* @param locatorUri the locator's URI (must not be <code>null</code>)
*
* @throws MalformedURLException if failed to create the locator (see {@link InvokerLocator#InvokerLocator(String)})
*/
public JBossRemotingRemoteCommunicator(String locatorUri) throws MalformedURLException {
this(new InvokerLocator(locatorUri), DEFAULT_SUBSYSTEM, null);
}
/**
* Constructor for {@link JBossRemotingRemoteCommunicator} that allows you to indicate the
* {@link InvokerLocator invoker locator} to use by specifying the locator's URI. The subsystem will be set to the
* {@link #DEFAULT_SUBSYSTEM}. The given <code>Map</code> should contain <code>Client</code> configuration
* attributes.
*
* @param locatorUri the locator's URI (must not be <code>null</code>)
* @param client_config the client configuration (may be <code>null</code> or empty)
*
* @throws MalformedURLException if failed to create the locator (see {@link InvokerLocator#InvokerLocator(String)})
*/
public JBossRemotingRemoteCommunicator(String locatorUri, Map<String, String> client_config)
throws MalformedURLException {
this(new InvokerLocator(locatorUri), DEFAULT_SUBSYSTEM, client_config);
}
/**
* Constructor for {@link JBossRemotingRemoteCommunicator} that allows you to specify the
* {@link InvokerLocator invoker locator} to use. <code>locator</code> may be <code>null</code>, in which case, it
* must later be specified through {@link #setInvokerLocator(InvokerLocator)} before any client commands can be
* issued.
*
* @param locator the locator to use (may be <code>null</code>)
* @param subsystem the subsystem (or command domain) in which commands will be invoked (may be <code>
* null</code>)
* @param client_config the client configuration (may be <code>null</code> or empty)
*/
public JBossRemotingRemoteCommunicator(InvokerLocator locator, String subsystem, Map<String, String> client_config) {
m_invokerLocator = locator;
m_subsystem = subsystem;
m_clientConfiguration = new HashMap<String, String>();
if (client_config != null) {
m_clientConfiguration.putAll(client_config);
}
m_needToCallInitializeCallback = false;
m_needToCallInitializeCallbackLock = new ReentrantReadWriteLock();
long mins;
try {
String minsStr = System.getProperty("rhq.communications.initial-callback-lock-wait-mins", "60");
mins = Long.parseLong(minsStr);
} catch (Exception e) {
mins = 60L;
}
m_initializeCallbackLockAcquisitionTimeoutMins = mins;
return;
}
/**
* Returns the invoker locator that is to be used to find the remote JBoss/Remoting server. If <code>null</code> is
* returned, this communicator will not be able to issue commands.
*
* @return invoker locator used by this client to issue commands
*/
public InvokerLocator getInvokerLocator() {
return m_invokerLocator;
}
/**
* Sets the invoker locator URI and creates a new locator that is to be used to find the remote JBoss/Remoting
* server for its subsequent command client invocations. Any existing remoting client is automatically disconnected.
* The client configuration properties will, however, remain the same as before - so the new clients that are
* created will have the same configuration attributes. See {@link #setInvokerLocator(String, Map)} if you want to
* reconfigure the client with different properties that are more appropriate for the new locator.
*
* @param locatorUri the new invoker locator's URI to use for future command client invocations (must not be <code>
* null</code>)
*
* @throws MalformedURLException if failed to create the locator (see {@link InvokerLocator#InvokerLocator(String)})
*/
public void setRemoteEndpoint(String endpoint) throws Exception {
InvokerLocator locator = new InvokerLocator(endpoint);
LOG.info(CommI18NResourceKeys.COMMUNICATOR_CHANGING_ENDPOINT, m_invokerLocator, locator);
m_invokerLocator = locator;
// since a new invoker locator is being specified, disconnect any old client that already exists
disconnect();
}
/**
* Returns the value of the subsystem that will be used to target command invocations. The subsystem is defined by
* the JBoss/Remoting API and can be used by the Command Framework to organize command processors into different
* domains.
*
* @return subsystem (may be <code>null</code>)
*/
public String getSubsystem() {
return m_subsystem;
}
public FailureCallback getFailureCallback() {
return m_failureCallback;
}
public void setFailureCallback(FailureCallback callback) {
m_failureCallback = callback;
}
public InitializeCallback getInitializeCallback() {
return m_initializeCallback;
}
public void setInitializeCallback(InitializeCallback callback) {
m_initializeCallback = callback;
m_needToCallInitializeCallback = (callback != null); // specifically do not synchronize by using lock, just set it
}
public String getRemoteEndpoint() {
return (m_invokerLocator != null) ? m_invokerLocator.getLocatorURI() : "<null>";
}
/**
* Returns the map of name/value pairs of client configuration settings used when creating the client. The returned
* map is a copy - changing its contents has no effect on the clients that already have been or will be created by
* this object. Use {@link #setInvokerLocator(InvokerLocator, Map)} or {@link #setInvokerLocator(String, Map)} to
* change the map contents.
*
* @return copy of the configuration that will be used when creating clients (may be <code>null</code> or empty)
*/
public Map<String, String> getClientConfiguration() {
return new HashMap<String, String>(m_clientConfiguration);
}
/**
* Does nothing; send a request to connect.
*/
public void connect() throws Exception {
/*
* For the HTTP invoker, simply calling connect() doesn't do anything. It makes
* sense to at least send something to test connectivity. However, the code doesn't
* make use of this method.
try {
send(new EchoCommand());
} catch (Exception e) {
throw e;
} catch (Throwable t) {
throw new Error(t);
}
*/
}
public void disconnect() {
cacheClient(null);
}
public boolean isConnected() {
Client client = m_client.get();
return (client != null) && client.isConnected();
}
public CommandResponse sendWithoutCallbacks(Command command) throws Throwable {
// handle NotPermittedException in here
CommandResponse ret_response = null;
boolean retry;
do {
retry = false;
ret_response = rawSend(command);
Throwable exception = ret_response.getException();
if (exception instanceof NotPermittedException) {
long pause = ((NotPermittedException) exception).getSleepBeforeRetry();
LOG.debug(CommI18NResourceKeys.COMMAND_NOT_PERMITTED, command, pause);
retry = true;
Thread.sleep(pause);
}
} while (retry);
return ret_response;
}
public CommandResponse sendWithoutInitializeCallback(Command command) throws Throwable {
CommandResponse ret_response = null;
boolean retry = false;
do {
try {
ret_response = sendWithoutCallbacks(command);
retry = invokeFailureCallbackIfNeeded(command, ret_response, null);
} catch (Throwable t) {
retry = invokeFailureCallbackIfNeeded(command, ret_response, t);
if (!retry) {
throw t;
}
}
} while (retry);
return ret_response;
}
public CommandResponse send(Command command) throws Throwable {
// invoke our initialize callback - if our method returns a response, it means
// the initialize callback had an error and we need to abort the sending of this command
CommandResponse initializeErrorResponse = invokeInitializeCallbackIfNeeded(command);
if (initializeErrorResponse != null) {
return initializeErrorResponse;
}
return sendWithoutInitializeCallback(command);
}
/**
* The code that sends the command via the remote client.
*
* @param command the command to send
*
* @return the command response
*
* @throws Throwable if a low-level, unhandled exception occurred
*/
private CommandResponse rawSend(Command command) throws Throwable {
Object ret_response;
try {
try {
OutgoingCommandTrace.start(command);
ret_response = invoke(command);
OutgoingCommandTrace.finish(command, ret_response);
} catch (ServerInvoker.InvalidStateException serverDown) {
// under rare condition, a bug in remoting 2.2 causes this when the server restarted
// try it one more time, this will get a new server thread on the server side (JBREM-745)
// once JBREM-745 is fixed, we can probably get rid of this catch block
ret_response = invoke(command);
OutgoingCommandTrace.finish(command, ret_response);
} catch (java.rmi.MarshalException rmie) {
// Due to JBREM-1245 we may fail due to SSL being shutdown and we need to retry.
if (rmie.getCause() != null && rmie.getCause() instanceof javax.net.ssl.SSLException
&& rmie.getCause().getMessage() != null
&& rmie.getCause().getMessage().startsWith("Connection has been shutdown")) { //$NON-NLS-1$
ret_response = invoke(command);
OutgoingCommandTrace.finish(command, ret_response);
} else {
throw rmie;
}
}
} catch (Throwable t) {
OutgoingCommandTrace.finish(command, t);
throw t;
}
// this is to support http(s) transport - those transports will return Exception objects when errors occur
if (ret_response instanceof Exception) {
throw (Exception) ret_response;
}
try {
return (CommandResponse) ret_response;
} catch (Exception e) {
// JBNADM-2461 - I don't know if this is the right thing to do but...
// I purposefully did not throw an exception here because the command actually did make a successful
// round trip, so I think we would want to have a CommandResponse sent back, not throw an exception.
// However, we got an exception when casting the remote endpoint's reply (the most likely cause here
// is a ClassCastException - the endpoint didn't reply with the expected CommandResponse).
LOG.error(CommI18NResourceKeys.COMM_CCE, ret_response);
return new GenericCommandResponse(command, false, ret_response, e);
}
}
/**
* This will determine if the initialize callback needs to be invoked and if it does, it will
* call it. The initialize callback has the responsibility to handle calling
* {@link #sendWithoutInitializeCallback(Command)} if it wants to send its own commands to the server
* but wants failover to happen when appropriate for those commands.
*
* If there is an initialize callback set, this method will block all callers until
* the callback has been invoked.
*
* @param command the command that it going to be sent after the callback is invoked
*
* @return if the initialize callback had an error, this response will be non-<code>null</code> and
* will indicate that the sending of <code>command</code> should be aborted.
*/
private CommandResponse invokeInitializeCallbackIfNeeded(Command command) {
InitializeCallback callback = getInitializeCallback();
if (callback != null) {
// block here - in effect, this will stop all commands from going out until the callback is done
// to avoid infinite blocking, we'll only wait for a set time (though long).
WriteLock writeLock = m_needToCallInitializeCallbackLock.writeLock();
boolean locked;
try {
locked = writeLock.tryLock(m_initializeCallbackLockAcquisitionTimeoutMins * 60, TimeUnit.SECONDS);
} catch (InterruptedException ie) {
Thread.currentThread().interrupt();
locked = false;
}
if (locked) {
try {
if (m_needToCallInitializeCallback) {
try {
m_needToCallInitializeCallback = (!callback.sendingInitialCommand(this, command));
LOG.debug(CommI18NResourceKeys.INITIALIZE_CALLBACK_DONE, m_needToCallInitializeCallback);
} catch (Throwable t) {
m_needToCallInitializeCallback = true; // callback failed, we'll want to call it again
LOG.error(t, CommI18NResourceKeys.INITIALIZE_CALLBACK_FAILED, ThrowableUtil
.getAllMessages(t));
return new GenericCommandResponse(command, false, null, t);
}
}
} finally {
writeLock.unlock();
}
} else {
Throwable t = new Throwable("Initialize callback lock could not be acquired");
LOG.error(CommI18NResourceKeys.INITIALIZE_CALLBACK_FAILED, t.getMessage());
return new GenericCommandResponse(command, false, null, t);
}
}
return null;
}
/**
* This will invoke the failure callback when necessary. It is necessary to call the callback
* when the throwable is not <code>null</code> or the command response has a non-<code>null</code> exception.
*
* This method will force a retry by returning <code>true</code>. If <code>false</code> is returned,
* the request need not be retried.
*
* @param command the command that was sent (or attempted to be sent)
* @param response the response of the command (may be <code>null</code>)
* @param throwable the exception that was thrown when the command was sent (may be <code>null</code>)
*
* @return <code>true</code> if the command should be retried, <code>false</code> otherwise
*/
private boolean invokeFailureCallbackIfNeeded(Command command, CommandResponse response, Throwable throwable) {
FailureCallback callback = getFailureCallback(); // get a local reference to avoid this being changed underneath us
boolean retry = false;
// only do something if there is a callback defined
if (callback != null) {
// only do something if the command resulted in an exception
if (throwable != null || ((response != null) && (response.getException() != null))) {
// tell our callback that we detected a failure and see if it wants us to retry.
// the callback is free to reconfigure us, in case it wants to failover to another endpoint.
try {
retry = callback.failureDetected(this, command, response, throwable);
} catch (Throwable t) {
// tsk tsk - why did the callback itself fail? just keep going...
}
}
}
return retry;
}
@Override
public String toString() {
return "remoting endpoint [" + ((m_invokerLocator != null) ? m_invokerLocator.getLocatorURI() : "?") + ']';
}
/**
* Invokes JBoss Remoting using the given command.
* Attempts to cache the client if sending the message was successful.
*
* @return object as a result of this call
*/
private Object invoke(Command command) throws Throwable {
InvokerLocator locator = m_invokerLocator;
if (locator == null) {
throw new IllegalStateException("m_invokerLocator is null");
}
Client client = m_client.get();
if (client != null && client.getInvoker() == null) {
client.disconnect();
}
if (client == null || !client.isConnected()) {
client = new Client(locator, getSubsystem(), m_clientConfiguration);
client.connect();
try {
return client.invoke(command);
} finally {
cacheClient(client);
}
}
// Note: Despite all the checks above, the client might have been
// disconnected before invoke is reached. Let's hope that doesn't happen.
return client.invoke(command);
}
/**
* Cache the client, disconnecting the old client.
*
* @param client optionally null; new client to cache
*/
private void cacheClient(Client client) {
Client old = m_client.getAndSet(client);
if (old != null) {
old.disconnect();
m_needToCallInitializeCallback = (getInitializeCallback() != null);
}
}
}