/*
* 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.client;
import java.net.ConnectException;
import java.util.Properties;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import mazz.i18n.Logger;
import org.jboss.remoting.CannotConnectException;
import org.rhq.core.util.exception.ThrowableUtil;
import org.rhq.core.util.stream.StreamUtil;
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.command.impl.remotepojo.RemotePojoInvocationCommand;
import org.rhq.enterprise.communications.i18n.CommI18NFactory;
import org.rhq.enterprise.communications.i18n.CommI18NResourceKeys;
/**
* This is runnable task that will be queued in the executor pool within the {@link ClientCommandSender}. It is a
* Callable to support the timed execution of the task (that is, it can be used to abort if command failed to be
* processed in a given amount of time. It is a Runnable to allow us to submit this as a task to a thread pool.
*
* <p>This is package-scoped because it must live with {@link ClientCommandSender}. This class must be careful calling
* into its {@link ClientCommandSender}, specifically those sender methods that require a monitor lock to protect
* against doing things while changing modes between sending/not sending. Deadlocks may occur if you are not careful
* calling back into the sender.</p>
*
* @author John Mazzitelli
*/
class ClientCommandSenderTask implements Callable<CommandResponse>, Runnable {
/**
* Logger
*/
private static final Logger LOG = CommI18NFactory.getLogger(ClientCommandSenderTask.class);
private ClientCommandSender m_sender;
private final CommandAndCallback m_cnc;
private final long m_timeout;
private final boolean m_isAsync;
private final CommandResponse[] m_response;
/**
* Constructor for {@link ClientCommandSenderTask}. The <code>reponse</code> is an array so it can be used as an
* "out" parameter; the response will be stored in the first element of that array. <code>response</code> may be
* <code>null</code>, in which case it will be ignored and the response will not be returned. The <code>async</code>
* flag must be <code>true</code> if the task is running asynchronously from the caller that submitted the command.
* This flag must be <code>false</code> if this task's {@link #run()} method is called outside of any thread pool
* and is synchronous with the caller that submitted the command.
*
* <p>Under current use-case scenarios, <code>response</code> will usually be non-<code>null</code> iff <code>
* async</code> is <code>false</code>. Rather than assume that will always be the case, that is not enforced - in
* other words, an asynchronously executed task can be told to store its response.</p>
*
* @param sender the object to use to actually send the command
* @param cnc the command to send and an optional callback object to notify when we are done
* @param timeout the amount of time to wait for the action to complete before aborting - if less than 1, will not
* timeout
* @param async <code>true</code> if this task is executed in a thread from a thread pool; <code>false</code> if
* synchronously executed by the caller that submitted the command
* @param response the response that was received after the command was processed (may be <code>null</code>)
*/
public ClientCommandSenderTask(ClientCommandSender sender, CommandAndCallback cnc, long timeout, boolean async,
CommandResponse[] response) {
m_sender = sender;
m_cnc = cnc;
m_timeout = timeout;
m_isAsync = async;
m_response = response;
return;
}
/**
* Performs the sending of the command to the server.
*
* @see Callable#call()
*/
public CommandResponse call() throws Exception {
CommandResponse response;
try {
response = send(m_sender, m_cnc);
} catch (Exception e) {
throw e;
} catch (Throwable t) {
// jboss/remoting can throw throwables, but Callable only allows for Exceptions to be thrown
throw new Exception(t);
}
return response;
}
/**
* Performs the sending of the command and waits for it to complete or timeout. Upon completion, the
* {@link #getCommandAndCallback() callback} will be notified of the response unless the command is to be retried
* due to the command having its guaranteed delivery flag enabled.
*
* @see java.lang.Runnable#run()
*/
public void run() {
CommandResponse response;
Command command = m_cnc.getCommand();
boolean notify_callback = (m_cnc.getCallback() != null); // only notify the callback if we actually have one
try {
m_sender.waitForSendThrottle(command);
if (m_timeout > 0) {
// this may need to spawn another thread and effect overall performance
// if the timer thread pool is null, the sender is shutdown, so immediately abort
ThreadPoolExecutor timerThreadPool = m_sender.getTimerThreadPool();
if (timerThreadPool == null) {
throw new InterruptedException();
}
Future<CommandResponse> futureTask = timerThreadPool.submit((Callable<CommandResponse>) this);
try {
response = futureTask.get(m_timeout, TimeUnit.MILLISECONDS);
} catch (ExecutionException ee) {
throw ee.getCause();
} catch (TimeoutException te) {
// our timeout has expired, cancel the command and abort
futureTask.cancel(true);
throw te;
} catch (InterruptedException ie) {
// waiting for the future was interrupted, the sender executor thread pool is probably shutting down
futureTask.cancel(true);
throw ie;
}
} else {
// we won't timeout - let the thread take as long as it needs - no need to spawn another thread
response = call();
}
} catch (Throwable t) {
// See if the failing command was a ping and th exception was a CanNotConnectException
boolean isPing = false;
if (command instanceof RemotePojoInvocationCommand) {
RemotePojoInvocationCommand rp = (RemotePojoInvocationCommand) command;
if (rp.getTargetInterfaceName().endsWith("Ping")) {
if (t instanceof CannotConnectException) {
isPing = true;
}
}
}
if (isPing) {
String agent = m_sender.getRemoteCommunicator().toString();
LOG.info(CommI18NResourceKeys.AGENT_PING_FAILED, agent);
} else {
LOG.error(t, CommI18NResourceKeys.SEND_FAILED, command, ThrowableUtil.getAllMessages(t));
}
response = new GenericCommandResponse(command, false, null, t);
boolean retry = shouldCommandBeRetried(command, t);
if (retry) {
notify_callback = false; // since we are going to retry this command, do not notify the callback
LOG.warn(CommI18NResourceKeys.QUEUING_FAILED_COMMAND);
try {
m_sender.retryGuaranteedTask(m_cnc);
} catch (Exception e) {
LOG.error(CommI18NResourceKeys.CLIENT_COMMAND_SENDER_TASK_REQUEUE_FAILED, command);
}
}
}
// if the command attempt finished (regardless of success or failure) we need to now notify our callback of the results
if (notify_callback) {
try {
m_cnc.getCallback().commandSent(response);
} catch (Throwable t) {
LOG.warn(t, CommI18NResourceKeys.CALLBACK_FAILED, response);
}
}
if (m_response != null) {
m_response[0] = response;
}
return;
}
/**
* Returns the command/callback pair that this task will use.
*
* @return command/callback pair
*/
public CommandAndCallback getCommandAndCallback() {
return m_cnc;
}
/**
* This actually sends the command to the sender. Subclasses are free to override this if they wish to send
* additional data.
*
* @param sender the object that will send the command
* @param cnc the object that contains the command to send
*
* @return the response from the server
*
* @throws Throwable if failed to send the command
*/
protected CommandResponse send(ClientCommandSender sender, CommandAndCallback cnc) throws Throwable {
return sender.send(cnc.getCommand());
}
/**
* This allows the task to get pointed to a different endpoint by switching its sender.
*
* <p>This is package scoped - only the command sender object is allowed to override a task's sender.</p>
*
* @param sender the new sender object to use when executing this task
*/
void setClientCommandSender(ClientCommandSender sender) {
m_sender = sender;
}
/**
* This method should be called when an excepton occurred during the sending of a command. This will determine if
* the command should be retried or not.
*
* <p>If the given command is not serializable, this returns <code>false</code> always.</p>
*
* <p>If the given command is not async with guaranteed delivery enabled, this returns <code>false</code>
* always.</p>
*
* <p>If the given command is serializable and the exception thrown indicates a failed connection to the server,
* then this will return <code>true</code> always.</p>
*
* <p>If the given command is serializable and the exception does not explicitly indicate a failed connection to the
* server, then this will return <code>true</code> unless this command was retried too many times.</p>
*
* @param command the command that failed
* @param throwable the exception that occurred; this is the cause of the failure
*
* @return <code>true</code> if the command should be retried; <code>false</code> if the command should abort
*/
private boolean shouldCommandBeRetried(Command command, Throwable throwable) {
// we only need to resend this command if it asked for its delivery to be guaranteed (we only guarantee async commands)
if (m_isAsync && m_sender.isDeliveryGuaranteed(command)) {
try {
final String RETRY_CONFIG_PROP = "rhq.retry";
Properties config = command.getConfiguration();
int retryCount = Integer.parseInt(config.getProperty(RETRY_CONFIG_PROP, "0"));
// throw exception if not serializable; no need to test if we already retried before
if (retryCount == 0) {
StreamUtil.serialize(command);
}
// increment the retry count in the command config
retryCount++;
config.put(RETRY_CONFIG_PROP, String.valueOf(retryCount));
return (isCannotConnectException(throwable) || (retryCount <= m_sender.getConfiguration().maxRetries));
} catch (Exception e) {
return false; // don't retry if not serializable or the retry number is munged in the command
}
}
return false;
}
/**
* Returns <code>true</code> if the given throwable was caused by a connection error - that is, if the remote
* endpoint could not be contacted. This does a best guess - hopefully, it won't catch any false positives or false
* negative.
*
* @param t the exception to check (may be <code>null</code>, in which case <code>false</code> is returned)
*
* @return <code>true</code> if the exception was caused by not being able to connect to the remote endpoint
*/
private boolean isCannotConnectException(Throwable t) {
if (t == null) {
return false;
}
boolean yes = (t instanceof ConnectException) || (t instanceof CannotConnectException);
// if this isn't it, go down the cause chain (e.g. t might be an invocation target exception)
if (!yes) {
yes = isCannotConnectException(t.getCause());
}
return yes;
}
}