001/*
002 * Copyright 2009-2019 Ping Identity Corporation
003 * All Rights Reserved.
004 */
005/*
006 * Copyright (C) 2009-2019 Ping Identity Corporation
007 *
008 * This program is free software; you can redistribute it and/or modify
009 * it under the terms of the GNU General Public License (GPLv2 only)
010 * or the terms of the GNU Lesser General Public License (LGPLv2.1 only)
011 * as published by the Free Software Foundation.
012 *
013 * This program is distributed in the hope that it will be useful,
014 * but WITHOUT ANY WARRANTY; without even the implied warranty of
015 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
016 * GNU General Public License for more details.
017 *
018 * You should have received a copy of the GNU General Public License
019 * along with this program; if not, see <http://www.gnu.org/licenses>.
020 */
021package com.unboundid.ldap.sdk;
022
023
024
025import java.util.ArrayList;
026import java.util.Collections;
027import java.util.EnumSet;
028import java.util.Iterator;
029import java.util.Map;
030import java.util.Set;
031import java.util.concurrent.ConcurrentHashMap;
032import java.util.concurrent.atomic.AtomicReference;
033
034import com.unboundid.ldap.sdk.schema.Schema;
035import com.unboundid.util.Debug;
036import com.unboundid.util.ObjectPair;
037import com.unboundid.util.StaticUtils;
038import com.unboundid.util.ThreadSafety;
039import com.unboundid.util.ThreadSafetyLevel;
040import com.unboundid.util.Validator;
041
042import static com.unboundid.ldap.sdk.LDAPMessages.*;
043
044
045
046/**
047 * This class provides an implementation of an LDAP connection pool which
048 * maintains a dedicated connection for each thread using the connection pool.
049 * Connections will be created on an on-demand basis, so that if a thread
050 * attempts to use this connection pool for the first time then a new connection
051 * will be created by that thread.  This implementation eliminates the need to
052 * determine how best to size the connection pool, and it can eliminate
053 * contention among threads when trying to access a shared set of connections.
054 * All connections will be properly closed when the connection pool itself is
055 * closed, but if any thread which had previously used the connection pool stops
056 * running before the connection pool is closed, then the connection associated
057 * with that thread will also be closed by the Java finalizer.
058 * <BR><BR>
059 * If a thread obtains a connection to this connection pool, then that
060 * connection should not be made available to any other thread.  Similarly, if
061 * a thread attempts to check out multiple connections from the pool, then the
062 * same connection instance will be returned each time.
063 * <BR><BR>
064 * The capabilities offered by this class are generally the same as those
065 * provided by the {@link LDAPConnectionPool} class, as is the manner in which
066 * applications should interact with it.  See the class-level documentation for
067 * the {@code LDAPConnectionPool} class for additional information and examples.
068 * <BR><BR>
069 * One difference between this connection pool implementation and that provided
070 * by the {@link LDAPConnectionPool} class is that this implementation does not
071 * currently support periodic background health checks.  You can define health
072 * checks that will be invoked when a new connection is created, just before it
073 * is checked out for use, just after it is released, and if an error occurs
074 * while using the connection, but it will not maintain a separate background
075 * thread
076 */
077@ThreadSafety(level=ThreadSafetyLevel.COMPLETELY_THREADSAFE)
078public final class LDAPThreadLocalConnectionPool
079       extends AbstractConnectionPool
080{
081  /**
082   * The default health check interval for this connection pool, which is set to
083   * 60000 milliseconds (60 seconds).
084   */
085  private static final long DEFAULT_HEALTH_CHECK_INTERVAL = 60_000L;
086
087
088
089  // The types of operations that should be retried if they fail in a manner
090  // that may be the result of a connection that is no longer valid.
091  private final AtomicReference<Set<OperationType>> retryOperationTypes;
092
093  // Indicates whether this connection pool has been closed.
094  private volatile boolean closed;
095
096  // The bind request to use to perform authentication whenever a new connection
097  // is established.
098  private volatile BindRequest bindRequest;
099
100  // The map of connections maintained for this connection pool.
101  private final ConcurrentHashMap<Thread,LDAPConnection> connections;
102
103  // The health check implementation that should be used for this connection
104  // pool.
105  private LDAPConnectionPoolHealthCheck healthCheck;
106
107  // The thread that will be used to perform periodic background health checks
108  // for this connection pool.
109  private final LDAPConnectionPoolHealthCheckThread healthCheckThread;
110
111  // The statistics for this connection pool.
112  private final LDAPConnectionPoolStatistics poolStatistics;
113
114  // The length of time in milliseconds between periodic health checks against
115  // the available connections in this pool.
116  private volatile long healthCheckInterval;
117
118  // The time that the last expired connection was closed.
119  private volatile long lastExpiredDisconnectTime;
120
121  // The maximum length of time in milliseconds that a connection should be
122  // allowed to be established before terminating and re-establishing the
123  // connection.
124  private volatile long maxConnectionAge;
125
126  // The minimum length of time in milliseconds that must pass between
127  // disconnects of connections that have exceeded the maximum connection age.
128  private volatile long minDisconnectInterval;
129
130  // The schema that should be shared for connections in this pool, along with
131  // its expiration time.
132  private volatile ObjectPair<Long,Schema> pooledSchema;
133
134  // The post-connect processor for this connection pool, if any.
135  private final PostConnectProcessor postConnectProcessor;
136
137  // The server set to use for establishing connections for use by this pool.
138  private volatile ServerSet serverSet;
139
140  // The user-friendly name assigned to this connection pool.
141  private String connectionPoolName;
142
143
144
145  /**
146   * Creates a new LDAP thread-local connection pool in which all connections
147   * will be clones of the provided connection.
148   *
149   * @param  connection  The connection to use to provide the template for the
150   *                     other connections to be created.  This connection will
151   *                     be included in the pool.  It must not be {@code null},
152   *                     and it must be established to the target server.  It
153   *                     does not necessarily need to be authenticated if all
154   *                     connections in the pool are to be unauthenticated.
155   *
156   * @throws  LDAPException  If the provided connection cannot be used to
157   *                         initialize the pool.  If this is thrown, then all
158   *                         connections associated with the pool (including the
159   *                         one provided as an argument) will be closed.
160   */
161  public LDAPThreadLocalConnectionPool(final LDAPConnection connection)
162         throws LDAPException
163  {
164    this(connection, null);
165  }
166
167
168
169  /**
170   * Creates a new LDAP thread-local connection pool in which all connections
171   * will be clones of the provided connection.
172   *
173   * @param  connection            The connection to use to provide the template
174   *                               for the other connections to be created.
175   *                               This connection will be included in the pool.
176   *                               It must not be {@code null}, and it must be
177   *                               established to the target server.  It does
178   *                               not necessarily need to be authenticated if
179   *                               all connections in the pool are to be
180   *                               unauthenticated.
181   * @param  postConnectProcessor  A processor that should be used to perform
182   *                               any post-connect processing for connections
183   *                               in this pool.  It may be {@code null} if no
184   *                               special processing is needed.  Note that this
185   *                               processing will not be invoked on the
186   *                               provided connection that will be used as the
187   *                               first connection in the pool.
188   *
189   * @throws  LDAPException  If the provided connection cannot be used to
190   *                         initialize the pool.  If this is thrown, then all
191   *                         connections associated with the pool (including the
192   *                         one provided as an argument) will be closed.
193   */
194  public LDAPThreadLocalConnectionPool(final LDAPConnection connection,
195              final PostConnectProcessor postConnectProcessor)
196         throws LDAPException
197  {
198    Validator.ensureNotNull(connection);
199
200    // NOTE:  The post-connect processor (if any) will be used in the server
201    // set that we create rather than in the connection pool itself.
202    this.postConnectProcessor = null;
203
204    healthCheck               = new LDAPConnectionPoolHealthCheck();
205    healthCheckInterval       = DEFAULT_HEALTH_CHECK_INTERVAL;
206    poolStatistics            = new LDAPConnectionPoolStatistics(this);
207    connectionPoolName        = null;
208    retryOperationTypes       = new AtomicReference<>(
209         Collections.unmodifiableSet(EnumSet.noneOf(OperationType.class)));
210
211    if (! connection.isConnected())
212    {
213      throw new LDAPException(ResultCode.PARAM_ERROR,
214                              ERR_POOL_CONN_NOT_ESTABLISHED.get());
215    }
216
217
218    bindRequest = connection.getLastBindRequest();
219    serverSet = new SingleServerSet(connection.getConnectedAddress(),
220                                    connection.getConnectedPort(),
221                                    connection.getLastUsedSocketFactory(),
222                                    connection.getConnectionOptions(), null,
223                                    postConnectProcessor);
224
225    connections = new ConcurrentHashMap<>(StaticUtils.computeMapCapacity(20));
226    connections.put(Thread.currentThread(), connection);
227
228    lastExpiredDisconnectTime = 0L;
229    maxConnectionAge          = 0L;
230    closed                    = false;
231    minDisconnectInterval     = 0L;
232
233    healthCheckThread = new LDAPConnectionPoolHealthCheckThread(this);
234    healthCheckThread.start();
235
236    final LDAPConnectionOptions opts = connection.getConnectionOptions();
237    if (opts.usePooledSchema())
238    {
239      try
240      {
241        final Schema schema = connection.getSchema();
242        if (schema != null)
243        {
244          connection.setCachedSchema(schema);
245
246          final long currentTime = System.currentTimeMillis();
247          final long timeout = opts.getPooledSchemaTimeoutMillis();
248          if ((timeout <= 0L) || (timeout+currentTime <= 0L))
249          {
250            pooledSchema = new ObjectPair<>(Long.MAX_VALUE, schema);
251          }
252          else
253          {
254            pooledSchema = new ObjectPair<>(timeout+currentTime, schema);
255          }
256        }
257      }
258      catch (final Exception e)
259      {
260        Debug.debugException(e);
261      }
262    }
263  }
264
265
266
267  /**
268   * Creates a new LDAP thread-local connection pool which will use the provided
269   * server set and bind request for creating new connections.
270   *
271   * @param  serverSet       The server set to use to create the connections.
272   *                         It is acceptable for the server set to create the
273   *                         connections across multiple servers.
274   * @param  bindRequest     The bind request to use to authenticate the
275   *                         connections that are established.  It may be
276   *                         {@code null} if no authentication should be
277   *                         performed on the connections.  Note that if the
278   *                         server set is configured to perform
279   *                         authentication, this bind request should be the
280   *                         same bind request used by the server set.  This
281   *                         is important because even though the server set
282   *                         may be used to perform the initial authentication
283   *                         on a newly established connection, this connection
284   *                         pool may still need to re-authenticate the
285   *                         connection.
286   */
287  public LDAPThreadLocalConnectionPool(final ServerSet serverSet,
288                                       final BindRequest bindRequest)
289  {
290    this(serverSet, bindRequest, null);
291  }
292
293
294
295  /**
296   * Creates a new LDAP thread-local connection pool which will use the provided
297   * server set and bind request for creating new connections.
298   *
299   * @param  serverSet             The server set to use to create the
300   *                               connections.  It is acceptable for the server
301   *                               set to create the connections across multiple
302   *                               servers.
303   * @param  bindRequest           The bind request to use to authenticate the
304   *                               connections that are established.  It may be
305   *                               {@code null} if no authentication should be
306   *                               performed on the connections.  Note that if
307   *                               the server set is configured to perform
308   *                               authentication, this bind request should be
309   *                               the same bind request used by the server set.
310   *                               This is important because even though the
311   *                               server set may be used to perform the
312   *                               initial authentication on a newly
313   *                               established connection, this connection
314   *                               pool may still need to re-authenticate the
315   *                               connection.
316   * @param  postConnectProcessor  A processor that should be used to perform
317   *                               any post-connect processing for connections
318   *                               in this pool.  It may be {@code null} if no
319   *                               special processing is needed.  Note that if
320   *                               the server set is configured with a
321   *                               non-{@code null} post-connect processor, then
322   *                               the post-connect processor provided to the
323   *                               pool must be {@code null}.
324   */
325  public LDAPThreadLocalConnectionPool(final ServerSet serverSet,
326              final BindRequest bindRequest,
327              final PostConnectProcessor postConnectProcessor)
328  {
329    Validator.ensureNotNull(serverSet);
330
331    this.serverSet            = serverSet;
332    this.bindRequest          = bindRequest;
333    this.postConnectProcessor = postConnectProcessor;
334
335    if (serverSet.includesAuthentication())
336    {
337      Validator.ensureTrue((bindRequest != null),
338           "LDAPThreadLocalConnectionPool.bindRequest must not be null if " +
339                "serverSet.includesAuthentication returns true");
340    }
341
342    if (serverSet.includesPostConnectProcessing())
343    {
344      Validator.ensureTrue((postConnectProcessor == null),
345           "LDAPThreadLocalConnectionPool.postConnectProcessor must be null " +
346                "if serverSet.includesPostConnectProcessing returns true.");
347    }
348
349    healthCheck               = new LDAPConnectionPoolHealthCheck();
350    healthCheckInterval       = DEFAULT_HEALTH_CHECK_INTERVAL;
351    poolStatistics            = new LDAPConnectionPoolStatistics(this);
352    connectionPoolName        = null;
353    retryOperationTypes       = new AtomicReference<>(
354         Collections.unmodifiableSet(EnumSet.noneOf(OperationType.class)));
355
356    connections = new ConcurrentHashMap<>(StaticUtils.computeMapCapacity(20));
357
358    lastExpiredDisconnectTime = 0L;
359    maxConnectionAge          = 0L;
360    minDisconnectInterval     = 0L;
361    closed                    = false;
362
363    healthCheckThread = new LDAPConnectionPoolHealthCheckThread(this);
364    healthCheckThread.start();
365  }
366
367
368
369  /**
370   * Creates a new LDAP connection for use in this pool.
371   *
372   * @return  A new connection created for use in this pool.
373   *
374   * @throws  LDAPException  If a problem occurs while attempting to establish
375   *                         the connection.  If a connection had been created,
376   *                         it will be closed.
377   */
378  @SuppressWarnings("deprecation")
379  private LDAPConnection createConnection()
380          throws LDAPException
381  {
382    final LDAPConnection c;
383    try
384    {
385      c = serverSet.getConnection(healthCheck);
386    }
387    catch (final LDAPException le)
388    {
389      Debug.debugException(le);
390      poolStatistics.incrementNumFailedConnectionAttempts();
391      throw le;
392    }
393    c.setConnectionPool(this);
394
395
396    // Auto-reconnect must be disabled for pooled connections, so turn it off
397    // if the associated connection options have it enabled for some reason.
398    LDAPConnectionOptions opts = c.getConnectionOptions();
399    if (opts.autoReconnect())
400    {
401      opts = opts.duplicate();
402      opts.setAutoReconnect(false);
403      c.setConnectionOptions(opts);
404    }
405
406
407    // Invoke pre-authentication post-connect processing.
408    if (postConnectProcessor != null)
409    {
410      try
411      {
412        postConnectProcessor.processPreAuthenticatedConnection(c);
413      }
414      catch (final Exception e)
415      {
416        Debug.debugException(e);
417
418        try
419        {
420          poolStatistics.incrementNumFailedConnectionAttempts();
421          c.setDisconnectInfo(DisconnectType.POOL_CREATION_FAILURE, null, e);
422          c.setClosed();
423        }
424        catch (final Exception e2)
425        {
426          Debug.debugException(e2);
427        }
428
429        if (e instanceof LDAPException)
430        {
431          throw ((LDAPException) e);
432        }
433        else
434        {
435          throw new LDAPException(ResultCode.CONNECT_ERROR,
436               ERR_POOL_POST_CONNECT_ERROR.get(
437                    StaticUtils.getExceptionMessage(e)),
438               e);
439        }
440      }
441    }
442
443
444    // Authenticate the connection if appropriate.
445    if ((bindRequest != null) && (! serverSet.includesAuthentication()))
446    {
447      BindResult bindResult;
448      try
449      {
450        bindResult = c.bind(bindRequest.duplicate());
451      }
452      catch (final LDAPBindException lbe)
453      {
454        Debug.debugException(lbe);
455        bindResult = lbe.getBindResult();
456      }
457      catch (final LDAPException le)
458      {
459        Debug.debugException(le);
460        bindResult = new BindResult(le);
461      }
462
463      try
464      {
465        healthCheck.ensureConnectionValidAfterAuthentication(c, bindResult);
466        if (bindResult.getResultCode() != ResultCode.SUCCESS)
467        {
468          throw new LDAPBindException(bindResult);
469        }
470      }
471      catch (final LDAPException le)
472      {
473        Debug.debugException(le);
474
475        try
476        {
477          poolStatistics.incrementNumFailedConnectionAttempts();
478          c.setDisconnectInfo(DisconnectType.BIND_FAILED, null, le);
479          c.setClosed();
480        }
481        catch (final Exception e)
482        {
483          Debug.debugException(e);
484        }
485
486        throw le;
487      }
488    }
489
490
491    // Invoke post-authentication post-connect processing.
492    if (postConnectProcessor != null)
493    {
494      try
495      {
496        postConnectProcessor.processPostAuthenticatedConnection(c);
497      }
498      catch (final Exception e)
499      {
500        Debug.debugException(e);
501        try
502        {
503          poolStatistics.incrementNumFailedConnectionAttempts();
504          c.setDisconnectInfo(DisconnectType.POOL_CREATION_FAILURE, null, e);
505          c.setClosed();
506        }
507        catch (final Exception e2)
508        {
509          Debug.debugException(e2);
510        }
511
512        if (e instanceof LDAPException)
513        {
514          throw ((LDAPException) e);
515        }
516        else
517        {
518          throw new LDAPException(ResultCode.CONNECT_ERROR,
519               ERR_POOL_POST_CONNECT_ERROR.get(
520                    StaticUtils.getExceptionMessage(e)),
521               e);
522        }
523      }
524    }
525
526
527    // Get the pooled schema if appropriate.
528    if (opts.usePooledSchema())
529    {
530      final long currentTime = System.currentTimeMillis();
531      if ((pooledSchema == null) || (currentTime > pooledSchema.getFirst()))
532      {
533        try
534        {
535          final Schema schema = c.getSchema();
536          if (schema != null)
537          {
538            c.setCachedSchema(schema);
539
540            final long timeout = opts.getPooledSchemaTimeoutMillis();
541            if ((timeout <= 0L) || (currentTime + timeout <= 0L))
542            {
543              pooledSchema = new ObjectPair<>(Long.MAX_VALUE, schema);
544            }
545            else
546            {
547              pooledSchema = new ObjectPair<>((currentTime+timeout), schema);
548            }
549          }
550        }
551        catch (final Exception e)
552        {
553          Debug.debugException(e);
554
555          // There was a problem retrieving the schema from the server, but if
556          // we have an earlier copy then we can assume it's still valid.
557          if (pooledSchema != null)
558          {
559            c.setCachedSchema(pooledSchema.getSecond());
560          }
561        }
562      }
563      else
564      {
565        c.setCachedSchema(pooledSchema.getSecond());
566      }
567    }
568
569
570    // Finish setting up the connection.
571    c.setConnectionPoolName(connectionPoolName);
572    poolStatistics.incrementNumSuccessfulConnectionAttempts();
573
574    return c;
575  }
576
577
578
579  /**
580   * {@inheritDoc}
581   */
582  @Override()
583  public void close()
584  {
585    close(true, 1);
586  }
587
588
589
590  /**
591   * {@inheritDoc}
592   */
593  @Override()
594  public void close(final boolean unbind, final int numThreads)
595  {
596    final boolean healthCheckThreadAlreadySignaled = closed;
597    closed = true;
598    healthCheckThread.stopRunning(! healthCheckThreadAlreadySignaled);
599
600    if (numThreads > 1)
601    {
602      final ArrayList<LDAPConnection> connList =
603           new ArrayList<>(connections.size());
604      final Iterator<LDAPConnection> iterator = connections.values().iterator();
605      while (iterator.hasNext())
606      {
607        connList.add(iterator.next());
608        iterator.remove();
609      }
610
611      if (! connList.isEmpty())
612      {
613        final ParallelPoolCloser closer =
614             new ParallelPoolCloser(connList, unbind, numThreads);
615        closer.closeConnections();
616      }
617    }
618    else
619    {
620      final Iterator<Map.Entry<Thread,LDAPConnection>> iterator =
621           connections.entrySet().iterator();
622      while (iterator.hasNext())
623      {
624        final LDAPConnection conn = iterator.next().getValue();
625        iterator.remove();
626
627        poolStatistics.incrementNumConnectionsClosedUnneeded();
628        conn.setDisconnectInfo(DisconnectType.POOL_CLOSED, null, null);
629        if (unbind)
630        {
631          conn.terminate(null);
632        }
633        else
634        {
635          conn.setClosed();
636        }
637      }
638    }
639  }
640
641
642
643  /**
644   * {@inheritDoc}
645   */
646  @Override()
647  public boolean isClosed()
648  {
649    return closed;
650  }
651
652
653
654  /**
655   * Processes a simple bind using a connection from this connection pool, and
656   * then reverts that authentication by re-binding as the same user used to
657   * authenticate new connections.  If new connections are unauthenticated, then
658   * the subsequent bind will be an anonymous simple bind.  This method attempts
659   * to ensure that processing the provided bind operation does not have a
660   * lasting impact the authentication state of the connection used to process
661   * it.
662   * <BR><BR>
663   * If the second bind attempt (the one used to restore the authentication
664   * identity) fails, the connection will be closed as defunct so that a new
665   * connection will be created to take its place.
666   *
667   * @param  bindDN    The bind DN for the simple bind request.
668   * @param  password  The password for the simple bind request.
669   * @param  controls  The optional set of controls for the simple bind request.
670   *
671   * @return  The result of processing the provided bind operation.
672   *
673   * @throws  LDAPException  If the server rejects the bind request, or if a
674   *                         problem occurs while sending the request or reading
675   *                         the response.
676   */
677  public BindResult bindAndRevertAuthentication(final String bindDN,
678                                                final String password,
679                                                final Control... controls)
680         throws LDAPException
681  {
682    return bindAndRevertAuthentication(
683         new SimpleBindRequest(bindDN, password, controls));
684  }
685
686
687
688  /**
689   * Processes the provided bind request using a connection from this connection
690   * pool, and then reverts that authentication by re-binding as the same user
691   * used to authenticate new connections.  If new connections are
692   * unauthenticated, then the subsequent bind will be an anonymous simple bind.
693   * This method attempts to ensure that processing the provided bind operation
694   * does not have a lasting impact the authentication state of the connection
695   * used to process it.
696   * <BR><BR>
697   * If the second bind attempt (the one used to restore the authentication
698   * identity) fails, the connection will be closed as defunct so that a new
699   * connection will be created to take its place.
700   *
701   * @param  bindRequest  The bind request to be processed.  It must not be
702   *                      {@code null}.
703   *
704   * @return  The result of processing the provided bind operation.
705   *
706   * @throws  LDAPException  If the server rejects the bind request, or if a
707   *                         problem occurs while sending the request or reading
708   *                         the response.
709   */
710  public BindResult bindAndRevertAuthentication(final BindRequest bindRequest)
711         throws LDAPException
712  {
713    LDAPConnection conn = getConnection();
714
715    try
716    {
717      final BindResult result = conn.bind(bindRequest);
718      releaseAndReAuthenticateConnection(conn);
719      return result;
720    }
721    catch (final Throwable t)
722    {
723      Debug.debugException(t);
724
725      if (t instanceof LDAPException)
726      {
727        final LDAPException le = (LDAPException) t;
728
729        boolean shouldThrow;
730        try
731        {
732          healthCheck.ensureConnectionValidAfterException(conn, le);
733
734          // The above call will throw an exception if the connection doesn't
735          // seem to be valid, so if we've gotten here then we should assume
736          // that it is valid and we will pass the exception onto the client
737          // without retrying the operation.
738          releaseAndReAuthenticateConnection(conn);
739          shouldThrow = true;
740        }
741        catch (final Exception e)
742        {
743          Debug.debugException(e);
744
745          // This implies that the connection is not valid.  If the pool is
746          // configured to re-try bind operations on a newly-established
747          // connection, then that will be done later in this method.
748          // Otherwise, release the connection as defunct and pass the bind
749          // exception onto the client.
750          if (! getOperationTypesToRetryDueToInvalidConnections().contains(
751                     OperationType.BIND))
752          {
753            releaseDefunctConnection(conn);
754            shouldThrow = true;
755          }
756          else
757          {
758            shouldThrow = false;
759          }
760        }
761
762        if (shouldThrow)
763        {
764          throw le;
765        }
766      }
767      else
768      {
769        releaseDefunctConnection(conn);
770        StaticUtils.rethrowIfError(t);
771        throw new LDAPException(ResultCode.LOCAL_ERROR,
772             ERR_POOL_OP_EXCEPTION.get(StaticUtils.getExceptionMessage(t)), t);
773      }
774    }
775
776
777    // If we've gotten here, then the bind operation should be re-tried on a
778    // newly-established connection.
779    conn = replaceDefunctConnection(conn);
780
781    try
782    {
783      final BindResult result = conn.bind(bindRequest);
784      releaseAndReAuthenticateConnection(conn);
785      return result;
786    }
787    catch (final Throwable t)
788    {
789      Debug.debugException(t);
790
791      if (t instanceof LDAPException)
792      {
793        final LDAPException le = (LDAPException) t;
794
795        try
796        {
797          healthCheck.ensureConnectionValidAfterException(conn, le);
798          releaseAndReAuthenticateConnection(conn);
799        }
800        catch (final Exception e)
801        {
802          Debug.debugException(e);
803          releaseDefunctConnection(conn);
804        }
805
806        throw le;
807      }
808      else
809      {
810        releaseDefunctConnection(conn);
811        StaticUtils.rethrowIfError(t);
812        throw new LDAPException(ResultCode.LOCAL_ERROR,
813             ERR_POOL_OP_EXCEPTION.get(StaticUtils.getExceptionMessage(t)), t);
814      }
815    }
816  }
817
818
819
820  /**
821   * {@inheritDoc}
822   */
823  @Override()
824  public LDAPConnection getConnection()
825         throws LDAPException
826  {
827    final Thread t = Thread.currentThread();
828    LDAPConnection conn = connections.get(t);
829
830    if (closed)
831    {
832      if (conn != null)
833      {
834        conn.terminate(null);
835        connections.remove(t);
836      }
837
838      poolStatistics.incrementNumFailedCheckouts();
839      throw new LDAPException(ResultCode.CONNECT_ERROR,
840                              ERR_POOL_CLOSED.get());
841    }
842
843    boolean created = false;
844    if ((conn == null) || (! conn.isConnected()))
845    {
846      conn = createConnection();
847      connections.put(t, conn);
848      created = true;
849    }
850
851    try
852    {
853      healthCheck.ensureConnectionValidForCheckout(conn);
854      if (created)
855      {
856        poolStatistics.incrementNumSuccessfulCheckoutsNewConnection();
857      }
858      else
859      {
860        poolStatistics.incrementNumSuccessfulCheckoutsWithoutWaiting();
861      }
862      return conn;
863    }
864    catch (final LDAPException le)
865    {
866      Debug.debugException(le);
867
868      conn.setClosed();
869      connections.remove(t);
870
871      if (created)
872      {
873        poolStatistics.incrementNumFailedCheckouts();
874        throw le;
875      }
876    }
877
878    try
879    {
880      conn = createConnection();
881      healthCheck.ensureConnectionValidForCheckout(conn);
882      connections.put(t, conn);
883      poolStatistics.incrementNumSuccessfulCheckoutsNewConnection();
884      return conn;
885    }
886    catch (final LDAPException le)
887    {
888      Debug.debugException(le);
889
890      poolStatistics.incrementNumFailedCheckouts();
891
892      if (conn != null)
893      {
894        conn.setClosed();
895      }
896
897      throw le;
898    }
899  }
900
901
902
903  /**
904   * {@inheritDoc}
905   */
906  @Override()
907  public void releaseConnection(final LDAPConnection connection)
908  {
909    if (connection == null)
910    {
911      return;
912    }
913
914    connection.setConnectionPoolName(connectionPoolName);
915    if (connectionIsExpired(connection))
916    {
917      try
918      {
919        final LDAPConnection newConnection = createConnection();
920        connections.put(Thread.currentThread(), newConnection);
921
922        connection.setDisconnectInfo(DisconnectType.POOLED_CONNECTION_EXPIRED,
923             null, null);
924        connection.terminate(null);
925        poolStatistics.incrementNumConnectionsClosedExpired();
926        lastExpiredDisconnectTime = System.currentTimeMillis();
927      }
928      catch (final LDAPException le)
929      {
930        Debug.debugException(le);
931      }
932    }
933
934    try
935    {
936      healthCheck.ensureConnectionValidForRelease(connection);
937    }
938    catch (final LDAPException le)
939    {
940      releaseDefunctConnection(connection);
941      return;
942    }
943
944    poolStatistics.incrementNumReleasedValid();
945
946    if (closed)
947    {
948      close();
949    }
950  }
951
952
953
954  /**
955   * Performs a bind on the provided connection before releasing it back to the
956   * pool, so that it will be authenticated as the same user as
957   * newly-established connections.  If newly-established connections are
958   * unauthenticated, then this method will perform an anonymous simple bind to
959   * ensure that the resulting connection is unauthenticated.
960   *
961   * Releases the provided connection back to this pool.
962   *
963   * @param  connection  The connection to be released back to the pool after
964   *                     being re-authenticated.
965   */
966  public void releaseAndReAuthenticateConnection(
967       final LDAPConnection connection)
968  {
969    if (connection == null)
970    {
971      return;
972    }
973
974    try
975    {
976      BindResult bindResult;
977      try
978      {
979        if (bindRequest == null)
980        {
981          bindResult = connection.bind("", "");
982        }
983        else
984        {
985          bindResult = connection.bind(bindRequest.duplicate());
986        }
987      }
988      catch (final LDAPBindException lbe)
989      {
990        Debug.debugException(lbe);
991        bindResult = lbe.getBindResult();
992      }
993
994      try
995      {
996        healthCheck.ensureConnectionValidAfterAuthentication(connection,
997             bindResult);
998        if (bindResult.getResultCode() != ResultCode.SUCCESS)
999        {
1000          throw new LDAPBindException(bindResult);
1001        }
1002      }
1003      catch (final LDAPException le)
1004      {
1005        Debug.debugException(le);
1006
1007        try
1008        {
1009          connection.setDisconnectInfo(DisconnectType.BIND_FAILED, null, le);
1010          connection.terminate(null);
1011          releaseDefunctConnection(connection);
1012        }
1013        catch (final Exception e)
1014        {
1015          Debug.debugException(e);
1016        }
1017
1018        throw le;
1019      }
1020
1021      releaseConnection(connection);
1022    }
1023    catch (final Exception e)
1024    {
1025      Debug.debugException(e);
1026      releaseDefunctConnection(connection);
1027    }
1028  }
1029
1030
1031
1032  /**
1033   * {@inheritDoc}
1034   */
1035  @Override()
1036  public void releaseDefunctConnection(final LDAPConnection connection)
1037  {
1038    if (connection == null)
1039    {
1040      return;
1041    }
1042
1043    connection.setConnectionPoolName(connectionPoolName);
1044    poolStatistics.incrementNumConnectionsClosedDefunct();
1045    handleDefunctConnection(connection);
1046  }
1047
1048
1049
1050  /**
1051   * Performs the real work of terminating a defunct connection and replacing it
1052   * with a new connection if possible.
1053   *
1054   * @param  connection  The defunct connection to be replaced.
1055   */
1056  private void handleDefunctConnection(final LDAPConnection connection)
1057  {
1058    final Thread t = Thread.currentThread();
1059
1060    connection.setDisconnectInfo(DisconnectType.POOLED_CONNECTION_DEFUNCT, null,
1061                                 null);
1062    connection.setClosed();
1063    connections.remove(t);
1064
1065    if (closed)
1066    {
1067      return;
1068    }
1069
1070    try
1071    {
1072      final LDAPConnection conn = createConnection();
1073      connections.put(t, conn);
1074    }
1075    catch (final LDAPException le)
1076    {
1077      Debug.debugException(le);
1078    }
1079  }
1080
1081
1082
1083  /**
1084   * {@inheritDoc}
1085   */
1086  @Override()
1087  public LDAPConnection replaceDefunctConnection(
1088                             final LDAPConnection connection)
1089         throws LDAPException
1090  {
1091    poolStatistics.incrementNumConnectionsClosedDefunct();
1092    connection.setDisconnectInfo(DisconnectType.POOLED_CONNECTION_DEFUNCT, null,
1093                                 null);
1094    connection.setClosed();
1095    connections.remove(Thread.currentThread(), connection);
1096
1097    if (closed)
1098    {
1099      throw new LDAPException(ResultCode.CONNECT_ERROR, ERR_POOL_CLOSED.get());
1100    }
1101
1102    final LDAPConnection newConnection = createConnection();
1103    connections.put(Thread.currentThread(), newConnection);
1104    return newConnection;
1105  }
1106
1107
1108
1109  /**
1110   * {@inheritDoc}
1111   */
1112  @Override()
1113  public Set<OperationType> getOperationTypesToRetryDueToInvalidConnections()
1114  {
1115    return retryOperationTypes.get();
1116  }
1117
1118
1119
1120  /**
1121   * {@inheritDoc}
1122   */
1123  @Override()
1124  public void setRetryFailedOperationsDueToInvalidConnections(
1125                   final Set<OperationType> operationTypes)
1126  {
1127    if ((operationTypes == null) || operationTypes.isEmpty())
1128    {
1129      retryOperationTypes.set(
1130           Collections.unmodifiableSet(EnumSet.noneOf(OperationType.class)));
1131    }
1132    else
1133    {
1134      final EnumSet<OperationType> s = EnumSet.noneOf(OperationType.class);
1135      s.addAll(operationTypes);
1136      retryOperationTypes.set(Collections.unmodifiableSet(s));
1137    }
1138  }
1139
1140
1141
1142  /**
1143   * Indicates whether the provided connection should be considered expired.
1144   *
1145   * @param  connection  The connection for which to make the determination.
1146   *
1147   * @return  {@code true} if the provided connection should be considered
1148   *          expired, or {@code false} if not.
1149   */
1150  private boolean connectionIsExpired(final LDAPConnection connection)
1151  {
1152    // If connection expiration is not enabled, then there is nothing to do.
1153    if (maxConnectionAge <= 0L)
1154    {
1155      return false;
1156    }
1157
1158    // If there is a minimum disconnect interval, then make sure that we have
1159    // not closed another expired connection too recently.
1160    final long currentTime = System.currentTimeMillis();
1161    if ((currentTime - lastExpiredDisconnectTime) < minDisconnectInterval)
1162    {
1163      return false;
1164    }
1165
1166    // Get the age of the connection and see if it is expired.
1167    final long connectionAge = currentTime - connection.getConnectTime();
1168    return (connectionAge > maxConnectionAge);
1169  }
1170
1171
1172
1173  /**
1174   * Specifies the bind request that will be used to authenticate subsequent new
1175   * connections that are established by this connection pool.  The
1176   * authentication state for existing connections will not be altered unless
1177   * one of the {@code bindAndRevertAuthentication} or
1178   * {@code releaseAndReAuthenticateConnection} methods are invoked on those
1179   * connections.
1180   *
1181   * @param  bindRequest  The bind request that will be used to authenticate new
1182   *                      connections that are established by this pool, or
1183   *                      that will be applied to existing connections via the
1184   *                      {@code bindAndRevertAuthentication} or
1185   *                      {@code releaseAndReAuthenticateConnection} method.  It
1186   *                      may be {@code null} if new connections should be
1187   *                      unauthenticated.
1188   */
1189  public void setBindRequest(final BindRequest bindRequest)
1190  {
1191    this.bindRequest = bindRequest;
1192  }
1193
1194
1195
1196  /**
1197   * Specifies the server set that should be used to establish new connections
1198   * for use in this connection pool.  Existing connections will not be
1199   * affected.
1200   *
1201   * @param  serverSet  The server set that should be used to establish new
1202   *                    connections for use in this connection pool.  It must
1203   *                    not be {@code null}.
1204   */
1205  public void setServerSet(final ServerSet serverSet)
1206  {
1207    Validator.ensureNotNull(serverSet);
1208    this.serverSet = serverSet;
1209  }
1210
1211
1212
1213  /**
1214   * {@inheritDoc}
1215   */
1216  @Override()
1217  public String getConnectionPoolName()
1218  {
1219    return connectionPoolName;
1220  }
1221
1222
1223
1224  /**
1225   * {@inheritDoc}
1226   */
1227  @Override()
1228  public void setConnectionPoolName(final String connectionPoolName)
1229  {
1230    this.connectionPoolName = connectionPoolName;
1231  }
1232
1233
1234
1235  /**
1236   * Retrieves the maximum length of time in milliseconds that a connection in
1237   * this pool may be established before it is closed and replaced with another
1238   * connection.
1239   *
1240   * @return  The maximum length of time in milliseconds that a connection in
1241   *          this pool may be established before it is closed and replaced with
1242   *          another connection, or {@code 0L} if no maximum age should be
1243   *          enforced.
1244   */
1245  public long getMaxConnectionAgeMillis()
1246  {
1247    return maxConnectionAge;
1248  }
1249
1250
1251
1252  /**
1253   * Specifies the maximum length of time in milliseconds that a connection in
1254   * this pool may be established before it should be closed and replaced with
1255   * another connection.
1256   *
1257   * @param  maxConnectionAge  The maximum length of time in milliseconds that a
1258   *                           connection in this pool may be established before
1259   *                           it should be closed and replaced with another
1260   *                           connection.  A value of zero indicates that no
1261   *                           maximum age should be enforced.
1262   */
1263  public void setMaxConnectionAgeMillis(final long maxConnectionAge)
1264  {
1265    if (maxConnectionAge > 0L)
1266    {
1267      this.maxConnectionAge = maxConnectionAge;
1268    }
1269    else
1270    {
1271      this.maxConnectionAge = 0L;
1272    }
1273  }
1274
1275
1276
1277  /**
1278   * Retrieves the minimum length of time in milliseconds that should pass
1279   * between connections closed because they have been established for longer
1280   * than the maximum connection age.
1281   *
1282   * @return  The minimum length of time in milliseconds that should pass
1283   *          between connections closed because they have been established for
1284   *          longer than the maximum connection age, or {@code 0L} if expired
1285   *          connections may be closed as quickly as they are identified.
1286   */
1287  public long getMinDisconnectIntervalMillis()
1288  {
1289    return minDisconnectInterval;
1290  }
1291
1292
1293
1294  /**
1295   * Specifies the minimum length of time in milliseconds that should pass
1296   * between connections closed because they have been established for longer
1297   * than the maximum connection age.
1298   *
1299   * @param  minDisconnectInterval  The minimum length of time in milliseconds
1300   *                                that should pass between connections closed
1301   *                                because they have been established for
1302   *                                longer than the maximum connection age.  A
1303   *                                value less than or equal to zero indicates
1304   *                                that no minimum time should be enforced.
1305   */
1306  public void setMinDisconnectIntervalMillis(final long minDisconnectInterval)
1307  {
1308    if (minDisconnectInterval > 0)
1309    {
1310      this.minDisconnectInterval = minDisconnectInterval;
1311    }
1312    else
1313    {
1314      this.minDisconnectInterval = 0L;
1315    }
1316  }
1317
1318
1319
1320  /**
1321   * {@inheritDoc}
1322   */
1323  @Override()
1324  public LDAPConnectionPoolHealthCheck getHealthCheck()
1325  {
1326    return healthCheck;
1327  }
1328
1329
1330
1331  /**
1332   * Sets the health check implementation for this connection pool.
1333   *
1334   * @param  healthCheck  The health check implementation for this connection
1335   *                      pool.  It must not be {@code null}.
1336   */
1337  public void setHealthCheck(final LDAPConnectionPoolHealthCheck healthCheck)
1338  {
1339    Validator.ensureNotNull(healthCheck);
1340    this.healthCheck = healthCheck;
1341  }
1342
1343
1344
1345  /**
1346   * {@inheritDoc}
1347   */
1348  @Override()
1349  public long getHealthCheckIntervalMillis()
1350  {
1351    return healthCheckInterval;
1352  }
1353
1354
1355
1356  /**
1357   * {@inheritDoc}
1358   */
1359  @Override()
1360  public void setHealthCheckIntervalMillis(final long healthCheckInterval)
1361  {
1362    Validator.ensureTrue(healthCheckInterval > 0L,
1363         "LDAPConnectionPool.healthCheckInterval must be greater than 0.");
1364    this.healthCheckInterval = healthCheckInterval;
1365    healthCheckThread.wakeUp();
1366  }
1367
1368
1369
1370  /**
1371   * {@inheritDoc}
1372   */
1373  @Override()
1374  protected void doHealthCheck()
1375  {
1376    final Iterator<Map.Entry<Thread,LDAPConnection>> iterator =
1377         connections.entrySet().iterator();
1378    while (iterator.hasNext())
1379    {
1380      final Map.Entry<Thread,LDAPConnection> e = iterator.next();
1381      final Thread                           t = e.getKey();
1382      final LDAPConnection                   c = e.getValue();
1383
1384      if (! t.isAlive())
1385      {
1386        c.setDisconnectInfo(DisconnectType.POOLED_CONNECTION_UNNEEDED, null,
1387                            null);
1388        c.terminate(null);
1389        iterator.remove();
1390      }
1391    }
1392  }
1393
1394
1395
1396  /**
1397   * {@inheritDoc}
1398   */
1399  @Override()
1400  public int getCurrentAvailableConnections()
1401  {
1402    return -1;
1403  }
1404
1405
1406
1407  /**
1408   * {@inheritDoc}
1409   */
1410  @Override()
1411  public int getMaximumAvailableConnections()
1412  {
1413    return -1;
1414  }
1415
1416
1417
1418  /**
1419   * {@inheritDoc}
1420   */
1421  @Override()
1422  public LDAPConnectionPoolStatistics getConnectionPoolStatistics()
1423  {
1424    return poolStatistics;
1425  }
1426
1427
1428
1429  /**
1430   * Closes this connection pool in the event that it becomes unreferenced.
1431   *
1432   * @throws  Throwable  If an unexpected problem occurs.
1433   */
1434  @Override()
1435  protected void finalize()
1436            throws Throwable
1437  {
1438    super.finalize();
1439
1440    close();
1441  }
1442
1443
1444
1445  /**
1446   * {@inheritDoc}
1447   */
1448  @Override()
1449  public void toString(final StringBuilder buffer)
1450  {
1451    buffer.append("LDAPThreadLocalConnectionPool(");
1452
1453    final String name = connectionPoolName;
1454    if (name != null)
1455    {
1456      buffer.append("name='");
1457      buffer.append(name);
1458      buffer.append("', ");
1459    }
1460
1461    buffer.append("serverSet=");
1462    serverSet.toString(buffer);
1463    buffer.append(')');
1464  }
1465}