/*
* Copyright (C) 2006-2016 DLR, Germany
*
* All rights reserved
*
* http://www.rcenvironment.de/
*/
package de.rcenvironment.core.communication.nodeproperties.internal;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger;
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.MessageChannelLifecycleListenerAdapter;
import de.rcenvironment.core.communication.channel.MessageChannelService;
import de.rcenvironment.core.communication.channel.MessageChannelState;
import de.rcenvironment.core.communication.common.IdentifierException;
import de.rcenvironment.core.communication.common.InstanceNodeSessionId;
import de.rcenvironment.core.communication.common.SerializationException;
import de.rcenvironment.core.communication.configuration.NodeConfigurationService;
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.model.NetworkMessage;
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.nodeproperties.NodePropertiesService;
import de.rcenvironment.core.communication.nodeproperties.NodeProperty;
import de.rcenvironment.core.communication.nodeproperties.NodePropertyConstants;
import de.rcenvironment.core.communication.nodeproperties.spi.RawNodePropertiesChangeListener;
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.transport.spi.MessageChannel;
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.RestartSafeIncreasingValueGenerator;
import de.rcenvironment.core.utils.common.StringUtils;
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.BatchAggregator;
import de.rcenvironment.toolkit.modules.concurrency.api.BatchProcessor;
/**
* Default {@link NodePropertiesService} implementation.
*
* @author Robert Mischke
*/
public class NodePropertiesServiceImpl implements NodePropertiesService {
private static final String MESSAGE_SUBTYPE_INITIAL = "init";
private static final String MESSAGE_SUBTYPE_INCREMENTAL = "delta";
// this limit only serves to avoid excessive aggregated delta sizes; the usual trigger should be the timer
private static final int MAX_DELTA_BATCH_SIZE = 25;
// tweak as necessary
private static final int MAX_DELTA_BATCH_LATENCY = 150;
private final Object knowledgeLock = new Object();
private final NodePropertiesRegistry completeKnowledgeRegistry;
/**
* A reduced {@link NodePropertiesRegistry} that only keeps the properties published by the local node. It is used for two purposes:
* avoid distributing the properties of different networks to each other in non-relay mode; and checking received updates for the local
* node against what was actually published in this session, and overwriting stale entries if necessary.
*
* This instance is always synchronized along with {@link #completeKnowledgeRegistry} via {@link #knowledgeLock}.
*/
private final NodePropertiesRegistry locallyPublishedKnowledgeRegistry;
private final RestartSafeIncreasingValueGenerator timeKeeper = new RestartSafeIncreasingValueGenerator();
private final AsyncOrderedCallbackManager<RawNodePropertiesChangeListener> callbackManager;
private NetworkRequestHandler networkRequestHandler;
private MessageChannelService connectionService;
private DirectMessagingSender directMessagingSender;
private InstanceNodeSessionId localNodeId;
private NodeConfigurationService nodeConfigurationService;
private final AsyncTaskService threadPool = ConcurrencyUtils.getAsyncTaskService();
private final boolean verboseLogging = DebugSettings.getVerboseLoggingEnabled(getClass());
private final BatchAggregator<UpdateDeltaForBroadcasting> deltaBroadcastAggregator;
private final Log log = LogFactory.getLog(getClass());
private boolean localNodeIsRelay;
private NodeIdentifierService nodeIdentifierService;
/**
* Represents the parsed form of a received update.
*
* @author Robert Mischke
*/
private final class IncomingUpdate {
private String[] rawParts;
private String subtype;
private boolean isInitialUpdate;
private List<NodePropertyImpl> entries;
IncomingUpdate(NetworkMessage request) {
entries = new ArrayList<NodePropertyImpl>();
try {
rawParts = tokenizeMessageBody(request);
} catch (SerializationException e) {
throw new IllegalArgumentException("Error deserializing node property update", e);
}
subtype = null;
for (String part : rawParts) {
if (subtype == null) {
subtype = part;
// log.info(" Message type: " + subtype);
continue;
}
// log.info(" extracted node property entry: " + part);
try {
NodePropertyImpl entry = new NodePropertyImpl(part, nodeIdentifierService);
entries.add(entry);
} catch (IdentifierException e) {
log.error(StringUtils.format(
"Ignoring a node property update from %s containing a malformed instance session id; content='%s'", request
.accessMetaData().getSender(),
part));
continue;
}
}
if (MESSAGE_SUBTYPE_INITIAL.equals(subtype)) {
isInitialUpdate = true;
} else if (MESSAGE_SUBTYPE_INCREMENTAL.equals(subtype)) {
isInitialUpdate = false;
} else {
throw new IllegalArgumentException("Invalid node property update sub-type: " + subtype);
}
}
private String[] tokenizeMessageBody(NetworkMessage request) throws SerializationException {
String bodyString = (String) request.getDeserializedContent();
if (bodyString == null) {
throw new IllegalArgumentException("Received node property update with 'null' as content");
}
// log.debug(localNodeId + ": Received node property update: " + bodyString);
String[] parts = StringUtils.splitAndUnescape(bodyString);
return parts;
}
}
/**
* Represents a single received or locally-generated set of node property updates, with an optional field to exclude the sender from
* receiving its own updates.
*
* @author Robert Mischke
*/
private class UpdateDeltaForBroadcasting {
private final Collection<NodePropertyImpl> properties;
private final InstanceNodeSessionId recipientExclusion;
// note: currently, the fact that new neighbors may appear while this delta is being aggregated with others
// is not considered a problem; if this changes, a snapshot of the current neighbors may be needed, too
UpdateDeltaForBroadcasting(Collection<NodePropertyImpl> properties, InstanceNodeSessionId recipientExclusion) {
this.properties = properties;
this.recipientExclusion = recipientExclusion;
}
}
public NodePropertiesServiceImpl() {
this.completeKnowledgeRegistry = new NodePropertiesRegistry();
this.locallyPublishedKnowledgeRegistry = new NodePropertiesRegistry();
this.callbackManager =
ConcurrencyUtils.getFactory().createAsyncOrderedCallbackManager(AsyncCallbackExceptionPolicy.LOG_AND_CANCEL_LISTENER);
this.networkRequestHandler = new NetworkRequestHandler() {
@Override
public NetworkResponse handleRequest(NetworkRequest request, InstanceNodeSessionId lastHopNodeId)
throws InternalMessagingException {
return handleIncomingPropertiesUpdate(request);
}
};
this.deltaBroadcastAggregator =
ConcurrencyUtils.getFactory().createBatchAggregator(MAX_DELTA_BATCH_SIZE, MAX_DELTA_BATCH_LATENCY,
new BatchProcessor<UpdateDeltaForBroadcasting>() {
private final AtomicInteger counter = new AtomicInteger();
@Override
public void processBatch(List<UpdateDeltaForBroadcasting> batch) {
// integer overflow is irrelevant here, the id is only used for logging
final int batchId = counter.incrementAndGet();
// to avoid overhead on single updates, check if no actual merging is needed
if (batch.size() == 1) {
UpdateDeltaForBroadcasting singleDelta = batch.get(0);
broadcastToAllNeighboursExcept(MESSAGE_SUBTYPE_INCREMENTAL, singleDelta.properties,
singleDelta.recipientExclusion, batchId);
} else {
broadcastIndividualMergedUpdates(MESSAGE_SUBTYPE_INCREMENTAL, batch, batchId);
}
StatsCounter.registerValue("Node property updates", "Number of aggregated deltas per batch", batch.size());
}
});
// register logging property listener on self
addRawNodePropertiesChangeListener(new RawNodePropertiesChangeListener() {
@Override
public void onRawNodePropertiesAddedOrModified(Collection<? extends NodeProperty> newProperties) {
if (verboseLogging) {
int i = 1;
for (NodeProperty property : newProperties) {
log.debug(StringUtils.format("Raw node property change (%d/%d) received by %s, published by %s: '%s' := '%s' [%d]",
i++, newProperties.size(), localNodeId.getInstanceNodeSessionIdString(),
property.getInstanceNodeSessionIdString(), property.getKey(), property.getValue(), property.getSequenceNo()));
}
}
}
});
}
/**
* OSGi-DS lifecycle method.
*/
public void activate() {
localNodeId = nodeConfigurationService.getInstanceNodeSessionId();
if (localNodeId == null) {
throw new NullPointerException();
}
localNodeIsRelay = nodeConfigurationService.isRelay();
connectionService.addChannelLifecycleListener(new MessageChannelLifecycleListenerAdapter() {
@Override
public void setInitialMessageChannels(Set<MessageChannel> currentChannels) {
// TODO handle existing channels? should not occur on usual startup, but possible in general - misc_ro
if (currentChannels.size() != 0) {
log.warn("Initial message channels not empty: " + currentChannels);
}
}
@Override
public void onOutgoingChannelEstablished(MessageChannel channel) {
log.debug(localNodeId + ": established channel (" + channel.getInitiatedByRemote()
+ "), sending initial node property to " + channel.getRemoteNodeInformation().getInstanceNodeSessionId());
if (channel.getState() == MessageChannelState.ESTABLISHED) {
// consistency check
Set<MessageChannel> allOutgoingChannels = connectionService.getAllOutgoingChannels();
if (!allOutgoingChannels.contains(channel)) {
log.warn("Channel " + channel + " established, but not contained in the 'all channels' set yet!");
}
performInitialPropertiesExchangeViaChannel(channel);
} else {
log.debug("Ignoring node property update for channel " + channel.getChannelId() + " as it is " + channel.getState());
}
}
});
}
/**
* OSGi-DS "bind" method; made public for integration testing.
*
* @param newInstance the new service instance to bind
*/
public void bindMessageChannelService(MessageChannelService newInstance) {
this.connectionService = newInstance;
// note: currently, MessageChannelService extends DirectMessagingSender; this may be changed in the future
this.directMessagingSender = newInstance;
}
/**
* OSGi-DS "bind" method; made public for integration testing.
*
* @param newInstance the new service instance to bind
*/
public void bindNodeConfigurationService(NodeConfigurationService newInstance) {
this.nodeConfigurationService = newInstance;
this.nodeIdentifierService = nodeConfigurationService.getNodeIdentifierService();
}
@Override
public void addOrUpdateLocalNodeProperty(String key, String value) {
// convert to map
Map<String, String> map = new HashMap<String, String>();
map.put(key, value);
// delegate
addOrUpdateLocalNodeProperties(map);
}
@Override
public void addOrUpdateLocalNodeProperties(Map<String, String> data) {
if (data.isEmpty()) {
log.debug(
"A node properties update was triggered with empty update data; logging stacktrace (no actual exception thrown)",
new IllegalArgumentException());
return;
}
synchronized (knowledgeLock) {
// create set of NodeProperty entries
long newSequenceNo = timeKeeper.invalidateAndGet();
List<NodePropertyImpl> newDelta = new ArrayList<NodePropertyImpl>();
for (Entry<String, String> entry : data.entrySet()) {
newDelta.add(new NodePropertyImpl(localNodeId, entry.getKey(), newSequenceNo, entry.getValue()));
}
// all entries are new, so the merging can be kept simple
completeKnowledgeRegistry.mergeUnchecked(newDelta);
locallyPublishedKnowledgeRegistry.mergeUnchecked(newDelta);
deltaBroadcastAggregator.enqueue(new UpdateDeltaForBroadcasting(newDelta, null)); // null = no broadcast exclusion
// broadcastToAllNeighbours(MESSAGE_SUBTYPE_INCREMENTAL, newDelta);
// guard against modification
newDelta = Collections.unmodifiableList(newDelta);
// register any local display name updates
registerContainedDisplayNameProperties(newDelta);
// notify listeners
reportImmutableDeltaToListeners(newDelta);
}
}
@Override
public Map<String, String> getNodeProperties(InstanceNodeSessionId nodeId) {
synchronized (knowledgeLock) {
return completeKnowledgeRegistry.getNodeProperties(nodeId);
}
}
@Override
public Map<InstanceNodeSessionId, Map<String, String>> getAllNodeProperties(Collection<InstanceNodeSessionId> nodeIds) {
synchronized (knowledgeLock) {
return completeKnowledgeRegistry.getAllNodeProperties(nodeIds);
}
}
@Override
public Map<InstanceNodeSessionId, Map<String, String>> getAllNodeProperties() {
synchronized (knowledgeLock) {
return completeKnowledgeRegistry.getAllNodeProperties();
}
}
@Override
public void addRawNodePropertiesChangeListener(RawNodePropertiesChangeListener listener) {
synchronized (knowledgeLock) {
final Collection<NodePropertyImpl> copyOfCompleteKnowledge = completeKnowledgeRegistry.getDetachedCopyOfEntries();
callbackManager.addListenerAndEnqueueCallback(listener, new AsyncCallback<RawNodePropertiesChangeListener>() {
@Override
public void performCallback(RawNodePropertiesChangeListener listener) {
listener.onRawNodePropertiesAddedOrModified(copyOfCompleteKnowledge);
}
});
}
}
@Override
public void removeRawNodePropertiesChangeListener(RawNodePropertiesChangeListener listener) {
callbackManager.removeListener(listener);
}
@Override
public NetworkRequestHandlerMap getNetworkRequestHandlers() {
return new NetworkRequestHandlerMap(ProtocolConstants.VALUE_MESSAGE_TYPE_NODE_PROPERTIES_UPDATE, networkRequestHandler);
}
private void performInitialPropertiesExchangeViaChannel(final MessageChannel channel) {
final Collection<NodePropertyImpl> knowledgeToPublish;
synchronized (knowledgeLock) {
if (localNodeIsRelay) {
knowledgeToPublish = completeKnowledgeRegistry.getDetachedCopyOfEntries();
} else {
knowledgeToPublish = locallyPublishedKnowledgeRegistry.getDetachedCopyOfEntries();
}
}
final NetworkRequest request = constructNetworkRequest(MESSAGE_SUBTYPE_INITIAL, knowledgeToPublish);
directMessagingSender.sendDirectMessageAsync(request, channel, new NetworkResponseHandler() {
@Override
public void onResponseAvailable(NetworkResponse response) {
final InstanceNodeSessionId sender = channel.getRemoteNodeInformation().getInstanceNodeSessionId();
// sanity check
if (sender == null) {
log.error("Consistency error: empty remote node id for channel " + channel + " after initial properties exchange");
// in this unlikely case, proceed; log messages will contain 'null', but all other behavior is valid - misc_ro
}
if (!response.isSuccess()) {
log.warn(StringUtils.format("Initial node property exchange with %s via channel %s failed: ",
sender, channel.getChannelId(), response.getResultCode()));
return;
}
try {
final IncomingUpdate parsedUpdate = new IncomingUpdate(response);
// TODO sanity/protocol check: test for proper subtype
final Collection<NodePropertyImpl> effectiveSubset =
mergeExternalUpdateIntoFullKnowledgeAndGetEffectiveSubset(parsedUpdate);
if (localNodeIsRelay) {
log.debug("Received initial node property response from " + sender
+ "; forwarding to all other connected instances");
forwardIfNotEmpty(sender, effectiveSubset);
} else {
log.debug("Received initial node property response from " + sender);
}
} catch (RuntimeException e) {
log.warn("Failed to deserialize response for initial node property exchange", e);
}
}
});
}
private NetworkResponse handleIncomingPropertiesUpdate(NetworkRequest request) throws InternalMessagingException {
try {
IncomingUpdate parsedUpdate = new IncomingUpdate(request);
InstanceNodeSessionId sender = request.accessMetaData().getSender();
// TODO warn/fail on remote modification of local data?
Collection<NodePropertyImpl> effectiveSubset = mergeExternalUpdateIntoFullKnowledgeAndGetEffectiveSubset(parsedUpdate);
if (localNodeIsRelay) {
forwardIfNotEmpty(sender, effectiveSubset);
}
if (parsedUpdate.isInitialUpdate) {
// respond with complementing knowledge
Collection<NodePropertyImpl> complementingKnowledge;
synchronized (knowledgeLock) {
if (localNodeIsRelay) {
// relay: calculate complementing knowledge using the full knowledge set
complementingKnowledge = completeKnowledgeRegistry.getComplementingKnowledge(parsedUpdate.entries);
log.debug(StringUtils.format("Responding to initial node property exchange with %d complementing entries "
+ "(out of %d in the complete set)",
complementingKnowledge.size(), completeKnowledgeRegistry.getEntryCount()));
} else {
// non-relay: calculate complementing knowledge using only the local entries set
complementingKnowledge = locallyPublishedKnowledgeRegistry.getComplementingKnowledge(parsedUpdate.entries);
log.debug(StringUtils.format("Responding to initial node property exchange with %d complementing entries "
+ "(out of %d in the local set)", complementingKnowledge.size(),
locallyPublishedKnowledgeRegistry.getEntryCount()));
}
}
byte[] responseBody = constructMessageBody(MESSAGE_SUBTYPE_INCREMENTAL, complementingKnowledge);
return NetworkResponseFactory.generateSuccessResponse(request, responseBody);
} else {
byte[] responseBody = new byte[0]; // dummy
return NetworkResponseFactory.generateSuccessResponse(request, responseBody);
}
} catch (RuntimeException e) {
throw new InternalMessagingException("Error processing node properties update", e);
}
}
private Collection<NodePropertyImpl> mergeExternalUpdateIntoFullKnowledgeAndGetEffectiveSubset(IncomingUpdate parsedUpdate) {
Collection<NodePropertyImpl> effectiveSubset;
synchronized (knowledgeLock) {
Map<String, String> propertiesToRepublishOrCancel = checkForPropertiesToRepublishOrCancel(parsedUpdate.entries);
if (!propertiesToRepublishOrCancel.isEmpty()) {
log.debug("Publishing a cancel/republish set containing " + propertiesToRepublishOrCancel.size() + " entries");
// note that this may result in the sender of an initial update receiving this as an incremental update *before*
// receiving the complementing knowledge set for its initial message; this should not cause problems, though - misc_ro
addOrUpdateLocalNodeProperties(propertiesToRepublishOrCancel);
}
// no relay/non-relay distinction here, as the result is only forwarded in relay mode
effectiveSubset = completeKnowledgeRegistry.mergeAndGetEffectiveSubset(parsedUpdate.entries);
// guard against modification
effectiveSubset = Collections.unmodifiableCollection(effectiveSubset);
// special treatment of display names; register them before invoking the asynchronous listeners
// to prevent "unknown" display names appearing in log and user messages
registerContainedDisplayNameProperties(effectiveSubset);
// notify listeners
// TODO (p3) this call seems to rely on the knowledgeLock monitor to ensure proper ordering; verify and document
reportImmutableDeltaToListeners(effectiveSubset);
}
return effectiveSubset;
}
private void registerContainedDisplayNameProperties(Collection<NodePropertyImpl> properties) {
for (NodeProperty property : properties) {
if (NodePropertyConstants.KEY_DISPLAY_NAME.equals(property.getKey())) {
String displayName = property.getValue();
if (verboseLogging) {
log.debug(StringUtils.format("Setting associated display name for node %s to '%s'",
property.getInstanceNodeSessionIdString(), displayName));
}
nodeIdentifierService.associateDisplayName(property.getInstanceNodeSessionId(), displayName);
}
}
}
private Map<String, String> checkForPropertiesToRepublishOrCancel(List<NodePropertyImpl> entries) {
Map<String, String> result = new HashMap<String, String>();
for (NodePropertyImpl receivedProperty : entries) {
if (localNodeId.isSameInstanceNodeSessionAs(receivedProperty.getInstanceNodeSessionId())) {
final String key = receivedProperty.getKey();
final NodeProperty existingProperty = locallyPublishedKnowledgeRegistry.getNodeProperty(localNodeId, key);
if (existingProperty == null) {
log.debug("Received a node property for the local node with no local counterpart (a canceling "
+ "update will be published): " + receivedProperty);
result.put(key, null);
} else if (existingProperty.getSequenceNo() < receivedProperty.getSequenceNo()) {
// should not happen, especially with instance node session ids now in place
log.warn("Received a node property for the local node that is 'newer' than the actual local state; "
+ "is there a node with the same id in the network? (attempting to re-publish the local value)");
log.warn("Local property: " + existingProperty);
log.warn("Received property: " + receivedProperty);
result.put(key, existingProperty.getValue());
}
}
}
return result;
}
private void reportImmutableDeltaToListeners(final Collection<NodePropertyImpl> immutableDelta) {
callbackManager.enqueueCallback(new AsyncCallback<RawNodePropertiesChangeListener>() {
@Override
public void performCallback(RawNodePropertiesChangeListener listener) {
listener.onRawNodePropertiesAddedOrModified(immutableDelta);
}
});
}
/**
* Forward to all *except* the given sender, unless the set is empty.
*
* @param sender the sender that caused this update
* @param effectiveSubset the subset of changes that caused local modifications (ie, that were unknown before)
*/
private void forwardIfNotEmpty(InstanceNodeSessionId sender, Collection<NodePropertyImpl> effectiveSubset) {
// always forward all new aspects to other neighbors
if (!effectiveSubset.isEmpty()) {
// broadcastToAllNeighboursExcept(MESSAGE_SUBTYPE_INCREMENTAL, effectiveSubset, sender, -1);
deltaBroadcastAggregator.enqueue(new UpdateDeltaForBroadcasting(effectiveSubset, sender)); // exclude sender
} else {
log.debug(localNodeId + ": node property update did not result in a local change; not forwarding)");
}
}
@SuppressWarnings("unused")
@Deprecated
private void broadcastToAllNeighbours(String updateType, Collection<NodePropertyImpl> entries, int batchId) {
broadcastToAllNeighboursExcept(updateType, entries, null, batchId); // null = no exclusion
}
private void broadcastToAllNeighboursExcept(String updateType, Collection<NodePropertyImpl> entries, InstanceNodeSessionId exclusion,
final int batchId) {
log.debug("Broadcasting non-batched node properties update " + batchId);
final Set<MessageChannel> channels = connectionService.getAllOutgoingChannels();
NetworkRequest request = null;
boolean firstRecipient = true;
for (final MessageChannel channel : channels) {
final InstanceNodeSessionId remoteNodeId = channel.getRemoteNodeInformation().getInstanceNodeSessionId();
if (exclusion == null || !remoteNodeId.equals(exclusion)) {
if (firstRecipient) {
// lazily construct request here in case there is no recipient at all
request = constructNetworkRequest(updateType, entries);
firstRecipient = false;
} else {
// make request ids unique if there is more than one recipient, but don't regenerate the content payload
request = NetworkRequestFactory.cloneWithNewRequestId(request);
}
directMessagingSender.sendDirectMessageAsync(request, channel, new NetworkResponseHandler() {
@Override
public void onResponseAvailable(NetworkResponse response) {
if (!response.isSuccess()) {
log.warn(StringUtils.format("Failed to send node properties update %d to %s via channel %s: %s", batchId,
channel.getRemoteNodeInformation().getInstanceNodeSessionId(), channel.getChannelId(),
response.getResultCode().toString()));
}
}
});
}
}
}
private void broadcastIndividualMergedUpdates(String updateType, List<UpdateDeltaForBroadcasting> batch, final int batchId) {
final Set<MessageChannel> channels = connectionService.getAllOutgoingChannels();
final Map<MessageChannel, Map<String, NodePropertyImpl>> channelToMergedUpdateMap = new HashMap<>();
for (UpdateDeltaForBroadcasting delta : batch) {
if (delta.properties.isEmpty()) {
// should have been filtered out before, so log a warning
log.warn("Node properties batch update " + batchId + " contained an empty delta; ignoring");
continue;
}
for (final MessageChannel channel : channels) {
if (delta.recipientExclusion == null
|| channel.getRemoteNodeInformation().getInstanceNodeSessionId() != delta.recipientExclusion) {
// channel should receive this delta; merge into outgoing state
Map<String, NodePropertyImpl> mergeMapForSingleRecipient = channelToMergedUpdateMap.get(channel);
if (mergeMapForSingleRecipient == null) {
// create merge map if not present yet (first delta for this recipient)
mergeMapForSingleRecipient = new HashMap<String, NodePropertyImpl>();
channelToMergedUpdateMap.put(channel, mergeMapForSingleRecipient);
}
// merge properties; note that once there is exactly one property update per composite key, their order is irrelevant
for (NodePropertyImpl incomingPropertyState : delta.properties) {
final String propertyKey = incomingPropertyState.getCompositeKey().getAsUniqueString();
// optimistically overwrite before checking sequence number, as this is the most frequent case
final NodePropertyImpl replacedPropertyState = mergeMapForSingleRecipient.put(propertyKey, incomingPropertyState);
if (replacedPropertyState != null
&& replacedPropertyState.getSequenceNo() > incomingPropertyState.getSequenceNo()) {
// restore newer entry into map
mergeMapForSingleRecipient.put(propertyKey, replacedPropertyState);
log.debug(StringUtils.format(
"Prevented an outdated property value from overwriting a newer one in batch aggregation: "
+ "prevented='%s', newer='%s'",
incomingPropertyState, replacedPropertyState));
}
}
}
}
}
for (final MessageChannel channel : channels) {
Map<String, NodePropertyImpl> mergeMapForSingleRecipient = channelToMergedUpdateMap.get(channel);
NetworkRequest request;
if (mergeMapForSingleRecipient != null) {
// has map -> at least one delta was relevant
if (mergeMapForSingleRecipient.isEmpty()) {
// consistency/sanity check failed
log.warn("Unexpected state: empty map of merged node property deltas, not sending an update via " + channel);
continue;
}
request = constructNetworkRequest(updateType, mergeMapForSingleRecipient.values());
if (verboseLogging) {
log.debug(StringUtils.format("Sending aggregated node properties update %d to %s via channel %s",
batchId, channel.getRemoteNodeInformation().getInstanceNodeSessionId(), channel.getChannelId()));
}
directMessagingSender.sendDirectMessageAsync(request, channel, new NetworkResponseHandler() {
@Override
public void onResponseAvailable(NetworkResponse response) {
if (!response.isSuccess()) {
log.warn(StringUtils.format("Failed to send aggregated node properties update %d to %s via channel %s: %s",
batchId, channel.getRemoteNodeInformation().getInstanceNodeSessionId(),
channel.getChannelId(), response.getResultCode().toString()));
}
}
});
}
}
}
private NetworkRequest constructNetworkRequest(String updateType, Collection<NodePropertyImpl> entries) {
byte[] contentBytes = constructMessageBody(updateType, entries);
NetworkRequest request = NetworkRequestFactory.createNetworkRequest(contentBytes,
ProtocolConstants.VALUE_MESSAGE_TYPE_NODE_PROPERTIES_UPDATE, localNodeId, null);
return request;
}
private byte[] constructMessageBody(String updateType, Collection<NodePropertyImpl> entries) {
List<String> stringParts = new ArrayList<String>();
stringParts.add(updateType);
for (NodePropertyImpl entry : entries) {
String compactForm = entry.toCompactForm();
stringParts.add(compactForm);
}
String body = StringUtils.escapeAndConcat(stringParts.toArray(new String[stringParts.size()]));
// log.debug(localNodeId + ": Constructed node property update: " + body);
return MessageUtils.serializeSafeObject(body);
}
}