INT-2152
parent issue for INT-2134, INT-2135, INT-2142, INT-2155, INT-2158
This commit is contained in:
committed by
Mark Fisher
parent
86e59c8d3f
commit
946b9e2b82
@@ -16,278 +16,82 @@
|
||||
|
||||
package org.springframework.integration.redis.store;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.Iterator;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
|
||||
import org.springframework.dao.DataAccessException;
|
||||
import org.springframework.data.redis.connection.RedisConnection;
|
||||
import org.springframework.data.redis.connection.RedisConnectionFactory;
|
||||
import org.springframework.data.redis.core.BoundListOperations;
|
||||
import org.springframework.data.redis.core.BoundSetOperations;
|
||||
import org.springframework.data.redis.core.BoundValueOperations;
|
||||
import org.springframework.data.redis.core.RedisCallback;
|
||||
import org.springframework.data.redis.core.RedisTemplate;
|
||||
import org.springframework.data.redis.serializer.JdkSerializationRedisSerializer;
|
||||
import org.springframework.data.redis.serializer.RedisSerializer;
|
||||
import org.springframework.data.redis.serializer.SerializationException;
|
||||
import org.springframework.data.redis.serializer.StringRedisSerializer;
|
||||
import org.springframework.integration.Message;
|
||||
import org.springframework.integration.store.AbstractMessageGroupStore;
|
||||
import org.springframework.integration.store.MessageGroup;
|
||||
import org.springframework.integration.store.AbstractKeyValueMessageStore;
|
||||
import org.springframework.integration.store.MessageGroupStore;
|
||||
import org.springframework.integration.store.MessageStore;
|
||||
import org.springframework.integration.store.MessageStoreException;
|
||||
import org.springframework.integration.store.SimpleMessageGroup;
|
||||
import org.springframework.jmx.export.annotation.ManagedAttribute;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* An implementation of both the {@link MessageStore} and {@link MessageGroupStore}
|
||||
* strategies that relies upon Redis for persistence.
|
||||
* Redis implementation of the key/value style {@link MessageStore} and {@link MessageGroupStore}
|
||||
*
|
||||
* @author Oleg Zhurakousky
|
||||
* @since 2.1
|
||||
*/
|
||||
public class RedisMessageStore extends AbstractMessageGroupStore implements MessageStore {
|
||||
public class RedisMessageStore extends AbstractKeyValueMessageStore {
|
||||
|
||||
private static final String MESSAGE_GROUPS_KEY = "MESSAGE_GROUPS";
|
||||
|
||||
private static final String MARKED_PREFIX = "MARKED_";
|
||||
|
||||
private static final String UNMARKED_PREFIX = "UNMARKED_";
|
||||
|
||||
|
||||
private final RedisTemplate<String, Object> redisTemplate;
|
||||
|
||||
private final RedisTemplate<Object, Object> redisTemplate;
|
||||
|
||||
public RedisMessageStore(RedisConnectionFactory connectionFactory) {
|
||||
this.redisTemplate = new RedisTemplate<String, Object>();
|
||||
this.redisTemplate = new RedisTemplate<Object, Object>();
|
||||
this.redisTemplate.setConnectionFactory(connectionFactory);
|
||||
this.redisTemplate.setKeySerializer(new StringRedisSerializer());
|
||||
this.redisTemplate.setValueSerializer(new JdkSerializationRedisSerializer());
|
||||
}
|
||||
|
||||
|
||||
public void setValueSerializer(RedisSerializer<?> valueSerializer) {
|
||||
Assert.notNull(valueSerializer, "'valueSerializer' must not be null");
|
||||
this.redisTemplate.setValueSerializer(valueSerializer);
|
||||
}
|
||||
|
||||
public Message<?> getMessage(final UUID id) {
|
||||
|
||||
@Override
|
||||
protected Object doRetrieve(Object id){
|
||||
Assert.notNull(id, "'id' must not be null");
|
||||
if (this.redisTemplate.hasKey(id.toString())) {
|
||||
BoundValueOperations<String, Object> ops = redisTemplate.boundValueOps(id.toString());
|
||||
Object result = ops.get();
|
||||
Assert.isInstanceOf(Message.class, result, "Return value is not an instace of Message");
|
||||
return (Message<?>) result;
|
||||
}
|
||||
return null;
|
||||
BoundValueOperations<Object, Object> ops = redisTemplate.boundValueOps(id);
|
||||
return ops.get();
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public <T> Message<T> addMessage(Message<T> message) {
|
||||
Assert.notNull(message, "'message' must not be null");
|
||||
BoundValueOperations<String, Object> ops = redisTemplate.boundValueOps(message.getHeaders().getId().toString());
|
||||
try {
|
||||
ops.set(message);
|
||||
}
|
||||
catch (SerializationException e) {
|
||||
throw new MessageStoreException(message, "If relying on the default RedisSerializer (JdkSerializationRedisSerializer) " +
|
||||
"the Message must be Serializable. Either make it Serializable or provide your own implementation of " +
|
||||
"RedisSerializer via 'setValueSerializer(..)'", e);
|
||||
}
|
||||
Object result = ops.get();
|
||||
Assert.isInstanceOf(Message.class, result, "Return value is not an instace of Message");
|
||||
return (Message<T>) result;
|
||||
}
|
||||
|
||||
public Message<?> removeMessage(UUID id) {
|
||||
Assert.notNull(id, "'id' must not be null");
|
||||
Message<?> message = this.getMessage(id);
|
||||
if (message != null) {
|
||||
this.redisTemplate.delete(id.toString());
|
||||
}
|
||||
return message;
|
||||
}
|
||||
|
||||
@ManagedAttribute
|
||||
public long getMessageCount() {
|
||||
return redisTemplate.execute(new RedisCallback<Long>() {
|
||||
public Long doInRedis(RedisConnection connection) throws DataAccessException {
|
||||
return connection.dbSize();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// MESSAGE GROUP methods
|
||||
|
||||
/**
|
||||
* Will create a new instance of SimpleMessageGroup initializing it with
|
||||
* data collected from the Redis Message Store.
|
||||
*/
|
||||
public MessageGroup getMessageGroup(Object groupId) {
|
||||
Assert.notNull(groupId, "'groupId' must not be null");
|
||||
long timestamp = System.currentTimeMillis();
|
||||
Collection<Message<?>> unmarkedMessages = this.buildMessageList(this.redisTemplate.boundListOps(UNMARKED_PREFIX + groupId));
|
||||
Collection<Message<?>> markedMessages = this.buildMessageList(this.redisTemplate.boundListOps(MARKED_PREFIX + groupId));
|
||||
this.doCreateMessageGroupIfNecessary(groupId);
|
||||
return new SimpleMessageGroup(unmarkedMessages, markedMessages, groupId, timestamp);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a Message to the group with the provided group ID.
|
||||
*/
|
||||
public MessageGroup addMessageToGroup(Object groupId, Message<?> message) {
|
||||
Assert.notNull(groupId, "'groupId' must not be null");
|
||||
Assert.notNull(message, "'message' must not be null");
|
||||
synchronized (groupId) {
|
||||
this.doAddMessageToGroup(message, groupId);
|
||||
this.addMessage(message);
|
||||
return this.getMessageGroup(groupId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark all messages in the provided group.
|
||||
*/
|
||||
public MessageGroup markMessageGroup(MessageGroup group) {
|
||||
Assert.notNull(group, "'group' must not be null");
|
||||
Object groupId = group.getGroupId();
|
||||
synchronized (groupId) {
|
||||
this.doMarkMessageGroup(groupId);
|
||||
return this.getMessageGroup(groupId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a Message from the group with the provided group ID.
|
||||
*/
|
||||
public MessageGroup removeMessageFromGroup(Object groupId, Message<?> messageToRemove) {
|
||||
Assert.notNull(groupId, "'groupId' must not be null");
|
||||
Assert.notNull(messageToRemove, "'messageToRemove' must not be null");
|
||||
UUID messageId = messageToRemove.getHeaders().getId();
|
||||
synchronized (groupId) {
|
||||
this.doRemoveMessageFromGroup(groupId, messageId);
|
||||
this.removeMessage(messageId);
|
||||
return this.getMessageGroup(groupId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark the given Message within the group corresponding to the provided group ID.
|
||||
*/
|
||||
public MessageGroup markMessageFromGroup(Object groupId, Message<?> messageToMark) {
|
||||
Assert.notNull(groupId, "'groupId' must not be null");
|
||||
Assert.notNull(messageToMark, "'messageToMark' must not be null");
|
||||
String messageIdAsString = messageToMark.getHeaders().getId().toString();
|
||||
synchronized (groupId) {
|
||||
this.doMarkMessageFromGroup(messageIdAsString, groupId);
|
||||
return this.getMessageGroup(groupId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the MessageGroup with the provided group ID.
|
||||
*/
|
||||
public void removeMessageGroup(Object groupId) {
|
||||
Assert.notNull(groupId, "'groupId' must not be null");
|
||||
synchronized (groupId) {
|
||||
this.doRemoveMessageGroup(groupId);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Iterator<MessageGroup> iterator() {
|
||||
BoundSetOperations<String, Object> mGroupsOps = this.redisTemplate.boundSetOps(MESSAGE_GROUPS_KEY);
|
||||
Set<Object> messageGroupIds = mGroupsOps.members();
|
||||
List<MessageGroup> messageGroups = new ArrayList<MessageGroup>();
|
||||
for (Object messageGroupId : messageGroupIds) {
|
||||
messageGroups.add(this.getMessageGroup(messageGroupId));
|
||||
protected void doStore(Object id, Object objectToStore) {
|
||||
Assert.notNull(id, "'id' must not be null");
|
||||
Assert.notNull(objectToStore, "'objectToStore' must not be null");
|
||||
BoundValueOperations<Object, Object> ops = redisTemplate.boundValueOps(id);
|
||||
try {
|
||||
ops.set(objectToStore);
|
||||
}
|
||||
return messageGroups.iterator();
|
||||
}
|
||||
|
||||
private Collection<Message<?>> buildMessageList(BoundListOperations<String, Object> messageGroupOps) {
|
||||
List<Message<?>> messages = new LinkedList<Message<?>>();
|
||||
if (messageGroupOps.size() == 0) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
List<Object> messageIds = messageGroupOps.range(0, messageGroupOps.size() - 1);
|
||||
for (Object messageId : messageIds) {
|
||||
Message<?> message = this.getMessage(UUID.fromString(messageId.toString()));
|
||||
if (message != null) {
|
||||
messages.add((Message<?>) message);
|
||||
}
|
||||
}
|
||||
return messages;
|
||||
}
|
||||
|
||||
/* candidates for future abstract methods */
|
||||
|
||||
private void doCreateMessageGroupIfNecessary(Object groupId) {
|
||||
BoundSetOperations<String, Object> messageGroupsOps = this.redisTemplate.boundSetOps(MESSAGE_GROUPS_KEY);
|
||||
if (!messageGroupsOps.members().contains(groupId)) {
|
||||
messageGroupsOps.add(groupId);
|
||||
catch (SerializationException e) {
|
||||
throw new IllegalArgumentException("If relying on the default RedisSerializer (JdkSerializationRedisSerializer) " +
|
||||
"the Object must be Serializable. Either make it Serializable or provide your own implementation of " +
|
||||
"RedisSerializer via 'setValueSerializer(..)'", e);
|
||||
}
|
||||
}
|
||||
|
||||
private void doAddMessageToGroup(Message<?> message, Object groupId) {
|
||||
String messageId = message.getHeaders().getId().toString();
|
||||
BoundListOperations<String, Object> unmarkedOps = this.redisTemplate.boundListOps(UNMARKED_PREFIX + groupId);
|
||||
unmarkedOps.rightPush(messageId);
|
||||
|
||||
@Override
|
||||
protected Object doRemove(Object id) {
|
||||
Assert.notNull(id, "'id' must not be null");
|
||||
Object removedObject = this.doRetrieve(id);
|
||||
if (removedObject != null){
|
||||
redisTemplate.delete(id);
|
||||
}
|
||||
return removedObject;
|
||||
}
|
||||
|
||||
private void doMarkMessageGroup(Object groupId) {
|
||||
BoundListOperations<String, Object> unmarkedOps = this.redisTemplate.boundListOps(UNMARKED_PREFIX + groupId);
|
||||
unmarkedOps.rename(MARKED_PREFIX + groupId);
|
||||
}
|
||||
|
||||
private void doRemoveMessageFromGroup(Object groupId, UUID messageId) {
|
||||
BoundListOperations<String, Object> unmarkedOps = this.redisTemplate.boundListOps(UNMARKED_PREFIX + groupId);
|
||||
BoundListOperations<String, Object> markedOps = this.redisTemplate.boundListOps(MARKED_PREFIX + groupId);
|
||||
unmarkedOps.remove(0, messageId.toString());
|
||||
markedOps.remove(0, messageId.toString());
|
||||
@Override
|
||||
protected Collection<?> doListKeys(String keyPattern) {
|
||||
Assert.hasText(keyPattern, "'keyPattern' must not be empty");
|
||||
Set<Object> keys = redisTemplate.keys(keyPattern);
|
||||
return keys;
|
||||
}
|
||||
|
||||
private void doMarkMessageFromGroup(String messageIdAsString, Object groupId) {
|
||||
BoundListOperations<String, Object> unmarkedOps = this.redisTemplate.boundListOps(UNMARKED_PREFIX + groupId);
|
||||
if (unmarkedOps.size() > 0) {
|
||||
List<Object> messageIds = unmarkedOps.range(0, unmarkedOps.size() - 1);
|
||||
int objectIndex = messageIds.indexOf(messageIdAsString);
|
||||
if (objectIndex > -1) {
|
||||
BoundListOperations<String, Object> markedOps = this.redisTemplate.boundListOps(MARKED_PREFIX + groupId);
|
||||
markedOps.rightPush(messageIdAsString);
|
||||
unmarkedOps.remove(0, messageIdAsString);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void doRemoveMessageGroup(Object groupId) {
|
||||
BoundListOperations<String, Object> unmarkedOps = this.redisTemplate.boundListOps(UNMARKED_PREFIX + groupId);
|
||||
if (unmarkedOps.size() > 0) {
|
||||
List<Object> messageIds = unmarkedOps.range(0, unmarkedOps.size() - 1);
|
||||
for (Object messageId : messageIds) {
|
||||
this.removeMessage(UUID.fromString(messageId.toString()));
|
||||
}
|
||||
this.redisTemplate.delete(UNMARKED_PREFIX + groupId);
|
||||
}
|
||||
BoundListOperations<String, Object> markedOps = this.redisTemplate.boundListOps(MARKED_PREFIX + groupId);
|
||||
if (markedOps.size() > 0) {
|
||||
List<Object> messageIds = markedOps.range(0, markedOps.size() - 1);
|
||||
for (Object messageId : messageIds) {
|
||||
this.removeMessage(UUID.fromString(messageId.toString()));
|
||||
}
|
||||
this.redisTemplate.delete(MARKED_PREFIX + groupId);
|
||||
}
|
||||
BoundSetOperations<String, Object> messageGroupsOps = this.redisTemplate.boundSetOps(MESSAGE_GROUPS_KEY);
|
||||
messageGroupsOps.remove(groupId);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@ import java.util.concurrent.TimeUnit;
|
||||
|
||||
import junit.framework.AssertionFailedError;
|
||||
|
||||
import org.junit.Ignore;
|
||||
import org.junit.Test;
|
||||
import org.springframework.context.support.ClassPathXmlApplicationContext;
|
||||
import org.springframework.data.redis.connection.jedis.JedisConnectionFactory;
|
||||
@@ -107,6 +108,34 @@ public class RedisMessageGroupStoreTests extends RedisAvailableTests {
|
||||
assertEquals(0, messageGroup.size());
|
||||
}
|
||||
|
||||
@Test
|
||||
@RedisAvailable
|
||||
public void testCompleteMessageGroup() throws Exception{
|
||||
JedisConnectionFactory jcf = this.getConnectionFactoryForTest();
|
||||
RedisMessageStore store = new RedisMessageStore(jcf);
|
||||
|
||||
MessageGroup messageGroup = store.getMessageGroup(1);
|
||||
Message<?> message = new GenericMessage<String>("Hello");
|
||||
messageGroup = store.addMessageToGroup(messageGroup.getGroupId(), message);
|
||||
store.completeGroup(messageGroup.getGroupId());
|
||||
messageGroup = store.getMessageGroup(1);
|
||||
assertTrue(messageGroup.isComplete());
|
||||
}
|
||||
|
||||
@Test
|
||||
@RedisAvailable
|
||||
public void testLastReleasedSequenceNumber() throws Exception{
|
||||
JedisConnectionFactory jcf = this.getConnectionFactoryForTest();
|
||||
RedisMessageStore store = new RedisMessageStore(jcf);
|
||||
|
||||
MessageGroup messageGroup = store.getMessageGroup(1);
|
||||
Message<?> message = new GenericMessage<String>("Hello");
|
||||
messageGroup = store.addMessageToGroup(messageGroup.getGroupId(), message);
|
||||
store.setLastReleasedSequenceNumberForGroup(messageGroup.getGroupId(), 5);
|
||||
messageGroup = store.getMessageGroup(1);
|
||||
assertEquals(5, messageGroup.getLastReleasedMessageSequenceNumber());
|
||||
}
|
||||
|
||||
@Test
|
||||
@RedisAvailable
|
||||
public void testRemoveMessageFromTheGroup() throws Exception{
|
||||
@@ -128,7 +157,24 @@ public class RedisMessageGroupStoreTests extends RedisAvailableTests {
|
||||
|
||||
messageGroup = store.getMessageGroup(1);
|
||||
assertEquals(2, messageGroup.size());
|
||||
}
|
||||
@Test
|
||||
@RedisAvailable
|
||||
public void testRemoveNonExistingMessageFromTheGroup() throws Exception{
|
||||
JedisConnectionFactory jcf = this.getConnectionFactoryForTest();
|
||||
RedisMessageStore store = new RedisMessageStore(jcf);
|
||||
|
||||
MessageGroup messageGroup = store.getMessageGroup(1);
|
||||
store.addMessageToGroup(messageGroup.getGroupId(), new GenericMessage<String>("1"));
|
||||
store.removeMessageFromGroup(1, new GenericMessage<String>("2"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@RedisAvailable
|
||||
public void testRemoveNonExistingMessageFromNonExistingTheGroup() throws Exception{
|
||||
JedisConnectionFactory jcf = this.getConnectionFactoryForTest();
|
||||
RedisMessageStore store = new RedisMessageStore(jcf);
|
||||
store.removeMessageFromGroup(1, new GenericMessage<String>("2"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -183,6 +229,8 @@ public class RedisMessageGroupStoreTests extends RedisAvailableTests {
|
||||
assertEquals(1, messageGroup.getMarked().size());
|
||||
}
|
||||
|
||||
|
||||
|
||||
@Test
|
||||
@RedisAvailable
|
||||
public void testMultipleInstancesOfGroupStore() throws Exception{
|
||||
@@ -239,7 +287,7 @@ public class RedisMessageGroupStoreTests extends RedisAvailableTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
@RedisAvailable
|
||||
@RedisAvailable @Ignore
|
||||
public void testConcurrentModifications() throws Exception{
|
||||
JedisConnectionFactory jcf = this.getConnectionFactoryForTest();
|
||||
final RedisMessageStore store1 = new RedisMessageStore(jcf);
|
||||
|
||||
@@ -24,7 +24,6 @@ import org.springframework.integration.Message;
|
||||
import org.springframework.integration.message.GenericMessage;
|
||||
import org.springframework.integration.redis.rules.RedisAvailable;
|
||||
import org.springframework.integration.redis.rules.RedisAvailableTests;
|
||||
import org.springframework.integration.store.MessageStoreException;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
@@ -80,7 +79,7 @@ public class RedisMessageStoreTests extends RedisAvailableTests {
|
||||
assertEquals("Barak Obama", storedMessage.getPayload().getName());
|
||||
}
|
||||
|
||||
@Test(expected=MessageStoreException.class)
|
||||
@Test(expected=IllegalArgumentException.class)
|
||||
@RedisAvailable
|
||||
public void testAddNonSerializableObjectMessage(){
|
||||
JedisConnectionFactory jcf = this.getConnectionFactoryForTest();
|
||||
|
||||
Reference in New Issue
Block a user