/*
* Copyright (C) 2006-2016 DLR, Germany
*
* All rights reserved
*
* http://www.rcenvironment.de/
*/
package de.rcenvironment.core.communication.transport.jms.common;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeoutException;
import javax.jms.Connection;
import javax.jms.JMSException;
import javax.jms.Message;
import javax.jms.MessageConsumer;
import javax.jms.Queue;
import javax.jms.Session;
import javax.jms.TemporaryQueue;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import de.rcenvironment.core.communication.channel.MessageChannelState;
import de.rcenvironment.core.communication.common.InstanceNodeSessionId;
import de.rcenvironment.core.communication.model.NetworkRequest;
import de.rcenvironment.core.communication.model.NetworkResponse;
import de.rcenvironment.core.communication.protocol.NetworkResponseFactory;
import de.rcenvironment.core.communication.transport.jms.common.NonBlockingResponseInboxConsumer.JmsResponseCallback;
import de.rcenvironment.core.communication.transport.spi.AbstractMessageChannel;
import de.rcenvironment.core.communication.transport.spi.MessageChannelResponseHandler;
import de.rcenvironment.core.toolkitbridge.transitional.ConcurrencyUtils;
import de.rcenvironment.core.toolkitbridge.transitional.StatsCounter;
import de.rcenvironment.core.utils.common.LogUtils;
import de.rcenvironment.core.utils.common.StringUtils;
import de.rcenvironment.toolkit.modules.concurrency.api.AsyncTaskService;
import de.rcenvironment.toolkit.modules.concurrency.api.TaskDescription;
/**
* The abstract superclass for both self-initiated and remote-initiated JMS connections.
*
* @author Robert Mischke
*/
public abstract class AbstractJmsMessageChannel extends AbstractMessageChannel implements JmsMessageChannel {
protected final AsyncTaskService threadPool = ConcurrencyUtils.getAsyncTaskService();
protected Connection connection;
protected InstanceNodeSessionId localNodeId;
protected final Log log = LogFactory.getLog(getClass());
private String outgoingRequestQueueName;
private String shutdownSecurityToken;
private String sharedResponseQueueName;
private RequestSender requestSender;
private NonBlockingResponseInboxConsumer responseInboxConsumer;
/**
* A {@link Runnable} that holds a single queue for outgoing {@link NetworkRequest}, and sends them sequentially.
*
* @author Robert Mischke
*/
private final class RequestSender implements Runnable {
private final LinkedBlockingQueue<Runnable> queue = new LinkedBlockingQueue<>();
private Session jmsSession;
private Queue jmsDestinationQueue;
private volatile boolean cancelled = false;
RequestSender(String queueName, Connection connection) {}
@Override
@TaskDescription("JMS Network Transport: Message channel request sender")
public void run() {
try {
try {
// IMPORTANT: although this is not stated in the JMS JavaDoc, this ActiveMQ call can block the thread! - misc_ro
jmsSession = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
jmsDestinationQueue = jmsSession.createQueue(outgoingRequestQueueName);
} catch (JMSException e) {
log.error("Error creating JMS session or destination for request sender loop", e);
return;
}
runDispatchLoop();
} finally {
try {
if (jmsSession != null) {
jmsSession.close();
}
} catch (JMSException e) {
log.error("Error closing JMS session after running request sender loop", e);
return;
}
}
}
private void runDispatchLoop() {
while (true) {
Runnable nextTask;
try {
nextTask = queue.take();
} catch (InterruptedException e) {
log.warn("Request sender interrupted; shutting down");
return;
}
if (cancelled) {
log.debug("Clean request sender shutdown");
return;
}
nextTask.run(); // important: run in same single thread, not dispatched to thread pool
}
}
void enqueue(final NetworkRequest request, final MessageChannelResponseHandler responseHandler, final int timeoutMsec) {
final long startTime = System.currentTimeMillis();
queue.add(new Runnable() {
private static final int SIZE_CATEGORY_DIVISOR = 100 * 1024;
@Override
public void run() {
sendNonBlockingRequest(jmsSession, jmsDestinationQueue, request, responseHandler, timeoutMsec);
// store transit time statistics; intended to check if JMS producer stalling occurs
final int rangeValue = request.getContentBytes().length / SIZE_CATEGORY_DIVISOR;
final String categoryString = StringUtils.format("Size range: %1$s00..%1$s99 kiB", rangeValue);
StatsCounter.registerValue("Messaging: Outgoing request queue transit time", categoryString,
System.currentTimeMillis() - startTime);
}
});
}
public void shutdown() {
// discard all enqueued messages when closing the channel; that the shutdown information signal is sent outside of this queue
// (note: there is no point in trying go get an exact number here, as new messages can be enqueued at any time)
int numDiscarded = queue.size();
queue.clear();
cancelled = true;
enqueue(null, null, 0); // ensure that the dispatcher wakes up to "see" the shutdown flag
if (numDiscarded != 0) {
log.debug(StringUtils.format("Discarded %d pending requests for %s as channel %s is shutting down", numDiscarded,
getRemoteNodeInformation().getInstanceNodeSessionId(), getChannelId()));
}
}
}
/**
* @param transportContext
*/
public AbstractJmsMessageChannel(InstanceNodeSessionId localNodeId) {
this.localNodeId = localNodeId;
}
@Override
public void sendRequest(final NetworkRequest request, final MessageChannelResponseHandler responseHandler, final int timeoutMsec) {
requestSender.enqueue(request, responseHandler, timeoutMsec); // new approach
// spawnBlockingRequestResponseTask(request, responseHandler, timeoutMsec); // old approach
}
protected void sendShutdownMessageToRemoteRequestInbox() {
try {
final Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
try {
final Queue destinationQueue = session.createQueue(outgoingRequestQueueName);
Message shutdownMessage = JmsProtocolUtils.createChannelShutdownMessage(session, getChannelId(), shutdownSecurityToken);
session.createProducer(destinationQueue).send(shutdownMessage); // disposed in finally block (as part of the session)
} finally {
session.close();
}
} catch (JMSException e) {
log.debug("Failed to send shutdown message while closing channel " + getChannelId(), e);
}
}
/**
* Sends a shutdown message to the broker-to-client (B2C) queue at the JMS broker. This allows the client-side queue listener to
* terminate cleanly.
*
* @throws JMSException on JMS errors
*/
protected void asyncSendShutdownMessageToB2CJmsQueue() throws JMSException {
threadPool.execute(new Runnable() {
@Override
@TaskDescription("JMS Network Transport: Send shutdown signal to Client-to-Broker queue")
public void run() {
Session session;
try {
session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
try {
final Queue destinationQueue = session.createQueue(outgoingRequestQueueName);
Message shutdownMessage = JmsProtocolUtils.createQueueShutdownMessage(session, shutdownSecurityToken);
session.createProducer(destinationQueue).send(shutdownMessage); // disposed in finally block (part of the session)
} finally {
session.close();
}
} catch (JMSException e) {
String message = e.toString();
// ignore exceptions that just report that the temporary queue is already gone,
// which is normal on client connection breakdown - misc_ro
if (!message.contains("")) {
log.warn(StringUtils.format("Exception on sending shutdown signal to Client-to-Broker JMS queue %s: %s",
outgoingRequestQueueName, message));
}
}
}
});
}
@Override
public String getOutgoingRequestQueueName() {
return outgoingRequestQueueName;
}
@Override
public void setupNonBlockingRequestSending(String outgoingRequestQueue, String incomingResponseQueue) throws JMSException {
// set up request sending
log.debug(StringUtils.format("Setting outgoing request queue for channel %s to %s", getChannelId(), outgoingRequestQueue));
this.outgoingRequestQueueName = outgoingRequestQueue;
startRequestSender(StringUtils.format("Request Sender for channel %s @ %s", getChannelId(), outgoingRequestQueue));
// set up response handling
log.debug(StringUtils.format("Setting incoming response queue for channel %s to %s", getChannelId(), incomingResponseQueue));
this.sharedResponseQueueName = incomingResponseQueue;
startResponseConsumer(StringUtils.format("Response Inbox Consumer for channel %s @ %s", getChannelId(), incomingResponseQueue));
}
@Override
protected void onClosedOrBroken() {
if (requestSender != null) {
requestSender.shutdown();
}
try {
if (responseInboxConsumer != null) {
responseInboxConsumer.triggerShutDown();
}
} catch (JMSException e) {
log.warn("Error while shutting down response consumer for channel " + getChannelId(), e);
}
}
private void startRequestSender(String taskName) throws JMSException {
requestSender = new RequestSender(outgoingRequestQueueName, connection);
threadPool.execute(requestSender, taskName);
}
private void startResponseConsumer(String taskName) throws JMSException {
responseInboxConsumer = new NonBlockingResponseInboxConsumer(sharedResponseQueueName, connection);
threadPool.execute(responseInboxConsumer, taskName);
}
@Override
public void setShutdownSecurityToken(String shutdownSecurityToken) {
this.shutdownSecurityToken = shutdownSecurityToken;
}
protected String getShutdownSecurityToken() {
return shutdownSecurityToken;
}
private void spawnBlockingRequestResponseTask(final NetworkRequest request, final MessageChannelResponseHandler responseHandler,
final int timeoutMsec) {
// note: old approach
threadPool.execute(new Runnable() {
@Override
@TaskDescription("JMS Network Transport: blocking request/response")
public void run() {
// check if channel was closed or marked as broken in the meantime
if (!isReadyToUse()) {
NetworkResponse response =
NetworkResponseFactory.generateResponseForCloseOrBrokenChannelDuringRequestDelivery(request, localNodeId, null);
responseHandler.onResponseAvailable(response);
return;
}
performBlockingRequestResponse(request, responseHandler, timeoutMsec);
}
}, request.getRequestId());
}
private void performBlockingRequestResponse(final NetworkRequest request, final MessageChannelResponseHandler responseHandler,
final int timeoutMsec) {
try {
// IMPORTANT: although this is not stated in the JMS JavaDoc, this ActiveMQ call can block the thread! - misc_ro
final Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
try {
final Queue destinationQueue = session.createQueue(outgoingRequestQueueName);
// construct message
Message jmsRequest = JmsProtocolUtils.createMessageFromNetworkRequest(request, session);
Message jmsResponse = performBlockingJmsRequestResponse(session, jmsRequest, destinationQueue, timeoutMsec);
NetworkResponse response = JmsProtocolUtils.createNetworkResponseFromMessage(jmsResponse, request);
responseHandler.onResponseAvailable(response);
} finally {
session.close();
}
} catch (TimeoutException e) {
// do not print the irrelevant stacktrace for this exception; only use message
log.debug(StringUtils.format("Timeout while waiting for response to request '%s' of type '%s': %s",
request.getRequestId(), request.getMessageType(), e.getMessage()));
NetworkResponse response =
NetworkResponseFactory.generateResponseForTimeoutWaitingForResponse(request, localNodeId);
responseHandler.onResponseAvailable(response);
} catch (JMSException e) {
// TODO detect broken connections
responseHandler.onChannelBroken(request, AbstractJmsMessageChannel.this);
String errorId = LogUtils.logErrorAndAssignUniqueMarker(
log, StringUtils.format("Error sending JMS message via channel %s; channel will be marked as broken (exception: %s) ",
getChannelId(), e.toString()));
NetworkResponse response = NetworkResponseFactory.generateResponseForErrorDuringDelivery(request, localNodeId, errorId);
responseHandler.onResponseAvailable(response);
}
}
protected final Message performBlockingJmsRequestResponse(final Session session, Message message,
final Queue destinationQueue, int timeoutMsec) throws JMSException, TimeoutException {
final TemporaryQueue tempResponseQueue = session.createTemporaryQueue();
try {
message.setJMSReplyTo(tempResponseQueue);
// send
sendRequest(session, message, destinationQueue);
// receive
return receiveResponse(session, timeoutMsec, tempResponseQueue);
} finally { // close temporary queue
try {
tempResponseQueue.delete();
} catch (JMSException e) {
// only log compact exception
log.debug(StringUtils.format(
"Exception on deleting a temporary response queue for channel %s (%s - %s): %s",
getChannelId(), tempResponseQueue.getQueueName(), getState(), e.toString()));
}
}
}
private void sendNonBlockingRequestInTempSession(final NetworkRequest request, final MessageChannelResponseHandler responseHandler,
final int timeoutMsec) {
Session session = null;
Queue destinationQueue = null;
try {
// IMPORTANT: although this is not stated in the JMS JavaDoc, this ActiveMQ call can block the thread! - misc_ro
try {
session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
destinationQueue = session.createQueue(outgoingRequestQueueName);
} catch (JMSException e) {
log.error("Error creating JMS session or destination for message sending", e);
return;
}
sendNonBlockingRequest(session, destinationQueue, request, responseHandler, timeoutMsec);
} finally {
try {
if (session != null) {
session.close();
}
} catch (JMSException e) {
log.error("Error closing JMS session after message sending", e);
return;
}
}
}
private void sendNonBlockingRequest(final Session session, final Queue destinationQueue, final NetworkRequest request,
final MessageChannelResponseHandler responseHandler, final int timeoutMsec) {
try {
// construct message
Message jmsRequest = JmsProtocolUtils.createMessageFromNetworkRequest(request, session);
final Queue replyToQueue = session.createQueue(sharedResponseQueueName);
jmsRequest.setJMSReplyTo(replyToQueue);
// send
sendRequest(session, jmsRequest, destinationQueue);
String messageId = jmsRequest.getJMSMessageID();
responseInboxConsumer.registerResponseListener(messageId, new JmsResponseCallback() {
@Override
public void onResponseReceived(Message jmsResponse) {
NetworkResponse response;
try {
response = JmsProtocolUtils.createNetworkResponseFromMessage(jmsResponse, request);
responseHandler.onResponseAvailable(response);
} catch (JMSException e) {
// check: log full stacktrace here, or compress it?
String errorId = LogUtils.logExceptionWithStacktraceAndAssignUniqueMarker(log,
"JMS exception while parsing response message", e);
response = NetworkResponseFactory.generateResponseForErrorDuringDelivery(request, localNodeId, errorId);
responseHandler.onResponseAvailable(response);
}
}
@Override
public void onTimeoutReached() {
log.debug(StringUtils.format("Timeout reached while waiting for response to request '%s' of type '%s'",
request.getRequestId(), request.getMessageType()));
NetworkResponse response =
NetworkResponseFactory.generateResponseForTimeoutWaitingForResponse(request, localNodeId);
responseHandler.onResponseAvailable(response);
}
@Override
public void onChannelClosed() {
log.debug(StringUtils.format("Message channel closed while waiting for response to request '%s' of type '%s'",
request.getRequestId(), request.getMessageType()));
NetworkResponse response =
NetworkResponseFactory.generateResponseForChannelCloseWhileWaitingForResponse(request, localNodeId, null);
responseHandler.onResponseAvailable(response);
}
}, timeoutMsec);
} catch (JMSException e) {
responseHandler.onChannelBroken(request, AbstractJmsMessageChannel.this);
String errorId = LogUtils.logErrorAndAssignUniqueMarker(log, StringUtils.format(
"Error sending JMS message via channel %s; channel will be marked as broken (exception: %s) ", getChannelId(),
e.toString()));
NetworkResponse response = NetworkResponseFactory.generateResponseForErrorDuringDelivery(request, localNodeId, errorId);
responseHandler.onResponseAvailable(response);
}
}
private void sendRequest(final Session session, Message message, final Queue destinationQueue) throws JMSException {
JmsProtocolUtils.sendWithTransientProducer(session, message, destinationQueue);
}
private Message receiveResponse(final Session session, int timeoutMsec, final TemporaryQueue tempResponseQueue) throws JMSException,
TimeoutException {
MessageConsumer consumer = session.createConsumer(tempResponseQueue);
try {
Message response;
response = consumer.receive(timeoutMsec);
if (response != null) {
return response;
} else {
// null return value indicates timeout
MessageChannelState currentState = getState();
if (currentState == MessageChannelState.CLOSED || currentState == MessageChannelState.MARKED_AS_BROKEN) {
throw new TimeoutException(StringUtils.format(
"Received JMS exception while waiting for a response from message channel %s (on queue %s), "
+ "which is already %s", getChannelId(), tempResponseQueue.getQueueName(), currentState));
} else {
throw new TimeoutException(StringUtils.format(
"Timeout (%d ms) exceeded while waiting for a response from message channel %s (on queue %s), "
+ "which is in state %s", timeoutMsec, getChannelId(), tempResponseQueue.getQueueName(), currentState));
}
}
} finally {
// close the consumer as it blocks the temporary queue from deletion otherwise
consumer.close();
}
}
}