/* * 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.AbstractQueue; import java.util.ArrayList; import java.util.Collection; import java.util.Iterator; import java.util.concurrent.BlockingQueue; import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.Condition; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.springframework.messaging.Message; import org.springframework.util.Assert; /** * A {@link BlockingQueue} that is backed by a {@link MessageGroupStore}. Can be used to ensure guaranteed delivery in * the face of transaction rollback (assuming the store is transactional) and also to ensure messages are not lost if * the process dies (assuming the store is durable). To use the queue across process re-starts, the same group id * must be provided, so it needs to be unique but identifiable with a single logical instance of the queue. * * @author Dave Syer * @author Oleg Zhurakousky * @author Gunnar Hillert * @author Gary Russell * * @since 2.0 * */ public class MessageGroupQueue extends AbstractQueue<Message<?>> implements BlockingQueue<Message<?>> { private final Log logger = LogFactory.getLog(getClass()); private static final int DEFAULT_CAPACITY = Integer.MAX_VALUE; private final BasicMessageGroupStore messageGroupStore; private final Object groupId; private final int capacity; //This one could be a global semaphore private final Lock storeLock; private final Condition messageStoreNotFull; private final Condition messageStoreNotEmpty; public MessageGroupQueue(BasicMessageGroupStore messageGroupStore, Object groupId) { this(messageGroupStore, groupId, DEFAULT_CAPACITY, new ReentrantLock(true)); } public MessageGroupQueue(BasicMessageGroupStore messageGroupStore, Object groupId, int capacity) { this(messageGroupStore, groupId, capacity, new ReentrantLock(true)); } public MessageGroupQueue(BasicMessageGroupStore messageGroupStore, Object groupId, Lock storeLock) { this(messageGroupStore, groupId, DEFAULT_CAPACITY, storeLock); } public MessageGroupQueue(BasicMessageGroupStore messageGroupStore, Object groupId, int capacity, Lock storeLock) { Assert.isTrue(capacity > 0, "'capacity' must be greater than 0"); Assert.notNull(storeLock, "'storeLock' must not be null"); Assert.notNull(messageGroupStore, "'messageGroupStore' must not be null"); Assert.notNull(groupId, "'groupId' must not be null"); this.storeLock = storeLock; this.messageStoreNotFull = this.storeLock.newCondition(); this.messageStoreNotEmpty = this.storeLock.newCondition(); this.messageGroupStore = messageGroupStore; this.groupId = groupId; this.capacity = capacity; if (this.logger.isWarnEnabled() && !(messageGroupStore instanceof ChannelMessageStore)) { this.logger.warn(messageGroupStore.getClass().getSimpleName() + " is not optimized for use " + "in a 'MessageGroupQueue'; consider using a `ChannelMessageStore'"); } } /** * If true, ensures that the message store supports priority. If false WARNs if the * message store uses priority to determine the message order when receiving. * @param priority true if priority is expected to be used. */ public void setPriority(boolean priority) { if (priority) { Assert.isInstanceOf(PriorityCapableChannelMessageStore.class, this.messageGroupStore); Assert.isTrue(((PriorityCapableChannelMessageStore) this.messageGroupStore).isPriorityEnabled(), "When using priority, the 'PriorityCapableChannelMessageStore' must have priority enabled."); } else { if (this.logger.isWarnEnabled() && this.messageGroupStore instanceof PriorityCapableChannelMessageStore && ((PriorityCapableChannelMessageStore) this.messageGroupStore).isPriorityEnabled()) { this.logger.warn("It's not recommended to use a priority-based message store " + "when declaring a non-priority 'MessageGroupQueue'; message retrieval may not be FIFO; " + "set 'priority' to 'true' if that is your intent. If you are using the namespace to " + "define a channel, use '<priority-queue message-store.../> instead."); } } } @Override public Iterator<Message<?>> iterator() { return getMessages().iterator(); } @Override public int size() { return this.messageGroupStore.messageGroupSize(this.groupId); } @Override public Message<?> peek() { Message<?> message = null; final Lock storeLock = this.storeLock; try { storeLock.lockInterruptibly(); try { Collection<Message<?>> messages = getMessages(); if (!messages.isEmpty()) { message = messages.iterator().next(); } } finally { storeLock.unlock(); } } catch (InterruptedException e) { Thread.currentThread().interrupt(); } return message; } @Override public Message<?> poll(long timeout, TimeUnit unit) throws InterruptedException { Message<?> message = null; long timeoutInNanos = unit.toNanos(timeout); final Lock storeLock = this.storeLock; storeLock.lockInterruptibly(); try { while (this.size() == 0 && timeoutInNanos > 0) { timeoutInNanos = this.messageStoreNotEmpty.awaitNanos(timeoutInNanos); } message = this.doPoll(); } finally { storeLock.unlock(); } return message; } @Override public Message<?> poll() { Message<?> message = null; final Lock storeLock = this.storeLock; try { storeLock.lockInterruptibly(); try { message = this.doPoll(); } finally { storeLock.unlock(); } } catch (InterruptedException e) { Thread.currentThread().interrupt(); } return message; } @Override public int drainTo(Collection<? super Message<?>> c) { return this.drainTo(c, Integer.MAX_VALUE); } @Override public int drainTo(Collection<? super Message<?>> collection, int maxElements) { Assert.notNull(collection, "'collection' must not be null"); int originalSize = collection.size(); ArrayList<Message<?>> list = new ArrayList<Message<?>>(); final Lock storeLock = this.storeLock; try { storeLock.lockInterruptibly(); try { Message<?> message = this.messageGroupStore.pollMessageFromGroup(this.groupId); for (int i = 0; i < maxElements && message != null; i++) { list.add(message); message = this.messageGroupStore.pollMessageFromGroup(this.groupId); } this.messageStoreNotFull.signal(); } finally { storeLock.unlock(); } } catch (InterruptedException e) { this.logger.warn("Queue may not have drained completely since this operation was interrupted", e); Thread.currentThread().interrupt(); } collection.addAll(list); return collection.size() - originalSize; } @Override public boolean offer(Message<?> message) { boolean offered = true; final Lock storeLock = this.storeLock; try { storeLock.lockInterruptibly(); try { offered = this.doOffer(message); } finally { storeLock.unlock(); } } catch (InterruptedException e) { Thread.currentThread().interrupt(); } return offered; } @Override public boolean offer(Message<?> message, long timeout, TimeUnit unit) throws InterruptedException { long timeoutInNanos = unit.toNanos(timeout); boolean offered = false; final Lock storeLock = this.storeLock; storeLock.lockInterruptibly(); try { if (this.capacity != Integer.MAX_VALUE) { while (this.size() == this.capacity && timeoutInNanos > 0) { timeoutInNanos = this.messageStoreNotFull.awaitNanos(timeoutInNanos); } } if (timeoutInNanos > 0) { offered = this.doOffer(message); } } finally { storeLock.unlock(); } return offered; } @Override public void put(Message<?> message) throws InterruptedException { final Lock storeLock = this.storeLock; storeLock.lockInterruptibly(); try { if (this.capacity != Integer.MAX_VALUE) { while (this.size() == this.capacity) { this.messageStoreNotFull.await(); } } this.doOffer(message); } finally { storeLock.unlock(); } } @Override public int remainingCapacity() { if (this.capacity == Integer.MAX_VALUE) { return Integer.MAX_VALUE; } return this.capacity - this.size(); } @Override public Message<?> take() throws InterruptedException { Message<?> message = null; final Lock storeLock = this.storeLock; storeLock.lockInterruptibly(); try { while (this.size() == 0) { this.messageStoreNotEmpty.await(); } message = this.doPoll(); } finally { storeLock.unlock(); } return message; } private Collection<Message<?>> getMessages() { return this.messageGroupStore.getMessageGroup(this.groupId).getMessages(); } /** * It is assumed that the 'storeLock' is being held by the caller, otherwise * IllegalMonitorStateException may be thrown */ private Message<?> doPoll() { Message<?> message = this.messageGroupStore.pollMessageFromGroup(this.groupId); this.messageStoreNotFull.signal(); return message; } /** * It is assumed that the 'storeLock' is being held by the caller, otherwise * IllegalMonitorStateException may be thrown */ private boolean doOffer(Message<?> message) { boolean offered = false; if (this.capacity == Integer.MAX_VALUE || this.size() < this.capacity) { this.messageGroupStore.addMessageToGroup(this.groupId, message); offered = true; this.messageStoreNotEmpty.signal(); } return offered; } }