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