package com.netifera.platform.internal.dispatcher;
import java.util.HashMap;
import java.util.Map;
import com.netifera.platform.api.channels.ChannelException;
import com.netifera.platform.api.channels.IChannelMessageSerializer;
import com.netifera.platform.api.dispatcher.IMessageDispatcher;
import com.netifera.platform.api.dispatcher.IProbeMessage;
import com.netifera.platform.api.dispatcher.MessengerException;
import com.netifera.platform.api.log.ILogger;
/**
* Create a thread which loops reading <code>ProbeMessage</code> objects from a
* channel and delivering them to threads waiting for responses to sent messages or
* to a general message dispatcher. Response messages can be retrieved by sequence
* number by calling the <code>#readResponse(int)</code> method.
* @author mike
*
*/
class MessageReader extends Thread {
/**
* Default time to wait for message response
* @see #setReadTimeout(long)
* @see #readResponse(int)
* @see IMessenger#exchangeMessage(ProbeMessage)
*
*/
private static final long DEFAULT_READ_TIMEOUT_MILLISECONDS = 50000;
/**
* Stores incoming response messages. The key is the message sequence
* number.
*/
private Map<Integer, IProbeMessage> responseMap;
/**
* Currently configured timeout for reading responses.
* @see #setReadTimeout(long)
* @see #readResponse(int)
* @see IMessenger#exchangeMessage(ProbeMessage)
*/
private long readTimeout;
/**
* Incoming messages which are not response messages are sent to the dispatcher.
*/
private IMessageDispatcher dispatcher;
/**
* Reference to the <code>Messenger</code> this <code>MessageReader</code> belongs to
*/
private final Messenger messenger;
/* serializer to read individual probe messages from */
private final IChannelMessageSerializer serializer;
private final ILogger logger;
/**
* Create a new message reader without starting it.
* @param channel The channel to read messages from.
*/
MessageReader(IChannelMessageSerializer serializer, Messenger messenger, ILogger logger) {
setName("Probe Message Reading Thread");
setDaemon(true);
this.serializer = serializer;
this.messenger = messenger;
this.logger = logger;
responseMap = new HashMap<Integer, IProbeMessage>();
readTimeout = DEFAULT_READ_TIMEOUT_MILLISECONDS;
}
/**
* Change timeout to wait for response messages
* @see IMessenger#exchangeMessage(ProbeMessage)
* @param milliseconds New timeout in milliseconds.
*/
void setReadTimeout(long milliseconds) {
readTimeout = milliseconds;
}
/**
* Set a <code>ProbeMessageDispatcher</code> to use for dispatching messages.
* @param dispatcher The new dispatcher.
*/
void setDispatcher(IMessageDispatcher dispatcher) {
this.dispatcher = dispatcher;
}
/**
* Wait for a response to a particular message.
* @see IMessenger#exchangeMessage(ProbeMessage)
* @param sequenceNumber The sequence number of the message to wait for a response to.
* @return The response message.
* @throws Exception on fatal errors
*/
IProbeMessage readResponse(int sequenceNumber) throws MessengerException {
long start = System.currentTimeMillis();
synchronized(responseMap) {
while(responseMap.containsKey(sequenceNumber) == false) {
long elapsed = System.currentTimeMillis() - start;
if(elapsed >= readTimeout) {
markAbandoned(sequenceNumber);
throw new MessengerException("Timeout reading response message");
}
if(isAlive() == false) {
/* probably redundant */
markAbandoned(sequenceNumber);
throw new MessengerClosedException();
}
try {
responseMap.wait(readTimeout-elapsed);
} catch (InterruptedException e) {
markAbandoned(sequenceNumber);
Thread.currentThread().interrupt();
// XXX should be chained?
throw new MessengerException("Interrupted while reading response");
}
}
return responseMap.remove(sequenceNumber);
}
}
/**
* Put a null entry in the map to indicate to the reader that we are not
* going to wait for this message.
* @see #isAbandoned(int)
* @param sequenceNumber The sequence number of the abandoned response.
*/
private void markAbandoned(int sequenceNumber) {
responseMap.put(sequenceNumber, null);
}
/**
* Main message reader loop.
*/
public void run() {
logger.debug("Starting message reader");
while(!interrupted()) {
processOneMessage();
}
responseMap.clear();
messenger.close();
}
/**
* Process one message and either place it into the <code>responseMap</code> for a waiting thread
* or send it to the dispatcher.
*/
private void processOneMessage() {
IProbeMessage message;
try {
message = serializer.readMessage();
} catch (ChannelException e) {
interrupt();
return;
} catch(Exception e) {
logger.error("Error reading message", e);
return;
}
if(message.isResponse()) {
synchronized(responseMap) {
if(isAbandoned(message.getSequenceNumber())) {
return;
}
responseMap.put(message.getSequenceNumber(), message);
responseMap.notifyAll();
return;
}
}
if(dispatcher != null) {
try {
dispatcher.dispatch(messenger, message);
} catch (MessengerException e) {
e.printStackTrace();
}
}
}
/**
* The thread waiting for a message response must place a <code>null</code> value
* in the sequence number slot if it decides to not wait for the response. This
* avoids leaking messages into the responseMap. This function detects an abandoned
* response and removes the <code>null</code> sentinel mapping.
* @see #markAbandoned(int)
* @param sequenceNumber The sequence number of the response to check.
* @return Returns true if abandoned.
*/
private boolean isAbandoned(int sequenceNumber) {
if(responseMap.containsKey(sequenceNumber)) {
IProbeMessage old = responseMap.remove(sequenceNumber);
assert(old == null);
return true;
}
return false;
}
}