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