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