/* * Copyright (C) 2006-2016 DLR, Germany * * All rights reserved * * http://www.rcenvironment.de/ */ package de.rcenvironment.core.communication.routing.internal; import java.io.Serializable; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import de.rcenvironment.core.communication.channel.MessageChannelLifecycleListener; import de.rcenvironment.core.communication.channel.MessageChannelLifecycleListenerAdapter; import de.rcenvironment.core.communication.channel.MessageChannelService; import de.rcenvironment.core.communication.common.CommunicationException; import de.rcenvironment.core.communication.common.InstanceNodeSessionId; import de.rcenvironment.core.communication.common.SerializationException; import de.rcenvironment.core.communication.messaging.NetworkRequestHandler; import de.rcenvironment.core.communication.messaging.NetworkRequestHandlerMap; import de.rcenvironment.core.communication.messaging.direct.api.DirectMessagingSender; import de.rcenvironment.core.communication.messaging.internal.InternalMessagingException; import de.rcenvironment.core.communication.messaging.internal.NetworkRequestUtils; import de.rcenvironment.core.communication.model.InitialNodeInformation; import de.rcenvironment.core.communication.model.NetworkContactPoint; import de.rcenvironment.core.communication.model.NetworkRequest; import de.rcenvironment.core.communication.model.NetworkResponse; import de.rcenvironment.core.communication.model.NetworkResponseHandler; import de.rcenvironment.core.communication.protocol.MessageMetaData; import de.rcenvironment.core.communication.protocol.NetworkRequestFactory; import de.rcenvironment.core.communication.protocol.NetworkResponseFactory; import de.rcenvironment.core.communication.protocol.ProtocolConstants; import de.rcenvironment.core.communication.spi.NetworkTopologyChangeListener; import de.rcenvironment.core.communication.transport.spi.MessageChannel; import de.rcenvironment.core.communication.utils.MessageUtils; import de.rcenvironment.core.utils.common.StringUtils; /** * Implementation of a link state based routing table. * * @author Phillip Kroll * @author Robert Mischke */ public class LinkStateRoutingProtocolManager { private static final int DEFAULT_TIME_TO_LIVE = 200; /** * Delegating adapter for {@link MessageChannelLifecycleListener} events. * * @author Robert Mischke */ private class MessageChannelLifecycleHandler extends MessageChannelLifecycleListenerAdapter { @Override public void onOutgoingChannelEstablished(final MessageChannel connection) { handleOutgoingChannelEstablished(connection); } @Override public void onOutgoingChannelTerminated(MessageChannel connection) { handleOutgoingChannelTerminated(connection); } } /** * Handler for incoming LSA (link state advertisement) requests. * * @author Robert Mischke */ private static class LSANetworkRequestHandler implements NetworkRequestHandler { private LinkStateRoutingProtocolManager protocolManager; LSANetworkRequestHandler(LinkStateRoutingProtocolManager protocolManager) { this.protocolManager = protocolManager; } @Override public NetworkResponse handleRequest(NetworkRequest request, InstanceNodeSessionId sourceId) throws InternalMessagingException { Serializable messageContent = NetworkRequestUtils.deserializeWithExceptionHandling(request); Serializable responseBody; if (messageContent instanceof LinkStateAdvertisementBatch) { responseBody = protocolManager.handleReceivedInitialLSABatch(messageContent); } else { // TODO @7.0 should not be in use anymore; remove responseBody = protocolManager.handleSingleLinkStateAdvertisement(messageContent, request.accessRawMetaData()); } byte[] responseBodyBytes = MessageUtils.serializeSafeObject(responseBody); return NetworkResponseFactory.generateSuccessResponse(request, responseBodyBytes); } } private static int timeToLive = DEFAULT_TIME_TO_LIVE; private static final int MESSAGE_BUFFER_SIZE = 50; private static final boolean DEBUG_DUMP_INITIAL_LSA_BATCHES = false; /** * TODO Enter comment. */ public volatile boolean sendCompactLsaLists = false; private final Log log = LogFactory.getLog(getClass()); private final TopologyMap topologyMap; private final NetworkRequestHandler networkRequestHandler; private final InitialNodeInformation ownNodeInformation; private final InstanceNodeSessionId ownNodeId; private final DirectMessagingSender directMessagingSender; private NetworkTopologyChangeListener topologyChangeListener; private final Map<String, Serializable> messageBuffer = new LinkedHashMap<String, Serializable>(MESSAGE_BUFFER_SIZE); private final NetworkStats networkStats; private final Map<String, MessageChannel> connectionsById = new HashMap<String, MessageChannel>(); // private final LinkStateAdvertisementBatch lsaCache = new LinkStateAdvertisementBatch(); // @Deprecated /** * Constructor needs to get services injected. * * @param communicationService * @param platformService * @param changeListener listener for topology change events */ public LinkStateRoutingProtocolManager(TopologyMap topologyMap, MessageChannelService connectionService, NetworkTopologyChangeListener changeListener) { this.topologyMap = topologyMap; this.networkRequestHandler = new LSANetworkRequestHandler(this); this.ownNodeInformation = topologyMap.getLocalNodeInformation(); this.ownNodeId = ownNodeInformation.getInstanceNodeSessionId(); this.directMessagingSender = connectionService; this.networkStats = new NetworkStats(); connectionService.addChannelLifecycleListener(new MessageChannelLifecycleHandler()); // initialize topology with self TopologyNode ownNode = topologyMap.addNode(ownNodeId); ownNode.setDisplayName(ownNodeInformation.getDisplayName()); ownNode.setIsWorkflowHost(false); // not used anymore // initialize own sequence number ownNode.invalidateSequenceNumber(); this.topologyChangeListener = changeListener; fireTopologyChangedListener(); } public NetworkRequestHandlerMap getNetworkRequestHandlers() { return new NetworkRequestHandlerMap(ProtocolConstants.VALUE_MESSAGE_TYPE_LSA, networkRequestHandler); } /** * Broadcast the information that this node is shutting down, and will disappear from the network. * * @throws CommunicationException The communication exception. */ public void announceShutdown() throws CommunicationException { broadcastLsa(topologyMap.generateShutdownLSA()); } /** * Test if a message with a given message id has been received recently. * * @param messageId The id of the message. * @return A boolean. */ public boolean messageReivedById(String messageId) { return messageBuffer.containsKey(messageId); } /** * Test if a message has recently been received that had a given content. * * @param messageContent The content. * @return A boolean. */ public boolean messageReivedByContent(Serializable messageContent) { return messageBuffer.containsValue(messageContent); } /** * This method is called, when the routing service receives an {@link LinkStateAdvertisement} from a remote instance. * * @param messageContent The message content. * @param metaData The meta data. * @return an optional {@link Serializable} response or null */ private Serializable handleSingleLinkStateAdvertisement(Serializable messageContent, Map<String, String> metaData) { boolean topologyChanged = false; synchronized (topologyMap) { if (!(messageContent instanceof LinkStateAdvertisement)) { throw new IllegalStateException("Received a non-LSA in handleLinkStateAdvertisement()"); } LinkStateAdvertisement lsa = (LinkStateAdvertisement) messageContent; networkStats.incReceivedLSAs(); // networkStats.incHopCountOfReceivedLSAs(MessageMetaData.wrap(metaData).getHopCount()); // TODO review: currently not sent; see LSA batch handling above // if (LinkStateAdvertisement.REASON_STARTUP.equals(lsa.getReason())) { // } // if the received LSA was accepted if (topologyMap.update(lsa)) { topologyChanged = true; // TODO Dynamically adjust maximum time to live for LSAs networkStats.setMaxTimeToLive(getTimeToLive()); // // fill cache // synchronized (lsaCache) { // lsaCache.put(lsa.getOwner(), lsa); // } broadcastLsa(lsa); // NetworkFormatter.nodeList(topologyMap)); // synchronized (lsaCache) { // LinkStateAdvertisementCache clonedCache; // clonedCache = new LinkStateAdvertisementCache(lsaCache); // return clonedCache; // } } else { networkStats.incRejectedLSAs(); networkStats.incHopCountOfRejectedLSAs(MessageMetaData.wrap(metaData).getHopCount()); // send null response } } if (topologyChanged) { fireTopologyChangedListener(); } return null; } /** * TODO Robert Mischke: Enter comment! * * @param messageContent The message content. * @return The link state advertisement batch. */ private Serializable handleReceivedInitialLSABatch(Serializable messageContent) { boolean topologyChanged = false; LinkStateAdvertisementBatch response; synchronized (topologyMap) { // sanity check if (!(messageContent instanceof LinkStateAdvertisementBatch)) { throw new IllegalStateException("Message content of wrong type."); } LinkStateAdvertisementBatch lsaCache = (LinkStateAdvertisementBatch) messageContent; if (DEBUG_DUMP_INITIAL_LSA_BATCHES) { // TODO add origin/sender information String dump = StringUtils.format("Processing LSA cache at %s (as incoming request):", ownNodeId); for (InstanceNodeSessionId id : lsaCache.keySet()) { dump += "\n" + id + " -> " + lsaCache.get(id); } log.debug(dump); } LinkStateAdvertisementBatch lsaCacheNew = new LinkStateAdvertisementBatch(); // TODO increment stats for (LinkStateAdvertisement lsa : lsaCache.values()) { if (topologyMap.update(lsa)) { topologyChanged = true; // update main cache // lsaCache.put(lsa.getOwner(), lsa); // lsaCacheNew.put(lsa.getOwner(), lsa); broadcastLsa(lsa); } } // return lsaCache; response = topologyMap.generateLsaBatchOfAllNodes(); } if (topologyChanged) { fireTopologyChangedListener(); } return response; } /** * Handles the response received from a remote node in response to the inital {@link LinkStateAdvertisementBatch}. * * @param messageContent The message content. * @return whether the received response caused a topology change */ private boolean handleInitialLSABatchResponse(Serializable messageContent) { boolean topologyChanged = false; synchronized (topologyMap) { // sanity check if (!(messageContent instanceof LinkStateAdvertisementBatch)) { log.warn("Message content was of wrong type."); return false; } LinkStateAdvertisementBatch lsaCache = (LinkStateAdvertisementBatch) messageContent; if (DEBUG_DUMP_INITIAL_LSA_BATCHES) { // TODO add origin/sender information String dump = StringUtils.format("Processing LSA cache at %s (as incoming response):", ownNodeId); for (InstanceNodeSessionId id : lsaCache.keySet()) { dump += "\n" + id + " -> " + lsaCache.get(id); } log.debug(dump); } // LinkStateAdvertisementCache lsaCacheNew = new LinkStateAdvertisementCache(); for (LinkStateAdvertisement lsa : lsaCache.values()) { if (topologyMap.update(lsa)) { topologyChanged = true; // lsaCacheNew.put(lsa.getOwner(), lsa); broadcastLsa(lsa); } } } return topologyChanged; } private void onOutgoingChannelHandshakeCompleted(MessageChannel connection, boolean topologyChanged) { broadcastNewLocalLSA(); if (topologyChanged) { fireTopologyChangedListener(); } } /** * Send link state advertisement of the own node. * * @return The message id. */ public String broadcastNewLocalLSA() { // extract fresh LSA from topology map // synchronized (lsaCache) { LinkStateAdvertisement ownLsa = topologyMap.generateNewLocalLSA(); // lsaCache.put(ownNodeId, ownLsa); return broadcastLsa(ownLsa); // } } /** * Send a given LSA to all neighbors. * * @param lsa * @throws CommunicationException * @return The message id. */ private String broadcastLsa(LinkStateAdvertisement lsa) { byte[] lsaBytes = MessageUtils.serializeSafeObject(lsa); String messageId = ""; // update metadata List<TopologyNode> neighbors = new ArrayList<TopologyNode>(topologyMap.getSuccessors()); // Use a randomized list Collections.shuffle(neighbors); /* * Changed in 3.0.0: LSAs are now broadcast into all outgoing channels, instead of sending a routed message to each neighbor. The * rationale is that if there are parallel message channels, they exist either for intentional redundancy, or one or more of them * are stale/broken. In both cases, it is appropriate to send to all channels. - misc_ro, July 2013 */ Collection<TopologyLink> links = topologyMap.getAllOutgoingLinks(ownNodeId); // iterate over all neighbor nodes of the current node // NOTE: "links" is not threadsafe (can cause a ConcurrentModificationException); not fixing as this code is deprecated for (TopologyLink link : links) { networkStats.incSentLSAs(); // networkStats.incHopCountOfSentLSAs(MessageMetaData.wrap(metaData).getHopCount()); // non-routed broadcast -> no recipient NetworkRequest request = NetworkRequestFactory.createNetworkRequest(lsaBytes, ProtocolConstants.VALUE_MESSAGE_TYPE_LSA, ownNodeId, null); final String channelId = link.getConnectionId(); directMessagingSender.sendDirectMessageAsync(request, connectionsById.get(channelId), new NetworkResponseHandler() { @Override public void onResponseAvailable(NetworkResponse response) { if (!response.isSuccess()) { // TODO add cause to log entry log.warn("Failed to send LSA via channel " + channelId); } } }); // sendToNeighbor(request, neighbor.getNodeIdentifier()); } return messageId; } /** * @param id a {@link MessageChannel}'s id * @return the associated {@link MessageChannel} */ // FIXME move/replace public MessageChannel getMessageChannelById(String id) { MessageChannel connection = null; synchronized (connectionsById) { connection = connectionsById.get(id); } if (connection == null) { throw new IllegalStateException("No registered connection for connection id " + id); } return connection; } private TopologyLink registerNewConnection(MessageChannel connection) { String connectionId = connection.getChannelId(); synchronized (connectionsById) { // consistency check: there should be no connection with the same id already if (connectionsById.get(connectionId) != null) { // consistency error throw new IllegalStateException("Existing connection found for connection id " + connectionId); } connectionsById.put(connectionId, connection); // LOGGER.debug(StringUtils.format("Registered new connection %s in node %s", // connection.toString(), // ownNodeInformation.getLogName())); // NOTE: this replaces the obsolete "pingNetworkContactPoint" method -- misc_ro InstanceNodeSessionId remoteNodeId = connection.getRemoteNodeInformation().getInstanceNodeSessionId(); // TODO restore onCommunicationSuccess callback (via traffic listener?) // onCommunicationSuccess("", MetaDataWrapper.createEmpty().getInnerMap(), connection, // remoteNodeId); // update graph model topologyMap.addNode(remoteNodeId); if (topologyMap.hasLinkForConnection(connection.getChannelId())) { // unexpected state / consistency error throw new IllegalStateException("Found existing link for new connection " + connectionId); } // add newly discovered link to network model TopologyLink newLink = topologyMap.addLink(getOwner(), remoteNodeId, connection.getChannelId()); return newLink; } } private void handleOutgoingChannelEstablished(final MessageChannel connection) { synchronized (topologyMap) { log.debug("Registering connection " + connection.getChannelId() + " at node " + ownNodeId); registerNewConnection(connection); if (!connection.getInitiatedByRemote()) { // only reply with an LSA batch if the connection was self-initiated LinkStateAdvertisementBatch payloadLsaCache = topologyMap.generateLsaBatchOfAllNodes(); log.debug("Sending initial LSA batch into connection " + connection.getChannelId()); byte[] lsaBytes = MessageUtils.serializeSafeObject(payloadLsaCache); NetworkRequest lsaRequest = NetworkRequestFactory.createNetworkRequest(lsaBytes, ProtocolConstants.VALUE_MESSAGE_TYPE_LSA, ownNodeId, connection.getRemoteNodeInformation().getInstanceNodeSessionId()); directMessagingSender.sendDirectMessageAsync(lsaRequest, connection, new NetworkResponseHandler() { @Override public void onResponseAvailable(NetworkResponse response) { if (!response.isSuccess()) { log.warn("Failed to send initial LSA batch via connection " + connection.getChannelId() + ": Code " + response.getResultCode()); return; } Serializable deserializedContent; try { deserializedContent = response.getDeserializedContent(); if (deserializedContent instanceof LinkStateAdvertisementBatch) { boolean topologyChanged = handleInitialLSABatchResponse(deserializedContent); onOutgoingChannelHandshakeCompleted(connection, topologyChanged); } else { log.error("Unexpected response to initial LSA batch: " + deserializedContent); } } catch (SerializationException e) { log.error("Failed to deserialize response to initial LSA batch", e); } } }); } else { // for a remote-initiated connection, an update LSA is sufficient broadcastNewLocalLSA(); } } fireTopologyChangedListener(); } private void handleOutgoingChannelTerminated(MessageChannel connection) { synchronized (connectionsById) { String channelId = connection.getChannelId(); // remove link from topology TopologyLink link = topologyMap.getLinkForConnection(channelId); if (link == null) { log.debug("Channel " + channelId + " to unregister does not exist in the topology; " + "the usual cause is that the remote node " + connection.getRemoteNodeInformation().getInstanceNodeSessionId() + " was removed after a shutdown notice"); } else { if (!topologyMap.removeLink(link)) { log.warn("Unexpected state: Channel was found in topology, but could not be removed; id=" + channelId); } } // is there already a connection to this NCP? MessageChannel registeredConnection = connectionsById.get(channelId); if (registeredConnection == null) { log.warn("No registered connection for id " + channelId); return; } if (registeredConnection != connection) { log.warn("Another connection is registered under id " + channelId + "; ignoring unregistration"); return; } connectionsById.remove(channelId); log.debug(StringUtils.format("Unregistered connection %s from %s", connection.toString(), ownNodeInformation.getLogDescription())); } broadcastNewLocalLSA(); fireTopologyChangedListener(); } /** * Event. * * @param messageContent * @param metaData * @param ncp */ private void onMaxTimeToLiveReached(Serializable messageContent, Map<String, String> metaData, NetworkContactPoint ncp) { networkStats.incFailedCommunications(); log.debug(StringUtils.format( "'%s' reports that a message that was issued by '%s' exeeded the maximum time to live (%s).", ownNodeId, MessageMetaData.wrap(metaData).getSender(), timeToLive)); } /** * * TODO krol_ph: Enter comment! * * @param messageId * @param messageContent */ private void onMessageReceived(String messageId, Serializable messageContent) { } /** * @return Returns the owner. */ public InstanceNodeSessionId getOwner() { return ownNodeId; } /** * Add received messages to a buffer so that they can be accessed later on. * * @param messageContent The message content. */ protected void addToMessageBuffer(String messageId, Serializable messageContent) { messageBuffer.put(messageId, messageContent); onMessageReceived(messageId, messageContent); } public TopologyMap getTopologyMap() { return topologyMap; } /** * @return Returns the networkStats. */ public NetworkStats getNetworkStats() { return networkStats; } /** * @return Returns the timeToLive. */ public int getTimeToLive() { return timeToLive; } public Map<String, Serializable> getMessageBuffer() { return messageBuffer; } private void fireTopologyChangedListener() { if (topologyChangeListener != null) { topologyChangeListener.onNetworkTopologyChanged(); } } }