INT-4550: Disallow multi aggregators on same MGS (#2622)

* INT-4550: Disallow multi aggregators on same MGS

JIRA: https://jira.spring.io/browse/INT-4550

**Cherry-pick to 5.0.x**

* * Introduce `UniqueExpiryCallback`
* Use `UniqueExpiryCallback` in the `AbstractCorrelatingMessageHandler`
* Check for uniqueness in the `AbstractMessageGroupStore`
* Remove duplicate code in the `ConfigurableMongoDbMessageStore`

* * Fix tests according a new logic

* * Address PR review

* Change `Assert.isTrue` to the `logger.error` for backward compatibility
* Revert changes in tests since we don't throw exception anymore
* Fix language on doc

* * Fix Checkstyle violation in the `AbstractMessageGroupStore`

* * Ignore `testDontReapMessageOfOtherHandler()`
This commit is contained in:
Artem Bilan
2018-11-15 14:56:14 -05:00
committed by Gary Russell
parent 3d696ef068
commit 5bf6161112
6 changed files with 71 additions and 120 deletions

View File

@@ -22,7 +22,6 @@ import java.util.Comparator;
import java.util.Date; import java.util.Date;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set;
import java.util.UUID; import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ScheduledFuture; import java.util.concurrent.ScheduledFuture;
@@ -51,6 +50,7 @@ import org.springframework.integration.store.MessageGroupStore;
import org.springframework.integration.store.MessageStore; import org.springframework.integration.store.MessageStore;
import org.springframework.integration.store.SimpleMessageGroup; import org.springframework.integration.store.SimpleMessageGroup;
import org.springframework.integration.store.SimpleMessageStore; import org.springframework.integration.store.SimpleMessageStore;
import org.springframework.integration.store.UniqueExpiryCallback;
import org.springframework.integration.support.AbstractIntegrationMessageBuilder; import org.springframework.integration.support.AbstractIntegrationMessageBuilder;
import org.springframework.integration.support.MessageBuilder; import org.springframework.integration.support.MessageBuilder;
import org.springframework.integration.support.locks.DefaultLockRegistry; import org.springframework.integration.support.locks.DefaultLockRegistry;
@@ -102,8 +102,6 @@ public abstract class AbstractCorrelatingMessageHandler extends AbstractMessageP
private final Map<UUID, ScheduledFuture<?>> expireGroupScheduledFutures = new ConcurrentHashMap<>(); private final Map<UUID, ScheduledFuture<?>> expireGroupScheduledFutures = new ConcurrentHashMap<>();
private final Set<Object> groupIds = ConcurrentHashMap.newKeySet();
private MessageGroupProcessor outputProcessor; private MessageGroupProcessor outputProcessor;
private MessageGroupStore messageStore; private MessageGroupStore messageStore;
@@ -188,8 +186,9 @@ public abstract class AbstractCorrelatingMessageHandler extends AbstractMessageP
public final void setMessageStore(MessageGroupStore store) { public final void setMessageStore(MessageGroupStore store) {
this.messageStore = store; this.messageStore = store;
store.registerMessageGroupExpiryCallback( UniqueExpiryCallback expiryCallback =
(messageGroupStore, group) -> this.forceReleaseProcessor.processMessageGroup(group)); (messageGroupStore, group) -> this.forceReleaseProcessor.processMessageGroup(group);
store.registerMessageGroupExpiryCallback(expiryCallback);
} }
public void setCorrelationStrategy(CorrelationStrategy correlationStrategy) { public void setCorrelationStrategy(CorrelationStrategy correlationStrategy) {
@@ -746,7 +745,6 @@ public abstract class AbstractCorrelatingMessageHandler extends AbstractMessageP
protected void remove(MessageGroup group) { protected void remove(MessageGroup group) {
Object correlationKey = group.getGroupId(); Object correlationKey = group.getGroupId();
this.messageStore.removeMessageGroup(correlationKey); this.messageStore.removeMessageGroup(correlationKey);
this.groupIds.remove(group.getGroupId());
} }
protected int findLastReleasedSequenceNumber(Object groupId, Collection<Message<?>> partialSequence) { protected int findLastReleasedSequenceNumber(Object groupId, Collection<Message<?>> partialSequence) {
@@ -755,7 +753,6 @@ public abstract class AbstractCorrelatingMessageHandler extends AbstractMessageP
} }
protected MessageGroup store(Object correlationKey, Message<?> message) { protected MessageGroup store(Object correlationKey, Message<?> message) {
this.groupIds.add(correlationKey);
return this.messageStore.addMessageToGroup(correlationKey, message); return this.messageStore.addMessageToGroup(correlationKey, message);
} }
@@ -949,9 +946,7 @@ public abstract class AbstractCorrelatingMessageHandler extends AbstractMessageP
@Override @Override
public Object processMessageGroup(MessageGroup group) { public Object processMessageGroup(MessageGroup group) {
if (AbstractCorrelatingMessageHandler.this.groupIds.contains(group.getGroupId())) { forceComplete(group);
forceComplete(group);
}
return null; return null;
} }

View File

@@ -42,7 +42,7 @@ public abstract class AbstractMessageGroupStore extends AbstractBatchingMessageG
protected final Log logger = LogFactory.getLog(getClass()); protected final Log logger = LogFactory.getLog(getClass());
private final Collection<MessageGroupCallback> expiryCallbacks = new LinkedHashSet<MessageGroupCallback>(); private final Collection<MessageGroupCallback> expiryCallbacks = new LinkedHashSet<>();
private final MessageGroupFactory persistentMessageGroupFactory = private final MessageGroupFactory persistentMessageGroupFactory =
new SimpleMessageGroupFactory(SimpleMessageGroupFactory.GroupType.PERSISTENT); new SimpleMessageGroupFactory(SimpleMessageGroupFactory.GroupType.PERSISTENT);
@@ -72,13 +72,10 @@ public abstract class AbstractMessageGroupStore extends AbstractBatchingMessageG
/** /**
* Convenient injection point for expiry callbacks in the message store. Each of the callbacks provided will simply * Convenient injection point for expiry callbacks in the message store. Each of the callbacks provided will simply
* be registered with the store using {@link #registerMessageGroupExpiryCallback(MessageGroupCallback)}. * be registered with the store using {@link #registerMessageGroupExpiryCallback(MessageGroupCallback)}.
*
* @param expiryCallbacks the expiry callbacks to add * @param expiryCallbacks the expiry callbacks to add
*/ */
public void setExpiryCallbacks(Collection<MessageGroupCallback> expiryCallbacks) { public void setExpiryCallbacks(Collection<MessageGroupCallback> expiryCallbacks) {
for (MessageGroupCallback callback : expiryCallbacks) { expiryCallbacks.forEach(this::registerMessageGroupExpiryCallback);
registerMessageGroupExpiryCallback(callback);
}
} }
public boolean isTimeoutOnIdle() { public boolean isTimeoutOnIdle() {
@@ -110,6 +107,17 @@ public abstract class AbstractMessageGroupStore extends AbstractBatchingMessageG
@Override @Override
public void registerMessageGroupExpiryCallback(MessageGroupCallback callback) { public void registerMessageGroupExpiryCallback(MessageGroupCallback callback) {
if (callback instanceof UniqueExpiryCallback) {
boolean uniqueExpiryCallbackPresent =
this.expiryCallbacks.stream()
.anyMatch(UniqueExpiryCallback.class::isInstance);
if (!uniqueExpiryCallbackPresent && this.logger.isErrorEnabled()) {
this.logger.error("Only one instance of 'UniqueExpiryCallback' can be registered in the " +
"'MessageGroupStore'. Use a separate 'MessageGroupStore' for each aggregator/resequencer.");
}
}
this.expiryCallbacks.add(callback); this.expiryCallbacks.add(callback);
} }

View File

@@ -0,0 +1,33 @@
/*
* Copyright 2018 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.integration.store;
/**
* A marker interface extension of the {@link MessageGroupStore.MessageGroupCallback}
* for components which should be registered in the {@link MessageGroupStore} only once.
* The {@link MessageGroupStore} implementation ensures that only once instance of this
* class is present in the expire callbacks.
*
* @author Meherzad Lahewala
* @author Artme Bilan
*
* @since 5.0.10
*/
@FunctionalInterface
public interface UniqueExpiryCallback extends MessageGroupStore.MessageGroupCallback {
}

View File

@@ -425,6 +425,7 @@ public class AbstractCorrelatingMessageHandlerTests {
} }
@Test @Test
@Ignore("Until 5.2 with new 'owner' feature on groups")
public void testDontReapMessageOfOtherHandler() { public void testDontReapMessageOfOtherHandler() {
MessageGroupStore groupStore = new SimpleMessageStore(); MessageGroupStore groupStore = new SimpleMessageStore();

View File

@@ -17,12 +17,12 @@
package org.springframework.integration.mongodb.store; package org.springframework.integration.mongodb.store;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection; import java.util.Collection;
import java.util.Iterator; import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.List; import java.util.List;
import java.util.UUID; import java.util.UUID;
import java.util.stream.Collectors;
import java.util.stream.StreamSupport;
import org.springframework.data.domain.Sort; import org.springframework.data.domain.Sort;
import org.springframework.data.mongodb.MongoDbFactory; import org.springframework.data.mongodb.MongoDbFactory;
@@ -32,12 +32,10 @@ import org.springframework.data.mongodb.core.query.Criteria;
import org.springframework.data.mongodb.core.query.Query; import org.springframework.data.mongodb.core.query.Query;
import org.springframework.data.mongodb.core.query.Update; import org.springframework.data.mongodb.core.query.Update;
import org.springframework.integration.store.MessageGroup; import org.springframework.integration.store.MessageGroup;
import org.springframework.integration.store.MessageGroupMetadata;
import org.springframework.integration.store.MessageGroupStore; import org.springframework.integration.store.MessageGroupStore;
import org.springframework.integration.store.MessageStore; import org.springframework.integration.store.MessageStore;
import org.springframework.integration.store.SimpleMessageGroup; import org.springframework.integration.store.SimpleMessageGroup;
import org.springframework.jmx.export.annotation.ManagedAttribute; import org.springframework.jmx.export.annotation.ManagedAttribute;
import org.springframework.jmx.export.annotation.ManagedOperation;
import org.springframework.messaging.Message; import org.springframework.messaging.Message;
import org.springframework.util.Assert; import org.springframework.util.Assert;
@@ -59,10 +57,6 @@ public class ConfigurableMongoDbMessageStore extends AbstractConfigurableMongoDb
public final static String DEFAULT_COLLECTION_NAME = "configurableStoreMessages"; public final static String DEFAULT_COLLECTION_NAME = "configurableStoreMessages";
private final Collection<MessageGroupCallback> expiryCallbacks = new LinkedHashSet<>();
private volatile boolean timeoutOnIdle;
public ConfigurableMongoDbMessageStore(MongoTemplate mongoTemplate) { public ConfigurableMongoDbMessageStore(MongoTemplate mongoTemplate) {
this(mongoTemplate, DEFAULT_COLLECTION_NAME); this(mongoTemplate, DEFAULT_COLLECTION_NAME);
@@ -86,35 +80,10 @@ public class ConfigurableMongoDbMessageStore extends AbstractConfigurableMongoDb
public ConfigurableMongoDbMessageStore(MongoDbFactory mongoDbFactory, MappingMongoConverter mappingMongoConverter, public ConfigurableMongoDbMessageStore(MongoDbFactory mongoDbFactory, MappingMongoConverter mappingMongoConverter,
String collectionName) { String collectionName) {
super(mongoDbFactory, mappingMongoConverter, collectionName); super(mongoDbFactory, mappingMongoConverter, collectionName);
} }
/**
* Convenient injection point for expiry callbacks in the message store. Each of the callbacks provided will simply
* be registered with the store using {@link #registerMessageGroupExpiryCallback(MessageGroupCallback)}.
* @param expiryCallbacks the expiry callbacks to add
*/
public void setExpiryCallbacks(Collection<MessageGroupCallback> expiryCallbacks) {
for (MessageGroupCallback callback : expiryCallbacks) {
registerMessageGroupExpiryCallback(callback);
}
}
public boolean isTimeoutOnIdle() {
return this.timeoutOnIdle;
}
/**
* Allows you to override the rule for the timeout calculation. Typical timeout is based from the time
* the {@link MessageGroup} was created. If you want the timeout to be based on the time
* the {@link MessageGroup} was idling (e.g., inactive from the last update) invoke this method with 'true'.
* Default is 'false'.
* @param timeoutOnIdle The boolean.
*/
public void setTimeoutOnIdle(boolean timeoutOnIdle) {
this.timeoutOnIdle = timeoutOnIdle;
}
@Override @Override
public <T> Message<T> addMessage(Message<T> message) { public <T> Message<T> addMessage(Message<T> message) {
Assert.notNull(message, "'message' must not be null"); Assert.notNull(message, "'message' must not be null");
@@ -205,7 +174,7 @@ public class ConfigurableMongoDbMessageStore extends AbstractConfigurableMongoDb
Assert.notNull(groupId, "'groupId' must not be null"); Assert.notNull(groupId, "'groupId' must not be null");
Assert.notNull(messages, "'messageToRemove' must not be null"); Assert.notNull(messages, "'messageToRemove' must not be null");
Collection<UUID> ids = new ArrayList<UUID>(); Collection<UUID> ids = new ArrayList<>();
for (Message<?> messageToRemove : messages) { for (Message<?> messageToRemove : messages) {
ids.add(messageToRemove.getHeaders().getId()); ids.add(messageToRemove.getHeaders().getId());
if (ids.size() >= getRemoveBatchSize()) { if (ids.size() >= getRemoveBatchSize()) {
@@ -225,11 +194,6 @@ public class ConfigurableMongoDbMessageStore extends AbstractConfigurableMongoDb
this.mongoTemplate.remove(query, this.collectionName); this.mongoTemplate.remove(query, this.collectionName);
} }
@Override
public void removeMessagesFromGroup(Object groupId, Message<?>... messages) {
removeMessagesFromGroup(groupId, Arrays.asList(messages));
}
@Override @Override
public Message<?> pollMessageFromGroup(final Object groupId) { public Message<?> pollMessageFromGroup(final Object groupId) {
Assert.notNull(groupId, "'groupId' must not be null"); Assert.notNull(groupId, "'groupId' must not be null");
@@ -247,52 +211,24 @@ public class ConfigurableMongoDbMessageStore extends AbstractConfigurableMongoDb
@Override @Override
public void setLastReleasedSequenceNumberForGroup(Object groupId, int sequenceNumber) { public void setLastReleasedSequenceNumberForGroup(Object groupId, int sequenceNumber) {
this.updateGroup(groupId, lastModifiedUpdate().set(MessageDocumentFields.LAST_RELEASED_SEQUENCE, sequenceNumber)); updateGroup(groupId, lastModifiedUpdate().set(MessageDocumentFields.LAST_RELEASED_SEQUENCE, sequenceNumber));
} }
@Override @Override
public void completeGroup(Object groupId) { public void completeGroup(Object groupId) {
this.updateGroup(groupId, lastModifiedUpdate().set(MessageDocumentFields.COMPLETE, true)); updateGroup(groupId, lastModifiedUpdate().set(MessageDocumentFields.COMPLETE, true));
} }
@Override @Override
public Iterator<MessageGroup> iterator() { public Iterator<MessageGroup> iterator() {
List<MessageGroup> messageGroups = new ArrayList<MessageGroup>();
Query query = Query.query(Criteria.where(MessageDocumentFields.GROUP_ID).exists(true)); Query query = Query.query(Criteria.where(MessageDocumentFields.GROUP_ID).exists(true));
Iterable<String> groupIds = mongoTemplate.getCollection(collectionName) Iterable<String> groupIds = mongoTemplate.getCollection(collectionName)
.distinct(MessageDocumentFields.GROUP_ID, query.getQueryObject(), String.class); .distinct(MessageDocumentFields.GROUP_ID, query.getQueryObject(), String.class);
for (Object groupId : groupIds) { return StreamSupport.stream(groupIds.spliterator(), false)
messageGroups.add(getMessageGroup(groupId)); .map(this::getMessageGroup)
} .iterator();
return messageGroups.iterator();
}
@Override
public void registerMessageGroupExpiryCallback(MessageGroupCallback callback) {
this.expiryCallbacks.add(callback);
}
@Override
@ManagedOperation
public int expireMessageGroups(long timeout) {
int count = 0;
long threshold = System.currentTimeMillis() - timeout;
for (MessageGroup group : this) {
long timestamp = group.getTimestamp();
if (this.isTimeoutOnIdle() && group.getLastModified() > 0) {
timestamp = group.getLastModified();
}
if (timestamp <= threshold) {
count++;
expire(group);
}
}
return count;
} }
@Override @Override
@@ -315,11 +251,6 @@ public class ConfigurableMongoDbMessageStore extends AbstractConfigurableMongoDb
.size(); .size();
} }
@Override
public MessageGroupMetadata getGroupMetadata(Object groupId) {
throw new UnsupportedOperationException("Not yet implemented for this store");
}
@Override @Override
public Message<?> getOneMessageFromGroup(Object groupId) { public Message<?> getOneMessageFromGroup(Object groupId) {
Assert.notNull(groupId, "'groupId' must not be null"); Assert.notNull(groupId, "'groupId' must not be null");
@@ -338,36 +269,12 @@ public class ConfigurableMongoDbMessageStore extends AbstractConfigurableMongoDb
Assert.notNull(groupId, "'groupId' must not be null"); Assert.notNull(groupId, "'groupId' must not be null");
Query query = groupOrderQuery(groupId); Query query = groupOrderQuery(groupId);
List<MessageDocument> documents = this.mongoTemplate.find(query, MessageDocument.class, this.collectionName); List<MessageDocument> documents = this.mongoTemplate.find(query, MessageDocument.class, this.collectionName);
List<Message<?>> messages = new ArrayList<Message<?>>();
for (MessageDocument document : documents) { return documents.stream()
messages.add(document.getMessage()); .map(MessageDocument::getMessage)
} .collect(Collectors.toList());
return messages;
} }
private void expire(MessageGroup group) {
RuntimeException exception = null;
for (MessageGroupCallback callback : this.expiryCallbacks) {
try {
callback.execute(this, group);
}
catch (RuntimeException e) {
if (exception == null) {
exception = e;
}
logger.error("Exception in expiry callback", e);
}
}
if (exception != null) {
throw exception;
}
}
private void updateGroup(Object groupId, Update update) { private void updateGroup(Object groupId, Update update) {
this.mongoTemplate.updateFirst(groupOrderQuery(groupId), update, this.collectionName); this.mongoTemplate.updateFirst(groupOrderQuery(groupId), update, this.collectionName);
} }

View File

@@ -812,6 +812,13 @@ The callback has direct access to the store and the message group so that it can
The `MessageGroupStore` maintains a list of these callbacks, which it applies, on demand, to all messages whose timestamps are earlier than a time supplied as a parameter (see the `registerMessageGroupExpiryCallback(..)` and `expireMessageGroups(..)` methods, described earlier). The `MessageGroupStore` maintains a list of these callbacks, which it applies, on demand, to all messages whose timestamps are earlier than a time supplied as a parameter (see the `registerMessageGroupExpiryCallback(..)` and `expireMessageGroups(..)` methods, described earlier).
For more detail, see <<reaper>>. For more detail, see <<reaper>>.
IMPORTANT: It is important not to use the same `MessageGroupStore` instance in different aggregator components, when you intend to rely on the `expireMessageGroups` functionality.
Every `AbstractCorrelatingMessageHandler` registers its own `MessageGroupCallback` based on the `forceComplete()` callback.
This way each group for expiration may be completed or discarded by the wrong aggregator.
Starting with version 5.0.10, a `UniqueExpiryCallback` is used from the `AbstractCorrelatingMessageHandler` for the registration callback in the `MessageGroupStore`.
The `MessageGroupStore`, in turn, checks for presence an instance of this class and logs an error with an appropriate message if one is already present in the callbacks set.
This way the Framework disallows usage of the `MessageGroupStore` instance in different aggregators/resequencers to avoid the mentioned side effect of expiration the groups not created by the particular correlation handler.
You can call the `expireMessageGroups` method with a timeout value. You can call the `expireMessageGroups` method with a timeout value.
Any message older than the current time minus this value is expired and has the callbacks applied. Any message older than the current time minus this value is expired and has the callbacks applied.
Thus, it is the user of the store that defines what is meant by message group "`expiry`". Thus, it is the user of the store that defines what is meant by message group "`expiry`".