package org.dcache.services.info.gathers;
import com.google.common.util.concurrent.MoreExecutors;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Date;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import diskCacheV111.vehicles.Message;
import dmg.cells.nucleus.CellEndpoint;
import dmg.cells.nucleus.CellMessage;
import dmg.cells.nucleus.CellMessageAnswerable;
import dmg.cells.nucleus.CellPath;
import dmg.cells.nucleus.NoRouteToCellException;
import dmg.cells.nucleus.SerializationException;
import dmg.cells.nucleus.UOID;
import dmg.cells.nucleus.CellMessageSender;
import static com.google.common.base.Preconditions.checkNotNull;
/**
* A MessageHandlerChain allows multiple MessageHandler subclass instances to attempt to
* process an incoming Message. This allows easy addition of extra monitoring by receiving
* additional messages.
*
* Zero or more MessageHandler subclass instances are registered with the MessageHandlerChain.
* When passed an incoming Message, the MessageHandlerChain instance will pass the Message to
* each MessageHandler subclass instance in turn until one succeeds in processing the Message.
*
* @author Paul Millar <paul.millar@desy.de>
*/
public class MessageHandlerChain implements MessageMetadataRepository<UOID>,
MessageSender, CellMessageAnswerable, CellMessageSender
{
/** The period between successive flushes of ancient metadata, in milliseconds */
private static final long METADATA_FLUSH_THRESHOLD = 3600000; // 1 hour
private static final long METADATA_FLUSH_PERIOD = 600000; // 10 minutes
/** Our default timeout for sending messages, in milliseconds */
private static final long STANDARD_TIMEOUT = 1000;
private static final Logger LOGGER = LoggerFactory.getLogger(MessageHandlerChain.class);
private final List<MessageHandler> _messageHandler = new LinkedList<>();
private CellEndpoint _endpoint;
@Override
public void setCellEndpoint(CellEndpoint endpoint)
{
_endpoint = endpoint;
}
/**
* @return a simple array of registered MessageHandlers subclass types.
*/
public String[] listMessageHandlers()
{
int i=0;
String[] msgHandlers;
synchronized (_messageHandler) {
msgHandlers = new String[_messageHandler.size()];
for (MessageHandler mh : _messageHandler) {
// We're assuming only one instance per Class
msgHandlers[i++] = mh.getClass().getSimpleName();
}
}
return msgHandlers;
}
/**
* Common method to send a CellMessage and register a handler for the return message.
* This is deprecated against using Vehicles and registering MessageHandlers.
* @param ttl lifetime of resulting metric, in seconds.
* @param handler the call-back handler for the return message
* @param path the CellPath to target cell
* @param requestString the String, requesting information
*/
@Override
public void sendMessage(long ttl, CellMessageAnswerable handler,
CellPath path, String requestString)
{
if (handler == null) {
LOGGER.error("ignoring attempt to send string-based message without call-back");
return;
}
CellMessage envelope = new CellMessage(path, requestString);
sendMessage(ttl, handler, envelope);
}
/**
* The preferred way of sending requests for information.
* @param ttl lifetime of resulting metric, in seconds.
* @param path the CellPath for the recipient of this message
* @param message the Message payload
*/
@Override
public void sendMessage(long ttl, CellPath path, Message message)
{
CellMessage envelope = new CellMessage(path, message);
sendMessage(ttl, null, envelope);
}
/**
* Send a message envelope and record metadata against it.
* @param ttl the metadata for the message
* @param handler the call-back for this method, or null if none should be used.
* @param envelope the message to send
* @throws SerializationException if the payload isn't serialisable.
*/
@Override
public void sendMessage(long ttl, CellMessageAnswerable handler,
CellMessage envelope) throws SerializationException
{
putMetricTTL(envelope.getUOID(), ttl);
_endpoint.sendMessage(envelope, handler != null ? handler : this, MoreExecutors.directExecutor(), STANDARD_TIMEOUT);
}
public void setHandlers(List<MessageHandler> handlers)
{
synchronized (_messageHandler) {
_messageHandler.clear();
_messageHandler.addAll(handlers);
}
}
/*
* SUPPORT FOR MessageMetadataRepository INTERFACE
*/
/**
* For each message we send, a small amount of metadata is recorded (when it was sent and a long).
* The long is so, when the return message is received, we can pass this parameter on
* to the message processing plug-in. The time is so we can (every so often) delete stale entries
* due to message-loss.
*/
private static class MessageMetadata
{
Date _timeSent;
final long _ttl;
MessageMetadata(long ttl)
{
_timeSent = new Date();
_ttl = ttl;
}
}
private final Map<UOID,MessageMetadata> _msgMetadata = new ConcurrentHashMap<>();
private Date _nextFlushOldMetadata;
@Override
public boolean containsMetricTTL(UOID messageId)
{
return _msgMetadata.containsKey(messageId);
}
@Override
public long getMetricTTL(UOID messageId)
{
flushOldMetadata();
LOGGER.trace("Querying for metric ttl stored against message-ID {}",
messageId);
MessageMetadata metadata = _msgMetadata.get(messageId);
if (metadata == null) {
throw new IllegalArgumentException("No metadata recorded for " +
"message " + messageId);
}
return metadata._ttl;
}
@Override
public void remove(UOID messageId)
{
if (_msgMetadata.remove(messageId) == null) {
throw new IllegalArgumentException("No metadata recorded for " +
"message " + messageId);
}
}
@Override
public void putMetricTTL(UOID messageId, long ttl)
{
checkNotNull(messageId, "Attempting to record ttl against null messageId");
LOGGER.trace("Adding metric ttl {} against message-ID {}", ttl, messageId);
_msgMetadata.put(messageId, new MessageMetadata(ttl));
}
/**
* Scan through our recorded Metadata and remove very old entries.
* This is only done "every so often" and adds some safety against
* lost packets resulting in accumulated memory usage.
*/
private void flushOldMetadata()
{
Date now = new Date();
if (_nextFlushOldMetadata != null && now.before(_nextFlushOldMetadata)) {
return;
}
// Flush ancient metadata
for (Iterator<MessageMetadata> itr = _msgMetadata.values().iterator(); itr.hasNext();) {
MessageMetadata item = itr.next();
if (now.getTime() - item._timeSent.getTime() > METADATA_FLUSH_THRESHOLD) {
itr.remove();
}
}
_nextFlushOldMetadata = new Date(System.currentTimeMillis() + METADATA_FLUSH_PERIOD);
}
/*
* The following three methods implement CellMessageAnswerable interface for
* Message methods.
*/
@Override
public void answerArrived(CellMessage request, CellMessage answer)
{
Object messagePayload = answer.getMessageObject();
if (!(messagePayload instanceof Message)) {
LOGGER.warn("Received msg where payload is not instanceof Message");
return;
}
if (!containsMetricTTL(request.getLastUOID())) {
LOGGER.warn("Attempt to add metrics without recorded metric TTL for msg {}", request);
return;
}
synchronized (_messageHandler) {
for (MessageHandler mh : _messageHandler) {
if (mh.handleMessage((Message) messagePayload,
getMetricTTL(request.getLastUOID()))) {
return;
}
}
}
}
@Override
public void answerTimedOut(CellMessage request)
{
remove(request.getLastUOID());
LOGGER.info("Message timed out");
}
@Override
public void exceptionArrived(CellMessage request, Exception exception)
{
remove(request.getLastUOID());
if (exception instanceof NoRouteToCellException) {
// This can happen after a cell dies and info hasn't caught up
LOGGER.debug("Sending message to {} failed: {}",
((NoRouteToCellException)exception).getDestinationPath(),
exception.getMessage());
} else if (exception instanceof IllegalArgumentException) {
// Can happen for a short while when a poolgroup is deleted
LOGGER.debug("Command failed: {}", exception.getMessage());
} else {
LOGGER.error("Received remote exception: ", exception);
}
}
}