001/*
002 * Copyright 2009-2018 Ping Identity Corporation
003 * All Rights Reserved.
004 */
005/*
006 * Copyright (C) 2009-2018 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<>(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<>(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        throw new LDAPException(ResultCode.LOCAL_ERROR,
771             ERR_POOL_OP_EXCEPTION.get(StaticUtils.getExceptionMessage(t)), t);
772      }
773    }
774
775
776    // If we've gotten here, then the bind operation should be re-tried on a
777    // newly-established connection.
778    conn = replaceDefunctConnection(conn);
779
780    try
781    {
782      final BindResult result = conn.bind(bindRequest);
783      releaseAndReAuthenticateConnection(conn);
784      return result;
785    }
786    catch (final Throwable t)
787    {
788      Debug.debugException(t);
789
790      if (t instanceof LDAPException)
791      {
792        final LDAPException le = (LDAPException) t;
793
794        try
795        {
796          healthCheck.ensureConnectionValidAfterException(conn, le);
797          releaseAndReAuthenticateConnection(conn);
798        }
799        catch (final Exception e)
800        {
801          Debug.debugException(e);
802          releaseDefunctConnection(conn);
803        }
804
805        throw le;
806      }
807      else
808      {
809        releaseDefunctConnection(conn);
810        throw new LDAPException(ResultCode.LOCAL_ERROR,
811             ERR_POOL_OP_EXCEPTION.get(StaticUtils.getExceptionMessage(t)), t);
812      }
813    }
814  }
815
816
817
818  /**
819   * {@inheritDoc}
820   */
821  @Override()
822  public LDAPConnection getConnection()
823         throws LDAPException
824  {
825    final Thread t = Thread.currentThread();
826    LDAPConnection conn = connections.get(t);
827
828    if (closed)
829    {
830      if (conn != null)
831      {
832        conn.terminate(null);
833        connections.remove(t);
834      }
835
836      poolStatistics.incrementNumFailedCheckouts();
837      throw new LDAPException(ResultCode.CONNECT_ERROR,
838                              ERR_POOL_CLOSED.get());
839    }
840
841    boolean created = false;
842    if ((conn == null) || (! conn.isConnected()))
843    {
844      conn = createConnection();
845      connections.put(t, conn);
846      created = true;
847    }
848
849    try
850    {
851      healthCheck.ensureConnectionValidForCheckout(conn);
852      if (created)
853      {
854        poolStatistics.incrementNumSuccessfulCheckoutsNewConnection();
855      }
856      else
857      {
858        poolStatistics.incrementNumSuccessfulCheckoutsWithoutWaiting();
859      }
860      return conn;
861    }
862    catch (final LDAPException le)
863    {
864      Debug.debugException(le);
865
866      conn.setClosed();
867      connections.remove(t);
868
869      if (created)
870      {
871        poolStatistics.incrementNumFailedCheckouts();
872        throw le;
873      }
874    }
875
876    try
877    {
878      conn = createConnection();
879      healthCheck.ensureConnectionValidForCheckout(conn);
880      connections.put(t, conn);
881      poolStatistics.incrementNumSuccessfulCheckoutsNewConnection();
882      return conn;
883    }
884    catch (final LDAPException le)
885    {
886      Debug.debugException(le);
887
888      poolStatistics.incrementNumFailedCheckouts();
889
890      if (conn != null)
891      {
892        conn.setClosed();
893      }
894
895      throw le;
896    }
897  }
898
899
900
901  /**
902   * {@inheritDoc}
903   */
904  @Override()
905  public void releaseConnection(final LDAPConnection connection)
906  {
907    if (connection == null)
908    {
909      return;
910    }
911
912    connection.setConnectionPoolName(connectionPoolName);
913    if (connectionIsExpired(connection))
914    {
915      try
916      {
917        final LDAPConnection newConnection = createConnection();
918        connections.put(Thread.currentThread(), newConnection);
919
920        connection.setDisconnectInfo(DisconnectType.POOLED_CONNECTION_EXPIRED,
921             null, null);
922        connection.terminate(null);
923        poolStatistics.incrementNumConnectionsClosedExpired();
924        lastExpiredDisconnectTime = System.currentTimeMillis();
925      }
926      catch (final LDAPException le)
927      {
928        Debug.debugException(le);
929      }
930    }
931
932    try
933    {
934      healthCheck.ensureConnectionValidForRelease(connection);
935    }
936    catch (final LDAPException le)
937    {
938      releaseDefunctConnection(connection);
939      return;
940    }
941
942    poolStatistics.incrementNumReleasedValid();
943
944    if (closed)
945    {
946      close();
947    }
948  }
949
950
951
952  /**
953   * Performs a bind on the provided connection before releasing it back to the
954   * pool, so that it will be authenticated as the same user as
955   * newly-established connections.  If newly-established connections are
956   * unauthenticated, then this method will perform an anonymous simple bind to
957   * ensure that the resulting connection is unauthenticated.
958   *
959   * Releases the provided connection back to this pool.
960   *
961   * @param  connection  The connection to be released back to the pool after
962   *                     being re-authenticated.
963   */
964  public void releaseAndReAuthenticateConnection(
965       final LDAPConnection connection)
966  {
967    if (connection == null)
968    {
969      return;
970    }
971
972    try
973    {
974      BindResult bindResult;
975      try
976      {
977        if (bindRequest == null)
978        {
979          bindResult = connection.bind("", "");
980        }
981        else
982        {
983          bindResult = connection.bind(bindRequest.duplicate());
984        }
985      }
986      catch (final LDAPBindException lbe)
987      {
988        Debug.debugException(lbe);
989        bindResult = lbe.getBindResult();
990      }
991
992      try
993      {
994        healthCheck.ensureConnectionValidAfterAuthentication(connection,
995             bindResult);
996        if (bindResult.getResultCode() != ResultCode.SUCCESS)
997        {
998          throw new LDAPBindException(bindResult);
999        }
1000      }
1001      catch (final LDAPException le)
1002      {
1003        Debug.debugException(le);
1004
1005        try
1006        {
1007          connection.setDisconnectInfo(DisconnectType.BIND_FAILED, null, le);
1008          connection.terminate(null);
1009          releaseDefunctConnection(connection);
1010        }
1011        catch (final Exception e)
1012        {
1013          Debug.debugException(e);
1014        }
1015
1016        throw le;
1017      }
1018
1019      releaseConnection(connection);
1020    }
1021    catch (final Exception e)
1022    {
1023      Debug.debugException(e);
1024      releaseDefunctConnection(connection);
1025    }
1026  }
1027
1028
1029
1030  /**
1031   * {@inheritDoc}
1032   */
1033  @Override()
1034  public void releaseDefunctConnection(final LDAPConnection connection)
1035  {
1036    if (connection == null)
1037    {
1038      return;
1039    }
1040
1041    connection.setConnectionPoolName(connectionPoolName);
1042    poolStatistics.incrementNumConnectionsClosedDefunct();
1043    handleDefunctConnection(connection);
1044  }
1045
1046
1047
1048  /**
1049   * Performs the real work of terminating a defunct connection and replacing it
1050   * with a new connection if possible.
1051   *
1052   * @param  connection  The defunct connection to be replaced.
1053   */
1054  private void handleDefunctConnection(final LDAPConnection connection)
1055  {
1056    final Thread t = Thread.currentThread();
1057
1058    connection.setDisconnectInfo(DisconnectType.POOLED_CONNECTION_DEFUNCT, null,
1059                                 null);
1060    connection.setClosed();
1061    connections.remove(t);
1062
1063    if (closed)
1064    {
1065      return;
1066    }
1067
1068    try
1069    {
1070      final LDAPConnection conn = createConnection();
1071      connections.put(t, conn);
1072    }
1073    catch (final LDAPException le)
1074    {
1075      Debug.debugException(le);
1076    }
1077  }
1078
1079
1080
1081  /**
1082   * {@inheritDoc}
1083   */
1084  @Override()
1085  public LDAPConnection replaceDefunctConnection(
1086                             final LDAPConnection connection)
1087         throws LDAPException
1088  {
1089    poolStatistics.incrementNumConnectionsClosedDefunct();
1090    connection.setDisconnectInfo(DisconnectType.POOLED_CONNECTION_DEFUNCT, null,
1091                                 null);
1092    connection.setClosed();
1093    connections.remove(Thread.currentThread(), connection);
1094
1095    if (closed)
1096    {
1097      throw new LDAPException(ResultCode.CONNECT_ERROR, ERR_POOL_CLOSED.get());
1098    }
1099
1100    final LDAPConnection newConnection = createConnection();
1101    connections.put(Thread.currentThread(), newConnection);
1102    return newConnection;
1103  }
1104
1105
1106
1107  /**
1108   * {@inheritDoc}
1109   */
1110  @Override()
1111  public Set<OperationType> getOperationTypesToRetryDueToInvalidConnections()
1112  {
1113    return retryOperationTypes.get();
1114  }
1115
1116
1117
1118  /**
1119   * {@inheritDoc}
1120   */
1121  @Override()
1122  public void setRetryFailedOperationsDueToInvalidConnections(
1123                   final Set<OperationType> operationTypes)
1124  {
1125    if ((operationTypes == null) || operationTypes.isEmpty())
1126    {
1127      retryOperationTypes.set(
1128           Collections.unmodifiableSet(EnumSet.noneOf(OperationType.class)));
1129    }
1130    else
1131    {
1132      final EnumSet<OperationType> s = EnumSet.noneOf(OperationType.class);
1133      s.addAll(operationTypes);
1134      retryOperationTypes.set(Collections.unmodifiableSet(s));
1135    }
1136  }
1137
1138
1139
1140  /**
1141   * Indicates whether the provided connection should be considered expired.
1142   *
1143   * @param  connection  The connection for which to make the determination.
1144   *
1145   * @return  {@code true} if the provided connection should be considered
1146   *          expired, or {@code false} if not.
1147   */
1148  private boolean connectionIsExpired(final LDAPConnection connection)
1149  {
1150    // If connection expiration is not enabled, then there is nothing to do.
1151    if (maxConnectionAge <= 0L)
1152    {
1153      return false;
1154    }
1155
1156    // If there is a minimum disconnect interval, then make sure that we have
1157    // not closed another expired connection too recently.
1158    final long currentTime = System.currentTimeMillis();
1159    if ((currentTime - lastExpiredDisconnectTime) < minDisconnectInterval)
1160    {
1161      return false;
1162    }
1163
1164    // Get the age of the connection and see if it is expired.
1165    final long connectionAge = currentTime - connection.getConnectTime();
1166    return (connectionAge > maxConnectionAge);
1167  }
1168
1169
1170
1171  /**
1172   * Specifies the bind request that will be used to authenticate subsequent new
1173   * connections that are established by this connection pool.  The
1174   * authentication state for existing connections will not be altered unless
1175   * one of the {@code bindAndRevertAuthentication} or
1176   * {@code releaseAndReAuthenticateConnection} methods are invoked on those
1177   * connections.
1178   *
1179   * @param  bindRequest  The bind request that will be used to authenticate new
1180   *                      connections that are established by this pool, or
1181   *                      that will be applied to existing connections via the
1182   *                      {@code bindAndRevertAuthentication} or
1183   *                      {@code releaseAndReAuthenticateConnection} method.  It
1184   *                      may be {@code null} if new connections should be
1185   *                      unauthenticated.
1186   */
1187  public void setBindRequest(final BindRequest bindRequest)
1188  {
1189    this.bindRequest = bindRequest;
1190  }
1191
1192
1193
1194  /**
1195   * Specifies the server set that should be used to establish new connections
1196   * for use in this connection pool.  Existing connections will not be
1197   * affected.
1198   *
1199   * @param  serverSet  The server set that should be used to establish new
1200   *                    connections for use in this connection pool.  It must
1201   *                    not be {@code null}.
1202   */
1203  public void setServerSet(final ServerSet serverSet)
1204  {
1205    Validator.ensureNotNull(serverSet);
1206    this.serverSet = serverSet;
1207  }
1208
1209
1210
1211  /**
1212   * {@inheritDoc}
1213   */
1214  @Override()
1215  public String getConnectionPoolName()
1216  {
1217    return connectionPoolName;
1218  }
1219
1220
1221
1222  /**
1223   * {@inheritDoc}
1224   */
1225  @Override()
1226  public void setConnectionPoolName(final String connectionPoolName)
1227  {
1228    this.connectionPoolName = connectionPoolName;
1229  }
1230
1231
1232
1233  /**
1234   * Retrieves the maximum length of time in milliseconds that a connection in
1235   * this pool may be established before it is closed and replaced with another
1236   * connection.
1237   *
1238   * @return  The maximum length of time in milliseconds that a connection in
1239   *          this pool may be established before it is closed and replaced with
1240   *          another connection, or {@code 0L} if no maximum age should be
1241   *          enforced.
1242   */
1243  public long getMaxConnectionAgeMillis()
1244  {
1245    return maxConnectionAge;
1246  }
1247
1248
1249
1250  /**
1251   * Specifies the maximum length of time in milliseconds that a connection in
1252   * this pool may be established before it should be closed and replaced with
1253   * another connection.
1254   *
1255   * @param  maxConnectionAge  The maximum length of time in milliseconds that a
1256   *                           connection in this pool may be established before
1257   *                           it should be closed and replaced with another
1258   *                           connection.  A value of zero indicates that no
1259   *                           maximum age should be enforced.
1260   */
1261  public void setMaxConnectionAgeMillis(final long maxConnectionAge)
1262  {
1263    if (maxConnectionAge > 0L)
1264    {
1265      this.maxConnectionAge = maxConnectionAge;
1266    }
1267    else
1268    {
1269      this.maxConnectionAge = 0L;
1270    }
1271  }
1272
1273
1274
1275  /**
1276   * Retrieves the minimum length of time in milliseconds that should pass
1277   * between connections closed because they have been established for longer
1278   * than the maximum connection age.
1279   *
1280   * @return  The minimum length of time in milliseconds that should pass
1281   *          between connections closed because they have been established for
1282   *          longer than the maximum connection age, or {@code 0L} if expired
1283   *          connections may be closed as quickly as they are identified.
1284   */
1285  public long getMinDisconnectIntervalMillis()
1286  {
1287    return minDisconnectInterval;
1288  }
1289
1290
1291
1292  /**
1293   * Specifies the minimum length of time in milliseconds that should pass
1294   * between connections closed because they have been established for longer
1295   * than the maximum connection age.
1296   *
1297   * @param  minDisconnectInterval  The minimum length of time in milliseconds
1298   *                                that should pass between connections closed
1299   *                                because they have been established for
1300   *                                longer than the maximum connection age.  A
1301   *                                value less than or equal to zero indicates
1302   *                                that no minimum time should be enforced.
1303   */
1304  public void setMinDisconnectIntervalMillis(final long minDisconnectInterval)
1305  {
1306    if (minDisconnectInterval > 0)
1307    {
1308      this.minDisconnectInterval = minDisconnectInterval;
1309    }
1310    else
1311    {
1312      this.minDisconnectInterval = 0L;
1313    }
1314  }
1315
1316
1317
1318  /**
1319   * {@inheritDoc}
1320   */
1321  @Override()
1322  public LDAPConnectionPoolHealthCheck getHealthCheck()
1323  {
1324    return healthCheck;
1325  }
1326
1327
1328
1329  /**
1330   * Sets the health check implementation for this connection pool.
1331   *
1332   * @param  healthCheck  The health check implementation for this connection
1333   *                      pool.  It must not be {@code null}.
1334   */
1335  public void setHealthCheck(final LDAPConnectionPoolHealthCheck healthCheck)
1336  {
1337    Validator.ensureNotNull(healthCheck);
1338    this.healthCheck = healthCheck;
1339  }
1340
1341
1342
1343  /**
1344   * {@inheritDoc}
1345   */
1346  @Override()
1347  public long getHealthCheckIntervalMillis()
1348  {
1349    return healthCheckInterval;
1350  }
1351
1352
1353
1354  /**
1355   * {@inheritDoc}
1356   */
1357  @Override()
1358  public void setHealthCheckIntervalMillis(final long healthCheckInterval)
1359  {
1360    Validator.ensureTrue(healthCheckInterval > 0L,
1361         "LDAPConnectionPool.healthCheckInterval must be greater than 0.");
1362    this.healthCheckInterval = healthCheckInterval;
1363    healthCheckThread.wakeUp();
1364  }
1365
1366
1367
1368  /**
1369   * {@inheritDoc}
1370   */
1371  @Override()
1372  protected void doHealthCheck()
1373  {
1374    final Iterator<Map.Entry<Thread,LDAPConnection>> iterator =
1375         connections.entrySet().iterator();
1376    while (iterator.hasNext())
1377    {
1378      final Map.Entry<Thread,LDAPConnection> e = iterator.next();
1379      final Thread                           t = e.getKey();
1380      final LDAPConnection                   c = e.getValue();
1381
1382      if (! t.isAlive())
1383      {
1384        c.setDisconnectInfo(DisconnectType.POOLED_CONNECTION_UNNEEDED, null,
1385                            null);
1386        c.terminate(null);
1387        iterator.remove();
1388      }
1389    }
1390  }
1391
1392
1393
1394  /**
1395   * {@inheritDoc}
1396   */
1397  @Override()
1398  public int getCurrentAvailableConnections()
1399  {
1400    return -1;
1401  }
1402
1403
1404
1405  /**
1406   * {@inheritDoc}
1407   */
1408  @Override()
1409  public int getMaximumAvailableConnections()
1410  {
1411    return -1;
1412  }
1413
1414
1415
1416  /**
1417   * {@inheritDoc}
1418   */
1419  @Override()
1420  public LDAPConnectionPoolStatistics getConnectionPoolStatistics()
1421  {
1422    return poolStatistics;
1423  }
1424
1425
1426
1427  /**
1428   * Closes this connection pool in the event that it becomes unreferenced.
1429   *
1430   * @throws  Throwable  If an unexpected problem occurs.
1431   */
1432  @Override()
1433  protected void finalize()
1434            throws Throwable
1435  {
1436    super.finalize();
1437
1438    close();
1439  }
1440
1441
1442
1443  /**
1444   * {@inheritDoc}
1445   */
1446  @Override()
1447  public void toString(final StringBuilder buffer)
1448  {
1449    buffer.append("LDAPThreadLocalConnectionPool(");
1450
1451    final String name = connectionPoolName;
1452    if (name != null)
1453    {
1454      buffer.append("name='");
1455      buffer.append(name);
1456      buffer.append("', ");
1457    }
1458
1459    buffer.append("serverSet=");
1460    serverSet.toString(buffer);
1461    buffer.append(')');
1462  }
1463}