io.bitsensor.plugins.shaded.org.springframework.amqp.rabbit.connection.CachingConnectionFactory Maven / Gradle / Ivy
/*
* Copyright 2002-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 io.bitsensor.plugins.shaded.io.bitsensor.plugins.shaded.org.springframework.amqp.rabbit.connection;
import java.io.IOException;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.net.URI;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Properties;
import java.util.Set;
import java.util.concurrent.BlockingDeque;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.LinkedBlockingDeque;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import io.bitsensor.plugins.shaded.org.apache.commons.logging.Log;
import io.bitsensor.plugins.shaded.io.bitsensor.plugins.shaded.org.springframework.amqp.AmqpException;
import io.bitsensor.plugins.shaded.io.bitsensor.plugins.shaded.org.springframework.amqp.AmqpTimeoutException;
import io.bitsensor.plugins.shaded.io.bitsensor.plugins.shaded.org.springframework.amqp.rabbit.support.PublisherCallbackChannel;
import io.bitsensor.plugins.shaded.io.bitsensor.plugins.shaded.org.springframework.amqp.rabbit.support.PublisherCallbackChannelImpl;
import io.bitsensor.plugins.shaded.io.bitsensor.plugins.shaded.org.springframework.amqp.support.ConditionalExceptionLogger;
import io.bitsensor.plugins.shaded.org.springframework.beans.BeansException;
import io.bitsensor.plugins.shaded.org.springframework.beans.factory.InitializingBean;
import io.bitsensor.plugins.shaded.org.springframework.context.ApplicationContext;
import io.bitsensor.plugins.shaded.org.springframework.context.ApplicationContextAware;
import io.bitsensor.plugins.shaded.org.springframework.context.ApplicationListener;
import io.bitsensor.plugins.shaded.org.springframework.context.SmartLifecycle;
import io.bitsensor.plugins.shaded.org.springframework.context.event.ContextClosedEvent;
import io.bitsensor.plugins.shaded.org.springframework.jmx.export.annotation.ManagedAttribute;
import io.bitsensor.plugins.shaded.org.springframework.jmx.export.annotation.ManagedResource;
import io.bitsensor.plugins.shaded.org.springframework.util.Assert;
import io.bitsensor.plugins.shaded.org.springframework.util.ObjectUtils;
import io.bitsensor.plugins.shaded.org.springframework.util.StringUtils;
import io.bitsensor.plugins.shaded.com.rabbitmq.client.AlreadyClosedException;
import io.bitsensor.plugins.shaded.com.rabbitmq.client.Channel;
import io.bitsensor.plugins.shaded.com.rabbitmq.client.ShutdownListener;
import io.bitsensor.plugins.shaded.com.rabbitmq.client.ShutdownSignalException;
/**
* A {@link ConnectionFactory} implementation that (when the cache mode is {@link CacheMode#CHANNEL} (default)
* returns the same Connection from all {@link #createConnection()}
* calls, and ignores calls to {@link io.bitsensor.plugins.shaded.com.rabbitmq.client.Connection#close()} and caches
* {@link io.bitsensor.plugins.shaded.com.rabbitmq.client.Channel}.
*
* By default, only one Channel will be cached, with further requested Channels being created and disposed on demand.
* Consider raising the {@link #setChannelCacheSize(int) "channelCacheSize" value} in case of a high-concurrency
* environment.
*
* When the cache mode is {@link CacheMode#CONNECTION}, a new (or cached) connection is used for each {@link #createConnection()};
* connections are cached according to the {@link #setConnectionCacheSize(int) "connectionCacheSize" value}.
* Both connections and channels are cached in this mode.
*
* {@link CacheMode#CONNECTION} is not compatible with a Rabbit Admin that auto-declares queues etc.
*
* NOTE: This ConnectionFactory requires explicit closing of all Channels obtained form its Connection(s).
* This is the usual recommendation for native Rabbit access code anyway. However, with this ConnectionFactory, its use
* is mandatory in order to actually allow for Channel reuse. {@link Channel#close()} returns the channel to the
* cache, if there is room, or physically closes the channel otherwise.
*
* @author Mark Pollack
* @author Mark Fisher
* @author Dave Syer
* @author Gary Russell
* @author Artem Bilan
* @author Steve Powell
*/
@ManagedResource
public class CachingConnectionFactory extends AbstractConnectionFactory
implements InitializingBean, ShutdownListener, ApplicationContextAware, ApplicationListener,
PublisherCallbackChannelConnectionFactory, SmartLifecycle {
private static final int DEFAULT_CHANNEL_CACHE_SIZE = 25;
private static final Set txStarts = new HashSet(Arrays.asList("basicPublish", "basicAck", "basicNack",
"basicReject"));
private static final Set txEnds = new HashSet(Arrays.asList("txCommit", "txRollback"));
private final ChannelCachingConnectionProxy connection = new ChannelCachingConnectionProxy(null);
public enum CacheMode {
/**
* Cache channels - single connection
*/
CHANNEL,
/**
* Cache connections and channels within each connection
*/
CONNECTION
}
private final Set allocatedConnections =
new HashSet();
private final Map>
allocatedConnectionNonTransactionalChannels = new HashMap>();
private final Map>
allocatedConnectionTransactionalChannels = new HashMap>();
private final BlockingDeque idleConnections =
new LinkedBlockingDeque();
private final Map checkoutPermits = new HashMap();
private final Map channelHighWaterMarks = new HashMap();
private final AtomicInteger connectionHighWaterMark = new AtomicInteger();
private ApplicationContext applicationContext;
private volatile long channelCheckoutTimeout = 0;
private volatile CacheMode cacheMode = CacheMode.CHANNEL;
private volatile int channelCacheSize = DEFAULT_CHANNEL_CACHE_SIZE;
private volatile int connectionCacheSize = 1;
private volatile int connectionLimit = Integer.MAX_VALUE;
private final LinkedList cachedChannelsNonTransactional = new LinkedList();
private final LinkedList cachedChannelsTransactional = new LinkedList();
private volatile boolean active = true;
private volatile boolean publisherConfirms;
private volatile boolean publisherReturns;
private volatile boolean initialized;
private volatile boolean contextStopped;
private volatile boolean stopped;
private volatile boolean running;
private int phase = Integer.MIN_VALUE + 1000;
private volatile ConditionalExceptionLogger closeExceptionLogger = new DefaultChannelCloseLogger();
/** Synchronization monitor for the shared Connection */
private final Object connectionMonitor = new Object();
/** Executor used for deferred close if no explicit executor set. */
private final ExecutorService deferredCloseExecutor = Executors.newCachedThreadPool();
/**
* Create a new CachingConnectionFactory initializing the hostname to be the value returned from
* InetAddress.getLocalHost(), or "localhost" if getLocalHost() throws an exception.
*/
public CachingConnectionFactory() {
this((String) null);
}
/**
* Create a new CachingConnectionFactory given a host name
* and port.
* @param hostname the host name to connect to
* @param port the port number
*/
public CachingConnectionFactory(String hostname, int port) {
super(newRabbitConnectionFactory());
if (!StringUtils.hasText(hostname)) {
hostname = getDefaultHostName();
}
setHost(hostname);
setPort(port);
}
/**
* Create a new CachingConnectionFactory given a {@link URI}.
* @param uri the amqp uri configuring the connection
* @since 1.5
*/
public CachingConnectionFactory(URI uri) {
super(newRabbitConnectionFactory());
setUri(uri);
}
/**
* Create a new CachingConnectionFactory given a port on the hostname returned from
* InetAddress.getLocalHost(), or "localhost" if getLocalHost() throws an exception.
* @param port the port number
*/
public CachingConnectionFactory(int port) {
this(null, port);
}
/**
* Create a new CachingConnectionFactory given a host name.
* @param hostname the host name to connect to
*/
public CachingConnectionFactory(String hostname) {
this(hostname, io.bitsensor.plugins.shaded.com.rabbitmq.client.ConnectionFactory.DEFAULT_AMQP_PORT);
}
/**
* Create a new CachingConnectionFactory for the given target ConnectionFactory.
* @param rabbitConnectionFactory the target ConnectionFactory
*/
public CachingConnectionFactory(io.bitsensor.plugins.shaded.com.rabbitmq.client.ConnectionFactory rabbitConnectionFactory) {
super(rabbitConnectionFactory);
if (rabbitConnectionFactory.isAutomaticRecoveryEnabled()) {
logger.warn("***\nAutomatic Recovery is Enabled in the provided connection factory;\n"
+ "while Spring AMQP is compatible with this feature, it\n"
+ "prefers to use its own recovery mechanisms; when this option is true, you may receive\n"
+ "'AutoRecoverConnectionNotCurrentlyOpenException's until the connection is recovered.");
}
}
private static io.bitsensor.plugins.shaded.com.rabbitmq.client.ConnectionFactory newRabbitConnectionFactory() {
io.bitsensor.plugins.shaded.com.rabbitmq.client.ConnectionFactory connectionFactory = new io.bitsensor.plugins.shaded.com.rabbitmq.client.ConnectionFactory();
connectionFactory.setAutomaticRecoveryEnabled(false);
return connectionFactory;
}
/**
* The number of channels to maintain in the cache. By default, channels are allocated on
* demand (unbounded) and this represents the maximum cache size. To limit the available
* channels, see {@link #setChannelCheckoutTimeout(long)}.
* @param sessionCacheSize the channel cache size.
* @see #setChannelCheckoutTimeout(long)
*/
public void setChannelCacheSize(int sessionCacheSize) {
Assert.isTrue(sessionCacheSize >= 1, "Channel cache size must be 1 or higher");
this.channelCacheSize = sessionCacheSize;
}
public int getChannelCacheSize() {
return this.channelCacheSize;
}
public CacheMode getCacheMode() {
return this.cacheMode;
}
public void setCacheMode(CacheMode cacheMode) {
Assert.isTrue(!this.initialized, "'cacheMode' cannot be changed after initialization.");
Assert.notNull(cacheMode, "'cacheMode' must not be null.");
this.cacheMode = cacheMode;
}
@Deprecated
public int getConnectionCachesize() {
return getConnectionCacheSize();
}
public int getConnectionCacheSize() {
return this.connectionCacheSize;
}
public void setConnectionCacheSize(int connectionCacheSize) {
Assert.isTrue(connectionCacheSize >= 1, "Connection cache size must be 1 or higher.");
this.connectionCacheSize = connectionCacheSize;
}
/**
* Set the connection limit when using cache mode CONNECTION. When the limit is
* reached and there are no idle connections, the
* {@link #setChannelCheckoutTimeout(long) channelCheckoutTimeLimit} is used to wait
* for a connection to become idle.
* @param connectionLimit the limit.
* @since 1.5.5
*/
public void setConnectionLimit(int connectionLimit) {
Assert.isTrue(connectionLimit >= 1, "Connection limit must be 1 or higher.");
this.connectionLimit = connectionLimit;
}
@Override
public boolean isPublisherConfirms() {
return this.publisherConfirms;
}
@Override
public boolean isPublisherReturns() {
return this.publisherReturns;
}
public void setPublisherReturns(boolean publisherReturns) {
this.publisherReturns = publisherReturns;
}
public void setPublisherConfirms(boolean publisherConfirms) {
this.publisherConfirms = publisherConfirms;
}
/**
* Sets the channel checkout timeout. When greater than 0, enables channel limiting
* in that the {@link #channelCacheSize} becomes the total number of available channels per
* connection rather than a simple cache size. Note that changing the {@link #channelCacheSize}
* does not affect the limit on existing connection(s), invoke {@link #destroy()} to cause a
* new connection to be created with the new limit.
*
* Since 1.5.5, also applies to getting a connection when the cache mode is CONNECTION.
* @param channelCheckoutTimeout the timeout in milliseconds; default 0 (channel limiting not enabled).
* @see #setConnectionLimit(int)
* @since 1.4.2
*/
public void setChannelCheckoutTimeout(long channelCheckoutTimeout) {
this.channelCheckoutTimeout = channelCheckoutTimeout;
}
/**
* Set the strategy for logging close exceptions; by default, if a channel is closed due to a failed
* passive queue declaration, it is logged at debug level. Normal channel closes (200 OK) are not
* logged. All others are logged at ERROR level (unless access is refused due to an exclusive consumer
* condition, in which case, it is logged at INFO level).
* @param closeExceptionLogger the {@link ConditionalExceptionLogger}.
* @since 1.5
*/
public void setCloseExceptionLogger(ConditionalExceptionLogger closeExceptionLogger) {
Assert.notNull(closeExceptionLogger, "'closeExceptionLogger' cannot be null");
this.closeExceptionLogger = closeExceptionLogger;
}
@Override
public void afterPropertiesSet() throws Exception {
this.initialized = true;
if (this.cacheMode == CacheMode.CHANNEL) {
Assert.isTrue(this.connectionCacheSize == 1,
"When the cache mode is 'CHANNEL', the connection cache size cannot be configured.");
}
initCacheWaterMarks();
}
private void initCacheWaterMarks() {
this.channelHighWaterMarks.put(ObjectUtils.getIdentityHexString(this.cachedChannelsNonTransactional),
new AtomicInteger());
this.channelHighWaterMarks.put(ObjectUtils.getIdentityHexString(this.cachedChannelsTransactional),
new AtomicInteger());
}
@Override
public void setConnectionListeners(List listeners) {
super.setConnectionListeners(listeners);
// If the connection is already alive we assume that the new listeners want to be notified
if (this.connection.target != null) {
this.getConnectionListener().onCreate(this.connection);
}
}
@Override
public void addConnectionListener(ConnectionListener listener) {
super.addConnectionListener(listener);
// If the connection is already alive we assume that the new listener wants to be notified
if (this.connection.target != null) {
listener.onCreate(this.connection);
}
}
@Override
public void shutdownCompleted(ShutdownSignalException cause) {
this.closeExceptionLogger.log(logger, "Channel shutdown", cause);
}
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
}
@Override
public void onApplicationEvent(ContextClosedEvent event) {
if (this.applicationContext == event.getApplicationContext()) {
this.contextStopped = true;
}
}
private Channel getChannel(ChannelCachingConnectionProxy connection, boolean transactional) {
if (this.channelCheckoutTimeout > 0) {
Semaphore checkoutPermits = this.checkoutPermits.get(connection);
if (checkoutPermits != null) {
try {
if (!checkoutPermits.tryAcquire(this.channelCheckoutTimeout, TimeUnit.MILLISECONDS)) {
throw new AmqpTimeoutException("No available channels");
}
if (logger.isDebugEnabled()) {
logger.debug(
"Acquired permit for " + connection + ", remaining:" + checkoutPermits.availablePermits());
}
}
catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new AmqpTimeoutException("Interrupted while acquiring a channel", e);
}
}
else {
throw new IllegalStateException("No permits map entry for " + connection);
}
}
LinkedList channelList;
if (this.cacheMode == CacheMode.CHANNEL) {
channelList = transactional ? this.cachedChannelsTransactional
: this.cachedChannelsNonTransactional;
}
else {
channelList = transactional ? this.allocatedConnectionTransactionalChannels.get(connection)
: this.allocatedConnectionNonTransactionalChannels.get(connection);
}
if (channelList == null) {
throw new IllegalStateException("No channel list for connection " + connection);
}
ChannelProxy channel = null;
if (connection.isOpen()) {
synchronized (channelList) {
while (!channelList.isEmpty()) {
channel = channelList.removeFirst();
if (logger.isTraceEnabled()) {
logger.trace(channel + " retrieved from cache");
}
if (channel.isOpen()) {
break;
}
else {
try {
Channel target = channel.getTargetChannel();
if (target != null) {
target.close();
/*
* To remove it from auto-recovery if so configured,
* and nack any pending confirms if PublisherCallbackChannel.
*/
}
}
catch (AlreadyClosedException e) {
if (logger.isTraceEnabled()) {
logger.trace(channel + " is already closed");
}
}
catch (IOException e) {
if (logger.isDebugEnabled()) {
logger.debug("Unexpected Exception closing channel " + e.getMessage());
}
}
catch (TimeoutException e) {
if (logger.isWarnEnabled()) {
logger.warn("TimeoutException closing channel " + e.getMessage());
}
}
channel = null;
}
}
}
if (channel != null) {
if (logger.isTraceEnabled()) {
logger.trace("Found cached Rabbit Channel: " + channel.toString());
}
}
}
if (channel == null) {
channel = getCachedChannelProxy(connection, channelList, transactional);
}
return channel;
}
private ChannelProxy getCachedChannelProxy(ChannelCachingConnectionProxy connection,
LinkedList channelList, boolean transactional) {
Channel targetChannel = createBareChannel(connection, transactional);
if (logger.isDebugEnabled()) {
logger.debug("Creating cached Rabbit Channel from " + targetChannel);
}
getChannelListener().onCreate(targetChannel, transactional);
Class[] interfaces;
if (this.publisherConfirms || this.publisherReturns) {
interfaces = new Class[] { ChannelProxy.class, PublisherCallbackChannel.class };
}
else {
interfaces = new Class[] { ChannelProxy.class };
}
return (ChannelProxy) Proxy.newProxyInstance(ChannelProxy.class.getClassLoader(),
interfaces, new CachedChannelInvocationHandler(connection, targetChannel, channelList,
transactional));
}
private Channel createBareChannel(ChannelCachingConnectionProxy connection, boolean transactional) {
if (this.cacheMode == CacheMode.CHANNEL) {
if (!this.connection.isOpen()) {
synchronized (this.connectionMonitor) {
if (!this.connection.isOpen()) {
this.connection.notifyCloseIfNecessary();
}
if (!this.connection.isOpen()) {
this.connection.target = null;
createConnection();
}
}
}
return doCreateBareChannel(this.connection, transactional);
}
else if (this.cacheMode == CacheMode.CONNECTION) {
if (!connection.isOpen()) {
synchronized (this.connectionMonitor) {
this.allocatedConnectionNonTransactionalChannels.get(connection).clear();
this.allocatedConnectionTransactionalChannels.get(connection).clear();
connection.notifyCloseIfNecessary();
refreshProxyConnection(connection);
}
}
return doCreateBareChannel(connection, transactional);
}
return null;
}
private Channel doCreateBareChannel(ChannelCachingConnectionProxy connection, boolean transactional) {
Channel channel = connection.createBareChannel(transactional);
if (this.publisherConfirms) {
try {
channel.confirmSelect();
}
catch (IOException e) {
logger.error("Could not configure the channel to receive publisher confirms", e);
}
}
if (this.publisherConfirms || this.publisherReturns) {
if (!(channel instanceof PublisherCallbackChannelImpl)) {
channel = new PublisherCallbackChannelImpl(channel);
}
}
if (channel != null) {
channel.addShutdownListener(this);
}
return channel;
}
@Override
public final Connection createConnection() throws AmqpException {
Assert.state(!this.stopped, "The ApplicationContext is closed and the ConnectionFactory can no longer create connections.");
synchronized (this.connectionMonitor) {
if (this.cacheMode == CacheMode.CHANNEL) {
if (this.connection.target == null) {
this.connection.target = super.createBareConnection();
// invoke the listener *after* this.connection is assigned
if (!this.checkoutPermits.containsKey(this.connection)) {
this.checkoutPermits.put(this.connection, new Semaphore(this.channelCacheSize));
}
this.connection.closeNotified.set(false);
getConnectionListener().onCreate(this.connection);
}
return this.connection;
}
else if (this.cacheMode == CacheMode.CONNECTION) {
ChannelCachingConnectionProxy connection = findIdleConnection();
long now = System.currentTimeMillis();
while (connection == null && System.currentTimeMillis() - now < this.channelCheckoutTimeout) {
if (countOpenConnections() >= this.connectionLimit) {
try {
this.connectionMonitor.wait(this.channelCheckoutTimeout);
connection = findIdleConnection();
}
catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new AmqpException("Interrupted while waiting for a connection", e);
}
}
}
if (connection == null) {
if (countOpenConnections() >= this.connectionLimit
&& System.currentTimeMillis() - now >= this.channelCheckoutTimeout) {
throw new AmqpTimeoutException("Timed out attempting to get a connection");
}
connection = new ChannelCachingConnectionProxy(super.createBareConnection());
if (logger.isDebugEnabled()) {
logger.debug("Adding new connection '" + connection + "'");
}
this.allocatedConnections.add(connection);
this.allocatedConnectionNonTransactionalChannels.put(connection, new LinkedList());
this.channelHighWaterMarks.put(ObjectUtils.getIdentityHexString(
this.allocatedConnectionNonTransactionalChannels.get(connection)), new AtomicInteger());
this.allocatedConnectionTransactionalChannels.put(connection, new LinkedList());
this.channelHighWaterMarks.put(
ObjectUtils.getIdentityHexString(this.allocatedConnectionTransactionalChannels.get(connection)),
new AtomicInteger());
this.checkoutPermits.put(connection, new Semaphore(this.channelCacheSize));
getConnectionListener().onCreate(connection);
}
else if (!connection.isOpen()) {
try {
refreshProxyConnection(connection);
}
catch (Exception e) {
this.idleConnections.addLast(connection);
}
}
else {
if (logger.isDebugEnabled()) {
logger.debug("Obtained connection '" + connection + "' from cache");
}
}
return connection;
}
}
return null;
}
/*
* Iterate over the idle connections looking for an open one. If there are no idle,
* return null, if there are no open idle, return the first closed idle so it can
* be reopened.
*/
private ChannelCachingConnectionProxy findIdleConnection() {
ChannelCachingConnectionProxy connection = null;
ChannelCachingConnectionProxy lastIdle = this.idleConnections.peekLast();
while (connection == null) {
connection = this.idleConnections.poll();
if (connection != null) {
if (!connection.isOpen()) {
if (logger.isDebugEnabled()) {
logger.debug("Skipping closed connection '" + connection + "'");
}
connection.notifyCloseIfNecessary();
this.idleConnections.addLast(connection);
if (connection.equals(lastIdle)) {
// all of the idle connections are closed.
connection = this.idleConnections.poll();
break;
}
connection = null;
}
}
else {
break;
}
}
return connection;
}
private void refreshProxyConnection(ChannelCachingConnectionProxy connection) {
connection.destroy();
connection.notifyCloseIfNecessary();
connection.target = super.createBareConnection();
connection.closeNotified.set(false);
getConnectionListener().onCreate(connection);
if (logger.isDebugEnabled()) {
logger.debug("Refreshed existing connection '" + connection + "'");
}
}
/**
* Close the underlying shared connection. The provider of this ConnectionFactory needs to care for proper shutdown.
*
* As this bean implements DisposableBean, a bean factory will automatically invoke this on destruction of its
* cached singletons.
*/
@Override
public final void destroy() {
resetConnection();
}
/**
* Close the connection(s). This will impact any in-process operations. New
* connection(s) will be created on demand after this method returns. This might be
* used to force a reconnect to the primary broker after failing over to a secondary
* broker.
*/
public void resetConnection() {
synchronized (this.connectionMonitor) {
if (this.connection.target != null) {
this.connection.destroy();
}
for (ChannelCachingConnectionProxy connection : this.allocatedConnections) {
connection.destroy();
}
for (AtomicInteger count : this.channelHighWaterMarks.values()) {
count.set(0);
}
this.connectionHighWaterMark.set(0);
}
}
@Override
public void start() {
this.running = true;
}
@Override
public boolean isRunning() {
return this.running;
}
@Override
public int getPhase() {
return this.phase;
}
/**
* Defaults to phase {@link Integer#MIN_VALUE - 1000} so the factory is
* stopped in a very late phase, allowing other beans to use the connection
* to clean up.
* @see #getPhase()
* @param phase the phase.
* @since 1.5.3
*/
public void setPhase(int phase) {
this.phase = phase;
}
@Override
public boolean isAutoStartup() {
return true;
}
/**
* Stop the connection factory to prevent its connection from being used.
* Note: ignored unless the application context is in the process of being stopped.
*/
@Override
public void stop() {
if (this.contextStopped) {
this.running = false;
this.stopped = true;
this.deferredCloseExecutor.shutdownNow();
}
else {
logger.warn("stop() is ignored unless the application context is being stopped");
}
}
@Override
public void stop(Runnable callback) {
stop();
callback.run();
}
/*
* Reset the Channel cache and underlying shared Connection, to be reinitialized on next access.
*/
protected void reset(List channels, List txChannels) {
this.active = false;
synchronized (channels) {
for (ChannelProxy channel : channels) {
try {
channel.close();
}
catch (Exception ex) {
logger.trace("Could not close cached Rabbit Channel", ex);
}
}
channels.clear();
}
synchronized (txChannels) {
for (ChannelProxy channel : txChannels) {
try {
channel.close();
}
catch (Exception ex) {
logger.trace("Could not close cached Rabbit Channel", ex);
}
}
txChannels.clear();
}
this.active = true;
}
@ManagedAttribute
public Properties getCacheProperties() {
Properties props = new Properties();
props.setProperty("cacheMode", this.cacheMode.name());
synchronized (this.connectionMonitor) {
props.setProperty("channelCacheSize", Integer.toString(this.channelCacheSize));
if (this.cacheMode.equals(CacheMode.CONNECTION)) {
props.setProperty("connectionCacheSize", Integer.toString(this.connectionCacheSize));
props.setProperty("openConnections", Integer.toString(countOpenConnections()));
props.setProperty("idleConnections", Integer.toString(this.idleConnections.size()));
props.setProperty("idleConnectionsHighWater", Integer.toString(this.connectionHighWaterMark.get()));
for (ChannelCachingConnectionProxy proxy : this.allocatedConnections) {
putConnectionName(props, proxy, ":" + proxy.getLocalPort());
}
for (Entry> entry :
this.allocatedConnectionTransactionalChannels.entrySet()) {
int port = entry.getKey().getLocalPort();
if (port > 0 && entry.getKey().isOpen()) {
LinkedList channelList = entry.getValue();
props.put("idleChannelsTx:" + port, Integer.toString(channelList.size()));
props.put("idleChannelsTxHighWater:" + port, Integer.toString(
this.channelHighWaterMarks.get(ObjectUtils.getIdentityHexString(channelList)).get()));
}
}
for (Entry> entry :
this.allocatedConnectionNonTransactionalChannels.entrySet()) {
int port = entry.getKey().getLocalPort();
if (port > 0 && entry.getKey().isOpen()) {
LinkedList channelList = entry.getValue();
props.put("idleChannelsNotTx:" + port, Integer.toString(channelList.size()));
props.put("idleChannelsNotTxHighWater:" + port, Integer.toString(
this.channelHighWaterMarks.get(ObjectUtils.getIdentityHexString(channelList)).get()));
}
}
}
else {
props.setProperty("localPort",
Integer.toString(this.connection.target == null ? 0 : this.connection.getLocalPort()));
props.setProperty("idleChannelsTx", Integer.toString(this.cachedChannelsTransactional.size()));
props.setProperty("idleChannelsNotTx", Integer.toString(this.cachedChannelsNonTransactional.size()));
props.setProperty("idleChannelsTxHighWater", Integer.toString(this.channelHighWaterMarks
.get(ObjectUtils.getIdentityHexString(this.cachedChannelsTransactional)).get()));
props.setProperty("idleChannelsNotTxHighWater", Integer.toString(this.channelHighWaterMarks
.get(ObjectUtils.getIdentityHexString(this.cachedChannelsNonTransactional)).get()));
putConnectionName(props, this.connection, "");
}
}
return props;
}
private void putConnectionName(Properties props, ConnectionProxy connection, String keySuffix) {
Connection targetConnection = connection.getTargetConnection();
if (targetConnection instanceof SimpleConnection) {
String name = ((SimpleConnection) targetConnection).getDelegate().getClientProvidedName();
if (name != null) {
props.put("connectionName" + keySuffix, name);
}
}
}
private int countOpenConnections() {
int n = 0;
for (ChannelCachingConnectionProxy proxy : this.allocatedConnections) {
if (proxy.isOpen()) {
n++;
}
}
return n;
}
@Override
public String toString() {
return "CachingConnectionFactory [channelCacheSize=" + this.channelCacheSize + ", host=" + getHost()
+ ", port=" + getPort() + ", active=" + this.active
+ " " + super.toString() + "]";
}
private final class CachedChannelInvocationHandler implements InvocationHandler {
private final ChannelCachingConnectionProxy theConnection;
private final LinkedList channelList;
private final String channelListIdentity;
private final Object targetMonitor = new Object();
private final boolean transactional;
private volatile Channel target;
private volatile boolean txStarted;
CachedChannelInvocationHandler(ChannelCachingConnectionProxy connection,
Channel target,
LinkedList channelList,
boolean transactional) {
this.theConnection = connection;
this.target = target;
this.channelList = channelList;
this.channelListIdentity = ObjectUtils.getIdentityHexString(channelList);
this.transactional = transactional;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
String methodName = method.getName();
if (methodName.equals("txSelect") && !this.transactional) {
throw new UnsupportedOperationException("Cannot start transaction on non-transactional channel");
}
if (methodName.equals("equals")) {
// Only consider equal when proxies are identical.
return (proxy == args[0]);
}
else if (methodName.equals("hashCode")) {
// Use hashCode of Channel proxy.
return System.identityHashCode(proxy);
}
else if (methodName.equals("toString")) {
return "Cached Rabbit Channel: " + this.target + ", conn: " + this.theConnection;
}
else if (methodName.equals("close")) {
// Handle close method: don't pass the call on.
if (CachingConnectionFactory.this.active) {
synchronized (this.channelList) {
if (!RabbitUtils.isPhysicalCloseRequired() &&
(this.channelList.size() < getChannelCacheSize()
|| this.channelList.contains(proxy))) {
releasePermitIfNecessary(proxy);
logicalClose((ChannelProxy) proxy);
return null;
}
}
}
// If we get here, we're supposed to shut down.
physicalClose();
releasePermitIfNecessary(proxy);
return null;
}
else if (methodName.equals("getTargetChannel")) {
// Handle getTargetChannel method: return underlying Channel.
return this.target;
}
else if (methodName.equals("isOpen")) {
// Handle isOpen method: we are closed if the target is closed
return this.target != null && this.target.isOpen();
}
else if (methodName.equals("isTransactional")) {
return this.transactional;
}
try {
if (this.target == null || !this.target.isOpen()) {
if (this.target instanceof PublisherCallbackChannel) {
this.target.close();
throw new InvocationTargetException(new AmqpException("PublisherCallbackChannel is closed"));
}
else if (this.txStarted) {
this.txStarted = false;
throw new IllegalStateException("Channel closed during transaction");
}
this.target = null;
}
synchronized (this.targetMonitor) {
if (this.target == null) {
this.target = createBareChannel(this.theConnection, this.transactional);
}
Object result = method.invoke(this.target, args);
if (this.transactional) {
if (txStarts.contains(methodName)) {
this.txStarted = true;
}
else if (txEnds.contains(methodName)) {
this.txStarted = false;
}
}
return result;
}
}
catch (InvocationTargetException ex) {
if (this.target == null || !this.target.isOpen()) {
// Basic re-connection logic...
if (logger.isDebugEnabled()) {
logger.debug("Detected closed channel on exception. Re-initializing: " + this.target);
}
this.target = null;
synchronized (this.targetMonitor) {
if (this.target == null) {
this.target = createBareChannel(this.theConnection, this.transactional);
}
}
}
throw ex.getTargetException();
}
}
private void releasePermitIfNecessary(Object proxy) {
if (CachingConnectionFactory.this.channelCheckoutTimeout > 0) {
/*
* Only release a permit if this is a normal close; if the channel is
* in the list, it means we're closing a cached channel (for which a permit
* has already been released).
*/
synchronized (this.channelList) {
if (this.channelList.contains(proxy)) {
return;
}
}
Semaphore checkoutPermits = CachingConnectionFactory.this.checkoutPermits.get(this.theConnection);
if (checkoutPermits != null) {
checkoutPermits.release();
if (logger.isDebugEnabled()) {
logger.debug("Released permit for " + this.theConnection + ", remaining:"
+ checkoutPermits.availablePermits());
}
}
else {
logger.error("LEAKAGE: No permits map entry for " + this.theConnection);
}
}
}
/**
* GUARDED by channelList
* @param proxy the channel to close
*/
private void logicalClose(ChannelProxy proxy) throws Exception {
if (this.target == null) {
return;
}
if (this.target != null && !this.target.isOpen()) {
synchronized (this.targetMonitor) {
if (this.target != null && !this.target.isOpen()) {
if (this.target instanceof PublisherCallbackChannel) {
this.target.close(); // emit nacks if necessary
}
if (this.channelList.contains(proxy)) {
this.channelList.remove(proxy);
}
this.target = null;
return;
}
}
}
// Allow for multiple close calls...
if (!this.channelList.contains(proxy)) {
if (logger.isTraceEnabled()) {
logger.trace("Returning cached Channel: " + this.target);
}
this.channelList.addLast(proxy);
setHighWaterMark();
}
}
private void setHighWaterMark() {
AtomicInteger hwm = CachingConnectionFactory.this.channelHighWaterMarks.get(this.channelListIdentity);
if (hwm != null) {
// No need for atomicity since we're sync'd on the channel list
int prev = hwm.get();
int size = this.channelList.size();
if (size > prev) {
hwm.set(size);
}
}
}
private void physicalClose() throws Exception {
if (logger.isDebugEnabled()) {
logger.debug("Closing cached Channel: " + this.target);
}
if (this.target == null) {
return;
}
try {
if (CachingConnectionFactory.this.active &&
(CachingConnectionFactory.this.publisherConfirms ||
CachingConnectionFactory.this.publisherReturns)) {
ExecutorService executorService = (getExecutorService() != null
? getExecutorService()
: CachingConnectionFactory.this.deferredCloseExecutor);
final Channel channel = CachedChannelInvocationHandler.this.target;
executorService.execute(new Runnable() {
@Override
public void run() {
try {
if (CachingConnectionFactory.this.publisherConfirms) {
channel.waitForConfirmsOrDie(5000);
}
else {
Thread.sleep(5000);
}
}
catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
catch (Exception e) { }
finally {
try {
channel.close();
}
catch (IOException e) { }
catch (AlreadyClosedException e) { }
catch (TimeoutException e) { }
}
}
});
}
else {
this.target.close();
}
}
catch (AlreadyClosedException e) {
if (logger.isTraceEnabled()) {
logger.trace(this.target + " is already closed");
}
}
finally {
this.target = null;
}
}
}
private class ChannelCachingConnectionProxy implements Connection, ConnectionProxy { // NOSONAR - final (tests spy)
private volatile Connection target;
private final AtomicBoolean closeNotified = new AtomicBoolean(false);
ChannelCachingConnectionProxy(Connection target) {
this.target = target;
}
private Channel createBareChannel(boolean transactional) {
return this.target.createChannel(transactional);
}
@Override
public Channel createChannel(boolean transactional) {
return getChannel(this, transactional);
}
@Override
public void close() {
if (CachingConnectionFactory.this.cacheMode == CacheMode.CONNECTION) {
synchronized (CachingConnectionFactory.this.connectionMonitor) {
/*
* Only connectionCacheSize open idle connections are allowed.
*/
if (!CachingConnectionFactory.this.idleConnections.contains(this)) {
if (!this.target.isOpen()
|| countOpenIdleConnections() >= CachingConnectionFactory.this.connectionCacheSize) {
if (logger.isDebugEnabled()) {
logger.debug("Completely closing connection '" + this + "'");
}
destroy();
}
if (logger.isDebugEnabled()) {
logger.debug("Returning connection '" + this + "' to cache");
}
CachingConnectionFactory.this.idleConnections.add(this);
if (CachingConnectionFactory.this.connectionHighWaterMark
.get() < CachingConnectionFactory.this.idleConnections.size()) {
CachingConnectionFactory.this.connectionHighWaterMark
.set(CachingConnectionFactory.this.idleConnections.size());
}
CachingConnectionFactory.this.connectionMonitor.notifyAll();
}
}
}
}
private int countOpenIdleConnections() {
int n = 0;
for (ChannelCachingConnectionProxy proxy : CachingConnectionFactory.this.idleConnections) {
if (proxy.isOpen()) {
n++;
}
}
return n;
}
public void destroy() {
if (CachingConnectionFactory.this.cacheMode == CacheMode.CHANNEL) {
reset(CachingConnectionFactory.this.cachedChannelsNonTransactional,
CachingConnectionFactory.this.cachedChannelsTransactional);
}
else {
reset(CachingConnectionFactory.this.allocatedConnectionNonTransactionalChannels.get(this),
CachingConnectionFactory.this.allocatedConnectionTransactionalChannels.get(this));
}
if (this.target != null) {
RabbitUtils.closeConnection(this.target);
this.notifyCloseIfNecessary();
}
this.target = null;
}
private void notifyCloseIfNecessary() {
if (!(this.closeNotified.getAndSet(true))) {
getConnectionListener().onClose(this);
}
}
@Override
public boolean isOpen() {
return this.target != null && this.target.isOpen();
}
@Override
public Connection getTargetConnection() {
return this.target;
}
@Override
public int getLocalPort() {
Connection target = this.target; // NOSONAR (close)
if (target != null) {
return target.getLocalPort();
}
return 0;
}
@Override
public String toString() {
return "Proxy@" + ObjectUtils.getIdentityHexString(this) + " "
+ (CachingConnectionFactory.this.cacheMode == CacheMode.CHANNEL ? "Shared " : "Dedicated ")
+ "Rabbit Connection: " + this.target;
}
}
/**
* Default implementation of {@link ConditionalExceptionLogger} for logging channel
* close exceptions.
* @since 1.5
*/
private static class DefaultChannelCloseLogger implements ConditionalExceptionLogger {
DefaultChannelCloseLogger() {
super();
}
@Override
public void log(Log logger, String message, Throwable t) {
if (t instanceof ShutdownSignalException) {
ShutdownSignalException cause = (ShutdownSignalException) t;
if (RabbitUtils.isPassiveDeclarationChannelClose(cause)) {
if (logger.isDebugEnabled()) {
logger.debug(message + ": " + cause.getMessage());
}
}
else if (RabbitUtils.isExclusiveUseChannelClose(cause)) {
if (logger.isInfoEnabled()) {
logger.info(message + ": " + cause.getMessage());
}
}
else if (!RabbitUtils.isNormalChannelClose(cause)) {
logger.error(message + ": " + cause.getMessage());
}
}
else {
logger.error("Unexpected invocation of " + this.getClass() + ", with message: " + message, t);
}
}
}
}