/* * Copyright (C) 2006-2016 DLR, Germany * * All rights reserved * * http://www.rcenvironment.de/ */ package de.rcenvironment.core.communication.routing.internal; import java.util.ArrayList; import java.util.Collection; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Set; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import de.rcenvironment.core.communication.api.NodeIdentifierService; import de.rcenvironment.core.communication.channel.MessageChannelService; import de.rcenvironment.core.communication.common.IdentifierException; import de.rcenvironment.core.communication.common.NetworkGraph; import de.rcenvironment.core.communication.common.NetworkGraphLink; import de.rcenvironment.core.communication.common.InstanceNodeSessionId; import de.rcenvironment.core.communication.common.NodeIdentifierContextHolder; import de.rcenvironment.core.communication.common.NodeIdentifierUtils; import de.rcenvironment.core.communication.configuration.NodeConfigurationService; import de.rcenvironment.core.communication.messaging.direct.api.DirectMessagingSender; import de.rcenvironment.core.communication.model.InitialNodeInformation; 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.model.internal.NetworkGraphImpl; import de.rcenvironment.core.communication.model.internal.NetworkGraphLinkImpl; 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.routing.MessageRoutingService; import de.rcenvironment.core.communication.routing.NetworkRoutingService; import de.rcenvironment.core.communication.routing.internal.v2.Link; import de.rcenvironment.core.communication.routing.internal.v2.LinkState; import de.rcenvironment.core.communication.routing.internal.v2.LinkStateKnowledgeChangeListener; import de.rcenvironment.core.communication.routing.internal.v2.NoRouteToNodeException; import de.rcenvironment.core.communication.spi.NetworkTopologyChangeListener; import de.rcenvironment.core.communication.spi.NetworkTopologyChangeListenerAdapter; import de.rcenvironment.core.toolkitbridge.transitional.StatsCounter; import de.rcenvironment.core.utils.common.StringUtils; import de.rcenvironment.core.utils.common.service.AdditionalServiceDeclaration; import de.rcenvironment.core.utils.common.service.AdditionalServicesProvider; import de.rcenvironment.core.utils.incubator.DebugSettings; /** * A implementation of the {@link NetworkRoutingService} interface. * * @author Phillip Kroll * @author Robert Mischke */ public class NetworkRoutingServiceImpl implements NetworkRoutingService, MessageRoutingService, AdditionalServicesProvider { /** * Keeps track of of distributed link state changes to adapt the local graph knowledge. * * @author Robert Mischke */ private final class LinkStateKnowledgeChangeTracker implements LinkStateKnowledgeChangeListener { // private LinkState localLinkState = new LinkState(new ArrayList<Link>()); @Override public void onLinkStateKnowledgeChanged(Map<InstanceNodeSessionId, LinkState> knowledge) { if (verboseLogging) { StringBuilder buffer = new StringBuilder(); buffer.append(StringUtils.format("New link state knowledge of %s (%d entries):", localInstanceSessionId, knowledge.size())); for (Entry<InstanceNodeSessionId, LinkState> entry : knowledge.entrySet()) { buffer.append(StringUtils.format("\n Link state for %s: %s", entry.getKey(), entry.getValue())); } log.debug(buffer.toString()); } if (knowledge.size() == 0) { // before any link state is known, remain at the initial placeholder model return; } // consistency check if (localInstanceSessionId == null) { throw new IllegalStateException(); } NetworkGraphImpl rawGraph = new NetworkGraphImpl(localInstanceSessionId); Set<InstanceNodeSessionId> nodeIdsWithLinkState = knowledge.keySet(); for (InstanceNodeSessionId nodeId : nodeIdsWithLinkState) { if (nodeId == null) { throw new IllegalArgumentException("Map contained 'null' node id"); } addNode(rawGraph, nodeId); } // consistency check int expectedGraphSize = knowledge.size(); if (!knowledge.containsKey(localInstanceSessionId)) { expectedGraphSize++; } if (rawGraph.getNodeCount() != expectedGraphSize) { throw new IllegalStateException(StringUtils.format("Graph with %d nodes constructed, but expectes size was %d", rawGraph.getNodeCount(), localInstanceSessionId)); } int totalLinks = 0; for (Map.Entry<InstanceNodeSessionId, LinkState> entry : knowledge.entrySet()) { InstanceNodeSessionId sourceNodeId = entry.getKey(); LinkState linkState = entry.getValue(); List<Link> links = linkState.getLinks(); addLinks(rawGraph, sourceNodeId, links); totalLinks += linkState.getLinks().size(); } // consistency check if (rawGraph.getLinkCount() != totalLinks) { throw new IllegalStateException(); } updateFromRawNetworkGraph(rawGraph); } private void addNode(NetworkGraphImpl rawGraph, InstanceNodeSessionId nodeId) { rawGraph.addNode(nodeId); } private void addLinks(NetworkGraphImpl rawGraph, InstanceNodeSessionId sourceNodeId, List<Link> links) { for (Link link : links) { InstanceNodeSessionId targetNodeId; try { targetNodeId = nodeIdentifierService.parseInstanceNodeSessionIdString(link.getNodeIdString()); } catch (IdentifierException e) { // note: currently not handling malformed ids here; will throw RTE on failure throw NodeIdentifierUtils.wrapIdentifierException(e); } rawGraph.addLink(new NetworkGraphLinkImpl(link.getLinkId(), sourceNodeId, targetNodeId)); } } @Override public void onLinkStatesUpdated(Map<InstanceNodeSessionId, LinkState> delta) { if (verboseLogging) { log.debug("Updated link states for " + delta.size() + " nodes: " + delta.keySet()); } } @Override public void onLocalLinkStateUpdated(LinkState linkState) { if (verboseLogging) { log.debug("Local link state updated (for " + localInstanceSessionId + "): " + linkState); } // localLinkState = linkState; } } /** * Initial listener for low-level topology changes. This is the only listener that receives change events from the routing layer, and * delegates these events to external listeners. Each callback to an external listener is performed in a separate thread to prevent * blocking listeners from affecting the calling code. * * @author Robert Mischke */ private class LowLevelNetworkTopologyChangeHandler extends NetworkTopologyChangeListenerAdapter { @Override public void onNetworkTopologyChanged() { NetworkGraphImpl rawNetworkGraph; synchronized (topologyMap) { log.debug(StringUtils.format("Low-level topology change detected; the topology map of %s" + " now contains %d node(s) and %d connection(s)", localInstanceSessionId, topologyMap.getNodeCount(), topologyMap.getLinkCount())); rawNetworkGraph = (NetworkGraphImpl) topologyMap.toRawNetworkGraph(); } // forward to outer class // updateFromRawNetworkGraph(rawNetworkGraph); } } private InitialNodeInformation ownNodeInformation; private MessageChannelService messageChannelService; private DirectMessagingSender directMessagingSender; private NodeConfigurationService nodeConfigurationService; private LinkStateRoutingProtocolManager protocolManager; private volatile NetworkGraphImpl cachedRawNetworkGraph; private volatile NetworkGraphImpl cachedReachableNetworkGraph; private InstanceNodeSessionId localInstanceSessionId; private TopologyMap topologyMap; private final NetworkTopologyChangeTracker topologyChangeTracker = new NetworkTopologyChangeTracker(); private final boolean verboseLogging = DebugSettings.getVerboseLoggingEnabled(getClass()); // NOTE: used in several locations private final boolean forceLocalRPCSerialization = System .getProperty(NodeConfigurationService.SYSTEM_PROPERTY_FORCE_LOCAL_RPC_SERIALIZATION) != null; private final Log log = LogFactory.getLog(getClass()); private int routedRequestTimeoutMsec; private int forwardingTimeoutMsec; private NodeIdentifierService nodeIdentifierService; /** * OSGi activate method. */ public void activate() { ownNodeInformation = nodeConfigurationService.getInitialNodeInformation(); routedRequestTimeoutMsec = nodeConfigurationService.getRequestTimeoutMsec(); forwardingTimeoutMsec = nodeConfigurationService.getForwardingTimeoutMsec(); localInstanceSessionId = ownNodeInformation.getInstanceNodeSessionId(); // create initial placeholders NetworkGraphImpl initialRawNetworkGraph = new NetworkGraphImpl(localInstanceSessionId); updateFromRawNetworkGraph(initialRawNetworkGraph); // initialize tracker with initial graph topologyChangeTracker.updateReachableNetwork(cachedReachableNetworkGraph); // topologyMap = new TopologyMap(ownNodeInformation); // protocolManager = new LinkStateRoutingProtocolManager(topologyMap, connectionService, new // LowLevelNetworkTopologyChangeHandler()); // TODO set here to break up cyclic dependency; refactor? - misc_ro messageChannelService.setForwardingService(this); } @Override public Collection<AdditionalServiceDeclaration> defineAdditionalServices() { List<AdditionalServiceDeclaration> result = new ArrayList<AdditionalServiceDeclaration>(); result.add(new AdditionalServiceDeclaration(LinkStateKnowledgeChangeListener.class, new LinkStateKnowledgeChangeTracker())); return result; } @Override public NetworkResponse performRoutedRequest(byte[] payload, String messageType, InstanceNodeSessionId receiver) { // TODO find a more generic solution to this final NodeIdentifierService previousService = NodeIdentifierContextHolder.setDeserializationServiceForCurrentThread(nodeIdentifierService); try { return performRoutedRequest(payload, messageType, receiver, routedRequestTimeoutMsec); } finally { NodeIdentifierContextHolder.setDeserializationServiceForCurrentThread(previousService); } } @Override public NetworkResponse performRoutedRequest(byte[] payload, String messageType, InstanceNodeSessionId receiver, int timeoutMsec) { final NetworkRequest request = NetworkRequestFactory.createNetworkRequest(payload, messageType, localInstanceSessionId, receiver); if (forceLocalRPCSerialization && receiver.equals(localInstanceSessionId)) { return messageChannelService.handleLocalForcedSerializationRPC(request, localInstanceSessionId); } return sendToNextHopAndAwaitResponse(request, timeoutMsec); } @Override public NetworkResponse forwardAndAwait(NetworkRequest forwardingRequest) { // TODO refactor/improve? return forwardToNextHop(forwardingRequest); } @Override public List<? extends NetworkGraphLink> getRouteTo(InstanceNodeSessionId destination) { return cachedReachableNetworkGraph.getRoutingInformation().getRouteTo(destination); } @Override public synchronized NetworkGraph getRawNetworkGraph() { return cachedRawNetworkGraph; } @Override public synchronized NetworkGraph getReachableNetworkGraph() { return cachedReachableNetworkGraph; } /** * TODO Restrict method visibility. * * @return Returns the protocol. */ @Override public LinkStateRoutingProtocolManager getProtocolManager() { return protocolManager; } /** * OSGi-DS bind method; public for integration test access. * * @param service The network connection service. */ public void bindMessageChannelService(MessageChannelService service) { // do not allow rebinding for now if (this.messageChannelService != null) { throw new IllegalStateException(); } this.messageChannelService = service; // note: currently extending each other this.directMessagingSender = service; } /** * OSGi-DS bind method; public for integration test access. * * @param service The configuration service. */ public void bindNodeConfigurationService(NodeConfigurationService service) { // do not allow rebinding for now if (this.nodeConfigurationService != null) { throw new IllegalStateException(); } this.nodeConfigurationService = service; this.nodeIdentifierService = nodeConfigurationService.getNodeIdentifierService(); } /** * Adds a new {@link NetworkTopologyChangeListener}. This method is not part of the service interface; it is only meant to be used via * OSGi-DS (whiteboard pattern) and integration tests. * * @param listener the listener */ public void addNetworkTopologyChangeListener(NetworkTopologyChangeListener listener) { topologyChangeTracker.addListener(listener); } /** * Removes a {@link NetworkTopologyChangeListener}. This method is not part of the service interface; it is only meant to be used via * OSGi-DS (whiteboard pattern) and integration tests. * * @param listener the listener */ public void removeNetworkTopologyChangeListener(NetworkTopologyChangeListener listener) { topologyChangeTracker.removeListener(listener); } @Override public String getFormattedNetworkInformation(String type) { if ("info".equals(type)) { return NetworkFormatter.networkGraphToConsoleInfo(cachedReachableNetworkGraph); } if ("graphviz".equals(type)) { return NetworkFormatter.networkGraphToGraphviz(cachedReachableNetworkGraph, true); } if ("graphviz-all".equals(type)) { return NetworkFormatter.networkGraphToGraphviz(cachedRawNetworkGraph, true); } throw new IllegalArgumentException("Invalid type: " + type); } protected synchronized void updateFromRawNetworkGraph(NetworkGraphImpl rawNetworkGraph) { cachedRawNetworkGraph = rawNetworkGraph; cachedReachableNetworkGraph = rawNetworkGraph.reduceToReachableGraph(); if (verboseLogging) { log.debug(StringUtils.format( "Updating %s with a raw graph of %d nodes and %d edges resulted in a reachable graph of %d nodes and %d edges", localInstanceSessionId, rawNetworkGraph.getNodeCount(), rawNetworkGraph.getLinkCount(), cachedReachableNetworkGraph.getNodeCount(), cachedReachableNetworkGraph.getLinkCount())); } // FIXME debug output; remove when done // log.debug("Raw network graph update:\n" + NetworkFormatter.networkGraphToGraphviz(cachedRawNetworkGraph, true)); // log.debug("Reachable network graph update:\n" + NetworkFormatter.networkGraphToGraphviz(cachedReachableNetworkGraph, true)); StatsCounter.count("Network topology/routing", "Network graph changes"); if (topologyChangeTracker.updateReachableNetwork(cachedReachableNetworkGraph)) { StatsCounter.count("Network topology/routing", "Set of reachable nodes changes"); } else { if (verboseLogging) { log.debug("Ignoring low-level topology change event, as it had no effect on the set of reachable nodes"); } } } private NetworkResponse forwardToNextHop(final NetworkRequest forwardingRequest) { // extract common metadata for logging MessageMetaData metadata = forwardingRequest.accessMetaData(); String requestId = forwardingRequest.getRequestId(); String localNodeIdString = localInstanceSessionId.getInstanceNodeSessionIdString(); String sender = metadata.getSenderIdString(); String receiver = metadata.getFinalRecipientIdString(); // TODO while improved in 7.0, this still blocks one thread for each forwarded request final NetworkResponse response = sendToNextHopAndAwaitResponse(forwardingRequest, forwardingTimeoutMsec); // should be redundant; can be removed in 8.0.0 if (response == null) { throw new IllegalStateException( StringUtils.format("NULL response after forwarding message from %s to %s at %s (ReqId=%s)", sender, receiver, localNodeIdString, requestId)); } return response; } private NetworkResponse sendToNextHopAndAwaitResponse(final NetworkRequest request, int timeoutMsec) { WaitForResponseBlocker responseBlocker = new WaitForResponseBlocker(request, localInstanceSessionId); sendToNextHopAsync(request, responseBlocker); return responseBlocker.await(timeoutMsec); // TODO review: attach error source or trace information here if the response does not represent success // try { // response = responseFuture.get(configurationService.getForwardingTimeoutMsec(), TimeUnit.MILLISECONDS); // } catch (TimeoutException e) { // log.warn(StringUtils.format("Timeout while forwarding message from %s to %s at %s (ReqId=%s)", sender, receiver, // ownNodeIdString, requestId)); // response = NetworkResponseFactory.generateResponseForExceptionDuringDelivery(forwardingRequest, ownNodeIdString, e); // } catch (InterruptedException e) { // log.warn(StringUtils.format("Interrupted while forwarding message from %s to %s at %s (ReqId=%s)", sender, receiver, // ownNodeIdString, requestId), e); // response = NetworkResponseFactory.generateResponseForExceptionDuringDelivery(forwardingRequest, ownNodeIdString, e); // } catch (ExecutionException e) { // log.warn( // StringUtils.format("Error while forwarding message from %s to %s at %s (ReqId=%s)", sender, // receiver, ownNodeIdString, requestId), e); // response = NetworkResponseFactory.generateResponseForExceptionDuringDelivery(forwardingRequest, ownNodeIdString, e); // } } private void sendToNextHopAsync(final NetworkRequest request, NetworkResponseHandler responseHandler) { InstanceNodeSessionId receiver = request.accessMetaData().getFinalRecipient(); NetworkGraphLink nextLink; try { // TODO move routing into Callable for faster return of caller thread? (still relevant @4.0?) - misc_ro nextLink = cachedReachableNetworkGraph.getRoutingInformation().getNextLinkTowards(receiver); } catch (NoRouteToNodeException e) { final InstanceNodeSessionId sender = request.accessMetaData().getSender(); log.debug(StringUtils.format("Found no route for a request from %s to %s (type=%s, trace=%s)", sender, receiver, request.getMessageType(), request.accessMetaData().getTrace())); // generate failure response final NetworkResponse response; if (localInstanceSessionId.equals(sender)) { response = NetworkResponseFactory.generateResponseForNoRouteAtSender(request, localInstanceSessionId); } else { response = NetworkResponseFactory.generateResponseForNoRouteWhileForwarding(request, localInstanceSessionId); } // send to result handler responseHandler.onResponseAvailable(response); return; } // if (verboseLogging) { // log.debug(StringUtils.format("Sending routed message for %s towards %s via link %s", // receiver, nextLink.getTargetNodeId(), nextLink.getLinkId())); // } sendDirectMessageAsync(request, nextLink, responseHandler); // TODO restore routing retry? (on higher call level?) // // if there is a route, use it // int routeRetries = 0; // while (route.validate()) { // // // forward message content to next network contact point on the route // WaitForResponseCallable responseCallable = new WaitForResponseCallable(); // if (protocolManager.sendTowardsNeighbor(messageContent, metaData, route.getFirstLink(), // responseCallable)) { // return executorService.submit(responseCallable); // } else { // routeRetries++; // // TODO make limit a constant // if (routeRetries >= 3) { // break; // } // // try to get a new route. // // TODO add retry limit? -- misc_ro // route = protocolManager.getRouteTo(receiver); // } // } // throw new CommunicationException(StringUtils.format("'%s' could not find a route to '%s'.", // ownNodeId, receiver)); } /** * Central method to send a {@link NetworkRequest} into a {@link NetworkGraphLink}. No routing is involved here anymore. * * @param request the {@link NetworkRequest} to send * @param link the {@link NetworkGraphLink} identifying the message channel to use * @param outerResponseHander the {@link NetworkResponseHandler} to report the response to */ private void sendDirectMessageAsync(NetworkRequest request, final NetworkGraphLink link, final NetworkResponseHandler outerResponseHander) { if (outerResponseHander == null) { throw new IllegalArgumentException("Outer response handler must not be null"); } NetworkResponseHandler responseHandler = new NetworkResponseHandler() { @Override public void onResponseAvailable(NetworkResponse response) { // TODO add failure backtrace route here? // if (!response.isSuccess()) { // } outerResponseHander.onResponseAvailable(response); } }; directMessagingSender.sendDirectMessageAsync(request, link.getLinkId(), responseHandler); } }