/*
* Copyright (C) 2006-2016 DLR, Germany
*
* All rights reserved
*
* http://www.rcenvironment.de/
*/
package de.rcenvironment.core.communication.transport.jms.common;
import java.util.HashMap;
import java.util.Map;
import javax.jms.Connection;
import javax.jms.JMSException;
import javax.jms.Message;
import de.rcenvironment.core.toolkitbridge.transitional.ConcurrencyUtils;
import de.rcenvironment.toolkit.modules.concurrency.api.AsyncTaskService;
import de.rcenvironment.toolkit.modules.concurrency.api.TaskDescription;
/**
* A single-threaded consumer that listens for all responses sent to a shared queue.
*
* @author Robert Mischke
*/
public final class NonBlockingResponseInboxConsumer extends AbstractJmsQueueConsumer implements Runnable {
/**
* The number of retries to make before deciding that there really is no registered local listener for a network response.
*
* Note that there is no harm in retrying for quite a while - the only side effect of a high value is that messages that actually arrive
* after the network timeout are reported/logged with an additional delay of MAX_RETRY_COUNT * WAIT_MSEC. - misc_ro
*/
private static final int RESPONSE_LISTENER_MAX_RETRY_COUNT = 20;
/**
* The time to wait per retry attempt.
*/
private static final int RESPONSE_LISTENER_RETRY_WAIT_MSEC = 500;
/**
* A simple callback for either a received response or a timeout event.
*
* @author Robert Mischke
*/
public interface JmsResponseCallback {
/**
* Called when an actual response was received.
*
* @param jmsResponse the received JMS response.
*/
void onResponseReceived(Message jmsResponse);
/**
* Called when the timeout was reached.
*/
void onTimeoutReached();
/**
* Called when the channel was closed while waiting for a response.
*/
void onChannelClosed();
}
private final AsyncTaskService threadPool = ConcurrencyUtils.getAsyncTaskService();
private final Map<String, JmsResponseCallback> responseListenerMap = new HashMap<>();
public NonBlockingResponseInboxConsumer(String queueName, Connection connection)
throws JMSException {
super(connection, queueName);
}
/**
* Registers the destination to send the non-blocking response to when it arrives, along with a timeout parameter.
*
* @param messageId the message correlation id
* @param jmsResponseListener the callback listener
* @param timeoutMsec the timeout in msec
*/
public void registerResponseListener(final String messageId, JmsResponseCallback jmsResponseListener, final long timeoutMsec) {
// sanity check
if (messageId == null) {
log.error("Internal consistency error: There was already a response listener registered for message id " + messageId);
jmsResponseListener.onTimeoutReached(); // arbitrary handling in case of this abnormal situation
return;
}
// log.debug("Registering response listener for message id " + messageId);
synchronized (responseListenerMap) {
JmsResponseCallback replaced = responseListenerMap.put(messageId, jmsResponseListener);
// sanity check
if (replaced != null) {
log.error("Internal consistency error: There was already a response listener registered for message id " + messageId);
jmsResponseListener.onTimeoutReached(); // arbitrary handling in case of this abnormal situation
return;
}
}
threadPool.scheduleAfterDelay(new Runnable() {
@Override
@TaskDescription("JMS Network Transport: Check for request completion after timeout")
public void run() {
final JmsResponseCallback unfulfilledResponseListener;
synchronized (responseListenerMap) {
unfulfilledResponseListener = responseListenerMap.remove(messageId);
}
if (unfulfilledResponseListener != null) {
log.debug("Reached timeout (" + timeoutMsec + "ms) for message id " + messageId);
unfulfilledResponseListener.onTimeoutReached();
}
}
}, timeoutMsec);
}
@Override
@TaskDescription("JMS Network Transport: Non-blocking response listener")
public void run() {
super.run();
synchronized (responseListenerMap) {
if (!responseListenerMap.isEmpty()) {
log.debug("Response listener for queue " + queueName + " has been shut down while " + responseListenerMap.size()
+ " request(s) were still pending; generating failure responses");
for (final JmsResponseCallback listener : responseListenerMap.values()) {
threadPool.execute(new Runnable() {
@Override
@TaskDescription("JMS Network Transport: Handle pending non-blocking request after queue listener shutdown")
public void run() {
listener.onChannelClosed();
}
});
}
// requests are handled; do not send timeout responses, too
responseListenerMap.clear();
}
}
}
@Override
protected void dispatchMessage(final Message message, final Connection jmsConnection) {
threadPool.execute(new Runnable() {
@Override
@TaskDescription("JMS Network Transport: Dispatch incoming response")
public void run() {
final String messageId;
try {
messageId = message.getJMSCorrelationID();
// sanity check
if (messageId == null) {
log.error("Unexpected state: null JMS message correlation id");
return; // no graceful handling possible
}
} catch (JMSException e) {
log.error("Unexpected error while handling JMS response", e);
// TODO add an error callback for this? right now, the timeout will handle it
return;
}
JmsResponseCallback responseListener;
// As the JMS broker-generated message ids are used for correlation, the response listener cannot be registered until after
// the JMS message has been sent. Usually, this is not a problem. If local CPU load and/or thread congestion is very high,
// however, the response can arrive before the sender has managed to register its response listener in the synchronized map.
// This retry loop fixes this problem by waiting briefly in case no listener is found. - misc_ro
int retryCount = 0;
while (true) {
synchronized (responseListenerMap) {
responseListener = responseListenerMap.remove(messageId);
}
if (responseListener != null) {
if (retryCount > 0) {
log.debug("Successfully fetched mapping information for a network response after retrying for "
+ retryCount * RESPONSE_LISTENER_RETRY_WAIT_MSEC
+ " msec; there is probably high CPU load on the local instance");
}
responseListener.onResponseReceived(message);
break;
}
if (retryCount >= RESPONSE_LISTENER_MAX_RETRY_COUNT) {
log.debug("No response listener for message " + messageId
+ " even after retrying - most likely, the response arrived after the timeout");
return;
}
retryCount++;
try {
Thread.sleep(RESPONSE_LISTENER_RETRY_WAIT_MSEC);
} catch (InterruptedException e) {
log.warn("Thread interrupted while retrying to fetch response mapping information");
return; // in case only this thread was interrupted, this will be handled by the standard timeout
}
}
}
});
}
}