
org.apache.qpid.jms.JmsMessageConsumer Maven / Gradle / Ivy
Show all versions of qpid-jms-client Show documentation
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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.apache.qpid.jms;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import javax.jms.IllegalStateException;
import javax.jms.JMSException;
import javax.jms.Message;
import javax.jms.MessageConsumer;
import javax.jms.MessageListener;
import javax.jms.Session;
import org.apache.qpid.jms.exceptions.JmsExceptionSupport;
import org.apache.qpid.jms.message.JmsInboundMessageDispatch;
import org.apache.qpid.jms.message.JmsMessage;
import org.apache.qpid.jms.meta.JmsConsumerId;
import org.apache.qpid.jms.meta.JmsConsumerInfo;
import org.apache.qpid.jms.provider.Provider;
import org.apache.qpid.jms.provider.ProviderConstants.ACK_TYPE;
import org.apache.qpid.jms.provider.ProviderFuture;
import org.apache.qpid.jms.util.FifoMessageQueue;
import org.apache.qpid.jms.util.MessageQueue;
import org.apache.qpid.jms.util.PriorityMessageQueue;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* implementation of a JMS Message Consumer
*/
public class JmsMessageConsumer implements AutoCloseable, MessageConsumer, JmsMessageAvailableConsumer, JmsMessageDispatcher {
private static final Logger LOG = LoggerFactory.getLogger(JmsMessageConsumer.class);
protected final JmsSession session;
protected final JmsConnection connection;
protected JmsConsumerInfo consumerInfo;
protected final int acknowledgementMode;
protected final AtomicBoolean closed = new AtomicBoolean();
protected boolean started;
protected MessageListener messageListener;
protected JmsMessageAvailableListener availableListener;
protected final MessageQueue messageQueue;
protected final Lock lock = new ReentrantLock();
protected final AtomicBoolean suspendedConnection = new AtomicBoolean();
protected final AtomicReference failureCause = new AtomicReference<>();
protected JmsMessageConsumer(JmsConsumerId consumerId, JmsSession session, JmsDestination destination,
String selector, boolean noLocal) throws JMSException {
this(consumerId, session, destination, null, selector, noLocal);
}
protected JmsMessageConsumer(JmsConsumerId consumerId, JmsSession session, JmsDestination destination,
String name, String selector, boolean noLocal) throws JMSException {
this.session = session;
this.connection = session.getConnection();
this.acknowledgementMode = session.acknowledgementMode();
if(destination.isTemporary()) {
connection.checkConsumeFromTemporaryDestination((JmsTemporaryDestination) destination);
}
if (connection.isLocalMessagePriority()) {
this.messageQueue = new PriorityMessageQueue();
} else {
this.messageQueue = new FifoMessageQueue();
}
JmsPrefetchPolicy policy = connection.getPrefetchPolicy();
JmsRedeliveryPolicy redeliveryPolicy = connection.getRedeliveryPolicy().copy();
consumerInfo = new JmsConsumerInfo(consumerId);
consumerInfo.setClientId(connection.getClientID());
consumerInfo.setSelector(selector);
consumerInfo.setSubscriptionName(name);
consumerInfo.setDestination(destination);
consumerInfo.setAcknowledgementMode(acknowledgementMode);
consumerInfo.setNoLocal(noLocal);
consumerInfo.setBrowser(isBrowser());
consumerInfo.setPrefetchSize(getConfiguredPrefetch(destination, policy));
consumerInfo.setRedeliveryPolicy(redeliveryPolicy);
consumerInfo.setLocalMessageExpiry(connection.isLocalMessageExpiry());
session.getConnection().createResource(consumerInfo);
}
public void init() throws JMSException {
session.add(this);
startConsumerResource();
}
private void startConsumerResource() throws JMSException {
try {
session.getConnection().startResource(consumerInfo);
} catch (JMSException ex) {
session.remove(this);
throw ex;
}
}
@Override
public void close() throws JMSException {
if (!closed.get()) {
session.getTransactionContext().addSynchronization(new JmsTransactionSynchronization() {
@Override
public boolean validate(JmsTransactionContext context) throws Exception {
if (isBrowser() || !context.isActiveInThisContext(getConsumerId())) {
doClose();
return false;
}
return true;
}
@Override
public void afterCommit() throws Exception {
doClose();
}
@Override
public void afterRollback() throws Exception {
doClose();
}
});
}
}
/**
* Called to initiate shutdown of Producer resources and request that the remote
* peer remove the registered producer.
*
* @throws JMSException if an error occurs during the consumer close operation.
*/
protected void doClose() throws JMSException {
shutdown();
this.connection.destroyResource(consumerInfo);
}
/**
* Called to release all producer resources without requiring a destroy request
* to be sent to the remote peer. This is most commonly needed when the parent
* Session is closing.
*
* @throws JMSException if an error occurs during shutdown.
*/
protected void shutdown() throws JMSException {
shutdown(null);
}
protected void shutdown(Exception cause) throws JMSException {
if (closed.compareAndSet(false, true)) {
setFailureCause(cause);
session.remove(this);
stop(true);
}
}
@Override
public Message receive() throws JMSException {
return receive(0);
}
@Override
public Message receive(long timeout) throws JMSException {
checkClosed();
checkMessageListener();
// Configure for infinite wait when timeout is zero (JMS Spec)
if (timeout == 0) {
timeout = -1;
}
return copy(ackFromReceive(dequeue(timeout, connection.isReceiveLocalOnly())));
}
@Override
public Message receiveNoWait() throws JMSException {
checkClosed();
checkMessageListener();
return copy(ackFromReceive(dequeue(0, connection.isReceiveNoWaitLocalOnly())));
}
/**
* Used to get an enqueued message from the unconsumedMessages list. The
* amount of time this method blocks is based on the timeout value.
*
* timeout < 0 then it blocks until a message is received.
* timeout = 0 then it returns a message or null if none available
* timeout > 0 then it blocks up to timeout amount of time.
*
* This method may consume messages that are expired or exceed a configured
* delivery count value but will continue to wait for the configured timeout.
*
* @param localCheckOnly
* if false, try pulling a message if a >= 0 timeout expires with no message arriving
*
* @return null if we timeout or if the consumer is closed concurrently.
*
* @throws JMSException if an error occurs during the dequeue.
*/
private JmsInboundMessageDispatch dequeue(long timeout, boolean localCheckOnly) throws JMSException {
boolean pullConsumer = isPullConsumer();
boolean pullForced = pullConsumer;
try {
long deadline = 0;
if (timeout > 0) {
deadline = System.currentTimeMillis() + timeout;
}
performPullIfRequired(timeout, false);
while (true) {
JmsInboundMessageDispatch envelope = null;
if (pullForced || pullConsumer) {
// Any waiting was done by the pull request, try immediate retrieval from the queue.
envelope = messageQueue.dequeue(0);
} else {
envelope = messageQueue.dequeue(timeout);
}
if (getFailureCause() != null) {
LOG.debug("{} receive failed: {}", getConsumerId(), getFailureCause().getMessage());
throw JmsExceptionSupport.create(getFailureCause());
}
if (envelope == null) {
if ((timeout == 0 && (pullForced || localCheckOnly)) || pullConsumer || messageQueue.isClosed()) {
return null;
} else if (timeout > 0) {
timeout = Math.max(deadline - System.currentTimeMillis(), 0);
}
if (timeout >= 0 && !localCheckOnly) {
// We don't do this for receive with no timeout since it
// is redundant: zero-prefetch consumers already pull, and
// the rest block indefinitely on the local messageQueue.
pullForced = true;
if(performPullIfRequired(timeout, true)) {
startConsumerResource();
// We refresh credit if it is a prefetching consumer, since the
// pull drained it. Processing acks can open the credit window, but
// not in all cases, and if we didn't get a message it would stay
// closed until future pulls were performed.
}
}
} else if (consumeExpiredMessage(envelope)) {
LOG.trace("{} filtered expired message: {}", getConsumerId(), envelope);
doAckExpired(envelope);
if (timeout > 0) {
timeout = Math.max(deadline - System.currentTimeMillis(), 0);
}
performPullIfRequired(timeout, false);
} else if (redeliveryExceeded(envelope)) {
LOG.debug("{} filtered message with excessive redelivery count: {}", getConsumerId(), envelope);
doAckUndeliverable(envelope);
if (timeout > 0) {
timeout = Math.max(deadline - System.currentTimeMillis(), 0);
}
performPullIfRequired(timeout, false);
} else {
if (LOG.isTraceEnabled()) {
LOG.trace(getConsumerId() + " received message: " + envelope);
}
return envelope;
}
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw JmsExceptionSupport.create(e);
}
}
private boolean consumeExpiredMessage(JmsInboundMessageDispatch dispatch) {
if (!isBrowser() && consumerInfo.isLocalMessageExpiry() && dispatch.getMessage().isExpired()) {
return true;
}
return false;
}
protected boolean redeliveryExceeded(JmsInboundMessageDispatch envelope) {
LOG.trace("checking envelope with {} redeliveries", envelope.getRedeliveryCount());
JmsRedeliveryPolicy redeliveryPolicy = consumerInfo.getRedeliveryPolicy();
return redeliveryPolicy != null &&
redeliveryPolicy.getMaxRedeliveries() != JmsRedeliveryPolicy.DEFAULT_MAX_REDELIVERIES &&
redeliveryPolicy.getMaxRedeliveries() < envelope.getRedeliveryCount();
}
protected void checkClosed() throws IllegalStateException {
if (closed.get()) {
IllegalStateException jmsEx = null;
if (getFailureCause() == null) {
jmsEx = new IllegalStateException("The MessageConsumer is closed");
} else {
jmsEx = new IllegalStateException("The MessageConsumer was closed due to an unrecoverable error.");
jmsEx.initCause(getFailureCause());
}
throw jmsEx;
}
}
void setFailureCause(Exception failureCause) {
this.failureCause.set(failureCause);
}
Exception getFailureCause() {
if (failureCause.get() == null) {
return session.getFailureCause();
}
return failureCause.get();
}
JmsMessage copy(final JmsInboundMessageDispatch envelope) throws JMSException {
if (envelope == null || envelope.getMessage() == null) {
return null;
}
return envelope.getMessage().copy();
}
JmsInboundMessageDispatch ackFromReceive(final JmsInboundMessageDispatch envelope) throws JMSException {
if (envelope != null && envelope.getMessage() != null) {
JmsMessage message = envelope.getMessage();
if (message.getAcknowledgeCallback() != null) {
// Message has been received by the app.. expand the credit
// window so that we receive more messages.
doAckDelivered(envelope);
} else {
doAckConsumed(envelope);
}
}
return envelope;
}
private JmsInboundMessageDispatch doAckConsumed(final JmsInboundMessageDispatch envelope) throws JMSException {
checkClosed();
try {
session.acknowledge(envelope, ACK_TYPE.ACCEPTED);
} catch (JMSException ex) {
session.onException(ex);
throw ex;
}
return envelope;
}
private JmsInboundMessageDispatch doAckDelivered(final JmsInboundMessageDispatch envelope) throws JMSException {
try {
session.acknowledge(envelope, ACK_TYPE.DELIVERED);
} catch (JMSException ex) {
session.onException(ex);
throw ex;
}
return envelope;
}
private void doAckExpired(final JmsInboundMessageDispatch envelope) throws JMSException {
try {
session.acknowledge(envelope, ACK_TYPE.EXPIRED);
} catch (JMSException ex) {
session.onException(ex);
throw ex;
}
}
private void doAckUndeliverable(final JmsInboundMessageDispatch envelope) throws JMSException {
try {
session.acknowledge(envelope, ACK_TYPE.MODIFIED_FAILED_UNDELIVERABLE);
} catch (JMSException ex) {
session.onException(ex);
throw ex;
}
}
private void doAckReleased(final JmsInboundMessageDispatch envelope) throws JMSException {
try {
session.acknowledge(envelope, ACK_TYPE.RELEASED);
} catch (JMSException ex) {
session.onException(ex);
throw ex;
}
}
/**
* Called from the session when a new Message has been dispatched to this Consumer
* from the connection.
*
* @param envelope
* the newly arrived message.
*/
@Override
public void onInboundMessage(final JmsInboundMessageDispatch envelope) {
lock.lock();
try {
if (acknowledgementMode == Session.CLIENT_ACKNOWLEDGE) {
envelope.getMessage().setAcknowledgeCallback(new JmsAcknowledgeCallback(session));
}
if (envelope.isEnqueueFirst()) {
this.messageQueue.enqueueFirst(envelope);
} else {
this.messageQueue.enqueue(envelope);
}
if (this.messageListener != null && this.started) {
session.getExecutor().execute(new MessageDeliverTask());
} else {
if (availableListener != null) {
session.getExecutor().execute(new Runnable() {
@Override
public void run() {
if (session.isStarted()) {
availableListener.onMessageAvailable(JmsMessageConsumer.this);
}
}
});
}
}
} finally {
lock.unlock();
}
}
public void start() {
lock.lock();
try {
this.started = true;
this.messageQueue.start();
drainMessageQueueToListener();
} finally {
lock.unlock();
}
}
public void stop() {
stop(false);
}
private void stop(boolean closeMessageQueue) {
lock.lock();
try {
this.started = false;
if (closeMessageQueue) {
this.messageQueue.close();
} else {
this.messageQueue.stop();
}
} finally {
lock.unlock();
}
}
void suspendForRollback() throws JMSException {
stop();
session.getConnection().stopResource(consumerInfo);
}
void resumeAfterRollback() throws JMSException {
if (!this.messageQueue.isEmpty()) {
List drain = this.messageQueue.removeAll();
for (JmsInboundMessageDispatch envelope : drain) {
doAckReleased(envelope);
}
drain.clear();
}
start();
startConsumerResource();
}
void drainMessageQueueToListener() {
if (this.messageListener != null && this.started) {
session.getExecutor().execute(new MessageDeliverTask());
}
}
/**
* @return the id
*/
public JmsConsumerId getConsumerId() {
return this.consumerInfo.getId();
}
/**
* @return the Destination
*/
public JmsDestination getDestination() {
return this.consumerInfo.getDestination();
}
@Override
public MessageListener getMessageListener() throws JMSException {
checkClosed();
return this.messageListener;
}
@Override
public void setMessageListener(MessageListener listener) throws JMSException {
checkClosed();
if (consumerInfo.getPrefetchSize() == 0) {
throw new JMSException("Illegal prefetch size of zero. This setting is not supported" +
"for asynchronous consumers please set a value of at least 1");
}
this.messageListener = listener;
drainMessageQueueToListener();
}
@Override
public String getMessageSelector() throws JMSException {
checkClosed();
return this.consumerInfo.getSelector();
}
/**
* Gets the configured prefetch size for this consumer.
* @return the prefetch size configuration for this consumer.
*/
public int getPrefetchSize() {
return this.consumerInfo.getPrefetchSize();
}
protected void checkMessageListener() throws JMSException {
session.checkMessageListener();
}
boolean hasMessageListener() {
return this.messageListener != null;
}
boolean isUsingDestination(JmsDestination destination) {
return this.consumerInfo.getDestination().equals(destination);
}
protected int getMessageQueueSize() {
return this.messageQueue.size();
}
protected boolean isNoLocal() {
return this.consumerInfo.isNoLocal();
}
public boolean isDurableSubscription() {
return false;
}
public boolean isBrowser() {
return false;
}
public boolean isPullConsumer() {
return getPrefetchSize() == 0;
}
@Override
public void setAvailableListener(JmsMessageAvailableListener availableListener) {
this.availableListener = availableListener;
}
@Override
public JmsMessageAvailableListener getAvailableListener() {
return availableListener;
}
protected void onConnectionInterrupted() {
messageQueue.clear();
}
protected void onConnectionRecovery(Provider provider) throws Exception {
ProviderFuture request = new ProviderFuture();
provider.create(consumerInfo, request);
request.sync();
}
protected void onConnectionRecovered(Provider provider) throws Exception {
ProviderFuture request = new ProviderFuture();
provider.start(consumerInfo, request);
request.sync();
}
protected void onConnectionRestored() {
}
/**
* Triggers a pull request from the connected Provider with the given timeout value
* if the consumer is a pull consumer or requested to be treated as one, and the
* local queue is still running, and is currently empty.
*
* The timeout value can be one of:
*
* {@literal < 0} to indicate that the request should never time out.
* {@literal = 0} to indicate that the request should expire immediately if no message.
* {@literal > 0} to indicate that the request should expire after the given time in milliseconds.
*
* @param timeout
* The amount of time the pull request should remain valid.
* @param treatAsPullConsumer
* Treat the consumer as if it were a pull consumer, even if it isn't.
* @return true if a pull was performed, false if it was not.
*/
protected boolean performPullIfRequired(long timeout, boolean treatAsPullConsumer) throws JMSException {
if ((isPullConsumer() || treatAsPullConsumer) && messageQueue.isRunning() && messageQueue.isEmpty()) {
connection.pull(getConsumerId(), timeout);
return true;
}
return false;
}
private int getConfiguredPrefetch(JmsDestination destination, JmsPrefetchPolicy policy) {
int prefetch = 0;
if (destination.isTopic()) {
if (isDurableSubscription()) {
prefetch = policy.getDurableTopicPrefetch();
} else {
prefetch = policy.getTopicPrefetch();
}
} else {
if (isBrowser()) {
prefetch = policy.getQueueBrowserPrefetch();
} else {
prefetch = policy.getQueuePrefetch();
}
}
return prefetch;
}
private final class MessageDeliverTask implements Runnable {
@Override
public void run() {
JmsInboundMessageDispatch envelope;
while (session.isStarted() && (envelope = messageQueue.dequeueNoWait()) != null) {
try {
JmsMessage copy = null;
if (consumeExpiredMessage(envelope)) {
LOG.trace("{} filtered expired message: {}", getConsumerId(), envelope);
doAckExpired(envelope);
} else if (redeliveryExceeded(envelope)) {
LOG.trace("{} filtered message with excessive redelivery count: {}", getConsumerId(), envelope);
doAckUndeliverable(envelope);
} else {
boolean deliveryFailed = false;
boolean autoAckOrDupsOk = acknowledgementMode == Session.AUTO_ACKNOWLEDGE ||
acknowledgementMode == Session.DUPS_OK_ACKNOWLEDGE;
if (autoAckOrDupsOk) {
copy = copy(doAckDelivered(envelope));
} else {
copy = copy(ackFromReceive(envelope));
}
session.clearSessionRecovered();
try {
messageListener.onMessage(copy);
} catch (RuntimeException rte) {
deliveryFailed = true;
}
if (autoAckOrDupsOk && !session.isSessionRecovered()) {
if (!deliveryFailed) {
doAckConsumed(envelope);
} else {
doAckReleased(envelope);
}
}
}
} catch (Exception e) {
// TODO - There are two cases where we can get an error here, one being
// and error returned from the attempted ACK that was sent and the
// other being an error while attempting to copy the incoming message.
// We need to decide how to respond to these.
session.getConnection().onException(e);
}
}
}
}
}