package com.hwlcn.ldap.ldap.sdk; import java.util.ArrayList; import java.util.Collections; import java.util.EnumSet; import java.util.Iterator; import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.atomic.AtomicReference; import com.hwlcn.ldap.ldap.sdk.schema.Schema; import com.hwlcn.ldap.util.ObjectPair; import com.hwlcn.core.annotation.ThreadSafety; import com.hwlcn.ldap.util.ThreadSafetyLevel; import static com.hwlcn.ldap.ldap.sdk.LDAPMessages.*; import static com.hwlcn.ldap.util.Debug.*; import static com.hwlcn.ldap.util.StaticUtils.*; import static com.hwlcn.ldap.util.Validator.*; @ThreadSafety(level=ThreadSafetyLevel.COMPLETELY_THREADSAFE) public final class LDAPThreadLocalConnectionPool extends AbstractConnectionPool { private static final long DEFAULT_HEALTH_CHECK_INTERVAL = 60000L; private final AtomicReference<Set<OperationType>> retryOperationTypes; private volatile boolean closed; private final BindRequest bindRequest; private final ConcurrentHashMap<Thread,LDAPConnection> connections; private LDAPConnectionPoolHealthCheck healthCheck; private final LDAPConnectionPoolHealthCheckThread healthCheckThread; private final LDAPConnectionPoolStatistics poolStatistics; private volatile long healthCheckInterval; private volatile long lastExpiredDisconnectTime; private volatile long maxConnectionAge; private volatile long minDisconnectInterval; private volatile ObjectPair<Long,Schema> pooledSchema; private final PostConnectProcessor postConnectProcessor; private final ServerSet serverSet; private String connectionPoolName; public LDAPThreadLocalConnectionPool(final LDAPConnection connection) throws LDAPException { this(connection, null); } public LDAPThreadLocalConnectionPool(final LDAPConnection connection, final PostConnectProcessor postConnectProcessor) throws LDAPException { ensureNotNull(connection); this.postConnectProcessor = postConnectProcessor; healthCheck = new LDAPConnectionPoolHealthCheck(); healthCheckInterval = DEFAULT_HEALTH_CHECK_INTERVAL; poolStatistics = new LDAPConnectionPoolStatistics(this); connectionPoolName = null; retryOperationTypes = new AtomicReference<Set<OperationType>>( Collections.unmodifiableSet(EnumSet.noneOf(OperationType.class))); if (! connection.isConnected()) { throw new LDAPException(ResultCode.PARAM_ERROR, ERR_POOL_CONN_NOT_ESTABLISHED.get()); } serverSet = new SingleServerSet(connection.getConnectedAddress(), connection.getConnectedPort(), connection.getLastUsedSocketFactory(), connection.getConnectionOptions()); bindRequest = connection.getLastBindRequest(); connections = new ConcurrentHashMap<Thread,LDAPConnection>(); connections.put(Thread.currentThread(), connection); lastExpiredDisconnectTime = 0L; maxConnectionAge = 0L; closed = false; minDisconnectInterval = 0L; healthCheckThread = new LDAPConnectionPoolHealthCheckThread(this); healthCheckThread.start(); final LDAPConnectionOptions opts = connection.getConnectionOptions(); if (opts.usePooledSchema()) { try { final Schema schema = connection.getSchema(); if (schema != null) { connection.setCachedSchema(schema); final long currentTime = System.currentTimeMillis(); final long timeout = opts.getPooledSchemaTimeoutMillis(); if ((timeout <= 0L) || (timeout+currentTime <= 0L)) { pooledSchema = new ObjectPair<Long,Schema>(Long.MAX_VALUE, schema); } else { pooledSchema = new ObjectPair<Long,Schema>(timeout+currentTime, schema); } } } catch (final Exception e) { debugException(e); } } } public LDAPThreadLocalConnectionPool(final ServerSet serverSet, final BindRequest bindRequest) { this(serverSet, bindRequest, null); } public LDAPThreadLocalConnectionPool(final ServerSet serverSet, final BindRequest bindRequest, final PostConnectProcessor postConnectProcessor) { ensureNotNull(serverSet); this.serverSet = serverSet; this.bindRequest = bindRequest; this.postConnectProcessor = postConnectProcessor; healthCheck = new LDAPConnectionPoolHealthCheck(); healthCheckInterval = DEFAULT_HEALTH_CHECK_INTERVAL; poolStatistics = new LDAPConnectionPoolStatistics(this); connectionPoolName = null; retryOperationTypes = new AtomicReference<Set<OperationType>>( Collections.unmodifiableSet(EnumSet.noneOf(OperationType.class))); connections = new ConcurrentHashMap<Thread,LDAPConnection>(); lastExpiredDisconnectTime = 0L; maxConnectionAge = 0L; minDisconnectInterval = 0L; closed = false; healthCheckThread = new LDAPConnectionPoolHealthCheckThread(this); healthCheckThread.start(); } private LDAPConnection createConnection() throws LDAPException { final LDAPConnection c = serverSet.getConnection(healthCheck); c.setConnectionPool(this); LDAPConnectionOptions opts = c.getConnectionOptions(); if (opts.autoReconnect()) { opts = opts.duplicate(); opts.setAutoReconnect(false); c.setConnectionOptions(opts); } if (postConnectProcessor != null) { try { postConnectProcessor.processPreAuthenticatedConnection(c); } catch (Exception e) { debugException(e); try { poolStatistics.incrementNumFailedConnectionAttempts(); c.setDisconnectInfo(DisconnectType.POOL_CREATION_FAILURE, null, e); c.terminate(null); } catch (Exception e2) { debugException(e2); } if (e instanceof LDAPException) { throw ((LDAPException) e); } else { throw new LDAPException(ResultCode.CONNECT_ERROR, ERR_POOL_POST_CONNECT_ERROR.get(getExceptionMessage(e)), e); } } } try { if (bindRequest != null) { c.bind(bindRequest.duplicate()); } } catch (Exception e) { debugException(e); try { poolStatistics.incrementNumFailedConnectionAttempts(); c.setDisconnectInfo(DisconnectType.BIND_FAILED, null, e); c.terminate(null); } catch (Exception e2) { debugException(e2); } if (e instanceof LDAPException) { throw ((LDAPException) e); } else { throw new LDAPException(ResultCode.CONNECT_ERROR, ERR_POOL_CONNECT_ERROR.get(getExceptionMessage(e)), e); } } if (postConnectProcessor != null) { try { postConnectProcessor.processPostAuthenticatedConnection(c); } catch (Exception e) { debugException(e); try { poolStatistics.incrementNumFailedConnectionAttempts(); c.setDisconnectInfo(DisconnectType.POOL_CREATION_FAILURE, null, e); c.terminate(null); } catch (Exception e2) { debugException(e2); } if (e instanceof LDAPException) { throw ((LDAPException) e); } else { throw new LDAPException(ResultCode.CONNECT_ERROR, ERR_POOL_POST_CONNECT_ERROR.get(getExceptionMessage(e)), e); } } } if (opts.usePooledSchema()) { final long currentTime = System.currentTimeMillis(); if ((pooledSchema == null) || (currentTime > pooledSchema.getFirst())) { try { final Schema schema = c.getSchema(); if (schema != null) { c.setCachedSchema(schema); final long timeout = opts.getPooledSchemaTimeoutMillis(); if ((timeout <= 0L) || (currentTime + timeout <= 0L)) { pooledSchema = new ObjectPair<Long,Schema>(Long.MAX_VALUE, schema); } else { pooledSchema = new ObjectPair<Long,Schema>((currentTime+timeout), schema); } } } catch (final Exception e) { debugException(e); // There was a problem retrieving the schema from the server, but if // we have an earlier copy then we can assume it's still valid. if (pooledSchema != null) { c.setCachedSchema(pooledSchema.getSecond()); } } } else { c.setCachedSchema(pooledSchema.getSecond()); } } c.setConnectionPoolName(connectionPoolName); poolStatistics.incrementNumSuccessfulConnectionAttempts(); return c; } @Override() public void close() { close(true, 1); } @Override() public void close(final boolean unbind, final int numThreads) { closed = true; healthCheckThread.stopRunning(); if (numThreads > 1) { final ArrayList<LDAPConnection> connList = new ArrayList<LDAPConnection>(connections.size()); final Iterator<LDAPConnection> iterator = connections.values().iterator(); while (iterator.hasNext()) { connList.add(iterator.next()); iterator.remove(); } final ParallelPoolCloser closer = new ParallelPoolCloser(connList, unbind, numThreads); closer.closeConnections(); } else { final Iterator<Map.Entry<Thread,LDAPConnection>> iterator = connections.entrySet().iterator(); while (iterator.hasNext()) { final LDAPConnection conn = iterator.next().getValue(); iterator.remove(); poolStatistics.incrementNumConnectionsClosedUnneeded(); conn.setDisconnectInfo(DisconnectType.POOL_CLOSED, null, null); if (unbind) { conn.terminate(null); } else { conn.setClosed(); } } } } @Override() public boolean isClosed() { return closed; } public BindResult bindAndRevertAuthentication(final String bindDN, final String password, final Control... controls) throws LDAPException { return bindAndRevertAuthentication( new SimpleBindRequest(bindDN, password, controls)); } public BindResult bindAndRevertAuthentication(final BindRequest bindRequest) throws LDAPException { LDAPConnection conn = getConnection(); try { final BindResult result = conn.bind(bindRequest); releaseAndReAuthenticateConnection(conn); return result; } catch (final Throwable t) { debugException(t); if (t instanceof LDAPException) { final LDAPException le = (LDAPException) t; boolean shouldThrow; try { healthCheck.ensureConnectionValidAfterException(conn, le); releaseAndReAuthenticateConnection(conn); shouldThrow = true; } catch (final Exception e) { debugException(e); if (! getOperationTypesToRetryDueToInvalidConnections().contains( OperationType.BIND)) { releaseDefunctConnection(conn); shouldThrow = true; } else { shouldThrow = false; } } if (shouldThrow) { throw le; } } else { releaseDefunctConnection(conn); throw new LDAPException(ResultCode.LOCAL_ERROR, ERR_POOL_OP_EXCEPTION.get(getExceptionMessage(t)), t); } } conn = replaceDefunctConnection(conn); try { final BindResult result = conn.bind(bindRequest); releaseAndReAuthenticateConnection(conn); return result; } catch (final Throwable t) { debugException(t); if (t instanceof LDAPException) { final LDAPException le = (LDAPException) t; try { healthCheck.ensureConnectionValidAfterException(conn, le); releaseAndReAuthenticateConnection(conn); } catch (final Exception e) { debugException(e); releaseDefunctConnection(conn); } throw le; } else { releaseDefunctConnection(conn); throw new LDAPException(ResultCode.LOCAL_ERROR, ERR_POOL_OP_EXCEPTION.get(getExceptionMessage(t)), t); } } } @Override() public LDAPConnection getConnection() throws LDAPException { final Thread t = Thread.currentThread(); LDAPConnection conn = connections.get(t); if (closed) { if (conn != null) { conn.terminate(null); connections.remove(t); } poolStatistics.incrementNumFailedCheckouts(); throw new LDAPException(ResultCode.CONNECT_ERROR, ERR_POOL_CLOSED.get()); } boolean created = false; if (conn == null) { conn = createConnection(); connections.put(t, conn); created = true; } try { healthCheck.ensureConnectionValidForCheckout(conn); if (created) { poolStatistics.incrementNumSuccessfulCheckoutsNewConnection(); } else { poolStatistics.incrementNumSuccessfulCheckoutsWithoutWaiting(); } return conn; } catch (LDAPException le) { debugException(le); conn.terminate(null); connections.remove(t); if (created) { poolStatistics.incrementNumFailedCheckouts(); throw le; } } try { conn = createConnection(); healthCheck.ensureConnectionValidForCheckout(conn); connections.put(t, conn); poolStatistics.incrementNumSuccessfulCheckoutsNewConnection(); return conn; } catch (LDAPException le) { debugException(le); poolStatistics.incrementNumFailedCheckouts(); if (conn != null) { conn.terminate(null); } throw le; } } @Override() public void releaseConnection(final LDAPConnection connection) { if (connection == null) { return; } connection.setConnectionPoolName(connectionPoolName); if (connectionIsExpired(connection)) { try { final LDAPConnection newConnection = createConnection(); connections.put(Thread.currentThread(), newConnection); connection.setDisconnectInfo(DisconnectType.POOLED_CONNECTION_EXPIRED, null, null); connection.terminate(null); poolStatistics.incrementNumConnectionsClosedExpired(); lastExpiredDisconnectTime = System.currentTimeMillis(); } catch (final LDAPException le) { debugException(le); } } try { healthCheck.ensureConnectionValidForRelease(connection); } catch (LDAPException le) { releaseDefunctConnection(connection); return; } poolStatistics.incrementNumReleasedValid(); if (closed) { close(); } } public void releaseAndReAuthenticateConnection( final LDAPConnection connection) { if (connection == null) { return; } try { if (bindRequest == null) { connection.bind("", ""); } else { connection.bind(bindRequest); } releaseConnection(connection); } catch (final Exception e) { debugException(e); releaseDefunctConnection(connection); } } @Override() public void releaseDefunctConnection(final LDAPConnection connection) { if (connection == null) { return; } connection.setConnectionPoolName(connectionPoolName); poolStatistics.incrementNumConnectionsClosedDefunct(); handleDefunctConnection(connection); } private void handleDefunctConnection(final LDAPConnection connection) { final Thread t = Thread.currentThread(); connection.setDisconnectInfo(DisconnectType.POOLED_CONNECTION_DEFUNCT, null, null); connection.terminate(null); connections.remove(t); if (closed) { return; } try { final LDAPConnection conn = createConnection(); connections.put(t, conn); } catch (LDAPException le) { debugException(le); } } @Override() public LDAPConnection replaceDefunctConnection( final LDAPConnection connection) throws LDAPException { connection.setDisconnectInfo(DisconnectType.POOLED_CONNECTION_DEFUNCT, null, null); connection.terminate(null); connections.remove(Thread.currentThread(), connection); if (closed) { throw new LDAPException(ResultCode.CONNECT_ERROR, ERR_POOL_CLOSED.get()); } final LDAPConnection newConnection = createConnection(); connections.put(Thread.currentThread(), newConnection); return newConnection; } @Override() public Set<OperationType> getOperationTypesToRetryDueToInvalidConnections() { return retryOperationTypes.get(); } @Override() public void setRetryFailedOperationsDueToInvalidConnections( final Set<OperationType> operationTypes) { if ((operationTypes == null) || operationTypes.isEmpty()) { retryOperationTypes.set( Collections.unmodifiableSet(EnumSet.noneOf(OperationType.class))); } else { final EnumSet<OperationType> s = EnumSet.noneOf(OperationType.class); s.addAll(operationTypes); retryOperationTypes.set(Collections.unmodifiableSet(s)); } } private boolean connectionIsExpired(final LDAPConnection connection) { if (maxConnectionAge <= 0L) { return false; } final long currentTime = System.currentTimeMillis(); if ((currentTime - lastExpiredDisconnectTime) < minDisconnectInterval) { return false; } final long connectionAge = currentTime - connection.getConnectTime(); return (connectionAge > maxConnectionAge); } @Override() public String getConnectionPoolName() { return connectionPoolName; } @Override() public void setConnectionPoolName(final String connectionPoolName) { this.connectionPoolName = connectionPoolName; } public long getMaxConnectionAgeMillis() { return maxConnectionAge; } public void setMaxConnectionAgeMillis(final long maxConnectionAge) { if (maxConnectionAge > 0L) { this.maxConnectionAge = maxConnectionAge; } else { this.maxConnectionAge = 0L; } } public long getMinDisconnectIntervalMillis() { return minDisconnectInterval; } public void setMinDisconnectIntervalMillis(final long minDisconnectInterval) { if (minDisconnectInterval > 0) { this.minDisconnectInterval = minDisconnectInterval; } else { this.minDisconnectInterval = 0L; } } @Override() public LDAPConnectionPoolHealthCheck getHealthCheck() { return healthCheck; } public void setHealthCheck(final LDAPConnectionPoolHealthCheck healthCheck) { ensureNotNull(healthCheck); this.healthCheck = healthCheck; } @Override() public long getHealthCheckIntervalMillis() { return healthCheckInterval; } @Override() public void setHealthCheckIntervalMillis(final long healthCheckInterval) { ensureTrue(healthCheckInterval > 0L, "LDAPConnectionPool.healthCheckInterval must be greater than 0."); this.healthCheckInterval = healthCheckInterval; healthCheckThread.wakeUp(); } @Override() protected void doHealthCheck() { final Iterator<Map.Entry<Thread,LDAPConnection>> iterator = connections.entrySet().iterator(); while (iterator.hasNext()) { final Map.Entry<Thread,LDAPConnection> e = iterator.next(); final Thread t = e.getKey(); final LDAPConnection c = e.getValue(); if (! t.isAlive()) { c.setDisconnectInfo(DisconnectType.POOLED_CONNECTION_UNNEEDED, null, null); c.terminate(null); iterator.remove(); } } } @Override() public int getCurrentAvailableConnections() { return -1; } @Override() public int getMaximumAvailableConnections() { return -1; } @Override() public LDAPConnectionPoolStatistics getConnectionPoolStatistics() { return poolStatistics; } @Override() protected void finalize() throws Throwable { super.finalize(); close(); } @Override() public void toString(final StringBuilder buffer) { buffer.append("LDAPThreadLocalConnectionPool("); final String name = connectionPoolName; if (name != null) { buffer.append("name='"); buffer.append(name); buffer.append("', "); } buffer.append("serverSet="); serverSet.toString(buffer); buffer.append(')'); } }