/* * Copyright (C) 2006-2016 DLR, Germany * * All rights reserved * * http://www.rcenvironment.de/ */ package de.rcenvironment.core.communication.connection.internal; import java.io.Serializable; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Set; import java.util.WeakHashMap; import java.util.concurrent.Callable; import java.util.concurrent.Future; import java.util.concurrent.ThreadLocalRandom; import java.util.concurrent.atomic.AtomicLong; 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.MessageChannelLifecycleListener; import de.rcenvironment.core.communication.channel.MessageChannelService; import de.rcenvironment.core.communication.channel.MessageChannelState; import de.rcenvironment.core.communication.channel.MessageChannelTrafficListener; import de.rcenvironment.core.communication.channel.ServerContactPoint; import de.rcenvironment.core.communication.common.CommunicationException; import de.rcenvironment.core.communication.common.IdentifierException; 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.common.SerializationException; import de.rcenvironment.core.communication.configuration.CommunicationConfiguration; import de.rcenvironment.core.communication.configuration.CommunicationIPFilterConfiguration; import de.rcenvironment.core.communication.configuration.IPWhitelistConnectionFilter; import de.rcenvironment.core.communication.configuration.NodeConfigurationService; import de.rcenvironment.core.communication.messaging.MessageEndpointHandler; import de.rcenvironment.core.communication.messaging.NetworkRequestHandler; 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.NetworkRequestFactory; import de.rcenvironment.core.communication.protocol.NetworkResponseFactory; import de.rcenvironment.core.communication.protocol.ProtocolConstants; import de.rcenvironment.core.communication.protocol.ProtocolConstants.ResultCode; import de.rcenvironment.core.communication.routing.MessageRoutingService; import de.rcenvironment.core.communication.routing.internal.WaitForResponseBlocker; import de.rcenvironment.core.communication.transport.spi.BrokenMessageChannelListener; import de.rcenvironment.core.communication.transport.spi.MessageChannel; import de.rcenvironment.core.communication.transport.spi.MessageChannelEndpointHandler; import de.rcenvironment.core.communication.transport.spi.MessageChannelResponseHandler; import de.rcenvironment.core.communication.transport.spi.NetworkTransportProvider; import de.rcenvironment.core.communication.utils.MessageUtils; import de.rcenvironment.core.toolkitbridge.transitional.ConcurrencyUtils; import de.rcenvironment.core.toolkitbridge.transitional.StatsCounter; import de.rcenvironment.core.utils.common.StringUtils; import de.rcenvironment.core.utils.common.textstream.TextOutputReceiver; import de.rcenvironment.core.utils.incubator.DebugSettings; import de.rcenvironment.toolkit.modules.concurrency.api.AsyncCallback; import de.rcenvironment.toolkit.modules.concurrency.api.AsyncCallbackExceptionPolicy; import de.rcenvironment.toolkit.modules.concurrency.api.AsyncOrderedCallbackManager; import de.rcenvironment.toolkit.modules.concurrency.api.AsyncTaskService; import de.rcenvironment.toolkit.modules.concurrency.api.TaskDescription; /** * Default implementation of {@link MessageChannelService} which also provides the {@link MessageChannelEndpointHandler} interface. * * @author Robert Mischke */ public class MessageChannelServiceImpl implements MessageChannelService { /** * String constant to satisfy CheckStyle; alternatively, introduce several StringUtils.format() calls. */ private static final String SINGLE_QUOTE = "'"; private InitialNodeInformation ownNodeInformation; private Map<String, NetworkTransportProvider> transportProviders; private AsyncOrderedCallbackManager<MessageChannelLifecycleListener> channelListeners; private AsyncOrderedCallbackManager<MessageChannelTrafficListener> trafficListeners; private AsyncTaskService threadPool = ConcurrencyUtils.getAsyncTaskService(); private NodeConfigurationService configurationService; private NodeIdentifierService nodeIdentifierService; // stored here to allow overriding in integration tests private String protocolVersion = ProtocolConstants.PROTOCOL_COMPATIBILITY_VERSION; private RawMessageChannelEndpointHandlerImpl rawMessageChannelEndpointHandler; private MessageRoutingService messageRoutingService; private final BrokenMessageChannelListenerImpl brokenConnectionListener; private MessageEndpointHandler messageEndpointHandler; private final Map<String, MessageChannel> activeOutgoingChannels; private final Map<MessageChannel, MessageChannelHealthState> connectionHealthStates = Collections .synchronizedMap(new WeakHashMap<MessageChannel, MessageChannelHealthState>()); private final AtomicLong healthCheckTaskCounter = new AtomicLong(); private final boolean verboseLogging = DebugSettings.getVerboseLoggingEnabled(getClass()); private final Log logger = LogFactory.getLog(getClass()); private boolean localNodeIsRelay; private final IPWhitelistConnectionFilter globalIPWhitelistFilter; private InstanceNodeSessionId localNodeId; private long requestTimeoutMsec; private volatile boolean shuttingDown = false; /** * Main implementation of {@link MessageChannelEndpointHandler}. * * @author Robert Mischke */ private class RawMessageChannelEndpointHandlerImpl implements MessageChannelEndpointHandler { @Override public InitialNodeInformation exchangeNodeInformation(InitialNodeInformation senderNodeInformation) { // logger.debug(StringUtils.format("Incoming channel from node '%s' (%s); sending identification '%s' (%s)", // senderNodeInformation.getLogName(), senderNodeInformation.getNodeIdentifier(), // ownNodeInformation.getLogName(), // ownNodeInformation.getNodeIdentifier())); mergeRemoteHandshakeInformationIntoGlobalNodeKnowledge(senderNodeInformation); return ownNodeInformation; } @Override public void onRemoteInitiatedChannelEstablished(MessageChannel channel, ServerContactPoint serverContactPoint) { if (!channel.getInitiatedByRemote()) { throw new IllegalStateException("Consistency error"); } registerNewOutgoingChannel(channel); logger.debug(StringUtils.format("Remote-initiated channel '%s' established from '%s' to '%s' via local SCP %s", channel, ownNodeInformation.getLogDescription(), channel.getRemoteNodeInformation().getLogDescription(), serverContactPoint)); } @Override public void onInboundChannelClosing(String idOfInboundChannel) { if (idOfInboundChannel == null) { throw new NullPointerException(idOfInboundChannel); } logger.debug("Inbound message channel is closing, checking for mirror channels; id=" + idOfInboundChannel); for (final MessageChannel channel : getAllOutgoingChannels()) { if (idOfInboundChannel.equals(channel.getAssociatedMirrorChannelId())) { logger.debug(StringUtils.format("Found matching mirror channel for closing inbound channel %s, closing: %s", idOfInboundChannel, channel)); // close the mirror channel in a separate thread to avoid deadlocks if the // caller holds any monitors threadPool.execute(new Runnable() { @Override @TaskDescription("Communication Layer: Close mirror channel after inbound channel was closed") public void run() { // only set flag if the local channel is not closing already, i.e. this is not a double indirect closing if (channel.getState() == MessageChannelState.ESTABLISHED) { channel.markAsClosedBecauseMirrorChannelClosed(); } closeOutgoingChannel(channel); } }, idOfInboundChannel); } } } @Override public NetworkResponse onRawRequestReceived(final NetworkRequest request, final String sourceIdString) { try { NodeIdentifierContextHolder.setDeserializationServiceForCurrentThread(nodeIdentifierService); return onRawRequestReceivedInternal(request, sourceIdString); } finally { NodeIdentifierContextHolder.setDeserializationServiceForCurrentThread(null); } } private NetworkResponse onRawRequestReceivedInternal(final NetworkRequest request, final String sourceIdString) { final InstanceNodeSessionId sourceId; try { sourceId = nodeIdentifierService.parseInstanceNodeSessionIdString(sourceIdString); } catch (IdentifierException e) { throw NodeIdentifierUtils.wrapIdentifierException(e); } // send "request received" event to listeners trafficListeners.enqueueCallback(new AsyncCallback<MessageChannelTrafficListener>() { @Override public void performCallback(MessageChannelTrafficListener listener) { listener.onRequestReceivedFromChannel(request, sourceId); } }); String messageType = request.getMessageType(); final NetworkResponse response; byte[] contentBytes = request.getContentBytes(); if (contentBytes != null) { StatsCounter.registerValue("Messaging: Request payload bytes received by type", messageType, contentBytes.length); } // forward or process? String finalRecipientIdString = request.accessMetaData().getFinalRecipientIdString(); if (finalRecipientIdString == null || ownNodeInformation.getInstanceNodeSessionIdString().equals(finalRecipientIdString)) { // handle locally StatsCounter.count("Messages arrived at destination by type", messageType); response = messageEndpointHandler.onRequestArrivedAtDestination(request); } else { if (!localNodeIsRelay) { // non-relays should never need to forward requests; malicious (forged) request? logger.error("Received a network request that would be forwarded, but the local node is not a relay: " + request); // TODO for future security, make sure this response does not differ from a "normal" routing failure - misc_ro return NetworkResponseFactory.generateResponseForNoRouteWhileForwarding(request, ownNodeInformation.getInstanceNodeSessionId()); } // forward final NetworkRequest forwardingRequest = NetworkRequestFactory.createNetworkRequestForForwarding(request, ownNodeInformation.getInstanceNodeSessionId()); // consistency check: ensure that the request id is maintained on forwarding if (!forwardingRequest.getRequestId().equals(request.getRequestId())) { throw new IllegalStateException("Wrong request id on forwarding"); } // TODO restore TTL/hop count checking here? StatsCounter.count("Messages forwarded by type", messageType); response = messageRoutingService.forwardAndAwait(forwardingRequest); } if (!response.accessMetaData().hasSender()) { // logger.debug("Filling in undefined 'sender' for response to message type " + // request.getMessageType()); response.accessMetaData().setSender(ownNodeInformation.getInstanceNodeSessionId()); } // send "response generated" event to listeners // send "request received" event to listeners trafficListeners.enqueueCallback(new AsyncCallback<MessageChannelTrafficListener>() { @Override public void performCallback(MessageChannelTrafficListener listener) { listener.onResponseSentIntoChannel(response, request, sourceId); } }); return response; } } /** * Listener interface for unexpected channel failures. * * @author Robert Mischke */ private class BrokenMessageChannelListenerImpl implements BrokenMessageChannelListener { @Override public void onChannelBroken(MessageChannel channel) { if (channel.getInitiatedByRemote()) { logger.warn("onChannelBroken called for remote-initiated channel " + channel.getChannelId() + "; ignoring"); return; } handleBrokenChannel(channel); } } /** * Internal status information for a single channel. * * @author Robert Mischke */ private static final class MessageChannelHealthState { private int healthCheckFailureCount; /** * Lock object to prevent concurrent active health checks. */ private final Object healthCheckInProgressLock = new Object(); public boolean healthCheckFailuresAtOrAboveLimit() { return healthCheckFailureCount >= CommunicationConfiguration.CONNECTION_HEALTH_CHECK_FAILURE_LIMIT; } } /** * Default constructor. */ public MessageChannelServiceImpl() { this.transportProviders = new HashMap<String, NetworkTransportProvider>(); this.channelListeners = ConcurrencyUtils.getFactory().createAsyncOrderedCallbackManager(AsyncCallbackExceptionPolicy.LOG_AND_CANCEL_LISTENER); this.trafficListeners = ConcurrencyUtils.getFactory().createAsyncOrderedCallbackManager(AsyncCallbackExceptionPolicy.LOG_AND_CANCEL_LISTENER); this.rawMessageChannelEndpointHandler = new RawMessageChannelEndpointHandlerImpl(); this.brokenConnectionListener = new BrokenMessageChannelListenerImpl(); this.activeOutgoingChannels = new HashMap<String, MessageChannel>(); this.globalIPWhitelistFilter = new IPWhitelistConnectionFilter(); } @Override // TODO rework to synchronous call? - misc_ro, Nov 2015 public Future<MessageChannel> connect(final NetworkContactPoint ncp, final boolean allowDuplex) throws CommunicationException { if (shuttingDown) { throw new CommunicationException("Ignoring a request to connect to " + ncp + " as the network layer is shutting down"); } final NetworkTransportProvider transportProvider = getTransportProvider(ncp.getTransportId()); if (transportProvider == null) { throw new CommunicationException("Unknown transport id: " + ncp.getTransportId()); } Callable<MessageChannel> connectTask = new Callable<MessageChannel>() { @Override @TaskDescription("Communication Layer: Connect to remote node (low-level task)") public MessageChannel call() throws Exception { MessageChannel channel; try { channel = transportProvider.connect(ncp, ownNodeInformation, protocolVersion, allowDuplex, rawMessageChannelEndpointHandler, brokenConnectionListener); } catch (RuntimeException e) { // FIXME add internal event handling of channel failures? logger.error("Failed to connect to " + ncp + " (local node: " + ownNodeInformation.getLogDescription() + ")", e); throw e; } // on success InitialNodeInformation remoteNodeInformation = channel.getRemoteNodeInformation(); mergeRemoteHandshakeInformationIntoGlobalNodeKnowledge(remoteNodeInformation); logger.debug(StringUtils.format("Channel '%s' established from '%s' to '%s' using remote NCP %s", channel, ownNodeInformation.getLogDescription(), remoteNodeInformation.getLogDescription(), ncp)); return channel; } }; return threadPool.submit(connectTask); } @Override public void registerNewOutgoingChannel(final MessageChannel channel) { connectionHealthStates.put(channel, new MessageChannelHealthState()); synchronized (activeOutgoingChannels) { activeOutgoingChannels.put(channel.getChannelId(), channel); } channelListeners.enqueueCallback(new AsyncCallback<MessageChannelLifecycleListener>() { @Override public void performCallback(MessageChannelLifecycleListener listener) { listener.onOutgoingChannelEstablished(channel); } }); } @Override public void closeOutgoingChannel(MessageChannel channel) { if (channel.close()) { unregisterClosedOrBrokenChannel(channel); } } @Override public Set<MessageChannel> getAllOutgoingChannels() { Set<MessageChannel> detachedCopy; synchronized (activeOutgoingChannels) { detachedCopy = new HashSet<MessageChannel>(activeOutgoingChannels.values()); } return detachedCopy; } @Override public MessageChannel getOutgoingChannelById(String id) { synchronized (activeOutgoingChannels) { return activeOutgoingChannels.get(id); } } @Override public void setShutdownFlag(boolean newShuttingDownState) { this.shuttingDown = newShuttingDownState; } @Override public void closeAllOutgoingChannels() { Set<MessageChannel> tempCopyofSet; synchronized (activeOutgoingChannels) { // create a copy as the original set will be modified tempCopyofSet = new HashSet<MessageChannel>(activeOutgoingChannels.values()); } for (MessageChannel channel : tempCopyofSet) { closeOutgoingChannel(channel); } // verify that all connections have been closed and unregistered synchronized (activeOutgoingChannels) { for (MessageChannel channel : activeOutgoingChannels.values()) { logger.warn("Channel list not empty after closing all outgoing connections: " + channel); } } } @Override public void sendDirectMessageAsync(final NetworkRequest request, final MessageChannel channel, final NetworkResponseHandler outerResponseHandler) { // default timeout sendDirectMessageAsync(request, channel, outerResponseHandler, configurationService.getRequestTimeoutMsec()); } @Override public void sendDirectMessageAsync(final NetworkRequest request, final MessageChannel channel, final NetworkResponseHandler outerResponseHandler, int timeoutMsec) { // sanity check to produce a useful stacktrace (in case a null channel slips through standard handling) if (channel == null) { throw new NullPointerException("Null channel passed to sendRequest(); request=" + request); } // except for specialized unit/integration tests, this is a place where all messaging // passes through, so it is a good place to gather statistics - misc_ro final byte[] contentBytes = request.getContentBytes(); final String messageType = request.getMessageType(); // StatsCounter.count("Messages sent by type", messageType); MessageChannelResponseHandler responseHandler = new MessageChannelResponseHandler() { @Override public void onResponseAvailable(NetworkResponse response) { outerResponseHandler.onResponseAvailable(response); byte[] contentBytes = response.getContentBytes(); if (contentBytes != null) { StatsCounter.registerValue("Messaging: Response payload bytes received by type", messageType, contentBytes.length); } } @Override public void onChannelBroken(NetworkRequest request, MessageChannel channel) { // send a proper response to the caller, instead of causing a timeout // note: currently not generating an error id, as there is no useful/helpful information to log here outerResponseHandler.onResponseAvailable(NetworkResponseFactory.generateResponseForChannelCloseWhileWaitingForResponse( request, ownNodeInformation.getInstanceNodeSessionId(), null)); handleBrokenChannel(channel); } }; // check for missing sender information (can be disabled after testing) if (request.accessMetaData().getSenderIdString() == null) { logger.warn("Sending message of type " + request.getMessageType() + " with empty 'sender' field"); } channel.sendRequest(request, responseHandler, timeoutMsec); trafficListeners.enqueueCallback(new AsyncCallback<MessageChannelTrafficListener>() { @Override public void performCallback(MessageChannelTrafficListener listener) { listener.onRequestSentIntoChannel(request); } }); if (contentBytes != null) { StatsCounter.registerValue("Messaging: Request payload bytes sent by type", messageType, contentBytes.length); } } @Override public void sendDirectMessageAsync(NetworkRequest request, String channelId, NetworkResponseHandler responseHandler) { // any MessageChannel seen by outside code is guaranteed to have been registered already; // it is possible, however, that it has been unregistered in the meantime because it was closed MessageChannel channel = getOutgoingChannelById(channelId); if (channel != null) { sendDirectMessageAsync(request, channel, responseHandler); } else { // no relevant additional information, so don't create an error id logger.debug("No message channel for id " + channelId + "; most likely, it has just been closed and therefore deregistered"); responseHandler.onResponseAvailable(NetworkResponseFactory.generateResponseForCloseOrBrokenChannelDuringRequestDelivery( request, localNodeId, null)); } } @Override public NetworkResponse sendDirectMessageBlocking(final NetworkRequest request, final MessageChannel channel, int timeout) { WaitForResponseBlocker responseBlocker = new WaitForResponseBlocker(request, localNodeId); // responseBlocker.setLogMarker(ownNodeInformation.getWrappedNodeId().getNodeId() + "/sendRequest"); sendDirectMessageAsync(request, channel, responseBlocker, timeout); return responseBlocker.await(timeout); } @Override public NetworkResponse handleLocalForcedSerializationRPC(NetworkRequest request, InstanceNodeSessionId sourceId) { return rawMessageChannelEndpointHandler.onRawRequestReceived(request, sourceId.getInstanceNodeSessionIdString()); } @Override public void registerRequestHandler(String messageType, NetworkRequestHandler handler) { messageEndpointHandler.registerRequestHandler(messageType, handler); } @Override public synchronized void addChannelLifecycleListener(MessageChannelLifecycleListener listener) { synchronized (activeOutgoingChannels) { final Set<MessageChannel> detachedCopy = Collections.unmodifiableSet( new HashSet<MessageChannel>(activeOutgoingChannels.values())); // send initial callback channelListeners.addListenerAndEnqueueCallback(listener, new AsyncCallback<MessageChannelLifecycleListener>() { @Override public void performCallback(MessageChannelLifecycleListener listener) { listener.setInitialMessageChannels(detachedCopy); } }); } } @Override public void removeChannelLifecycleListener(MessageChannelLifecycleListener listener) { channelListeners.removeListener(listener); } @Override public void addTrafficListener(MessageChannelTrafficListener listener) { trafficListeners.addListener(listener); } @Override public ServerContactPoint startServer(NetworkContactPoint ncp) throws CommunicationException { NetworkTransportProvider transportProvider = getTransportProvider(ncp.getTransportId()); ServerContactPoint scp = new ServerContactPoint(transportProvider, ncp, protocolVersion, rawMessageChannelEndpointHandler, globalIPWhitelistFilter); scp.start(); return scp; } @Override public void loadAndApplyIPFilterConfiguration() { CommunicationIPFilterConfiguration ipFilterConfiguration = configurationService.getIPFilterConfiguration(); if (ipFilterConfiguration.getEnabled()) { globalIPWhitelistFilter.configure(ipFilterConfiguration.getAllowedIPs()); } else { globalIPWhitelistFilter.configure(null); } } @Override public void printIPFilterInformation(TextOutputReceiver outputReceiver) { Set<String> acceptedIps = globalIPWhitelistFilter.getAcceptedIps(); if (acceptedIps != null) { outputReceiver.addOutput(StringUtils.format("IP filtering is ENABLED; incoming connections are restricted to %d source IPs", acceptedIps.size())); } else { outputReceiver.addOutput(StringUtils.format("IP filtering is DISABLED; all incoming connections are accepted")); } } @Override public void triggerHealthCheckForAllChannels() { synchronized (activeOutgoingChannels) { for (final MessageChannel channel : activeOutgoingChannels.values()) { String uniqueTaskId = StringUtils.format("%s-%s", channel.getChannelId(), Long.toString(healthCheckTaskCounter.incrementAndGet())); threadPool.execute(new Runnable() { @Override @TaskDescription("Communication Layer: Channel health check") public void run() { // random delay ("jitter") to avoid all connections being checked at once try { try { Thread.sleep(ThreadLocalRandom.current().nextInt( CommunicationConfiguration.CONNECTION_HEALTH_CHECK_MAX_JITTER_MSEC)); } catch (InterruptedException e) { logger.debug("Interrupted while waiting to perform the next connection health check, skipping"); return; } performHealthCheckAndActOnResult(channel); } catch (InterruptedException e) { logger.debug("Interruption during channel health check", e); } } }, uniqueTaskId); } } } @Override public void addNetworkTransportProvider(NetworkTransportProvider provider) { logger.debug("Registering transport provider for id '" + provider.getTransportId() + SINGLE_QUOTE); synchronized (transportProviders) { String id = provider.getTransportId(); NetworkTransportProvider previous = transportProviders.put(id, provider); if (previous != null) { transportProviders.put(id, previous); throw new IllegalStateException("Duplicate transport for id '" + id + SINGLE_QUOTE); } } } /** * Removes a registered {@link NetworkTransportProvider}. Trying to remove a non-existing provider results in an * {@link IllegalStateException}. * * @param provider the transport provider to remove */ public void removeNetworkTransportProvider(NetworkTransportProvider provider) { logger.debug("Unregistering transport provider for id '" + provider.getTransportId() + SINGLE_QUOTE); synchronized (transportProviders) { // consistency check NetworkTransportProvider removed = transportProviders.remove(provider.getTransportId()); if (removed != provider) { throw new IllegalStateException("Transport to remove was not actually registered: " + provider); } } } /** * FIXME Should be called 'getNetworkTransportProvider'. --krol_ph * * @param transportId * @return */ private NetworkTransportProvider getTransportProvider(String transportId) { synchronized (transportProviders) { NetworkTransportProvider provider = transportProviders.get(transportId); if (provider == null) { throw new IllegalStateException("No transport registered for id " + transportId); } return provider; } } /** * OSGi-DS bind method; public for integration test access. * * @param newService the service to bind */ @Override public void setMessageEndpointHandler(MessageEndpointHandler newService) { this.messageEndpointHandler = newService; } @Override public void setForwardingService(MessageRoutingService newService) { this.messageRoutingService = newService; } @Override public void overrideProtocolVersion(String newVersion) { this.protocolVersion = newVersion; } /** * OSGi-DS bind method; public for integration test access. * * @param newService the service to bind */ public void bindNodeConfigurationService(NodeConfigurationService newService) { // do not allow rebinding for now if (this.configurationService != null) { throw new IllegalStateException(); } this.configurationService = newService; this.nodeIdentifierService = configurationService.getNodeIdentifierService(); } /** * OSGi-DS lifecycle method. */ public void activate() { ownNodeInformation = configurationService.getInitialNodeInformation(); localNodeId = ownNodeInformation.getInstanceNodeSessionId(); localNodeIsRelay = configurationService.isRelay(); requestTimeoutMsec = configurationService.getRequestTimeoutMsec(); synchronized (transportProviders) { int numTransports = transportProviders.size(); logger.debug(StringUtils.format( "Activated network channel service; instance log name='%s'; node id='%s'; %d registered transport providers", ownNodeInformation.getLogDescription(), ownNodeInformation.getInstanceNodeSessionId(), numTransports)); } loadAndApplyIPFilterConfiguration(); } /** * OSGi-DS lifecycle method. */ public void deactivate() { logger.debug("Deactivating"); } /** * Field access for unit tests. * * @param nodeInformation */ protected void setNodeInformation(InitialNodeInformation nodeInformation) { ownNodeInformation = nodeInformation; } /** * Field access for unit tests. * * @return the {@link NodeIdentifierService} of the message-receiving node */ public NodeIdentifierService getNodeIdentifierService() { return nodeIdentifierService; } /** * Updates the associated information for a node from a received or locally-generated {@link InitialNodeInformation} object. * * @param remoteNodeInformation the object to update from */ private void mergeRemoteHandshakeInformationIntoGlobalNodeKnowledge(InitialNodeInformation remoteNodeInformation) { nodeIdentifierService.associateDisplayName(remoteNodeInformation.getInstanceNodeSessionId(), remoteNodeInformation.getDisplayName()); } private void performHealthCheckAndActOnResult(final MessageChannel channel) throws InterruptedException { final MessageChannelHealthState connectionState = connectionHealthStates.get(channel); if (connectionState == null) { logger.error("Internal error: Found no health state object for channel " + channel + "; closing channel"); triggerAsyncClosingOfBrokenChannel(channel); return; } // synchronize on lock object to prevent concurrent checks synchronized (connectionState.healthCheckInProgressLock) { // check if the channel was closed or marked as broken in the meantime if (!channel.isReadyToUse()) { logger.debug(StringUtils.format("Channel %s is %s; skipping scheduled health check", channel.getChannelId(), channel.getState())); return; } if (verboseLogging) { logger.debug("Performing health check on " + channel); } boolean checkSuccessful = performConnectionHealthCheckRequestResponse(channel); boolean considerChannelBroken = false; // keep synchronization on state object short synchronized (connectionState) { if (checkSuccessful) { // log if this was a recovery if (connectionState.healthCheckFailureCount > 0) { logger.debug(StringUtils.format( "Channel %s to %s passed its health check after %d previous failures", channel.getChannelId(), channel.getRemoteNodeInformation().getInstanceNodeSessionId(), connectionState.healthCheckFailureCount)); } // reset counter connectionState.healthCheckFailureCount = 0; } else { // increase counter and log connectionState.healthCheckFailureCount++; // TODO try to merge with other log message ("Received no response for a health check ...") logger.debug(StringUtils.format( "Channel %s to %s failed a health check (%d consecutive failures)", channel.getChannelId(), channel.getRemoteNodeInformation().getInstanceNodeSessionId(), connectionState.healthCheckFailureCount)); // limit exceeded? -> consider broken if (connectionState.healthCheckFailuresAtOrAboveLimit()) { considerChannelBroken = true; } } } if (considerChannelBroken) { triggerAsyncClosingOfBrokenChannel(channel); } } } private void triggerAsyncClosingOfBrokenChannel(final MessageChannel channel) { threadPool.execute(new Runnable() { @Override @TaskDescription("Communication Layer: Close broken channel after health check failure") public void run() { handleBrokenChannel(channel); } }); } /** * Performs a single request/response attempt with a random token. The receiver should reply with the same token as the response * content. * * @param channel the channel to test * @return true if the check was successful * @throws InterruptedException on thread interruption */ private boolean performConnectionHealthCheckRequestResponse(final MessageChannel channel) throws InterruptedException { String randomToken = Integer.toString(ThreadLocalRandom.current().nextInt()); byte[] contentBytes = MessageUtils.serializeSafeObject(randomToken); NetworkRequest request = NetworkRequestFactory.createNetworkRequest(contentBytes, ProtocolConstants.VALUE_MESSAGE_TYPE_HEALTH_CHECK, ownNodeInformation.getInstanceNodeSessionId(), null); NetworkResponse response = sendDirectMessageBlocking(request, channel, CommunicationConfiguration.CONNECTION_HEALTH_CHECK_TIMEOUT_MSEC); if (response.isSuccess()) { try { Serializable deserializedContent = response.getDeserializedContent(); // verify that the response contained the same token; this check *should* never fail if (!randomToken.equals(deserializedContent)) { logger.warn(StringUtils.format( "Received successful response for a health check on channel '%s', but it contained unexpected content: %s", channel.getChannelId(), deserializedContent)); return false; } } catch (SerializationException e) { logger.warn(StringUtils.format( "Received successful response for a health check on channel '%s', but there was an error deserializing its content", channel.getChannelId()), e); return false; } if (verboseLogging) { logger.debug("Health check on channel " + channel + " passed"); } return true; } else { final int numericResultCode = response.getResultCode().getCode(); Serializable deserializedContent = null; try { deserializedContent = response.getDeserializedContent(); } catch (SerializationException e) { logger.warn( StringUtils.format( "Received non-successful response for a health check on channel '%s' (error code %d), " + "and there was also an error deserializing its content", channel.getChannelId(), numericResultCode), e); } if (response.getResultCode() == ResultCode.TIMEOUT_WAITING_FOR_RESPONSE) { logger.warn(StringUtils.format( "Received no response for a health check on message channel '%s' within %,d milliseconds; " + "the connection or the remote instance may be overloaded", channel.getChannelId(), CommunicationConfiguration.CONNECTION_HEALTH_CHECK_TIMEOUT_MSEC)); } else if (response.getResultCode() == ResultCode.CHANNEL_OR_RESPONSE_LISTENER_SHUT_DOWN_WHILE_WAITING_FOR_RESPONSE) { // in this case, the channel should be shutting down anyway, so don't log a health check failure logger.debug(StringUtils.format( "Message channel '%s' was closed while waiting for a health check response; " + "not counting as an additional health check failure", channel.getChannelId(), CommunicationConfiguration.CONNECTION_HEALTH_CHECK_TIMEOUT_MSEC)); return true; } else { logger.warn(StringUtils.format("Received non-success response for a health check on channel '%s': " + "error code %d, content: %s", channel.getChannelId(), numericResultCode, deserializedContent)); } return false; } } private void handleBrokenChannel(MessageChannel channel) { String remoteNodeText = "(no node information available)"; // guard against the case when the handshake has not completed yet if (channel.getRemoteNodeInformation() != null) { remoteNodeText = channel.getRemoteNodeInformation().getInstanceNodeSessionId().toString(); } logger.debug("Closing broken channel to " + remoteNodeText + " (id=" + channel.getChannelId() + ")"); if (channel.markAsBroken()) { unregisterClosedOrBrokenChannel(channel); } // TODO possible optimization: find related requests waiting for response and cancel them } private void unregisterClosedOrBrokenChannel(final MessageChannel channel) { synchronized (activeOutgoingChannels) { MessageChannel removed = activeOutgoingChannels.remove(channel.getChannelId()); // consistency check if (removed != channel) { logger.warn(StringUtils.format("Unexpected state: Expected to find same registered channel object for " + "closed or broken channel %s, but found '%s' instead", channel.getChannelId(), removed)); } } logger.debug("Notifying listeners of shutdown of channel " + channel.getChannelId()); channelListeners.enqueueCallback(new AsyncCallback<MessageChannelLifecycleListener>() { @Override public void performCallback(MessageChannelLifecycleListener listener) { listener.onOutgoingChannelTerminated(channel); } }); } }