INT-3387: MessageGroupStore Improvements

JIRA: https://jira.spring.io/browse/INT-3387,
https://jira.spring.io/browse/INT-3806

* Introduce
```
MessageGroupStore

void addMessagesToGroup(Object groupId, Message<?>... messages);
```
And implement it in all stores.

* Use new `addMessagesToGroup` where it is reasonable, e.g. `DelayHandler`
* Optimize test-case to use a new store method (where it is possible)
* Fix timing delays in the `JdbcMessageStoreTests`
* Introduce `PersistentMessageGroup`
* Add `AbstractMessageGroupStore#proxyMessageGroupForLazyLoad` to wrap the raw `MessageGroup` to the `PersistentMessageGroup` for lazy-load
* Rework `MessageGroupMetadata` do not be `immutable` and allow to store/restore in the `AbstractKeyValueMessageStore` only the `MessageGroupMetadata`
* Refactor `ResequencingMessageHandler` and `SequenceSizeReleaseStrategy` a bit for better performance when interact with the `MessageGroup`
* Add `AbstractMessageGroupStore#setLazyLoadMessageGroups` to switch off the `lazy-load` behavior and restore the previous full `MessageGroup` logic
* Add `What's New` note and `message-store.adoc` paragraph for the lazy-load functionality

`GroupType.PERSISTENT` and not lazy by default

PR Comments

Fix `JdbcMessageStoreTests` timing issues

Address PR comments

* Add performance test to the `ConfigurableMongoDbMessageGroupStoreTests`
* Add JavaDocs for the `MessageGroupFactory` methods
* Add `log4j.properties` into the `test` MongoDB module for better traceability
* Fix `JdbcMessageStore#getOneMessageFromGroup()` over the `doPollForMessage()` delegation.
The `jdbcTemplate.queryForObject()` requires exactly one and only one raw in `resultSet`
* Add performance test results into the `message-store.adoc`
This commit is contained in:
Artem Bilan
2016-04-12 13:38:21 -04:00
committed by Gary Russell
parent 4e4763d24f
commit 286c421c1a
34 changed files with 1085 additions and 505 deletions

View File

@@ -95,7 +95,7 @@ public class CorrelatingMessageBarrier extends AbstractMessageHandler implements
Object correlationKey = this.correlationStrategy.getCorrelationKey(message);
Object lock = getLock(correlationKey);
synchronized (lock) {
this.store.addMessageToGroup(correlationKey, message);
this.store.addMessagesToGroup(correlationKey, message);
}
if (log.isDebugEnabled()) {
log.debug(String.format("Handled message for key [%s]: %s.", correlationKey, message));

View File

@@ -18,7 +18,6 @@ package org.springframework.integration.aggregator;
import java.util.Collection;
import org.springframework.integration.IntegrationMessageHeaderAccessor;
import org.springframework.integration.store.MessageGroup;
import org.springframework.integration.store.MessageGroupStore;
import org.springframework.integration.store.SimpleMessageStore;
@@ -74,12 +73,9 @@ public class ResequencingMessageHandler extends AbstractCorrelatingMessageHandle
@Override
protected void afterRelease(MessageGroup messageGroup, Collection<Message<?>> completedMessages, boolean timeout) {
int size = messageGroup.getMessages().size();
int sequenceSize = 0;
Message<?> message = messageGroup.getOne();
if (message != null) {
sequenceSize = new IntegrationMessageHeaderAccessor(message).getSequenceSize();
}
int size = messageGroup.size();
int sequenceSize = messageGroup.getSequenceSize();
// If there is no sequence then it must be incomplete or unbounded
if (sequenceSize > 0 && sequenceSize == size) {
remove(messageGroup);

View File

@@ -36,6 +36,7 @@ import org.springframework.messaging.Message;
* @author Dave Syer
* @author Iwein Fuld
* @author Oleg Zhurakousky
* @author Artem Bilan
* @author Enrique Rodríguez
*/
public class SequenceSizeReleaseStrategy implements ReleaseStrategy {
@@ -55,9 +56,9 @@ public class SequenceSizeReleaseStrategy implements ReleaseStrategy {
}
/**
* Flag that determines if partial sequences are allowed. If true then as soon as enough messages arrive that can be
* ordered they will be released, provided they all have sequence numbers greater than those already released.
*
* Flag that determines if partial sequences are allowed. If true then as soon as
* enough messages arrive that can be ordered they will be released, provided they
* all have sequence numbers greater than those already released.
* @param releasePartialSequences true when partial sequences should be released.
*/
public void setReleasePartialSequences(boolean releasePartialSequences) {
@@ -69,13 +70,12 @@ public class SequenceSizeReleaseStrategy implements ReleaseStrategy {
boolean canRelease = false;
Collection<Message<?>> messages = messageGroup.getMessages();
if (this.releasePartialSequences && !messages.isEmpty()) {
int size = messageGroup.size();
if (this.releasePartialSequences && size > 0) {
if (logger.isTraceEnabled()) {
logger.trace("Considering partial release of group [" + messageGroup + "]");
}
Collection<Message<?>> messages = messageGroup.getMessages();
Message<?> minMessage = Collections.min(messages, this.comparator);
int nextSequenceNumber = new IntegrationMessageHeaderAccessor(minMessage).getSequenceNumber();
@@ -86,13 +86,11 @@ public class SequenceSizeReleaseStrategy implements ReleaseStrategy {
}
}
else {
int size = messages.size();
if (size == 0) {
canRelease = true;
}
else {
int sequenceSize = new IntegrationMessageHeaderAccessor(messageGroup.getOne()).getSequenceSize();
int sequenceSize = messageGroup.getSequenceSize();
// If there is no sequence then it must be incomplete....
if (sequenceSize == size) {
canRelease = true;

View File

@@ -94,38 +94,62 @@ public abstract class AbstractKeyValueMessageStore extends AbstractMessageGroupS
*/
@Override
public MessageGroup getMessageGroup(Object groupId) {
return buildMessageGroup(groupId, false);
MessageGroupMetadata metadata = getGroupMetadata(groupId);
if (metadata != null) {
MessageGroup messageGroup = getMessageGroupFactory()
.create(this, groupId, metadata.getTimestamp(), metadata.isComplete());
messageGroup.setLastModified(metadata.getLastModified());
messageGroup.setLastReleasedMessageSequenceNumber(metadata.getLastReleasedMessageSequenceNumber());
return messageGroup;
}
else {
return new SimpleMessageGroup(groupId);
}
}
/**
* Add a Message to the group with the provided group ID.
*/
@Override
public MessageGroup addMessageToGroup(Object groupId, Message<?> message) {
public MessageGroupMetadata getGroupMetadata(Object groupId) {
Assert.notNull(groupId, "'groupId' must not be null");
Assert.notNull(message, "'message' must not be null");
Object mgm = this.doRetrieve(MESSAGE_GROUP_KEY_PREFIX + groupId);
if (mgm != null) {
Assert.isInstanceOf(MessageGroupMetadata.class, mgm);
return (MessageGroupMetadata) mgm;
}
return null;
}
// add message as is to the MG accessible by the caller
MessageGroup messageGroup = getMessageGroup(groupId);
@Override
public void addMessagesToGroup(Object groupId, Message<?>... messages) {
Assert.notNull(groupId, "'groupId' must not be null");
Assert.notNull(messages, "'messages' must not be null");
messageGroup.add(message);
MessageGroupMetadata metadata = getGroupMetadata(groupId);
SimpleMessageGroup group = null;
if (metadata == null) {
group = new SimpleMessageGroup(groupId);
}
// enrich Message with additional headers and add it to MS
Message<?> enrichedMessage = enrichMessage(message);
for (Message<?> message : messages) {
// enrich Message with additional headers and add it to MS
Message<?> enrichedMessage = enrichMessage(message);
addMessage(enrichedMessage);
if (metadata != null) {
metadata.add(enrichedMessage.getHeaders().getId());
}
else {
group.add(enrichedMessage);
}
}
addMessage(enrichedMessage);
if (group != null) {
metadata = new MessageGroupMetadata(group);
}
// build raw MessageGroup and add enriched Message to it
MessageGroup rawGroup = buildMessageGroup(groupId, true);
rawGroup.setLastModified(System.currentTimeMillis());
rawGroup.add(enrichedMessage);
metadata.setLastModified(System.currentTimeMillis());
// store MessageGroupMetadata built from enriched MG
doStore(MESSAGE_GROUP_KEY_PREFIX + groupId, new MessageGroupMetadata(rawGroup));
// return clean MG
return getMessageGroup(groupId);
doStore(MESSAGE_GROUP_KEY_PREFIX + groupId, metadata);
}
/**
@@ -137,32 +161,17 @@ public abstract class AbstractKeyValueMessageStore extends AbstractMessageGroupS
Assert.notNull(groupId, "'groupId' must not be null");
Assert.notNull(messageToRemove, "'messageToRemove' must not be null");
// build raw MG
MessageGroup rawGroup = buildMessageGroup(groupId, true);
UUID id = messageToRemove.getHeaders().getId();
removeMessage(id);
// create a clean instance of
MessageGroup messageGroup = normalizeSimpleMessageGroup(rawGroup);
Message<?> actualMessageToRemove = null;
for (Message<?> message : rawGroup.getMessages()) {
if (message.getHeaders().getId().equals(messageToRemove.getHeaders().getId())) {
actualMessageToRemove = message;
break;
}
MessageGroupMetadata metadata = getGroupMetadata(groupId);
if (metadata != null) {
metadata.remove(id);
metadata.setLastModified(System.currentTimeMillis());
doStore(MESSAGE_GROUP_KEY_PREFIX + groupId, metadata);
}
if (actualMessageToRemove != null) {
rawGroup.remove(actualMessageToRemove);
removeMessage(messageToRemove.getHeaders().getId());
rawGroup.setLastModified(System.currentTimeMillis());
doStore(MESSAGE_GROUP_KEY_PREFIX + groupId, new MessageGroupMetadata(rawGroup));
messageGroup = getMessageGroup(groupId);
}
return messageGroup;
return getMessageGroup(groupId);
}
@@ -188,10 +197,12 @@ public abstract class AbstractKeyValueMessageStore extends AbstractMessageGroupS
@Override
public void completeGroup(Object groupId) {
Assert.notNull(groupId, "'groupId' must not be null");
MessageGroup messageGroup = buildMessageGroup(groupId, true);
messageGroup.complete();
messageGroup.setLastModified(System.currentTimeMillis());
doStore(MESSAGE_GROUP_KEY_PREFIX + groupId, new MessageGroupMetadata(messageGroup));
MessageGroupMetadata metadata = getGroupMetadata(groupId);
if (metadata != null) {
metadata.complete();
metadata.setLastModified(System.currentTimeMillis());
doStore(MESSAGE_GROUP_KEY_PREFIX + groupId, metadata);
}
}
/**
@@ -215,37 +226,62 @@ public abstract class AbstractKeyValueMessageStore extends AbstractMessageGroupS
@Override
public void setLastReleasedSequenceNumberForGroup(Object groupId, int sequenceNumber) {
Assert.notNull(groupId, "'groupId' must not be null");
MessageGroup messageGroup = buildMessageGroup(groupId, true);
messageGroup.setLastReleasedMessageSequenceNumber(sequenceNumber);
messageGroup.setLastModified(System.currentTimeMillis());
doStore(MESSAGE_GROUP_KEY_PREFIX + groupId, new MessageGroupMetadata(messageGroup));
MessageGroupMetadata metadata = getGroupMetadata(groupId);
if (metadata == null) {
SimpleMessageGroup messageGroup = new SimpleMessageGroup(groupId);
metadata = new MessageGroupMetadata(messageGroup);
}
metadata.setLastReleasedMessageSequenceNumber(sequenceNumber);
metadata.setLastModified(System.currentTimeMillis());
doStore(MESSAGE_GROUP_KEY_PREFIX + groupId, metadata);
}
@Override
public Message<?> pollMessageFromGroup(Object groupId) {
Assert.notNull(groupId, "'groupId' must not be null");
Object mgm = doRetrieve(MESSAGE_GROUP_KEY_PREFIX + groupId);
if (mgm != null) {
Assert.isInstanceOf(MessageGroupMetadata.class, mgm);
MessageGroupMetadata messageGroupMetadata = (MessageGroupMetadata) mgm;
UUID firstId = messageGroupMetadata.firstId();
MessageGroupMetadata groupMetadata = getGroupMetadata(groupId);
if (groupMetadata != null) {
UUID firstId = groupMetadata.firstId();
if (firstId != null) {
messageGroupMetadata.remove(firstId);
messageGroupMetadata.setLastModified(System.currentTimeMillis());
doStore(MESSAGE_GROUP_KEY_PREFIX + groupId, messageGroupMetadata);
groupMetadata.remove(firstId);
groupMetadata.setLastModified(System.currentTimeMillis());
doStore(MESSAGE_GROUP_KEY_PREFIX + groupId, groupMetadata);
return removeMessage(firstId);
}
}
return null;
}
@Override
public Message<?> getOneMessageFromGroup(Object groupId) {
MessageGroupMetadata groupMetadata = getGroupMetadata(groupId);
if (groupMetadata != null) {
UUID messageId = groupMetadata.firstId();
if (messageId != null) {
return getMessage(messageId);
}
}
return null;
}
@Override
public Collection<Message<?>> getMessagesForGroup(Object groupId) {
MessageGroupMetadata groupMetadata = getGroupMetadata(groupId);
ArrayList<Message<?>> messages = new ArrayList<Message<?>>();
if (groupMetadata != null) {
Iterator<UUID> messageIds = groupMetadata.messageIdIterator();
while (messageIds.hasNext()) {
messages.add(getMessage(messageIds.next()));
}
}
return messages;
}
@Override
@SuppressWarnings("unchecked")
public Iterator<MessageGroup> iterator() {
final Iterator<?> idIterator = normalizeKeys(
(Collection<String>) doListKeys(MESSAGE_GROUP_KEY_PREFIX + "*"))
.iterator();
.iterator();
return new MessageGroupIterator(idIterator);
}
@@ -266,13 +302,13 @@ public abstract class AbstractKeyValueMessageStore extends AbstractMessageGroupS
@Override
public int messageGroupSize(Object groupId) {
Object mgm = doRetrieve(MESSAGE_GROUP_KEY_PREFIX + groupId);
MessageGroupMetadata mgm = getGroupMetadata(groupId);
if (mgm != null) {
Assert.isInstanceOf(MessageGroupMetadata.class, mgm);
MessageGroupMetadata messageGroupMetadata = (MessageGroupMetadata) mgm;
return messageGroupMetadata.size();
return mgm.size();
}
else {
return 0;
}
return 0;
}
protected abstract Object doRetrieve(Object id);
@@ -308,48 +344,6 @@ public abstract class AbstractKeyValueMessageStore extends AbstractMessageGroupS
return enrichedMessage;
}
private MessageGroup buildMessageGroup(Object groupId, boolean raw) {
Assert.notNull(groupId, "'groupId' must not be null");
Object mgm = doRetrieve(MESSAGE_GROUP_KEY_PREFIX + groupId);
if (mgm != null) {
Assert.isInstanceOf(MessageGroupMetadata.class, mgm);
MessageGroupMetadata messageGroupMetadata = (MessageGroupMetadata) mgm;
ArrayList<Message<?>> messages = new ArrayList<Message<?>>();
Iterator<UUID> messageIds = messageGroupMetadata.messageIdIterator();
while (messageIds.hasNext()) {
UUID next = messageIds.next();
if (next != null) {
if (raw) {
messages.add(getRawMessage(next));
}
else {
messages.add(getMessage(next));
}
}
}
MessageGroup messageGroup = getMessageGroupFactory()
.create(messages, groupId, messageGroupMetadata.getTimestamp(), messageGroupMetadata.isComplete());
messageGroup.setLastModified(messageGroupMetadata.getLastModified());
messageGroup.setLastReleasedMessageSequenceNumber(
messageGroupMetadata.getLastReleasedMessageSequenceNumber());
return messageGroup;
}
else {
return getMessageGroupFactory().create(groupId);
}
}
private MessageGroup normalizeSimpleMessageGroup(MessageGroup messageGroup) {
MessageGroup normalizedGroup = getMessageGroupFactory().create(messageGroup.getGroupId());
for (Message<?> message : messageGroup.getMessages()) {
Message<?> normalizedMessage = normalizeMessage(message);
normalizedGroup.add(normalizedMessage);
}
return normalizedGroup;
}
private Message<?> getRawMessage(UUID id) {
Assert.notNull(id, "'id' must not be null");
Object message = doRetrieve(MESSAGE_KEY_PREFIX + id);
@@ -380,4 +374,5 @@ public abstract class AbstractKeyValueMessageStore extends AbstractMessageGroupS
throw new UnsupportedOperationException();
}
}
}

View File

@@ -49,6 +49,9 @@ public abstract class AbstractMessageGroupStore extends AbstractBatchingMessageG
private final Collection<MessageGroupCallback> expiryCallbacks = new LinkedHashSet<MessageGroupCallback>();
private final MessageGroupFactory persistentMessageGroupFactory =
new SimpleMessageGroupFactory(SimpleMessageGroupFactory.GroupType.PERSISTENT);
private volatile boolean timeoutOnIdle;
private volatile BeanFactory beanFactory;
@@ -57,6 +60,8 @@ public abstract class AbstractMessageGroupStore extends AbstractBatchingMessageG
private volatile boolean messageBuilderFactorySet;
private boolean lazyLoadMessageGroups = true;
public AbstractMessageGroupStore() {
super();
}
@@ -77,6 +82,16 @@ public abstract class AbstractMessageGroupStore extends AbstractBatchingMessageG
return this.messageBuilderFactory;
}
@Override
protected MessageGroupFactory getMessageGroupFactory() {
if (this.lazyLoadMessageGroups) {
return this.persistentMessageGroupFactory;
}
else {
return super.getMessageGroupFactory();
}
}
/**
* 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)}.
@@ -98,13 +113,24 @@ public abstract class AbstractMessageGroupStore extends AbstractBatchingMessageG
* 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;
}
/**
* Specify if the result of the {@link #getMessageGroup(Object)} should be wrapped
* to the {@link PersistentMessageGroup} - a lazy-load proxy for messages in group
* Defaults to {@code true}.
* <p> The target logic is based on the {@link SimpleMessageGroupFactory.GroupType#PERSISTENT}.
* @param lazyLoadMessageGroups the {@code boolean} flag to use.
* @since 4.3
*/
public void setLazyLoadMessageGroups(boolean lazyLoadMessageGroups) {
this.lazyLoadMessageGroups = lazyLoadMessageGroups;
}
@Override
public void registerMessageGroupExpiryCallback(MessageGroupCallback callback) {
this.expiryCallbacks.add(callback);
@@ -119,7 +145,7 @@ public abstract class AbstractMessageGroupStore extends AbstractBatchingMessageG
long timestamp = group.getTimestamp();
if (this.isTimeoutOnIdle() && group.getLastModified() > 0) {
timestamp = group.getLastModified();
timestamp = group.getLastModified();
}
if (timestamp <= threshold) {
@@ -175,8 +201,9 @@ public abstract class AbstractMessageGroupStore extends AbstractBatchingMessageG
}
@Override
public Message<?> getOneMessageFromGroup(Object groupId) {
throw new UnsupportedOperationException("Not yet implemented for this store");
public MessageGroup addMessageToGroup(Object groupId, Message<?> message) {
addMessagesToGroup(groupId, message);
return getMessageGroup(groupId);
}
private void expire(MessageGroup group) {

View File

@@ -29,11 +29,57 @@ import org.springframework.messaging.Message;
*/
public interface MessageGroupFactory {
/**
* Create a {@link MessageGroup} instance based on the provided {@code groupId}.
* @param groupId the group id to use.
* @return the {@link MessageGroup} instance.
*/
MessageGroup create(Object groupId);
/**
* Create a {@link MessageGroup} instance based on the provided {@code groupId}
* and with the {@code messages} for the group.
* @param messages the messages for the group.
* @param groupId the group id to use.
* @return the {@link MessageGroup} instance.
*/
MessageGroup create(Collection<? extends Message<?>> messages, Object groupId);
/**
* Create a {@link MessageGroup} instance based on the provided {@code groupId}
* and with the {@code messages} for the group.
* In addition the creating {@code timestamp} and {@code complete} flag may be used to customize
* the target {@link MessageGroup} object.
* @param messages the messages for the group.
* @param groupId the group id to use.
* @param timestamp the creation time.
* @param complete the {@code boolean} flag to indicate that group is completed.
* @return the {@link MessageGroup} instance.
*/
MessageGroup create(Collection<? extends Message<?>> messages, Object groupId, long timestamp,
boolean complete);
/**
* Create a {@link MessageGroup} instance based on the provided {@code groupId}.
* The {@link MessageGroupStore} may be consulted for the messages and metadata for the {@link MessageGroup}.
* @param messageGroupStore the {@link MessageGroupStore} for additional {@link MessageGroup} information.
* @param groupId the group id to use.
* @return the {@link MessageGroup} instance.
*/
MessageGroup create(MessageGroupStore messageGroupStore, Object groupId);
/**
* Create a {@link MessageGroup} instance based on the provided {@code groupId}.
* The {@link MessageGroupStore} may be consulted for the messages and metadata for the {@link MessageGroup}.
* In addition the creating {@code timestamp} and {@code complete} flag may be used to customize
* the target {@link MessageGroup} object.
* @param messageGroupStore the {@link MessageGroupStore} for additional {@link MessageGroup} information.
* @param groupId the group id to use.
* @param timestamp the creation time.
* @param complete the {@code boolean} flag to indicate that group is completed.
* @return the {@link MessageGroup} instance.
*/
MessageGroup create(MessageGroupStore messageGroupStore, Object groupId, long timestamp,
boolean complete);
}

View File

@@ -26,10 +26,11 @@ import org.springframework.messaging.Message;
import org.springframework.util.Assert;
/**
* Immutable Value Object holding metadata about a MessageGroup.
* Value Object holding metadata about a MessageGroup in the MessageGroupStore.
*
* @author Oleg Zhurakousky
* @author Gary Russell
* @author Artem Bilan
* @since 2.1
*/
public class MessageGroupMetadata implements Serializable {
@@ -40,47 +41,35 @@ public class MessageGroupMetadata implements Serializable {
private final List<UUID> messageIds = new LinkedList<UUID>();
private final boolean complete;
private final long timestamp;
private volatile boolean complete;
private volatile long lastModified;
private final int lastReleasedMessageSequenceNumber;
private final boolean hasMessages;
private final UUID first;
private volatile int lastReleasedMessageSequenceNumber;
public MessageGroupMetadata(MessageGroup messageGroup) {
this(messageGroup, true, null);
}
public MessageGroupMetadata(MessageGroup messageGroup, boolean hasMessages, UUID first) {
Assert.notNull(messageGroup, "'messageGroup' must not be null");
this.groupId = messageGroup.getGroupId();
if (hasMessages) {
for (Message<?> message : messageGroup.getMessages()) {
this.messageIds.add(message.getHeaders().getId());
}
for (Message<?> message : messageGroup.getMessages()) {
this.messageIds.add(message.getHeaders().getId());
}
this.complete = messageGroup.isComplete();
this.timestamp = messageGroup.getTimestamp();
this.lastReleasedMessageSequenceNumber = messageGroup.getLastReleasedMessageSequenceNumber();
this.lastModified = messageGroup.getLastModified();
this.hasMessages = hasMessages;
this.first = first;
}
public void remove(UUID messageId) {
if (!this.hasMessages) {
throw new IllegalStateException("Messages are not available, fetch the entire group");
}
this.messageIds.remove(messageId);
}
public void setLastModified(long lastModified) {
boolean add(UUID messageId) {
return !this.messageIds.contains(messageId) && this.messageIds.add(messageId);
}
void setLastModified(long lastModified) {
this.lastModified = lastModified;
}
@@ -89,9 +78,6 @@ public class MessageGroupMetadata implements Serializable {
}
public Iterator<UUID> messageIdIterator() {
if (!this.hasMessages) {
throw new IllegalStateException("Messages are not available, fetch the entire group");
}
return this.messageIds.iterator();
}
@@ -100,18 +86,16 @@ public class MessageGroupMetadata implements Serializable {
}
public UUID firstId() {
if (this.first != null) {
return this.first;
}
if (!this.hasMessages) {
throw new IllegalStateException("Messages are not available, fetch the entire group");
}
if (this.messageIds.size() > 0) {
return this.messageIds.iterator().next();
return this.messageIds.get(0);
}
return null;
}
void complete() {
this.complete = true;
}
public boolean isComplete() {
return this.complete;
}
@@ -127,4 +111,9 @@ public class MessageGroupMetadata implements Serializable {
public int getLastReleasedMessageSequenceNumber() {
return this.lastReleasedMessageSequenceNumber;
}
void setLastReleasedMessageSequenceNumber(int lastReleasedMessageSequenceNumber) {
this.lastReleasedMessageSequenceNumber = lastReleasedMessageSequenceNumber;
}
}

View File

@@ -29,6 +29,7 @@ import org.springframework.messaging.Message;
* @author Dave Syer
* @author Oleg Zhurakousky
* @author Gary Russell
* @author Artem Bilan
*
* @since 2.0
*
@@ -36,9 +37,8 @@ import org.springframework.messaging.Message;
public interface MessageGroupStore extends BasicMessageGroupStore {
/**
* Optional attribute giving the number of messages in the store over all groups. Implementations may decline to
* respond by throwing an exception.
*
* Optional attribute giving the number of messages in the store over all groups.
* Implementations may decline to respond by throwing an exception.
* @return the number of messages
* @throws UnsupportedOperationException if not implemented
*/
@@ -46,9 +46,8 @@ public interface MessageGroupStore extends BasicMessageGroupStore {
int getMessageCountForAllMessageGroups();
/**
* Optional attribute giving the number of message groups. Implementations may decline
* to respond by throwing an exception.
*
* Optional attribute giving the number of message groups.
* Implementations may decline to respond by throwing an exception.
* @return the number message groups
* @throws UnsupportedOperationException if not implemented
*/
@@ -56,9 +55,8 @@ public interface MessageGroupStore extends BasicMessageGroupStore {
int getMessageGroupCount();
/**
* Persist the deletion of a single message from the group. The group is modified to reflect that 'messageToRemove' is
* no longer present in the group.
*
* Persist the deletion of a single message from the group.
* The group is modified to reflect that 'messageToRemove' is no longer present in the group.
* @param key The groupId for the group containing the message.
* @param messageToRemove The message to be removed.
* @return The message Group.
@@ -69,27 +67,22 @@ public interface MessageGroupStore extends BasicMessageGroupStore {
/**
* Persist the deletion of messages from the group.
*
* @param key The groupId for the group containing the message(s).
* @param messages The messages to be removed.
*
* @since 4.2
*/
void removeMessagesFromGroup(Object key, Collection<Message<?>> messages);
/**
* Persist the deletion of messages from the group.
*
* @param key The groupId for the group containing the message(s).
* @param messages The messages to be removed.
*
* @since 4.2
*/
void removeMessagesFromGroup(Object key, Message<?>... messages);
/**
* Register a callback for when a message group is expired through {@link #expireMessageGroups(long)}.
*
* @param callback A callback to execute when a message group is cleaned up.
*/
void registerMessageGroupExpiryCallback(MessageGroupCallback callback);
@@ -99,10 +92,8 @@ public interface MessageGroupStore extends BasicMessageGroupStore {
* each of the registered callbacks on them in turn. For example: call with a timeout of 100 to expire all groups
* that were created more than 100 milliseconds ago, and are not yet complete. Use a timeout of 0 (or negative to be
* on the safe side) to expire all message groups.
*
* @param timeout the timeout threshold to use
* @return the number of message groups expired
*
* @see #registerMessageGroupExpiryCallback(MessageGroupCallback)
*/
@ManagedOperation
@@ -110,7 +101,6 @@ public interface MessageGroupStore extends BasicMessageGroupStore {
/**
* Allows you to set the sequence number of the last released Message. Used for Resequencing use cases
*
* @param groupId The group identifier.
* @param sequenceNumber The sequence number.
*/
@@ -125,7 +115,6 @@ public interface MessageGroupStore extends BasicMessageGroupStore {
* Completes this MessageGroup. Completion of the MessageGroup generally means
* that this group should not be allowing any more mutating operation to be performed on it.
* For example any attempt to add/remove new Message form the group should not be allowed.
*
* @param groupId The group identifier.
*/
void completeGroup(Object groupId);
@@ -140,13 +129,30 @@ public interface MessageGroupStore extends BasicMessageGroupStore {
MessageGroupMetadata getGroupMetadata(Object groupId);
/**
* Return the one {@link org.springframework.messaging.Message} from {@link org.springframework.integration.store.MessageGroup}.
* Return the one {@link Message} from {@link MessageGroup}.
* @param groupId The group identifier.
* @return the {@link org.springframework.messaging.Message}.
* @return the {@link Message}.
* @since 4.0
*/
Message<?> getOneMessageFromGroup(Object groupId);
/**
* Store messages with an association to a group id.
* This can be used to group messages together.
* @param groupId The group id to store messages under.
* @param messages The messages to add.
* @since 4.3
*/
void addMessagesToGroup(Object groupId, Message<?>... messages);
/**
* Retrieve messages for the provided group id.
* @param groupId The group id to retrieve messages for.
* @return the messages for group.
* @since 4.3
*/
Collection<Message<?>> getMessagesForGroup(Object groupId);
/**
* Invoked when a MessageGroupStore expires a group.
*/

View File

@@ -0,0 +1,226 @@
/*
* Copyright 2016 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;
import java.util.AbstractCollection;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.integration.IntegrationMessageHeaderAccessor;
import org.springframework.messaging.Message;
/**
* @author Artem Bilan
* @since 4.3
*/
class PersistentMessageGroup implements MessageGroup {
private static final Log logger = LogFactory.getLog(PersistentMessageGroup.class);
private MessageGroupStore messageGroupStore;
private final Collection<Message<?>> messages = new PersistentCollection();
private final MessageGroup original;
private volatile Message<?> oneMessage;
private volatile int size;
PersistentMessageGroup(MessageGroupStore messageGroupStore, MessageGroup original) {
this.messageGroupStore = messageGroupStore;
this.original = original;
}
public void setSize(int size) {
this.size = size;
}
@Override
public Collection<Message<?>> getMessages() {
return Collections.unmodifiableCollection(this.messages);
}
@Override
public Message<?> getOne() {
if (this.oneMessage == null) {
synchronized (this) {
if (this.oneMessage == null) {
if (logger.isDebugEnabled()) {
logger.debug("Lazy loading of one message for messageGroup: " + this.original.getGroupId());
}
this.oneMessage = this.messageGroupStore.getOneMessageFromGroup(this.original.getGroupId());
}
}
}
return this.oneMessage;
}
@Override
public int getSequenceSize() {
if (size() == 0) {
return 0;
}
else {
Message<?> message = getOne();
if (message != null) {
Integer sequenceSize = message.getHeaders()
.get(IntegrationMessageHeaderAccessor.SEQUENCE_SIZE, Integer.class);
return (sequenceSize != null ? sequenceSize : 0);
}
else {
return 0;
}
}
}
@Override
public int size() {
if (this.size == 0) {
synchronized (this) {
if (this.size == 0) {
if (logger.isDebugEnabled()) {
logger.debug("Lazy loading of group size for messageGroup: " + this.original.getGroupId());
}
this.size = this.messageGroupStore.messageGroupSize(this.original.getGroupId());
}
}
}
return this.size;
}
@Override
public Object getGroupId() {
return this.original.getGroupId();
}
@Override
public boolean canAdd(Message<?> message) {
return this.original.canAdd(message);
}
@Override
public int getLastReleasedMessageSequenceNumber() {
return this.original.getLastReleasedMessageSequenceNumber();
}
@Override
public boolean isComplete() {
return this.original.isComplete();
}
@Override
public void complete() {
this.original.complete();
}
@Override
public long getTimestamp() {
return this.original.getTimestamp();
}
@Override
public long getLastModified() {
return this.original.getLastModified();
}
@Override
public void setLastModified(long lastModified) {
this.original.setLastModified(lastModified);
}
@Override
public void add(Message<?> messageToAdd) {
this.original.add(messageToAdd);
}
@Override
public boolean remove(Message<?> messageToRemove) {
return this.original.remove(messageToRemove);
}
@Override
public void setLastReleasedMessageSequenceNumber(int sequenceNumber) {
this.original.setLastReleasedMessageSequenceNumber(sequenceNumber);
}
@Override
public void clear() {
this.original.clear();
}
private class PersistentCollection extends AbstractCollection<Message<?>> {
private volatile Collection<Message<?>> collection;
private void load() {
if (this.collection == null) {
synchronized (this) {
if (this.collection == null) {
Object groupId = PersistentMessageGroup.this.original.getGroupId();
if (logger.isDebugEnabled()) {
logger.debug("Lazy loading of messages for messageGroup: " + groupId);
}
this.collection = PersistentMessageGroup.this.messageGroupStore.getMessagesForGroup(groupId);
}
}
}
}
@Override
public boolean contains(Object o) {
load();
return this.collection.contains(o);
}
@Override
public Object[] toArray() {
load();
return this.collection.toArray();
}
@Override
public <T> T[] toArray(T[] a) {
load();
return this.collection.toArray(a);
}
@Override
public boolean containsAll(Collection<?> c) {
load();
return this.collection.containsAll(c);
}
@Override
public Iterator<Message<?>> iterator() {
load();
return this.collection.iterator();
}
@Override
public int size() {
return PersistentMessageGroup.this.size();
}
}
}

View File

@@ -59,6 +59,28 @@ public class SimpleMessageGroupFactory implements MessageGroupFactory {
return new SimpleMessageGroup(this.type.get(), messages, groupId, timestamp, complete);
}
@Override
public MessageGroup create(MessageGroupStore messageGroupStore, Object groupId) {
if (GroupType.PERSISTENT.equals(this.type)) {
return new PersistentMessageGroup(messageGroupStore, new SimpleMessageGroup(groupId));
}
else {
return create(messageGroupStore.getMessagesForGroup(groupId), groupId);
}
}
@Override
public MessageGroup create(MessageGroupStore messageGroupStore, Object groupId, long timestamp, boolean complete) {
if (GroupType.PERSISTENT.equals(this.type)) {
SimpleMessageGroup original = new SimpleMessageGroup(Collections.<Message<?>>emptyList(), groupId,
timestamp, complete);
return new PersistentMessageGroup(messageGroupStore, original);
}
else {
return create(messageGroupStore.getMessagesForGroup(groupId), groupId, timestamp, complete);
}
}
public enum GroupType {
BLOCKING_QUEUE {
@@ -84,6 +106,14 @@ public class SimpleMessageGroupFactory implements MessageGroupFactory {
return Collections.<Message<?>>synchronizedSet(new LinkedHashSet<Message<?>>());
}
},
PERSISTENT {
@Override
Collection<Message<?>> get() {
return HASH_SET.get();
}
};
abstract Collection<Message<?>> get();

View File

@@ -239,7 +239,10 @@ public class SimpleMessageStore extends AbstractMessageGroupStore
}
@Override
public MessageGroup addMessageToGroup(Object groupId, Message<?> message) {
public void addMessagesToGroup(Object groupId, Message<?>... messages) {
Assert.notNull(groupId, "'groupId' must not be null");
Assert.notNull(messages, "'messages' must not be null");
Lock lock = this.lockRegistry.obtain(groupId);
try {
lock.lockInterruptibly();
@@ -247,31 +250,38 @@ public class SimpleMessageStore extends AbstractMessageGroupStore
try {
UpperBound upperBound;
MessageGroup group = this.groupIdToMessageGroup.get(groupId);
MessagingException outOfCapacityException =
new MessagingException(getClass().getSimpleName() +
" was out of capacity (" + this.groupCapacity + ") for group '" + groupId +
"', try constructing it with a larger capacity.");
if (group == null) {
if (this.groupCapacity > 0 && messages.length > this.groupCapacity) {
throw outOfCapacityException;
}
group = getMessageGroupFactory().create(groupId);
this.groupIdToMessageGroup.putIfAbsent(groupId, group);
upperBound = new UpperBound(this.groupCapacity);
upperBound.tryAcquire(-1);
for (Message<?> message : messages) {
upperBound.tryAcquire(-1);
group.add(message);
}
this.groupToUpperBound.putIfAbsent(groupId, upperBound);
}
else {
upperBound = this.groupToUpperBound.get(groupId);
Assert.state(upperBound != null, "'upperBound' must not be null.");
lock.unlock();
if (!upperBound.tryAcquire(this.upperBoundTimeout)) {
unlocked = true;
throw new MessagingException(this.getClass().getSimpleName()
+ " was out of capacity ("
+ this.groupCapacity
+ ") for group '"
+ groupId
+ "', try constructing it with a larger capacity.");
for (Message<?> message : messages) {
lock.unlock();
if (!upperBound.tryAcquire(this.upperBoundTimeout)) {
unlocked = true;
throw outOfCapacityException;
}
lock.lockInterruptibly();
group.add(message);
}
lock.lockInterruptibly();
}
group.add(message);
group.setLastModified(System.currentTimeMillis());
return group;
}
finally {
if (!unlocked) {
@@ -445,6 +455,11 @@ public class SimpleMessageStore extends AbstractMessageGroupStore
return getMessageGroup(groupId).getOne();
}
@Override
public Collection<Message<?>> getMessagesForGroup(Object groupId) {
return getMessageGroup(groupId).getMessages();
}
public void clearMessageGroup(Object groupId) {
Lock lock = this.lockRegistry.obtain(groupId);
try {

View File

@@ -293,9 +293,8 @@ public class AbstractCorrelatingMessageHandlerTests {
Method forceComplete =
AbstractCorrelatingMessageHandler.class.getDeclaredMethod("forceComplete", MessageGroup.class);
forceComplete.setAccessible(true);
mgs.addMessageToGroup("foo", new GenericMessage<String>("foo"));
GenericMessage<String> secondMessage = new GenericMessage<String>("bar");
mgs.addMessageToGroup("foo", secondMessage);
mgs.addMessagesToGroup("foo", new GenericMessage<String>("foo"), secondMessage);
MessageGroup group = mgs.getMessageGroup("foo");
// remove a message
mgs.removeMessagesFromGroup("foo", secondMessage);
@@ -324,7 +323,7 @@ public class AbstractCorrelatingMessageHandlerTests {
QueueChannel outputChannel = new QueueChannel();
handler.setOutputChannel(outputChannel);
MessageGroupStore mgs = TestUtils.getPropertyValue(handler, "messageStore", MessageGroupStore.class);
mgs.addMessageToGroup("foo", new GenericMessage<String>("foo"));
mgs.addMessagesToGroup("foo", new GenericMessage<String>("foo"));
mgs.completeGroup("foo");
mgs = spy(mgs);
new DirectFieldAccessor(handler).setPropertyValue("messageStore", mgs);
@@ -357,7 +356,7 @@ public class AbstractCorrelatingMessageHandlerTests {
QueueChannel outputChannel = new QueueChannel();
handler.setOutputChannel(outputChannel);
MessageGroupStore mgs = TestUtils.getPropertyValue(handler, "messageStore", MessageGroupStore.class);
mgs.addMessageToGroup("foo", new GenericMessage<String>("foo"));
mgs.addMessagesToGroup("foo", new GenericMessage<String>("foo"));
MessageGroup group = new SimpleMessageGroup(mgs.getMessageGroup("foo"));
mgs.completeGroup("foo");
mgs = spy(mgs);
@@ -393,7 +392,7 @@ public class AbstractCorrelatingMessageHandlerTests {
QueueChannel outputChannel = new QueueChannel();
handler.setOutputChannel(outputChannel);
MessageGroupStore mgs = TestUtils.getPropertyValue(handler, "messageStore", MessageGroupStore.class);
mgs.addMessageToGroup("foo", new GenericMessage<String>("foo"));
mgs.addMessagesToGroup("foo", new GenericMessage<String>("foo"));
MessageGroup group = new SimpleMessageGroup(mgs.getMessageGroup("foo"));
mgs = spy(mgs);
new DirectFieldAccessor(handler).setPropertyValue("messageStore", mgs);

View File

@@ -36,7 +36,6 @@ import java.util.concurrent.atomic.AtomicReference;
import org.apache.commons.logging.Log;
import org.junit.Test;
import org.junit.runner.RunWith;
import reactor.rx.Promise;
import org.springframework.beans.DirectFieldAccessor;
import org.springframework.beans.factory.BeanNameAware;
@@ -65,6 +64,8 @@ import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import reactor.rx.Promise;
/**
* @author Mark Fisher
* @author Artem Bilan
@@ -173,7 +174,7 @@ public class GatewayParserTests {
this.startResponder(requestChannel, replyChannel);
TestService service = context.getBean("promise", TestService.class);
Promise<Message<?>> result = service.promise("foo");
Message<?> reply = result.await(1, TimeUnit.SECONDS);
Message<?> reply = result.await(10, TimeUnit.SECONDS);
assertEquals("foo", reply.getPayload());
assertNotNull(TestUtils.getPropertyValue(context.getBean("&promise"), "asyncExecutor"));
}

View File

@@ -34,6 +34,7 @@ import org.springframework.test.util.ReflectionTestUtils;
/**
* @author Dave Syer
* @author Gary Russell
* @author Artem Bilan
*/
public class MessageStoreTests {
@@ -80,7 +81,7 @@ public class MessageStoreTests {
assertEquals(1, store.getMessageCountForAllMessageGroups());
}
private static class TestMessageStore extends AbstractMessageGroupStore {
private static class TestMessageStore extends SimpleMessageStore {
@SuppressWarnings("unchecked")
MessageGroup testMessages =
@@ -95,7 +96,7 @@ public class MessageStoreTests {
}
@Override
public MessageGroup addMessageToGroup(Object correlationKey, Message<?> message) {
public void addMessagesToGroup(Object groupId, Message<?>... messages) {
throw new UnsupportedOperationException();
}