/* * Copyright 2016-2017 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.amqp.rabbit.listener; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import org.springframework.amqp.core.AcknowledgeMode; import org.springframework.amqp.core.Address; import org.springframework.amqp.core.MessageListener; import org.springframework.amqp.rabbit.connection.ConnectionFactory; import org.springframework.amqp.rabbit.core.ChannelAwareMessageListener; import org.springframework.util.Assert; import com.rabbitmq.client.Channel; /** * Listener container for Direct ReplyTo only listens to the pseudo queue * {@link Address#AMQ_RABBITMQ_REPLY_TO}. Consumers are added on-demand and * terminated when idle for {@link #setIdleEventInterval(long) idleEventInterval} * (default 60 seconds). * * @author Gary Russell * @since 2.0 * */ public class DirectReplyToMessageListenerContainer extends DirectMessageListenerContainer { private static final int DEFAULT_IDLE = 60000; private final ConcurrentMap<Channel, SimpleConsumer> inUseConsumerChannels = new ConcurrentHashMap<>(); private final ConcurrentMap<SimpleConsumer, Long> whenUsed = new ConcurrentHashMap<>(); private int consumerCount; public DirectReplyToMessageListenerContainer(ConnectionFactory connectionFactory) { super(connectionFactory); super.setQueueNames(Address.AMQ_RABBITMQ_REPLY_TO); setAcknowledgeMode(AcknowledgeMode.NONE); super.setConsumersPerQueue(0); super.setIdleEventInterval(DEFAULT_IDLE); } @Override public final void setConsumersPerQueue(int consumersPerQueue) { throw new UnsupportedOperationException(); } @Override public final void setMonitorInterval(long monitorInterval) { throw new UnsupportedOperationException(); } @Override public final void setQueueNames(String... queueName) { throw new UnsupportedOperationException(); } @Override public final void addQueueNames(String... queueNames) { throw new UnsupportedOperationException(); } @Override public final boolean removeQueueNames(String... queueNames) { throw new UnsupportedOperationException(); } @Override public void setMessageListener(Object messageListener) { throw new UnsupportedOperationException( "'messageListener' must be a 'MessageListener' or 'ChannelAwareMessageListener'"); } @Override public void setChannelAwareMessageListener(ChannelAwareMessageListener messageListener) { super.setChannelAwareMessageListener((message, channel) -> { try { messageListener.onMessage(message, channel); } finally { this.inUseConsumerChannels.remove(channel); } }); } @Override public void setMessageListener(MessageListener messageListener) { super.setChannelAwareMessageListener((message, channel) -> { try { messageListener.onMessage(message); } finally { this.inUseConsumerChannels.remove(channel); } }); } @Override protected void doStart() throws Exception { if (!isRunning()) { this.consumerCount = 0; super.setConsumersPerQueue(0); super.doStart(); } } @Override protected void processMonitorTask() { long now = System.currentTimeMillis(); synchronized (this.consumersMonitor) { long reduce = this.consumers.stream() .filter(c -> this.whenUsed.containsKey(c) && !this.inUseConsumerChannels.containsValue(c) && this.whenUsed.get(c) < now - getIdleEventInterval()) .count(); if (reduce > 0) { if (logger.isDebugEnabled()) { logger.debug("Reducing idle consumes by " + reduce); } this.consumerCount = (int) Math.max(0, this.consumerCount - reduce); super.setConsumersPerQueue(this.consumerCount); } } } @Override protected void consumerRemoved(SimpleConsumer consumer) { this.inUseConsumerChannels.remove(consumer.getChannel()); this.whenUsed.remove(consumer); } /** * Get the channel holder associated with a direct reply-to consumer; contains a * consumer epoch to prevent inappropriate releases. * @return the channel holder. */ public ChannelHolder getChannelHolder() { synchronized (this.consumersMonitor) { ChannelHolder channelHolder = null; while (channelHolder == null) { if (!isRunning()) { throw new IllegalStateException("Direct reply-to container is not running"); } for (SimpleConsumer consumer : this.consumers) { Channel candidate = consumer.getChannel(); if (candidate.isOpen() && this.inUseConsumerChannels.putIfAbsent(candidate, consumer) == null) { channelHolder = new ChannelHolder(candidate, consumer.incrementAndGetEpoch()); this.whenUsed.put(consumer, System.currentTimeMillis()); break; } } if (channelHolder == null) { this.consumerCount++; super.setConsumersPerQueue(this.consumerCount); } } return channelHolder; } } /** * Release the consumer associated with the channel for reuse. * Set cancelConsumer to true if the client is not prepared to handle/discard a * late arriving reply. * @param channelHolder the channel holder. * @param cancelConsumer true to cancel the consumer. * @param message a message to be included in the cancel event if cancelConsumer is true. */ public void releaseConsumerFor(ChannelHolder channelHolder, boolean cancelConsumer, String message) { synchronized (this.consumersMonitor) { SimpleConsumer consumer = this.inUseConsumerChannels.get(channelHolder.getChannel()); if (consumer != null) { if (consumer.getEpoch() == channelHolder.getConsumerEpoch()) { this.inUseConsumerChannels.remove(channelHolder.getChannel()); if (cancelConsumer) { Assert.isTrue(message != null, "A 'message' is required when 'cancelConsumer' is 'true'"); consumer.cancelConsumer("Consumer " + this + " canceled due to " + message); } } } } } /** * Holder for a channel; contains a consumer epoch used to prevent inappropriate release * of the consumer after it has been allocated for reuse. */ public static class ChannelHolder { private final Channel channel; private final int consumerEpoch; ChannelHolder(Channel channel, int consumerEpoch) { this.channel = channel; this.consumerEpoch = consumerEpoch; } public Channel getChannel() { return this.channel; } public int getConsumerEpoch() { return this.consumerEpoch; } @Override public String toString() { return "ChannelHolder [channel=" + this.channel + ", consumerEpoch=" + this.consumerEpoch + "]"; } } }