/* See LICENSE for licensing and NOTICE for copyright. */
package org.ldaptive.pool;
import java.time.Duration;
import java.util.NoSuchElementException;
import java.util.concurrent.TimeUnit;
import org.ldaptive.Connection;
import org.ldaptive.DefaultConnectionFactory;
/**
* Implements a pool of connections that has a set minimum and maximum size. The pool will not grow beyond the maximum
* size and when the pool is exhausted, requests for new connections will block. The length of time the pool will block
* is determined by {@link #getBlockWaitTime()}. By default the pool will block indefinitely and there is no guarantee
* that waiting threads will be serviced in the order in which they made their request. This implementation should be
* used when you need to control the <em>exact</em> number of connections that can be created. See {@link
* AbstractConnectionPool}.
*
* @author Middleware Services
*/
public class BlockingConnectionPool extends AbstractConnectionPool
{
/** Duration to wait for an available connection. */
private Duration blockWaitTime;
/** Creates a new blocking pool. */
public BlockingConnectionPool() {}
/**
* Creates a new blocking pool. The pool config is initialized with the default values.
*
* @param cf connection factory
*/
public BlockingConnectionPool(final DefaultConnectionFactory cf)
{
this(new PoolConfig(), cf);
}
/**
* Creates a new blocking pool.
*
* @param pc pool configuration
* @param cf connection factory
*/
public BlockingConnectionPool(final PoolConfig pc, final DefaultConnectionFactory cf)
{
setPoolConfig(pc);
setConnectionFactory(cf);
}
/**
* Returns the block wait time. Default time is null, which will wait indefinitely.
*
* @return time to wait for available connections
*/
public Duration getBlockWaitTime()
{
return blockWaitTime;
}
/**
* Sets the block wait time. Default time is null, which will wait indefinitely.
*
* @param time to wait for available connections
*/
public void setBlockWaitTime(final Duration time)
{
if (time != null && time.isNegative()) {
throw new IllegalArgumentException("Block wait time cannot be negative");
}
blockWaitTime = time;
}
@Override
public Connection getConnection()
throws PoolException
{
throwIfNotInitialized();
PooledConnectionProxy pc = null;
boolean create = false;
logger.trace("waiting on pool lock for check out {}", poolLock.getQueueLength());
poolLock.lock();
try {
// if an available connection exists, use it
// if no available connections and the pool can grow, attempt to create
// otherwise the pool is full, block until a connection is returned
if (!available.isEmpty()) {
try {
logger.trace("retrieve available connection from pool of size {}", available.size());
pc = retrieveAvailableConnection();
} catch (NoSuchElementException e) {
logger.error("could not remove connection from list", e);
throw new IllegalStateException("Pool is empty", e);
}
} else if (active.size() < getPoolConfig().getMaxPoolSize()) {
logger.trace("pool can grow, attempt to create active connection in pool of " +
"size {}", active.size());
create = true;
} else {
logger.trace("pool is full, block until connection is available");
pc = blockAvailableConnection();
}
} finally {
poolLock.unlock();
}
if (create) {
// previous block determined a creation should occur
// block here until create occurs without locking the whole pool
// if the pool is already maxed or creates are failing,
// block until a connection is available
checkOutLock.lock();
try {
boolean b = true;
poolLock.lock();
try {
logger.trace("create connection in pool of size {}", available.size() + active.size());
if (available.size() + active.size() == getPoolConfig().getMaxPoolSize()) {
logger.trace("pool at maximum size, create not allowed");
b = false;
}
} finally {
poolLock.unlock();
}
if (b) {
pc = createActiveConnection();
}
} finally {
checkOutLock.unlock();
}
if (pc == null) {
if (available.isEmpty() && active.isEmpty()) {
logger.error("Could not service check out request");
throw new PoolExhaustedException("Pool is empty and connection creation failed");
}
logger.debug("create failed, block until connection is available");
pc = blockAvailableConnection();
} else {
logger.trace("created new active connection: {}", pc);
}
}
if (pc != null) {
activateAndValidateConnection(pc);
} else {
logger.error("Could not service check out request");
throw new PoolExhaustedException("Pool is empty and connection creation failed");
}
return createConnectionProxy(pc);
}
/**
* Attempts to retrieve a connection from the available queue.
*
* @return connection from the pool
*
* @throws NoSuchElementException if the available queue is empty
*/
protected PooledConnectionProxy retrieveAvailableConnection()
{
PooledConnectionProxy pc = null;
logger.trace("waiting on pool lock for retrieve available {}", poolLock.getQueueLength());
poolLock.lock();
try {
pc = available.remove();
active.add(pc);
pc.getPooledConnectionStatistics().addActiveStat();
logger.trace("retrieved available connection: {}", pc);
} finally {
poolLock.unlock();
}
return pc;
}
/**
* This blocks until a connection can be acquired.
*
* @return connection from the pool
*
* @throws PoolException if this method fails
* @throws BlockingTimeoutException if this pool is configured with a block time and it occurs
* @throws PoolInterruptedException if the current thread is interrupted
*/
protected PooledConnectionProxy blockAvailableConnection()
throws PoolException
{
PooledConnectionProxy pc = null;
logger.trace("waiting on pool lock for block available {}", poolLock.getQueueLength());
poolLock.lock();
try {
while (pc == null) {
logger.trace("available pool is empty, waiting...");
if (blockWaitTime != null) {
if (!poolNotEmpty.await(blockWaitTime.toMillis(), TimeUnit.MILLISECONDS)) {
logger.debug("block time exceeded, throwing exception");
throw new BlockingTimeoutException("Block time exceeded");
}
} else {
poolNotEmpty.await();
}
logger.trace("notified to continue...");
try {
pc = retrieveAvailableConnection();
} catch (NoSuchElementException e) {
logger.trace("notified to continue but pool was empty");
}
}
} catch (InterruptedException e) {
logger.error("waiting for available connection interrupted", e);
throw new PoolInterruptedException("Interrupted while waiting for an available connection", e);
} finally {
poolLock.unlock();
}
return pc;
}
@Override
public void putConnection(final Connection c)
{
throwIfNotInitialized();
final PooledConnectionProxy pc = retrieveConnectionProxy(c);
final boolean valid = validateAndPassivateConnection(pc);
logger.trace("waiting on pool lock for check in {}", poolLock.getQueueLength());
poolLock.lock();
try {
if (active.remove(pc)) {
if (valid) {
available.add(pc);
pc.getPooledConnectionStatistics().addAvailableStat();
logger.trace("returned active connection: {}", pc);
poolNotEmpty.signal();
}
} else if (available.contains(pc)) {
logger.warn("returned available connection: {}", pc);
} else {
logger.warn("attempt to return unknown connection: {}", pc);
}
} finally {
poolLock.unlock();
}
}
}