/*
* Copyright 2002-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.Collection;
import java.util.HashSet;
import java.util.Iterator;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.locks.Lock;
import org.springframework.integration.support.locks.DefaultLockRegistry;
import org.springframework.integration.support.locks.LockRegistry;
import org.springframework.integration.util.UpperBound;
import org.springframework.jmx.export.annotation.ManagedAttribute;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessagingException;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
/**
* Map-based in-memory implementation of {@link MessageStore} and {@link MessageGroupStore}.
* Enforces a maximum capacity for the store.
*
* @author Iwein Fuld
* @author Mark Fisher
* @author Dave Syer
* @author Oleg Zhurakousky
* @author Gary Russell
* @author Ryan Barker
* @author Artem Bilan
*
* @since 2.0
*/
public class SimpleMessageStore extends AbstractMessageGroupStore
implements MessageStore, ChannelMessageStore {
private final ConcurrentMap<UUID, Message<?>> idToMessage = new ConcurrentHashMap<UUID, Message<?>>();
private final ConcurrentMap<Object, MessageGroup> groupIdToMessageGroup =
new ConcurrentHashMap<Object, MessageGroup>();
private final ConcurrentMap<Object, UpperBound> groupToUpperBound = new ConcurrentHashMap<Object, UpperBound>();
private final int groupCapacity;
private final int individualCapacity;
private final UpperBound individualUpperBound;
private volatile LockRegistry lockRegistry;
private volatile boolean isUsed;
private volatile boolean copyOnGet = false;
private final long upperBoundTimeout;
/**
* Creates a SimpleMessageStore with a maximum size limited by the given capacity, or unlimited size if the given
* capacity is less than 1. The capacities are applied independently to messages stored via
* {@link #addMessage(Message)} and to those stored via {@link #addMessageToGroup(Object, Message)}. In both cases
* the capacity applies to the number of messages that can be stored, and once that limit is reached attempting to
* store another will result in an exception.
* @param individualCapacity The message capacity.
* @param groupCapacity The capacity of each group.
*/
public SimpleMessageStore(int individualCapacity, int groupCapacity) {
this(individualCapacity, groupCapacity, new DefaultLockRegistry());
}
/**
* Creates a SimpleMessageStore with a maximum size limited by the given capacity and the timeout in millisecond
* to wait for the empty slot in the store.
* @param individualCapacity The message capacity.
* @param groupCapacity The capacity of each group.
* @param upperBoundTimeout The time to wait if the store is at max capacity.
* @see #SimpleMessageStore(int, int)
* @since 4.3
*/
public SimpleMessageStore(int individualCapacity, int groupCapacity, long upperBoundTimeout) {
this(individualCapacity, groupCapacity, upperBoundTimeout, new DefaultLockRegistry());
}
/**
* Creates a SimpleMessageStore with a maximum size limited by the given capacity and LockRegistry
* for the message group operations concurrency.
* @param individualCapacity The message capacity.
* @param groupCapacity The capacity of each group.
* @param lockRegistry The lock registry.
* @see #SimpleMessageStore(int, int, long, LockRegistry)
*/
public SimpleMessageStore(int individualCapacity, int groupCapacity, LockRegistry lockRegistry) {
this(individualCapacity, groupCapacity, 0, lockRegistry);
}
/**
* Creates a SimpleMessageStore with a maximum size limited by the given capacity,
* the timeout in millisecond to wait for the empty slot in the store and LockRegistry
* for the message group operations concurrency.
* @param individualCapacity The message capacity.
* @param groupCapacity The capacity of each group.
* @param upperBoundTimeout The time to wait if the store is at max capacity
* @param lockRegistry The lock registry.
* @since 4.3
*/
public SimpleMessageStore(int individualCapacity, int groupCapacity, long upperBoundTimeout,
LockRegistry lockRegistry) {
super(false);
Assert.notNull(lockRegistry, "The LockRegistry cannot be null");
this.individualUpperBound = new UpperBound(individualCapacity);
this.individualCapacity = individualCapacity;
this.groupCapacity = groupCapacity;
this.lockRegistry = lockRegistry;
this.upperBoundTimeout = upperBoundTimeout;
}
/**
* Creates a SimpleMessageStore with the same capacity for individual and grouped messages.
* @param capacity The capacity.
*/
public SimpleMessageStore(int capacity) {
this(capacity, capacity);
}
/**
* Creates a SimpleMessageStore with unlimited capacity
*/
public SimpleMessageStore() {
this(0);
}
/**
* Set to false to disable copying the group in {@link #getMessageGroup(Object)}.
* Starting with 4.1, this is false by default.
* @param copyOnGet True to copy, false to not.
* @since 4.0.1
*/
public void setCopyOnGet(boolean copyOnGet) {
this.copyOnGet = copyOnGet;
}
public void setLockRegistry(LockRegistry lockRegistry) {
Assert.notNull(lockRegistry, "The LockRegistry cannot be null");
Assert.isTrue(!(this.isUsed), "Cannot change the lock registry after the store has been used");
this.lockRegistry = lockRegistry;
}
@Override
public void setLazyLoadMessageGroups(boolean lazyLoadMessageGroups) {
throw new UnsupportedOperationException("The lazy-load isn't supported for in-memory 'SimpleMessageStore'");
}
@Override
@ManagedAttribute
public long getMessageCount() {
return this.idToMessage.size();
}
@Override
public <T> Message<T> addMessage(Message<T> message) {
this.isUsed = true;
if (!this.individualUpperBound.tryAcquire(this.upperBoundTimeout)) {
throw new MessagingException(this.getClass().getSimpleName()
+ " was out of capacity ("
+ this.individualCapacity
+ "), try constructing it with a larger capacity.");
}
this.idToMessage.put(message.getHeaders().getId(), message);
return message;
}
@Override
public Message<?> getMessage(UUID key) {
return (key != null) ? this.idToMessage.get(key) : null;
}
@Override
public MessageMetadata getMessageMetadata(UUID id) {
Message<?> message = getMessage(id);
if (message != null) {
MessageMetadata messageMetadata = new MessageMetadata(id);
messageMetadata.setTimestamp(message.getHeaders().getTimestamp());
return messageMetadata;
}
else {
return null;
}
}
@Override
public Message<?> removeMessage(UUID key) {
if (key != null) {
Message<?> message = this.idToMessage.remove(key);
if (message != null) {
this.individualUpperBound.release();
}
return message;
}
else {
return null;
}
}
@Override
public MessageGroup getMessageGroup(Object groupId) {
Assert.notNull(groupId, "'groupId' must not be null");
MessageGroup group = this.groupIdToMessageGroup.get(groupId);
if (group == null) {
return getMessageGroupFactory().create(groupId);
}
if (this.copyOnGet) {
return copy(group);
}
else {
return group;
}
}
@Override
protected MessageGroup copy(MessageGroup group) {
Object groupId = group.getGroupId();
Lock lock = this.lockRegistry.obtain(groupId);
try {
lock.lockInterruptibly();
try {
MessageGroup simpleMessageGroup = getMessageGroupFactory()
.create(group.getMessages(), groupId, group.getTimestamp(), group.isComplete());
simpleMessageGroup.setLastModified(group.getLastModified());
simpleMessageGroup.setLastReleasedMessageSequenceNumber(group.getLastReleasedMessageSequenceNumber());
return simpleMessageGroup;
}
finally {
lock.unlock();
}
}
catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new MessagingException("Interrupted while obtaining lock", e);
}
}
@Override
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();
boolean unlocked = false;
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.put(groupId, group);
upperBound = new UpperBound(this.groupCapacity);
for (Message<?> message : messages) {
upperBound.tryAcquire(-1);
group.add(message);
}
this.groupToUpperBound.put(groupId, upperBound);
}
else {
upperBound = this.groupToUpperBound.get(groupId);
Assert.state(upperBound != null, "'upperBound' must not be null.");
for (Message<?> message : messages) {
lock.unlock();
if (!upperBound.tryAcquire(this.upperBoundTimeout)) {
unlocked = true;
throw outOfCapacityException;
}
lock.lockInterruptibly();
group.add(message);
}
}
group.setLastModified(System.currentTimeMillis());
}
finally {
if (!unlocked) {
lock.unlock();
}
}
}
catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new MessagingException("Interrupted while obtaining lock", e);
}
}
@Override
public void removeMessageGroup(Object groupId) {
Lock lock = this.lockRegistry.obtain(groupId);
try {
lock.lockInterruptibly();
try {
MessageGroup messageGroup = this.groupIdToMessageGroup.remove(groupId);
if (messageGroup != null) {
UpperBound upperBound = this.groupToUpperBound.remove(groupId);
Assert.state(upperBound != null, "'upperBound' must not be null.");
upperBound.release(this.groupCapacity);
}
}
finally {
lock.unlock();
}
}
catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new MessagingException("Interrupted while obtaining lock", e);
}
}
@Override
public void removeMessagesFromGroup(Object groupId, Collection<Message<?>> messages) {
Lock lock = this.lockRegistry.obtain(groupId);
try {
lock.lockInterruptibly();
try {
MessageGroup group = this.groupIdToMessageGroup.get(groupId);
Assert.notNull(group, "MessageGroup for groupId '" + groupId + "' " +
"can not be located while attempting to remove Message(s) from the MessageGroup");
UpperBound upperBound = this.groupToUpperBound.get(groupId);
Assert.state(upperBound != null, "'upperBound' must not be null.");
boolean modified = false;
for (Message<?> messageToRemove : messages) {
if (group.remove(messageToRemove)) {
upperBound.release();
modified = true;
}
}
if (modified) {
group.setLastModified(System.currentTimeMillis());
}
}
finally {
lock.unlock();
}
}
catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new MessagingException("Interrupted while obtaining lock", e);
}
}
@Override
public Iterator<MessageGroup> iterator() {
return new HashSet<MessageGroup>(this.groupIdToMessageGroup.values()).iterator();
}
@Override
public void setLastReleasedSequenceNumberForGroup(Object groupId, int sequenceNumber) {
Lock lock = this.lockRegistry.obtain(groupId);
try {
lock.lockInterruptibly();
try {
MessageGroup group = this.groupIdToMessageGroup.get(groupId);
Assert.notNull(group, "MessageGroup for groupId '" + groupId + "' " +
"can not be located while attempting to set 'lastReleasedSequenceNumber'");
group.setLastReleasedMessageSequenceNumber(sequenceNumber);
group.setLastModified(System.currentTimeMillis());
}
finally {
lock.unlock();
}
}
catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new MessagingException("Interrupted while obtaining lock", e);
}
}
@Override
public void completeGroup(Object groupId) {
Lock lock = this.lockRegistry.obtain(groupId);
try {
lock.lockInterruptibly();
try {
MessageGroup group = this.groupIdToMessageGroup.get(groupId);
Assert.notNull(group, "MessageGroup for groupId '" + groupId + "' " +
"can not be located while attempting to complete the MessageGroup");
group.complete();
group.setLastModified(System.currentTimeMillis());
}
finally {
lock.unlock();
}
}
catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new MessagingException("Interrupted while obtaining lock", e);
}
}
@Override
public Message<?> pollMessageFromGroup(Object groupId) {
Collection<Message<?>> messageList = getMessageGroup(groupId).getMessages();
Message<?> message = null;
if (!CollectionUtils.isEmpty(messageList)) {
message = messageList.iterator().next();
if (message != null) {
this.removeMessagesFromGroup(groupId, message);
}
}
return message;
}
@Override
public int messageGroupSize(Object groupId) {
return getMessageGroup(groupId).size();
}
@Override
public MessageGroupMetadata getGroupMetadata(Object groupId) {
return new MessageGroupMetadata(getMessageGroup(groupId));
}
@Override
public Message<?> getOneMessageFromGroup(Object groupId) {
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 {
lock.lockInterruptibly();
try {
MessageGroup group = this.groupIdToMessageGroup.get(groupId);
Assert.notNull(group, "MessageGroup for groupId '" + groupId + "' " +
"can not be located while attempting to complete the MessageGroup");
group.clear();
group.setLastModified(System.currentTimeMillis());
UpperBound upperBound = this.groupToUpperBound.get(groupId);
Assert.state(upperBound != null, "'upperBound' must not be null.");
upperBound.release(this.groupCapacity);
}
finally {
lock.unlock();
}
}
catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new MessagingException("Interrupted while obtaining lock", e);
}
}
}