/*
* Copyright (C) 2006-2016 DLR, Germany
*
* All rights reserved
*
* http://www.rcenvironment.de/
*/
package de.rcenvironment.core.communication.testutils;
import java.io.Serializable;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import de.rcenvironment.core.communication.api.CommunicationService;
import de.rcenvironment.core.communication.channel.MessageChannelLifecycleListener;
import de.rcenvironment.core.communication.channel.MessageChannelService;
import de.rcenvironment.core.communication.channel.MessageChannelTrafficListener;
import de.rcenvironment.core.communication.common.CommunicationException;
import de.rcenvironment.core.communication.common.InstanceNodeId;
import de.rcenvironment.core.communication.common.InstanceNodeSessionId;
import de.rcenvironment.core.communication.common.NetworkGraph;
import de.rcenvironment.core.communication.common.NetworkGraphLink;
import de.rcenvironment.core.communication.common.SerializationException;
import de.rcenvironment.core.communication.configuration.NodeConfigurationService;
import de.rcenvironment.core.communication.connection.api.ConnectionSetup;
import de.rcenvironment.core.communication.connection.api.ConnectionSetupService;
import de.rcenvironment.core.communication.connection.api.ConnectionSetupState;
import de.rcenvironment.core.communication.management.CommunicationManagementService;
import de.rcenvironment.core.communication.model.NetworkContactPoint;
import de.rcenvironment.core.communication.model.NetworkResponse;
import de.rcenvironment.core.communication.nodeproperties.NodePropertiesService;
import de.rcenvironment.core.communication.nodeproperties.NodePropertyConstants;
import de.rcenvironment.core.communication.protocol.ProtocolConstants;
import de.rcenvironment.core.communication.routing.MessageRoutingService;
import de.rcenvironment.core.communication.routing.NetworkRoutingService;
import de.rcenvironment.core.communication.routing.internal.NetworkFormatter;
import de.rcenvironment.core.communication.routing.internal.NetworkRoutingServiceImpl;
import de.rcenvironment.core.communication.spi.NetworkTopologyChangeListenerAdapter;
import de.rcenvironment.core.communication.transport.spi.NetworkTransportProvider;
import de.rcenvironment.core.communication.utils.MessageUtils;
import de.rcenvironment.core.utils.common.StringUtils;
/**
* Provides a simulated/"virtual" node instance. Intended for use in integration testing; a major use case is setting up networks of virtual
* nodes to test network dynamics and routing behavior.
*
* @author Robert Mischke
*/
public class VirtualInstance extends VirtualInstanceSkeleton implements CommonVirtualInstanceControl {
private static volatile boolean rememberRuntimePeersAfterRestart;
private CommunicationManagementService managementService;
private NetworkRoutingService networkRoutingService;
private MessageRoutingService messageRoutingService;
private MessageChannelService messageChannelService;
private ConnectionSetupService connectionSetupService;
private NodeConfigurationService nodeConfigurationService;
private VirtualCommunicationBundle virtualCommunicationBundle;
private CommunicationService communicationService;
/**
* Creates a virtual instance with a given log/display name, and its "relay" flag set to "true".
*
* Note that enabling "is relay" by default is done for backward compatibility of integration tests; the actual default configuration
* setting is "false".
*
* @param displayName the string to use as display/log name
*/
public VirtualInstance(String displayName) {
this(displayName, true);
}
/**
* Creates a virtual instance with a given log/display name, and a selectable "relay" flag value.
*
* Note that enabling "is relay" by default is done for backward compatibility of integration tests; the actual default configuration
* setting is "false".
*
* @param displayName the string to use as display/log name
* @param isRelay whether the "is relay" flag of this node should be set
*/
public VirtualInstance(String displayName, boolean isRelay) {
this(null, displayName, isRelay); // null = assign a random instance id
}
/**
* Creates a virtual instance with the given log/display name, a custom {@link InstanceNodeSessionId}, and a selectable "relay" flag
* value.
*
* @param predefinedInstanceId an optional predefined instance id's string form; if null, an instance id is created automatically
* @param displayName the string to use as display/log name
* @param isRelay whether the "is relay" flag of this node should be set
*/
public VirtualInstance(String predefinedInstanceId, String displayName, boolean isRelay) {
super(predefinedInstanceId, displayName, isRelay);
virtualCommunicationBundle = VirtualCommunicationBundleFactory.createFromNodeConfigurationService(getNodeConfigurationService());
virtualCommunicationBundle.setAutoStartNetworkOnActivation(false);
virtualCommunicationBundle.activate();
nodeConfigurationService = virtualCommunicationBundle.getService(NodeConfigurationService.class);
messageChannelService = virtualCommunicationBundle.getService(MessageChannelService.class);
connectionSetupService = virtualCommunicationBundle.getService(ConnectionSetupService.class);
networkRoutingService = virtualCommunicationBundle.getService(NetworkRoutingService.class);
messageRoutingService = virtualCommunicationBundle.getService(MessageRoutingService.class);
managementService = virtualCommunicationBundle.getService(CommunicationManagementService.class);
communicationService = virtualCommunicationBundle.getService(CommunicationService.class);
// register custom test message type
messageChannelService.registerRequestHandler(ProtocolConstants.VALUE_MESSAGE_TYPE_TEST,
new TestNetworkRequestHandler(nodeConfigurationService.getInstanceNodeSessionId()));
}
public static void setRememberRuntimePeersAfterRestarts(boolean rememberRuntimePeers) {
VirtualInstance.rememberRuntimePeersAfterRestart = rememberRuntimePeers;
}
/**
* Convenience method to send a payload to another node, using the default request timeout.
*
* @param messageContent the request payload
* @param messageType the message type to send; see {@link ProtocolConstants}
* @param targetNodeId the id of the destination node
* @return the response
* @throws CommunicationException on messaging errors
* @throws InterruptedException on interruption
* @throws ExecutionException on internal errors
* @throws SerializationException on serialization failure
*/
public NetworkResponse performRoutedRequest(Serializable messageContent, String messageType, InstanceNodeSessionId targetNodeId)
throws CommunicationException, InterruptedException, ExecutionException, SerializationException {
byte[] serializedBody = MessageUtils.serializeObject(messageContent);
return messageRoutingService.performRoutedRequest(serializedBody, messageType, targetNodeId);
}
/**
* Convenience method to send a payload to another node. This method blocks until it has received a response, or the timeout was
* exceeded.
*
* @param messageContent the request payload
* @param targetNodeId the id of the destination node
* @param timeoutMsec the maximum time to wait for the response
* @return the response
* @throws CommunicationException on messaging errors
* @throws InterruptedException on interruption
* @throws TimeoutException on timeout
* @throws ExecutionException on internal errors
* @throws SerializationException on serialization failure
*/
public NetworkResponse performRoutedRequest(Serializable messageContent, InstanceNodeSessionId targetNodeId, int timeoutMsec)
throws CommunicationException, InterruptedException, ExecutionException, TimeoutException, SerializationException {
// FIXME review: after API changes, the given timeout is ignored/overruled by the low-level messaging timeout here
return performRoutedRequest(messageContent, ProtocolConstants.VALUE_MESSAGE_TYPE_TEST, targetNodeId);
}
@Override
public void addNetworkConnectionListener(MessageChannelLifecycleListener listener) {
getMessageChannelService().addChannelLifecycleListener(listener);
}
@Override
public void addNetworkTrafficListener(MessageChannelTrafficListener listener) {
getMessageChannelService().addTrafficListener(listener);
}
@Override
public void simulateCustomProtocolVersion(String version) {
getService(MessageChannelService.class).overrideProtocolVersion(version);
}
@Override
public synchronized void addInitialNetworkPeer(NetworkContactPoint contactPoint) {
VirtualInstanceState currentState = getCurrentState();
if (currentState == VirtualInstanceState.STARTED) {
// FIXME transitional code; rewrite calls
log.warn("addInitialNetworkPeer() called for an instance in the STARTED state; change to addRuntimeNetworkPeer()");
connectAsync(contactPoint);
return;
}
if (currentState != VirtualInstanceState.INITIAL) {
throw new IllegalStateException("Initial peers can only be added in the INITIAL state");
}
super.addInitialNetworkPeer(contactPoint);
}
/**
* Adds and connects to a {@link NetworkContactPoint}.
*
* @param contactPoint the {@link NetworkContactPoint} to add and connect to
* @return the new {@link ConnectionSetup}
*/
public synchronized ConnectionSetup connectAsync(NetworkContactPoint contactPoint) {
if (getCurrentState() != VirtualInstanceState.STARTED) {
throw new IllegalStateException("Runtime peers can only be added in the STARTED state (is " + getCurrentState() + ")");
}
if (rememberRuntimePeersAfterRestart) {
// add this as an initial peer for next network startup
super.addInitialNetworkPeer(contactPoint);
}
ConnectionSetup connectionSetup = connectionSetupService.createConnectionSetup(contactPoint, null, true);
connectionSetup.signalStartIntent();
return connectionSetup;
}
/**
* Initiates a connection to the given {@link NetworkContactPoint} and then waits until it is in the
* {@link ConnectionSetupState#CONNECTED} state or the timeout is reached, whichever comes first.
*
* @param contactPoint the {@link NetworkContactPoint} to connect to
* @param timeoutMsec the maximum time to wait return the new {@link ConnectionSetup}
* @return the new {@link ConnectionSetup}
* @throws TimeoutException on timeout
* @throws InterruptedException on thread interruption
*/
public ConnectionSetup connectAndWait(NetworkContactPoint contactPoint, int timeoutMsec) throws TimeoutException, InterruptedException {
ConnectionSetup connection = connectAsync(contactPoint);
connection.awaitState(ConnectionSetupState.CONNECTED, timeoutMsec);
return connection;
}
/**
* Blocks until the given node is part of this instance's reachable network topology, or until the timeout is reached.
*
* @param targetNodeId the node to wait for
* @param timeout the maximum time to wait
* @throws InterruptedException on interruption
* @throws TimeoutException on timeout
*/
public void waitUntilContainsInReachableNodes(final InstanceNodeSessionId targetNodeId, int timeout) throws InterruptedException,
TimeoutException {
if (containsInReachableNodes(targetNodeId)) {
return;
}
final CountDownLatch latch = new CountDownLatch(1);
NetworkRoutingServiceImpl networkRoutingServiceImpl = (NetworkRoutingServiceImpl) networkRoutingService;
NetworkTopologyChangeListenerAdapter topologyChangeListener = new NetworkTopologyChangeListenerAdapter() {
@Override
public void onReachableNodesChanged(Set<InstanceNodeSessionId> reachableNodes, Set<InstanceNodeSessionId> addedNodes,
Set<InstanceNodeSessionId> removedNodes) {
// log.debug("Reachable nodes for " + getNodeId() + " have changed: " + Arrays.toString(reachableNodes.toArray()));
if (reachableNodes.contains(targetNodeId)) {
latch.countDown();
}
}
};
try {
networkRoutingServiceImpl.addNetworkTopologyChangeListener(topologyChangeListener);
// after registering the listener, check again to prevent race conditions
if (containsInReachableNodes(targetNodeId)) {
return;
}
long startTime = System.currentTimeMillis();
if (!latch.await(timeout, TimeUnit.MILLISECONDS)) {
throw new TimeoutException();
}
log.debug(StringUtils.format("Node %s became reachable for node %s after waiting for %,d msec", targetNodeId,
getInstanceNodeSessionId(),
System.currentTimeMillis() - startTime));
} finally {
networkRoutingServiceImpl.removeNetworkTopologyChangeListener(topologyChangeListener);
}
}
/**
* Returns whether the given node is part of this instance's reachable network topology.
*
* @param targetNodeId the node to check for
* @return true if the given node is reachable via the current network
*/
public boolean containsInReachableNodes(InstanceNodeSessionId targetNodeId) {
Set<InstanceNodeSessionId> reachable = networkRoutingService.getReachableNetworkGraph().getNodeIds();
// log.debug("Current reachable nodes for " + getNodeId() + ": " + Arrays.toString(reachable.toArray()));
return reachable.contains(targetNodeId);
}
// TODO examine callers and check if they need to be adapted
// former return code:
// return NetworkFormatter.formatTopologyMap(getRoutingService().getProtocolManager().getTopologyMap(), true);
@Deprecated
public String getFormattedLegacyNetworkGraph() {
return getFormattedRawNetworkGraph();
}
public String getFormattedRawNetworkGraph() {
return NetworkFormatter.networkGraphToGraphviz(getRoutingService().getRawNetworkGraph(), true);
}
public String getFormattedReachableNetworkGraph() {
return NetworkFormatter.networkGraphToGraphviz(getRoutingService().getReachableNetworkGraph(), true);
}
/**
* @return a formatted summary of this instance's LSA (routing) knowledge
*/
public String getFormattedLSAKnowledge() {
StringBuilder buffer = new StringBuilder();
Map<InstanceNodeSessionId, Map<String, String>> properties = getService(NodePropertiesService.class).getAllNodeProperties();
buffer
.append(StringUtils.format("LSA properties as seen by %s (%d entries):", getInstanceNodeSessionId(), properties.size()));
for (Entry<InstanceNodeSessionId, Map<String, String>> entry : properties.entrySet()) {
buffer.append(StringUtils.format("\n %s: %s", entry.getKey(), entry.getValue().get(NodePropertyConstants.KEY_LSA)));
}
return buffer.toString();
}
public String getFormattedNetworkStats() {
return NetworkFormatter.networkStats(getRoutingService().getProtocolManager().getNetworkStats());
}
/**
* Test method: verifies same reporded topology hashes for all known nodes.
*
* TODO verify description
*
* @return true if all hashes are consistent
*/
// TODO only called by obsolete test case in RoutingProtocolTest
@Deprecated
public boolean hasSameTopologyHashesForAllNodes() {
throw new UnsupportedOperationException("Obsolete method");
// return getRoutingService().getProtocolManager().getTopologyMap().hasSameTopologyHashesForAllNodes();
}
/**
* Test method: determine the messaging route to the given node.
*
* @param destination the destination node
* @return the calculated sequence of {@link NetworkGraphLink}s
*/
public List<? extends NetworkGraphLink> getRouteTo(VirtualInstance destination) {
return messageRoutingService.getRouteTo(destination.getInstanceNodeSessionId());
}
public NetworkGraph getRawNetworkGraph() {
return networkRoutingService.getRawNetworkGraph();
}
public NetworkGraph getReachableNetworkGraph() {
return networkRoutingService.getReachableNetworkGraph();
}
// note: before migration to the new network code, it was kind of undefined whether this method should refer
// to the *raw* or *reachable* graph; now it does the former - misc_ro
public int getKnownNodeCount() {
return getReachableNetworkGraph().getNodeCount();
}
/**
* @param targetInstance the other instance to
* @return if there is a direct link/channel to the other instance in this instance's known topology
*/
public boolean knownTopologyContainsLinkTo(VirtualInstance targetInstance) {
// note: before migration to the new network code, it was kind of undefined whether this method should refer
// to the *raw* or *reachable* graph; now it does the former - misc_ro
return getReachableNetworkGraph().containsLinkBetween(this.getInstanceNodeSessionId(),
targetInstance.getInstanceNodeSessionId());
}
/**
* Test method; probably obsolete.
*
* @param messageId a message id
* @return whether this message was received (?)
*/
@Deprecated
public boolean checkMessageReceivedById(String messageId) {
return networkRoutingService.getProtocolManager().messageReivedById(messageId);
}
/**
* Test method; probably obsolete.
*
* @param messageContent a message content
* @return whether this content was received (?)
*/
@Deprecated
// check the response received by the initiating node instead
public boolean checkMessageReceivedByContent(Serializable messageContent) {
return networkRoutingService.getProtocolManager().messageReivedByContent(messageContent);
}
@Override
public void registerNetworkTransportProvider(NetworkTransportProvider provider) {
messageChannelService.addNetworkTransportProvider(provider);
}
/**
* @param <T> the service class to fetch
* @param clazz the service class to fetch
* @return the registered service of the virtual communication bundle
*/
public <T> T getService(Class<T> clazz) {
return virtualCommunicationBundle.getService(clazz);
}
/**
* Allows injection of service instances; useful for remote service call testing.
*
* @param <T> the service interface class
* @param clazz the service interface class
* @param implementation the service instance
*/
public <T> void injectService(Class<T> clazz, T implementation) {
virtualCommunicationBundle.injectService(clazz, implementation);
}
public VirtualCommunicationBundle getVirtualCommunicationBundle() {
return virtualCommunicationBundle;
}
/**
* Provide unit/integration test access to the management service.
*
* @return The management service.
*/
public CommunicationManagementService getManagementService() {
return managementService;
}
/**
* Provide unit/integration test access to the routing service.
*
* @return The communication service.
*/
public CommunicationService getCommunicationService() {
return communicationService;
}
/**
* Provide unit/integration test access to the routing service.
*
* @return The routing service.
*/
public NetworkRoutingService getRoutingService() {
return networkRoutingService;
}
/**
* Provide unit/integration test access to the connection service.
*
* @return The connection service.
*/
public MessageChannelService getMessageChannelService() {
return messageChannelService;
}
/**
* @return The configuration service.
*/
public NodeConfigurationService getConfigurationService() {
return super.getNodeConfigurationService();
}
public ConnectionSetupService getConnectionSetupService() {
return virtualCommunicationBundle.getService(ConnectionSetupService.class);
}
public NodePropertiesService getNodePropertiesService() {
return virtualCommunicationBundle.getService(NodePropertiesService.class);
}
@Override
protected void performStartup() throws InterruptedException, CommunicationException {
managementService.startUpNetwork();
}
@Override
protected void performShutdown() throws InterruptedException {
managementService.shutDownNetwork();
}
@Override
protected void performSimulatedCrash() throws InterruptedException {
// simply shut down the network; this should not send any "goodbye" messages etc.
managementService.simulateUncleanShutdown();
}
/**
* @return the (pseudo-)persistent instance node id of this virtual instance; stable across the lifetime of this {@link VirtualInstance}
* object
*/
public InstanceNodeId getPersistentInstanceNodeId() {
// TODO WIP
return getInstanceNodeSessionId().convertToInstanceNodeId();
}
/**
* @return the current instance node session id of this virtual instance; note that this is only defined and stable between calls to
* {@link #start()} and {@link #shutDown()}.
*/
public InstanceNodeSessionId getInstanceNodeSessionId() {
return nodeInformation.getInstanceNodeSessionId();
}
/**
* @return the equivalent of calling getInstanceNodeSessionId().getInstanceNodeSessionIdString()
*/
public String getInstanceNodeSessionIdString() {
return nodeInformation.getInstanceNodeSessionId().getInstanceNodeSessionIdString();
}
@Override
public String toString() {
// TODO Auto-generated method stub
return getInstanceNodeSessionId().toString();
}
/**
* @return the first server contact point; if none exists, an exception is thrown
*/
public NetworkContactPoint getDefaultContactPoint() {
List<NetworkContactPoint> serverContactPoints = getConfigurationService().getServerContactPoints();
if (serverContactPoints.isEmpty()) {
throw new IllegalStateException("No server contact points configured");
}
return serverContactPoints.get(0);
}
}