/* See LICENSE for licensing and NOTICE for copyright. */ package org.ldaptive.pool; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.lang.reflect.Proxy; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import java.util.concurrent.locks.Condition; import java.util.concurrent.locks.ReentrantLock; import org.ldaptive.Connection; import org.ldaptive.DefaultConnectionFactory; import org.ldaptive.LdapException; import org.ldaptive.LdapUtils; import org.ldaptive.Response; /** * Contains the base implementation for pooling connections. The main design objective for the supplied pooling * implementations is to provide a pool that does not block on connection creation or destruction. This is what accounts * for the multiple locks available on this class. The pool is backed by two queues, one for available connections and * one for active connections. Connections that are available via {@link #getConnection()} exist in the available queue. * Connections that are actively in use exist in the active queue. This implementation uses FIFO operations for each * queue. * * @author Middleware Services */ public abstract class AbstractConnectionPool extends AbstractPool<Connection> implements ConnectionPool { /** Lock for the entire pool. */ protected final ReentrantLock poolLock = new ReentrantLock(); /** Condition for notifying threads that a connection was returned. */ protected final Condition poolNotEmpty = poolLock.newCondition(); /** Lock for check ins. */ protected final ReentrantLock checkInLock = new ReentrantLock(); /** Lock for check outs. */ protected final ReentrantLock checkOutLock = new ReentrantLock(); /** List of available connections in the pool. */ protected Queue<PooledConnectionProxy> available; /** List of connections in use. */ protected Queue<PooledConnectionProxy> active; /** Connection factory to create connections with. */ private DefaultConnectionFactory connectionFactory; /** Whether to connect to the ldap on connection creation. */ private boolean connectOnCreate = true; /** Type of queue. LIFO or FIFO. */ private QueueType queueType = QueueType.LIFO; /** Executor for scheduling pool tasks. */ private ScheduledExecutorService poolExecutor; /** Whether {@link #initialize()} has been invoked. */ private boolean initialized; /** * Whether {@link #initialize()} should throw if pooling configuration requirements are not met. */ private boolean failFastInitialize = true; /** * Returns the connection factory for this pool. * * @return connection factory */ public DefaultConnectionFactory getConnectionFactory() { return connectionFactory; } /** * Sets the connection factory for this pool. * * @param cf connection factory */ public void setConnectionFactory(final DefaultConnectionFactory cf) { logger.trace("setting connectionFactory: {}", cf); connectionFactory = cf; } /** * Returns whether connections will attempt to connect after creation. Default is true. * * @return whether connections will attempt to connect after creation */ public boolean getConnectOnCreate() { return connectOnCreate; } /** * Sets whether newly created connections will attempt to connect. Default is true. * * @param b connect on create */ public void setConnectOnCreate(final boolean b) { logger.trace("setting connectOnCreate: {}", b); connectOnCreate = b; } /** * Returns the type of queue used for this connection pool. * * @return queue type */ public QueueType getQueueType() { return queueType; } /** * Sets the type of queue used for this connection pool. This property may have an impact on the success of the prune * strategy. * * @param type of queue */ public void setQueueType(final QueueType type) { logger.trace("setting queueType: {}", type); queueType = type; } /** * Returns whether {@link #initialize()} should throw if pooling configuration requirements are not met. * * @return whether {@link #initialize()} should throw */ public boolean getFailFastInitialize() { return failFastInitialize; } /** * Sets whether {@link #initialize()} should throw if pooling configuration requirements are not met. * * @param b whether {@link #initialize()} should throw */ public void setFailFastInitialize(final boolean b) { logger.trace("setting failFastInitialize: {}", b); failFastInitialize = b; } /** * Returns whether this pool has been initialized. * * @return whether this pool has been initialized */ public boolean isInitialized() { return initialized; } /** * Used to determine whether {@link #initialize()} has been invoked for this pool. * * @throws IllegalStateException if this pool has not been initialized */ protected void throwIfNotInitialized() { if (!initialized) { throw new IllegalStateException("Pool has not been initialized"); } } /** * Initialize this pool for use. Once invoked the pool config is made immutable. See {@link * PoolConfig#makeImmutable()}. * * @throws IllegalStateException if this pool has already been initialized, the pooling configuration is * inconsistent or the pool does not contain at least one connection and it's minimum * size is greater than zero */ @Override public void initialize() { if (initialized) { throw new IllegalStateException("Pool has already been initialized"); } logger.debug("beginning pool initialization for {}", this); // sanity check the configuration if ((getPoolConfig().isValidatePeriodically() || getPoolConfig().isValidateOnCheckIn() || getPoolConfig().isValidateOnCheckOut()) && getValidator() == null) { throw new IllegalStateException("Validation is enabled, but no validator has been configured"); } if ((!getPoolConfig().isValidatePeriodically() && !getPoolConfig().isValidateOnCheckIn() && !getPoolConfig().isValidateOnCheckOut()) && getValidator() != null) { throw new IllegalStateException("Validator configured, but no validate flag has been set"); } getPoolConfig().makeImmutable(); if (getPruneStrategy() == null) { setPruneStrategy(new IdlePruneStrategy()); logger.debug("no prune strategy configured, using default prune strategy: {}", getPruneStrategy()); } // sanity check the scheduler periods if (getPruneStrategy().getPrunePeriod().toMillis() <= 0) { throw new IllegalStateException( "Prune period " + getPruneStrategy().getPrunePeriod() + " must be greater than zero"); } if (getPoolConfig().getValidatePeriod().toMillis() <= 0) { throw new IllegalStateException( "Validate period " + getPoolConfig().getValidatePeriod() + " must be greater than zero"); } if (getPoolConfig().getValidateTimeout() != null && getPoolConfig().getValidateTimeout().toMillis() <= 0) { throw new IllegalStateException( "Validate timeout " + getPoolConfig().getValidateTimeout() + " must be greater than zero"); } available = new Queue<>(queueType); active = new Queue<>(queueType); IllegalStateException growException = null; try { grow(getPoolConfig().getMinPoolSize(), true); } catch (IllegalStateException e) { growException = e; } if (available.isEmpty() && getPoolConfig().getMinPoolSize() > 0) { if (failFastInitialize) { throw new IllegalStateException( "Could not initialize pool size", growException != null ? growException.getCause() : null); } else { logger.warn("Could not initialize pool size, pool is empty"); } } logger.debug("initialized available queue: {}", available); poolExecutor = Executors.newSingleThreadScheduledExecutor( r -> { final Thread t = new Thread(r); t.setDaemon(true); return t; }); poolExecutor.scheduleAtFixedRate( () -> { logger.debug("begin prune task for {}", AbstractConnectionPool.this); try { prune(); } catch (Exception e) { logger.error("prune task failed for {}", AbstractConnectionPool.this); } logger.debug("end prune task for {}", AbstractConnectionPool.this); }, getPruneStrategy().getPrunePeriod().toMillis(), getPruneStrategy().getPrunePeriod().toMillis(), TimeUnit.MILLISECONDS); logger.debug("prune pool task scheduled for {}", this); poolExecutor.scheduleAtFixedRate( () -> { logger.debug("begin validate task for {}", AbstractConnectionPool.this); try { validate(); } catch (Exception e) { logger.error("validation task failed for {}", AbstractConnectionPool.this); } logger.debug("end validate task for {}", AbstractConnectionPool.this); }, getPoolConfig().getValidatePeriod().toMillis(), getPoolConfig().getValidatePeriod().toMillis(), TimeUnit.MILLISECONDS); logger.debug("validate pool task scheduled for {}", this); initialized = true; logger.info("pool initialized {}", this); } /** * Attempts to grow the pool to the supplied size. If the pool size is greater than or equal to the supplied size, * this method is a no-op. * * @param size to grow the pool to */ protected void grow(final int size) { grow(size, false); } /** * Attempts to grow the pool to the supplied size. If the pool size is greater than or equal to the supplied size, * this method is a no-op. * * @param size to grow the pool to * @param throwOnFailure whether to throw illegal state exception * * @throws IllegalStateException if the pool cannot grow to the supplied size and {@link * #createAvailableConnection(boolean)} throws */ protected void grow(final int size, final boolean throwOnFailure) { logger.trace("waiting for pool lock to initialize pool {}", poolLock.getQueueLength()); int count = 0; poolLock.lock(); try { IllegalStateException lastThrown = null; int currentPoolSize = active.size() + available.size(); logger.debug("checking connection pool size >= {} for {}", size, this); while (currentPoolSize < size && count < size * 2) { try { final PooledConnectionProxy pc = createAvailableConnection(throwOnFailure); if (pc != null && getPoolConfig().isValidateOnCheckIn()) { if (validate(pc.getConnection())) { logger.trace("connection passed initialize validation: {}", pc); } else { logger.warn("connection failed initialize validation: {}", pc); removeAvailableConnection(pc); } } } catch (IllegalStateException e) { lastThrown = e; } currentPoolSize = active.size() + available.size(); count++; } if (lastThrown != null && currentPoolSize < size) { throw lastThrown; } } finally { poolLock.unlock(); } } /** * Empty this pool, freeing any resources. * * @throws IllegalStateException if this pool has not been initialized */ @Override public void close() { throwIfNotInitialized(); logger.debug("closing connection pool of size {} for {}", available.size() + active.size(), this); poolLock.lock(); try { while (!available.isEmpty()) { final PooledConnectionProxy pc = available.remove(); pc.getConnection().close(); logger.trace("destroyed connection: {}", pc); } while (!active.isEmpty()) { final PooledConnectionProxy pc = active.remove(); pc.getConnection().close(); logger.trace("destroyed connection: {}", pc); } logger.debug("pool closed"); } finally { poolLock.unlock(); } logger.debug("shutting down executor"); poolExecutor.shutdown(); logger.debug("executor shutdown"); logger.info("pool closed {}", this); initialized = false; } /** * Returns a connection from the pool. * * @return connection * * @throws PoolException if this operation fails * @throws BlockingTimeoutException if this pool is configured with a block time and it occurs * @throws PoolInterruptedException if this pool is configured with a block time and the current thread is * interrupted * @throws IllegalStateException if this pool has not been initialized */ @Override public abstract Connection getConnection() throws PoolException; /** * Returns a connection to the pool. * * @param c connection * * @throws IllegalStateException if this pool has not been initialized */ public abstract void putConnection(final Connection c); /** * Create a new connection. If {@link #connectOnCreate} is true, the connection will be opened. * * @return pooled connection */ protected PooledConnectionProxy createConnection() { return createConnection(false); } /** * Create a new connection. If {@link #connectOnCreate} is true, the connection will be opened. * * @param throwOnFailure whether to throw illegal state exception * * @return pooled connection * * @throws IllegalStateException if {@link #connectOnCreate} is true and the connection cannot be opened */ protected PooledConnectionProxy createConnection(final boolean throwOnFailure) { Connection c = connectionFactory.getConnection(); Response<Void> r = null; if (connectOnCreate) { try { r = c.open(); } catch (LdapException e) { logger.error("{} unable to connect to the ldap", this, e); c = null; if (throwOnFailure) { throw new IllegalStateException("unable to connect to the ldap", e); } } } if (c != null) { return new DefaultPooledConnectionProxy(c, r); } else { return null; } } /** * Create a new connection and place it in the available pool. * * @return connection that was placed in the available pool */ protected PooledConnectionProxy createAvailableConnection() { return createAvailableConnection(false); } /** * Create a new connection and place it in the available pool. * * @param throwOnFailure whether to throw illegal state exception * * @return connection that was placed in the available pool * * @throws IllegalStateException if {@link #createConnection(boolean)} throws */ protected PooledConnectionProxy createAvailableConnection(final boolean throwOnFailure) { final PooledConnectionProxy pc = createConnection(throwOnFailure); if (pc != null) { poolLock.lock(); try { available.add(pc); pc.getPooledConnectionStatistics().addAvailableStat(); logger.info("added available connection: {}", pc); } finally { poolLock.unlock(); } } else { logger.warn("unable to create available connection"); } return pc; } /** * Create a new connection and place it in the active pool. * * @return connection that was placed in the active pool */ protected PooledConnectionProxy createActiveConnection() { return createActiveConnection(false); } /** * Create a new connection and place it in the active pool. * * @param throwOnFailure whether to throw illegal state exception * * @return connection that was placed in the active pool * * @throws IllegalStateException if {@link #createConnection(boolean)} throws */ protected PooledConnectionProxy createActiveConnection(final boolean throwOnFailure) { final PooledConnectionProxy pc = createConnection(throwOnFailure); if (pc != null) { poolLock.lock(); try { active.add(pc); pc.getPooledConnectionStatistics().addActiveStat(); logger.info("added active connection: {}", pc); } finally { poolLock.unlock(); } } else { logger.warn("unable to create active connection"); } return pc; } /** * Remove a connection from the available pool. * * @param pc connection that is in the available pool */ protected void removeAvailableConnection(final PooledConnectionProxy pc) { boolean destroy = false; poolLock.lock(); try { if (available.remove(pc)) { destroy = true; } else { logger.warn("attempt to remove unknown available connection: {}", pc); } } finally { poolLock.unlock(); } if (destroy) { pc.getConnection().close(); logger.info("destroyed connection: {}", pc); } } /** * Remove a connection from the active pool. * * @param pc connection that is in the active pool */ protected void removeActiveConnection(final PooledConnectionProxy pc) { boolean destroy = false; poolLock.lock(); try { if (active.remove(pc)) { destroy = true; } else { logger.warn("attempt to remove unknown active connection: {}", pc); } } finally { poolLock.unlock(); } if (destroy) { pc.getConnection().close(); logger.info("destroyed connection: {}", pc); } } /** * Remove a connection from both the available and active pools. * * @param pc connection that is in both the available and active pools */ protected void removeAvailableAndActiveConnection(final PooledConnectionProxy pc) { boolean destroy = false; poolLock.lock(); try { if (available.remove(pc)) { destroy = true; } else { logger.debug("attempt to remove unknown available connection: {}", pc); } if (active.remove(pc)) { destroy = true; } else { logger.debug("attempt to remove unknown active connection: {}", pc); } } finally { poolLock.unlock(); } if (destroy) { pc.getConnection().close(); logger.info("destroyed connection: {}", pc); } } /** * Attempts to activate and validate a connection. Performed before a connection is returned from {@link * #getConnection()}. * * @param pc connection * * @throws PoolException if this method fails * @throws ActivationException if the connection cannot be activated * @throws ValidationException if the connection cannot be validated */ protected void activateAndValidateConnection(final PooledConnectionProxy pc) throws PoolException { if (!activate(pc.getConnection())) { logger.warn("connection failed activation: {}", pc); removeAvailableAndActiveConnection(pc); throw new ActivationException("Activation of connection failed"); } if (getPoolConfig().isValidateOnCheckOut() && !validate(pc.getConnection())) { logger.warn("connection failed check out validation: {}", pc); removeAvailableAndActiveConnection(pc); throw new ValidationException("Validation of connection failed"); } } /** * Attempts to validate and passivate a connection. Performed when a connection is given to {@link * #putConnection(Connection)}. * * @param pc connection * * @return whether both validate and passivation succeeded */ protected boolean validateAndPassivateConnection(final PooledConnectionProxy pc) { if (!pc.getConnection().isOpen()) { logger.debug("connection not open: {}", pc); return false; } boolean valid = false; if (getPoolConfig().isValidateOnCheckIn()) { if (!validate(pc.getConnection())) { logger.warn("connection failed check in validation: {}", pc); } else { valid = true; } } else { valid = true; } if (valid && !passivate(pc.getConnection())) { valid = false; logger.warn("connection failed passivation: {}", pc); } return valid; } /** * Attempts to reduce the size of the pool back to it's configured minimum. {@link PoolConfig#setMinPoolSize(int)}. * * @throws IllegalStateException if this pool has not been initialized */ public void prune() { throwIfNotInitialized(); logger.trace("waiting for pool lock to prune {}", poolLock.getQueueLength()); poolLock.lock(); try { if (!available.isEmpty()) { final int minPoolSize = getPoolConfig().getMinPoolSize(); int currentPoolSize = active.size() + available.size(); if (currentPoolSize > minPoolSize) { logger.debug("pruning available pool of size {} for {}", available.size(), this); final int numConnToPrune = available.size(); final Iterator<PooledConnectionProxy> connIter = available.iterator(); for (int i = 0; i < numConnToPrune && currentPoolSize > minPoolSize; i++) { final PooledConnectionProxy pc = connIter.next(); if (getPruneStrategy().prune(pc)) { connIter.remove(); pc.getConnection().close(); logger.trace("destroyed connection: {}", pc); currentPoolSize--; } } if (numConnToPrune == available.size()) { logger.debug("prune strategy did not remove any connections"); } else { logger.info("available pool size pruned to {}", available.size()); } } else { logger.debug("pool size is {}, no connections pruned for {}", currentPoolSize, this); } } else { logger.debug("no available connections, no connections pruned for {}", this); } } finally { poolLock.unlock(); } } /** * Attempts to validate all objects in the pool. {@link PoolConfig#setValidatePeriodically(boolean)}. * * @throws IllegalStateException if this pool has not been initialized */ public void validate() { throwIfNotInitialized(); poolLock.lock(); try { if (!available.isEmpty()) { if (getPoolConfig().isValidatePeriodically()) { logger.debug("validate available pool of size {} for {}", available.size(), this); final List<PooledConnectionProxy> remove = new ArrayList<>(); if (getPoolConfig().getValidateTimeout() == null) { for (PooledConnectionProxy pc : available) { logger.trace("validating {}", pc); if (validate(pc.getConnection())) { logger.trace("{} passed validation", pc); } else { logger.warn("{} failed validation", pc); remove.add(pc); } } } else { final ExecutorService es = Executors.newCachedThreadPool(); try { final Map<PooledConnectionProxy, Future<Boolean>> results = new HashMap<>(available.size()); for (PooledConnectionProxy pc : available) { logger.trace("validating {}", pc); results.put(pc, es.submit(() -> validate(pc.getConnection()))); } for (Map.Entry<PooledConnectionProxy, Future<Boolean>> entry : results.entrySet()) { final Future<Boolean> future = entry.getValue(); boolean validateResult = false; try { validateResult = future.get(getPoolConfig().getValidateTimeout().toMillis(), TimeUnit.MILLISECONDS); } catch (InterruptedException e) { logger.debug("validating {} interrupted", entry.getKey(), e); future.cancel(true); } catch (ExecutionException e) { logger.debug("validating {} threw unexpected exception", entry.getKey(), e); future.cancel(true); } catch (TimeoutException e) { logger.debug("validating {} timed out", entry.getKey(), e); future.cancel(true); } if (validateResult) { logger.trace("{} passed validation", entry.getKey()); } else { logger.warn("{} failed validation", entry.getKey()); remove.add(entry.getKey()); } } } finally { es.shutdownNow(); } } for (PooledConnectionProxy pc : remove) { logger.trace("removing {} from the pool", pc); available.remove(pc); pc.getConnection().close(); logger.trace("destroyed connection: {}", pc); } } } else { logger.debug("no available connections, no validation performed for {}", this); } grow(getPoolConfig().getMinPoolSize()); logger.debug("pool size after validation is {}", available.size() + active.size()); } finally { poolLock.unlock(); } } @Override public int availableCount() { if (available == null) { return 0; } return available.size(); } @Override public int activeCount() { if (active == null) { return 0; } return active.size(); } @Override public Set<PooledConnectionStatistics> getPooledConnectionStatistics() { throwIfNotInitialized(); final Set<PooledConnectionStatistics> stats = Collections.unmodifiableSet( new HashSet<>()); poolLock.lock(); try { for (PooledConnectionProxy cp : available) { stats.add(cp.getPooledConnectionStatistics()); } for (PooledConnectionProxy cp : active) { stats.add(cp.getPooledConnectionStatistics()); } } finally { poolLock.unlock(); } return stats; } /** * Creates a connection proxy using the supplied pool connection. * * @param pc pool connection to create proxy with * * @return connection proxy */ protected Connection createConnectionProxy(final PooledConnectionProxy pc) { return (Connection) Proxy.newProxyInstance(Connection.class.getClassLoader(), new Class[] {Connection.class}, pc); } /** * Retrieves the invocation handler from the supplied connection proxy. * * @param proxy connection proxy * * @return pooled connection proxy */ protected PooledConnectionProxy retrieveConnectionProxy(final Connection proxy) { return (PooledConnectionProxy) Proxy.getInvocationHandler(proxy); } /** * Called by the garbage collector on an object when garbage collection determines that there are no more references * to the object. * * @throws Throwable if an exception is thrown by this method */ @Override protected void finalize() throws Throwable { try { close(); } finally { super.finalize(); } } @Override public String toString() { return String.format( "[%s@%d::name=%s, poolConfig=%s, activator=%s, passivator=%s, " + "validator=%s pruneStrategy=%s, connectOnCreate=%s, " + "connectionFactory=%s, initialized=%s, availableCount=%s, " + "activeCount=%s]", getClass().getName(), hashCode(), getName(), getPoolConfig(), getActivator(), getPassivator(), getValidator(), getPruneStrategy(), connectOnCreate, connectionFactory, initialized, availableCount(), activeCount()); } /** * Contains a connection that is participating in this pool. Used to track how long a connection has been in use and * override certain method invocations. */ protected class DefaultPooledConnectionProxy implements PooledConnectionProxy { /** hash code seed. */ private static final int HASH_CODE_SEED = 503; /** Underlying connection. */ private final Connection conn; /** Response produced when the connection was opened. */ private Response<Void> openResponse; /** Time this connection was created. */ private final long createdTime = System.currentTimeMillis(); /** Statistics for this connection. */ private final PooledConnectionStatistics statistics = new PooledConnectionStatistics( getPruneStrategy().getStatisticsSize()); /** * Creates a new pooled connection. * * @param c connection to participate in this pool * @param r response produced by opening the connection */ public DefaultPooledConnectionProxy(final Connection c, final Response<Void> r) { conn = c; openResponse = r; } @Override public ConnectionPool getConnectionPool() { return AbstractConnectionPool.this; } @Override public Connection getConnection() { return conn; } @Override public long getCreatedTime() { return createdTime; } @Override public PooledConnectionStatistics getPooledConnectionStatistics() { return statistics; } @Override public boolean equals(final Object o) { if (o == this) { return true; } if (o instanceof DefaultPooledConnectionProxy) { final DefaultPooledConnectionProxy v = (DefaultPooledConnectionProxy) o; return LdapUtils.areEqual(conn, v.conn); } return false; } @Override public int hashCode() { return LdapUtils.computeHashCode(HASH_CODE_SEED, conn); } @Override @SuppressWarnings("unchecked") public Object invoke(final Object proxy, final Method method, final Object[] args) throws Throwable { Object retValue = null; if ("open".equals(method.getName())) { // if the connection has been closed, invoke open if (!conn.isOpen()) { try { openResponse = (Response<Void>) method.invoke(conn, args); } catch (InvocationTargetException e) { throw e.getTargetException(); } } retValue = openResponse; } else if ("reopen".equals(method.getName())) { try { openResponse = (Response<Void>) method.invoke(conn, args); } catch (InvocationTargetException e) { throw e.getTargetException(); } retValue = openResponse; } else if ("close".equals(method.getName())) { putConnection((Connection) proxy); } else { try { retValue = method.invoke(conn, args); } catch (InvocationTargetException e) { throw e.getTargetException(); } } return retValue; } } }