Package org.springframework.amqp.rabbit.connection

Source Code of org.springframework.amqp.rabbit.connection.CachingConnectionFactory$ChannelCachingConnectionProxy

/*
* Copyright 2002-2014 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.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.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.atomic.AtomicBoolean;

import org.springframework.amqp.AmqpException;
import org.springframework.amqp.rabbit.support.PublisherCallbackChannel;
import org.springframework.amqp.rabbit.support.PublisherCallbackChannelImpl;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;

import com.rabbitmq.client.AlreadyClosedException;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.ShutdownListener;
import 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 com.rabbitmq.client.Connection#close()} and caches
* {@link com.rabbitmq.client.Channel}.
*
* <p>
* 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.
*
* <p>
* When the cache mode is {@link CacheMode#CONNECTION}, a new (or cached) connection is used for each request. In this case,
* no channels are cached, just connections. The intended use case is a dedicated connection for long-lived
* channels, such as those used in listener container threads. In those cases, the channel must be closed
* anyway in order to re-queue any un-acked messages.
* <p>
* <b>{@link CacheMode#CONNECTION} is not compatible with a Rabbit Admin that auto-declares queues etc.</b>
* <p>
* <b>NOTE: This ConnectionFactory requires explicit closing of all Channels obtained form its shared Connection.</b>
* 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.
*
* @author Mark Pollack
* @author Mark Fisher
* @author Dave Syer
* @author Gary Russell
* @author Artem Bilan
*/
public class CachingConnectionFactory extends AbstractConnectionFactory implements InitializingBean, ShutdownListener {

  public enum CacheMode {
    /**
     * Cache channels - single connection
     */
    CHANNEL,
    /**
     * Cache connections - no channel caching
     */
    CONNECTION
  }

  private volatile CacheMode cacheMode = CacheMode.CHANNEL;

  private final Set<ChannelCachingConnectionProxy> openConnections = new HashSet<ChannelCachingConnectionProxy>();

  private final Map<ChannelCachingConnectionProxy, LinkedList<ChannelProxy>>
      openConnectionNonTransactionalChannels = new HashMap<ChannelCachingConnectionProxy, LinkedList<ChannelProxy>>();

  private final Map<ChannelCachingConnectionProxy, LinkedList<ChannelProxy>>
      openConnectionTransactionalChannels = new HashMap<ChannelCachingConnectionProxy, LinkedList<ChannelProxy>>();

  private final BlockingQueue<ChannelCachingConnectionProxy> idleConnections = new LinkedBlockingQueue<ChannelCachingConnectionProxy>();

  private volatile int channelCacheSize = 1;

  private volatile int connectionCacheSize = 1;

  private final LinkedList<ChannelProxy> cachedChannelsNonTransactional = new LinkedList<ChannelProxy>();

  private final LinkedList<ChannelProxy> cachedChannelsTransactional = new LinkedList<ChannelProxy>();

  private volatile boolean active = true;

  private volatile ChannelCachingConnectionProxy connection;

  private volatile boolean publisherConfirms;

  private volatile boolean publisherReturns;

  private volatile boolean initialized;

  /** 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(new com.rabbitmq.client.ConnectionFactory());
    if (!StringUtils.hasText(hostname)) {
      hostname = getDefaultHostName();
    }
    setHost(hostname);
    setPort(port);
  }

  /**
   * 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, com.rabbitmq.client.ConnectionFactory.DEFAULT_AMQP_PORT);
  }

  /**
   * Create a new CachingConnectionFactory for the given target ConnectionFactory.
   *
   * @param rabbitConnectionFactory the target ConnectionFactory
   */
  public CachingConnectionFactory(com.rabbitmq.client.ConnectionFactory rabbitConnectionFactory) {
    super(rabbitConnectionFactory);
  }

  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 cacheMode;
  }

  public void setCacheMode(CacheMode cacheMode) {
    Assert.isTrue(!initialized, "'cacheMode' cannot be changed after initialization.");
    Assert.notNull(cacheMode, "'cacheMode' must not be null.");
    this.cacheMode = cacheMode;
  }

  public int getConnectionCachesize() {
    return connectionCacheSize;
  }

  public void setConnectionCacheSize(int connectionCacheSize) {
    Assert.isTrue(connectionCacheSize >= 1, "Connection cache size must be 1 or higher.");
    this.connectionCacheSize = connectionCacheSize;
  }

  public boolean isPublisherConfirms() {
    return publisherConfirms;
  }

  public boolean isPublisherReturns() {
    return publisherReturns;
  }

  public void setPublisherReturns(boolean publisherReturns) {
    this.publisherReturns = publisherReturns;
  }

  public void setPublisherConfirms(boolean publisherConfirms) {
    this.publisherConfirms = publisherConfirms;
  }

  @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.");
    }
  }

  @Override
  public void setConnectionListeners(List<? extends ConnectionListener> listeners) {
    super.setConnectionListeners(listeners);
    // If the connection is already alive we assume that the new listeners want to be notified
    if (this.connection != 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 != null) {
      listener.onCreate(this.connection);
    }
  }

  @Override
  public void shutdownCompleted(ShutdownSignalException cause) {
    if (!RabbitUtils.isNormalChannelClose(cause)) {
      logger.error("Channel shutdown: " + cause.getMessage());
    }
  }

  private Channel getChannel(ChannelCachingConnectionProxy connection, boolean transactional) {
    LinkedList<ChannelProxy> channelList;
    if (this.cacheMode == CacheMode.CHANNEL) {
      channelList = transactional ? this.cachedChannelsTransactional
          : this.cachedChannelsNonTransactional;
    }
    else {
      channelList = transactional ? this.openConnectionTransactionalChannels.get(connection)
          : this.openConnectionNonTransactionalChannels.get(connection);
    }
    if (channelList == null) {
      channelList = new LinkedList<ChannelProxy>();
      if (transactional) {
        this.openConnectionTransactionalChannels.put(connection, channelList);
      }
      else {
        this.openConnectionNonTransactionalChannels.put(connection, channelList);
      }
    }
    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.getTargetChannel().close(); // to remove it from auto-recovery if so configured
            }
            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());
              }
            }
            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<ChannelProxy> 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 == null || !this.connection.isOpen()) {
        synchronized (this.connectionMonitor) {
          if (this.connection != null && !this.connection.isOpen()) {
            this.connection.notifyCloseIfNecessary();
          }
          if (this.connection == null || !this.connection.isOpen()) {
            this.connection = null;
            createConnection();
          }
        }
      }
      return doCreateBareChannel(this.connection, transactional);
    }
    else if (this.cacheMode == CacheMode.CONNECTION) {
      if (!connection.isOpen()) {
        synchronized(connectionMonitor) {
          this.openConnectionNonTransactionalChannels.get(connection).clear();
          this.openConnectionTransactionalChannels.get(connection).clear();
          connection.notifyCloseIfNecessary();
          ChannelCachingConnectionProxy newConnection = (ChannelCachingConnectionProxy) createConnection();
          /*
           * Applications already have a reference to the proxy, so we steal the new (or idle) connection's
           * target and remove the connection from the open list.
           */
          connection.target = newConnection.target;
          connection.closeNotified.set(false);
          this.openConnections.remove(newConnection);
        }
      }
      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 {
    synchronized (this.connectionMonitor) {
      if (this.cacheMode == CacheMode.CHANNEL) {
        if (this.connection == null) {
          this.connection = new ChannelCachingConnectionProxy(super.createBareConnection());
          // invoke the listener *after* this.connection is assigned
          getConnectionListener().onCreate(connection);
        }
        return this.connection;
      }
      else if (this.cacheMode == CacheMode.CONNECTION) {
        ChannelCachingConnectionProxy connection = null;
        while (connection == null && !this.idleConnections.isEmpty()) {
          connection = this.idleConnections.poll();
          if (connection != null) {
            if (!connection.isOpen()) {
              if (logger.isDebugEnabled()) {
                logger.debug("Removing closed connection '" + connection + "'");
              }
              connection.notifyCloseIfNecessary();
              this.openConnections.remove(connection);
              this.openConnectionNonTransactionalChannels.remove(connection);
              this.openConnectionTransactionalChannels.remove(connection);
              connection = null;
            }
          }
        }
        if (connection == null) {
          connection = new ChannelCachingConnectionProxy(super.createBareConnection());
          getConnectionListener().onCreate(connection);
          if (logger.isDebugEnabled()) {
            logger.debug("Adding new connection '" + connection + "'");
          }
          this.openConnections.add(connection);
          this.openConnectionNonTransactionalChannels.put(connection, new LinkedList<ChannelProxy>());
          this.openConnectionTransactionalChannels.put(connection, new LinkedList<ChannelProxy>());
        }
        else {
          if (logger.isDebugEnabled()) {
            logger.debug("Obtained connection '" + connection + "' from cache");
          }
        }
        return connection;
      }
    }
    return null;
  }

  /**
   * Close the underlying shared connection. The provider of this ConnectionFactory needs to care for proper shutdown.
   * <p>
   * As this bean implements DisposableBean, a bean factory will automatically invoke this on destruction of its
   * cached singletons.
   */
  @Override
  public final void destroy() {
    synchronized (this.connectionMonitor) {
      if (connection != null) {
        this.connection.destroy();
        this.connection = null;
      }
      for (ChannelCachingConnectionProxy connection : this.openConnections) {
        connection.destroy();
      }
      this.openConnections.clear();
      this.idleConnections.clear();
      this.openConnectionNonTransactionalChannels.clear();
      this.openConnectionTransactionalChannels.clear();
    }
  }

  /*
   * Reset the Channel cache and underlying shared Connection, to be reinitialized on next access.
   */
  protected void reset(List<ChannelProxy> channels, List<ChannelProxy> txChannels) {
    this.active = false;
    if (this.cacheMode == CacheMode.CHANNEL) {
      synchronized (channels) {
        for (ChannelProxy channel : channels) {
          try {
            channel.close();
          }
          catch (Throwable ex) {
            logger.trace("Could not close cached Rabbit Channel", ex);
          }
        }
        channels.clear();
      }
      synchronized (txChannels) {
        for (ChannelProxy channel : txChannels) {
          try {
            channel.close();
          } catch (Throwable ex) {
            logger.trace("Could not close cached Rabbit Channel", ex);
          }
        }
        txChannels.clear();
      }
    }
    this.active = true;
    this.connection = null;
  }

  @Override
  public String toString() {
    return "CachingConnectionFactory [channelCacheSize=" + channelCacheSize + ", host=" + this.getHost()
        + ", port=" + this.getPort() + ", active=" + active + "]";
  }

  private class CachedChannelInvocationHandler implements InvocationHandler {

    private final ChannelCachingConnectionProxy theConnection;

    private volatile Channel target;

    private final LinkedList<ChannelProxy> channelList;

    private final Object targetMonitor = new Object();

    private final boolean transactional;

    public CachedChannelInvocationHandler(ChannelCachingConnectionProxy connection,
        Channel target,
        LinkedList<ChannelProxy> channelList,
        boolean transactional) {
      this.theConnection = connection;
      this.target = target;
      this.channelList = 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;
      }
      else if (methodName.equals("close")) {
        // Handle close method: don't pass the call on.
        if (active) {
          synchronized (this.channelList) {
            if (!RabbitUtils.isPhysicalCloseRequired() && this.channelList.size() < getChannelCacheSize()) {
              logicalClose((ChannelProxy) proxy);
              // Remain open in the channel list.
              return null;
            }
          }
        }

        // If we get here, we're supposed to shut down.
        physicalClose();
        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();
      }
      try {
        if (this.target == null || !this.target.isOpen()) {
          this.target = null;
        }
        synchronized (targetMonitor) {
          if (this.target == null) {
            this.target = createBareChannel(theConnection, transactional);
          }
          return method.invoke(this.target, args);
        }
      }
      catch (InvocationTargetException ex) {
        if (this.target == null || !this.target.isOpen()) {
          // Basic re-connection logic...
          this.target = null;
          if (logger.isDebugEnabled()) {
            logger.debug("Detected closed channel on exception.  Re-initializing: " + target);
          }
          synchronized (targetMonitor) {
            if (this.target == null) {
              this.target = createBareChannel(theConnection, transactional);
            }
          }
        }
        throw ex.getTargetException();
      }
    }

    /**
     * GUARDED by channelList
     *
     * @param proxy the channel to close
     */
    private void logicalClose(ChannelProxy proxy) throws Exception {
      if (this.target != null && !this.target.isOpen()) {
        synchronized (targetMonitor) {
          if (this.target != null && !this.target.isOpen()) {
            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);
      }
    }

    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 {
                  if (channel.isOpen()) {
                    channel.close();
                  }
                }
                catch (IOException e) {}
                catch (AlreadyClosedException 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 {

    private volatile Connection target;

    private final AtomicBoolean closeNotified = new AtomicBoolean(false);

    public ChannelCachingConnectionProxy(Connection target) {
      this.target = target;
    }

    private Channel createBareChannel(boolean transactional) {
      return target.createChannel(transactional);
    }

    @Override
    public Channel createChannel(boolean transactional) {
      return getChannel(this, transactional);
    }

    @Override
    public void close() {
      if (cacheMode == CacheMode.CONNECTION) {
        synchronized (connectionMonitor) {
          if (!this.target.isOpen() || idleConnections.size() >= connectionCacheSize) {
            if (logger.isDebugEnabled()) {
              logger.debug("Completely closing connection '" + this + "'");
            }
            if (this.target.isOpen()) {
              RabbitUtils.closeConnection(this.target);
            }
            this.notifyCloseIfNecessary();
            openConnections.remove(this);
            openConnectionNonTransactionalChannels.remove(this);
            openConnectionTransactionalChannels.remove(this);
          }
          else {
            if (!idleConnections.contains(this)) {
              if (logger.isDebugEnabled()) {
                logger.debug("Returning connection '" + this + "' to cache");
              }
              idleConnections.add(this);
            }
          }
        }
      }
    }

    public void destroy() {
      if (CachingConnectionFactory.this.cacheMode == CacheMode.CHANNEL) {
        reset(cachedChannelsNonTransactional, cachedChannelsTransactional);
      }
      else {
        reset(openConnectionNonTransactionalChannels.get(this), openConnectionTransactionalChannels.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 target != null && target.isOpen();
    }

    @Override
    public Connection getTargetConnection() {
      return target;
    }

    @Override
    public int hashCode() {
      return 31 + ((target == null) ? 0 : target.hashCode());
    }

    @Override
    public boolean equals(Object obj) {
      if (this == obj) {
        return true;
      }
      if (obj == null) {
        return false;
      }
      if (getClass() != obj.getClass()) {
        return false;
      }
      ChannelCachingConnectionProxy other = (ChannelCachingConnectionProxy) obj;
      if (target == null) {
        if (other.target != null) {
          return false;
        }
      } else if (!target.equals(other.target)) {
        return false;
      }
      return true;
    }

    @Override
    public String toString() {
      return cacheMode == CacheMode.CHANNEL ? "Shared " : "Dedicated " +
          "Rabbit Connection: " + this.target;
    }

  }

}
TOP

Related Classes of org.springframework.amqp.rabbit.connection.CachingConnectionFactory$ChannelCachingConnectionProxy

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.