All Downloads are FREE. Search and download functionalities are using the official Maven repository.

io.bitsensor.plugins.shaded.org.springframework.amqp.rabbit.connection.CachingConnectionFactory Maven / Gradle / Ivy

The newest version!
/*
 * 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); } } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy