/* * Copyright (c) 2010-2012 Sonatype, Inc. All rights reserved. * * This program is licensed to you under the Apache License Version 2.0, * and you may not use this file except in compliance with the Apache License Version 2.0. * You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0. * * Unless required by applicable law or agreed to in writing, * software distributed under the Apache License Version 2.0 is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the Apache License Version 2.0 for the specific language governing permissions and limitations there under. */ package com.ning.http.client.providers.netty; import java.util.ArrayList; import java.util.List; import java.util.Timer; import java.util.TimerTask; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.atomic.AtomicBoolean; import org.jboss.netty.channel.Channel; import com.ning.http.client.ConnectionsPool; /** * A simple implementation of {@link com.ning.http.client.ConnectionsPool} based * on a {@link java.util.concurrent.ConcurrentHashMap} */ public class NettyConnectionsPool implements ConnectionsPool<String, Channel> { private final ConcurrentHashMap<String, ConcurrentLinkedQueue<IdleChannel>> connectionsPool = new ConcurrentHashMap<String, ConcurrentLinkedQueue<IdleChannel>>(); private final ConcurrentHashMap<Channel, IdleChannel> channel2IdleChannel = new ConcurrentHashMap<Channel, IdleChannel>(); private final AtomicBoolean isClosed = new AtomicBoolean(false); private final Timer idleConnectionDetector = new Timer(true); private final boolean sslConnectionPoolEnabled; private final int maxTotalConnections; private final int maxConnectionPerHost; private final long maxIdleTime; public NettyConnectionsPool(final NettyAsyncHttpProvider provider) { this.maxTotalConnections = provider.getConfig() .getMaxTotalConnections(); this.maxConnectionPerHost = provider.getConfig() .getMaxConnectionPerHost(); this.sslConnectionPoolEnabled = provider.getConfig() .isSslConnectionPoolEnabled(); this.maxIdleTime = provider.getConfig() .getIdleConnectionInPoolTimeoutInMs(); this.idleConnectionDetector.schedule(new IdleChannelDetector(), maxIdleTime, maxIdleTime); } private static class IdleChannel { final String uri; final Channel channel; final long start; IdleChannel(final String uri, final Channel channel) { this.uri = uri; this.channel = channel; this.start = System.currentTimeMillis(); } @Override public boolean equals(final Object o) { if (this == o) return true; if (!(o instanceof IdleChannel)) return false; final IdleChannel that = (IdleChannel) o; if (channel != null ? !channel.equals(that.channel) : that.channel != null) return false; return true; } @Override public int hashCode() { return channel != null ? channel.hashCode() : 0; } } private class IdleChannelDetector extends TimerTask { @Override public void run() { try { if (isClosed.get()) return; final List<IdleChannel> channelsInTimeout = new ArrayList<IdleChannel>(); final long currentTime = System.currentTimeMillis(); for (final IdleChannel idleChannel : channel2IdleChannel .values()) { final long age = currentTime - idleChannel.start; if (age > maxIdleTime) { // store in an unsynchronized list to minimize the // impact on the ConcurrentHashMap. channelsInTimeout.add(idleChannel); } } final long endConcurrentLoop = System.currentTimeMillis(); for (final IdleChannel idleChannel : channelsInTimeout) { final Object attachment = idleChannel.channel.getPipeline() .getContext(NettyAsyncHttpProvider.class) .getAttachment(); if (attachment != null) { if (NettyResponseFuture.class .isAssignableFrom(attachment.getClass())) { final NettyResponseFuture<?> future = (NettyResponseFuture<?>) attachment; if (!future.isDone() && !future.isCancelled()) { continue; } } } if (remove(idleChannel)) { close(idleChannel.channel); } } } catch (final Throwable t) { } } } /** * {@inheritDoc} */ @Override public boolean offer(final String uri, final Channel channel) { if (isClosed.get()) return false; if (!sslConnectionPoolEnabled && uri.startsWith("https")) { return false; } channel.getPipeline().getContext(NettyAsyncHttpProvider.class) .setAttachment(new NettyAsyncHttpProvider.DiscardEvent()); ConcurrentLinkedQueue<IdleChannel> idleConnectionForHost = connectionsPool .get(uri); if (idleConnectionForHost == null) { final ConcurrentLinkedQueue<IdleChannel> newPool = new ConcurrentLinkedQueue<IdleChannel>(); idleConnectionForHost = connectionsPool.putIfAbsent(uri, newPool); if (idleConnectionForHost == null) idleConnectionForHost = newPool; } boolean added; final int size = idleConnectionForHost.size(); if (maxConnectionPerHost == -1 || size < maxConnectionPerHost) { final IdleChannel idleChannel = new IdleChannel(uri, channel); synchronized (idleConnectionForHost) { added = idleConnectionForHost.add(idleChannel); if (channel2IdleChannel.put(channel, idleChannel) != null) { } } } else { added = false; } return added; } /** * {@inheritDoc} */ @Override public Channel poll(final String uri) { if (!sslConnectionPoolEnabled && uri.startsWith("https")) { return null; } IdleChannel idleChannel = null; final ConcurrentLinkedQueue<IdleChannel> idleConnectionForHost = connectionsPool .get(uri); if (idleConnectionForHost != null) { boolean poolEmpty = false; while (!poolEmpty && idleChannel == null) { if (idleConnectionForHost.size() > 0) { synchronized (idleConnectionForHost) { idleChannel = idleConnectionForHost.poll(); if (idleChannel != null) { channel2IdleChannel.remove(idleChannel.channel); } } } if (idleChannel == null) { poolEmpty = true; } else if (!idleChannel.channel.isConnected() || !idleChannel.channel.isOpen()) { idleChannel = null; } } } return idleChannel != null ? idleChannel.channel : null; } private boolean remove(final IdleChannel pooledChannel) { if (pooledChannel == null || isClosed.get()) return false; boolean isRemoved = false; final ConcurrentLinkedQueue<IdleChannel> pooledConnectionForHost = connectionsPool .get(pooledChannel.uri); if (pooledConnectionForHost != null) { isRemoved = pooledConnectionForHost.remove(pooledChannel); } isRemoved |= channel2IdleChannel.remove(pooledChannel.channel) != null; return isRemoved; } /** * {@inheritDoc} */ @Override public boolean removeAll(final Channel channel) { return !isClosed.get() && remove(channel2IdleChannel.get(channel)); } /** * {@inheritDoc} */ @Override public boolean canCacheConnection() { if (!isClosed.get() && maxTotalConnections != -1 && channel2IdleChannel.size() >= maxTotalConnections) { return false; } else { return true; } } /** * {@inheritDoc} */ @Override public void destroy() { if (isClosed.getAndSet(true)) return; // stop timer idleConnectionDetector.cancel(); idleConnectionDetector.purge(); for (final Channel channel : channel2IdleChannel.keySet()) { close(channel); } connectionsPool.clear(); channel2IdleChannel.clear(); } private void close(final Channel channel) { try { channel.getPipeline().getContext(NettyAsyncHttpProvider.class) .setAttachment(new NettyAsyncHttpProvider.DiscardEvent()); channel.close(); } catch (final Throwable t) { // noop } } @Override public final String toString() { return String.format("NettyConnectionPool: {pool-size: %d}", channel2IdleChannel.size()); } }