// ================================================================================================= // Copyright 2011 Twitter, Inc. // ------------------------------------------------------------------------------------------------- // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this work except in compliance with the License. // You may obtain a copy of the License in the LICENSE file, or 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 com.twitter.common.net.pool; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Preconditions; import com.google.common.collect.Iterables; import com.google.common.collect.Maps; import com.google.common.collect.Sets; import com.google.common.util.concurrent.ThreadFactoryBuilder; import com.twitter.common.base.Supplier; import com.twitter.common.quantity.Amount; import com.twitter.common.quantity.Time; import com.twitter.common.stats.Stats; import com.twitter.common.stats.StatsProvider; import java.util.Set; import java.util.concurrent.Executor; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.locks.Condition; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; import java.util.logging.Level; import java.util.logging.Logger; /** * A generic connection pool that delegates growth policy to a {@link ConnectionFactory} and * connection choice to a supplied strategy. * * <p>TODO(John Sirois): implement a reaper to clean up connections that may become invalid when not in * use. * * <p> TODO(John Sirois): take a ShutdownRegistry and register a close command * * @author John Sirois */ public final class ConnectionPool<S extends Connection<?, ?>> implements ObjectPool<S> { private static final Logger LOG = Logger.getLogger(ConnectionPool.class.getName()); private final Set<S> leasedConnections = Sets.newSetFromMap(Maps.<S, Boolean>newIdentityHashMap()); private final Set<S> availableConnections = Sets.newHashSet(); private final Lock poolLock; private final Condition available; private final ConnectionFactory<S> connectionFactory; private final Executor executor; private volatile boolean closed; private final AtomicLong connectionsCreated; private final AtomicLong connectionsDestroyed; private final AtomicLong connectionsReturned; /** * Creates a connection pool with a connection picker that selects the first item in the set of * available connections, exporting statistics to stats provider {@link Stats#STATS_PROVIDER}. * * @param connectionFactory Factory to create and destroy connections. */ public ConnectionPool(ConnectionFactory<S> connectionFactory) { this(connectionFactory, Stats.STATS_PROVIDER); } /** * Creates a connection pool with a connection picker that selects the first item in the set of * available connections and uses the supplied StatsProvider to register stats with. * * @param connectionFactory Factory to create and destroy connections. * @param statsProvider Stats export provider. */ public ConnectionPool(ConnectionFactory<S> connectionFactory, StatsProvider statsProvider) { this(Executors.newCachedThreadPool( new ThreadFactoryBuilder() .setNameFormat("CP-" + connectionFactory + "[%d]") .setDaemon(true) .build()), new ReentrantLock(true), connectionFactory, statsProvider); } @VisibleForTesting ConnectionPool(Executor executor, Lock poolLock, ConnectionFactory<S> connectionFactory, StatsProvider statsProvider) { Preconditions.checkNotNull(executor); Preconditions.checkNotNull(poolLock); Preconditions.checkNotNull(connectionFactory); Preconditions.checkNotNull(statsProvider); this.executor = executor; this.poolLock = poolLock; available = poolLock.newCondition(); this.connectionFactory = connectionFactory; String cfName = Stats.normalizeName(connectionFactory.toString()); statsProvider.makeGauge("cp_leased_connections_" + cfName, new Supplier<Integer>() { @Override public Integer get() { return leasedConnections.size(); } }); statsProvider.makeGauge("cp_available_connections_" + cfName, new Supplier<Integer>() { @Override public Integer get() { return availableConnections.size(); } }); this.connectionsCreated = statsProvider.makeCounter("cp_created_connections_" + cfName); this.connectionsDestroyed = statsProvider.makeCounter("cp_destroyed_connections_" + cfName); this.connectionsReturned = statsProvider.makeCounter("cp_returned_connections_" + cfName); } @Override public String toString() { return "CP-" + connectionFactory; } @Override public S get() throws ResourceExhaustedException, TimeoutException { checkNotClosed(); poolLock.lock(); try { return leaseConnection(NO_TIMEOUT); } finally { poolLock.unlock(); } } @Override public S get(Amount<Long, Time> timeout) throws ResourceExhaustedException, TimeoutException { checkNotClosed(); Preconditions.checkNotNull(timeout); if (timeout.getValue() == 0) { return get(); } try { long start = System.nanoTime(); long timeBudgetNs = timeout.as(Time.NANOSECONDS); if (poolLock.tryLock(timeBudgetNs, TimeUnit.NANOSECONDS)) { try { timeBudgetNs -= (System.nanoTime() - start); return leaseConnection(Amount.of(timeBudgetNs, Time.NANOSECONDS)); } finally { poolLock.unlock(); } } else { throw new TimeoutException("Timed out waiting for pool lock"); } } catch (InterruptedException e) { throw new TimeoutException("Interrupted waiting for pool lock"); } } private S leaseConnection(Amount<Long, Time> timeout) throws ResourceExhaustedException, TimeoutException { S connection = getConnection(timeout); if (connection == null) { throw new ResourceExhaustedException("Connection pool resources exhausted"); } return leaseConnection(connection); } @Override public void release(S connection) { release(connection, false); } /** * Equivalent to releasing a Connection with isValid() == false. * @see ObjectPool#remove(Object) */ @Override public void remove(S connection) { release(connection, true); } // TODO(John Sirois): release could block indefinitely if someone is blocked in get() on a create // connection - reason about this and potentially submit release to our executor private void release(S connection, boolean remove) { poolLock.lock(); try { if (!leasedConnections.remove(connection)) { throw new IllegalArgumentException("Connection not controlled by this connection pool: " + connection); } if (!closed && !remove && connection.isValid()) { addConnection(connection); connectionsReturned.incrementAndGet(); } else { connectionFactory.destroy(connection); connectionsDestroyed.incrementAndGet(); } } finally { poolLock.unlock(); } } @Override public void close() { poolLock.lock(); try { for (S availableConnection : availableConnections) { connectionFactory.destroy(availableConnection); } } finally { closed = true; poolLock.unlock(); } } private void checkNotClosed() { Preconditions.checkState(!closed); } private S leaseConnection(S connection) { leasedConnections.add(connection); return connection; } // TODO(John Sirois): pool growth is serialized by poolLock currently - it seems like this could be // fixed but there may be no need - do gedankanalysis private S getConnection(final Amount<Long, Time> timeout) throws ResourceExhaustedException, TimeoutException { if (availableConnections.isEmpty()) { if (leasedConnections.isEmpty()) { // Completely empty pool try { return createConnection(timeout); } catch (Exception e) { throw new ResourceExhaustedException("failed to create a new connection", e); } } else { // If the pool is allowed to grow - let the connection factory race a release if (connectionFactory.mightCreate()) { executor.execute(new Runnable() { @Override public void run() { try { // The connection timeout is not needed here to honor the callers get requested // timeout, but we don't want to have an infinite timeout which could exhaust a // thread pool over many backgrounded create calls S connection = createConnection(timeout); if (connection != null) { addConnection(connection); } else { LOG.log(Level.WARNING, "Failed to create a new connection for a waiting client " + "due to due to maximum pool size or timeout"); } } catch (Exception e) { LOG.log(Level.WARNING, "Failed to create a new connection for a waiting client", e); } } }); } try { // We wait for a returned/new connection here in loops to guard against the // "spurious wakeups" that are documented can occur with Condition.await() if (timeout.getValue() == 0) { while(availableConnections.isEmpty()) { available.await(); } } else { long timeRemainingNs = timeout.as(Time.NANOSECONDS); while(availableConnections.isEmpty()) { long start = System.nanoTime(); if (!available.await(timeRemainingNs, TimeUnit.NANOSECONDS)) { throw new TimeoutException( "timeout waiting for a connection to be released to the pool"); } else { timeRemainingNs -= (System.nanoTime() - start); } } if (availableConnections.isEmpty()) { throw new TimeoutException( "timeout waiting for a connection to be released to the pool"); } } } catch (InterruptedException e) { throw new TimeoutException("Interrupted while waiting for a connection."); } } } return getAvailableConnection(); } private S getAvailableConnection() { S connection = (availableConnections.size() == 1) ? Iterables.getOnlyElement(availableConnections) : availableConnections.iterator().next(); if (!availableConnections.remove(connection)) { throw new IllegalArgumentException("Connection picked not in pool: " + connection); } return connection; } private S createConnection(Amount<Long, Time> timeout) throws Exception { S connection = connectionFactory.create(timeout); if (connection != null) { connectionsCreated.incrementAndGet(); } return connection; } private void addConnection(S connection) { poolLock.lock(); try { availableConnections.add(connection); available.signal(); } finally { poolLock.unlock(); } } }