001/*
002 * Copyright 2009-2024 Ping Identity Corporation
003 * All Rights Reserved.
004 */
005/*
006 * Copyright 2009-2024 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-2024 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    // Auto-reconnect must be disabled for pooled connections, so turn it off
420    // if the associated connection options have it enabled for some reason.
421    LDAPConnectionOptions opts = c.getConnectionOptions();
422    if (opts.autoReconnect())
423    {
424      opts = opts.duplicate();
425      opts.setAutoReconnect(false);
426      c.setConnectionOptions(opts);
427    }
428
429
430    // Invoke pre-authentication post-connect processing.
431    if (postConnectProcessor != null)
432    {
433      try
434      {
435        postConnectProcessor.processPreAuthenticatedConnection(c);
436      }
437      catch (final Exception e)
438      {
439        Debug.debugException(e);
440
441        try
442        {
443          poolStatistics.incrementNumFailedConnectionAttempts();
444          Debug.debugConnectionPool(Level.SEVERE, this, c,
445               "Exception in pre-authentication post-connect processing", e);
446          c.setDisconnectInfo(DisconnectType.POOL_CREATION_FAILURE, null, e);
447          c.setClosed();
448        }
449        catch (final Exception e2)
450        {
451          Debug.debugException(e2);
452        }
453
454        if (e instanceof LDAPException)
455        {
456          throw ((LDAPException) e);
457        }
458        else
459        {
460          throw new LDAPException(ResultCode.CONNECT_ERROR,
461               ERR_POOL_POST_CONNECT_ERROR.get(
462                    StaticUtils.getExceptionMessage(e)),
463               e);
464        }
465      }
466    }
467
468
469    // Authenticate the connection if appropriate.
470    if ((bindRequest != null) && (! serverSet.includesAuthentication()))
471    {
472      BindResult bindResult;
473      try
474      {
475        bindResult = c.bind(bindRequest.duplicate());
476      }
477      catch (final LDAPBindException lbe)
478      {
479        Debug.debugException(lbe);
480        bindResult = lbe.getBindResult();
481      }
482      catch (final LDAPException le)
483      {
484        Debug.debugException(le);
485        bindResult = new BindResult(le);
486      }
487
488      try
489      {
490        healthCheck.ensureConnectionValidAfterAuthentication(c, bindResult);
491        if (bindResult.getResultCode() != ResultCode.SUCCESS)
492        {
493          throw new LDAPBindException(bindResult);
494        }
495      }
496      catch (final LDAPException le)
497      {
498        Debug.debugException(le);
499
500        try
501        {
502          poolStatistics.incrementNumFailedConnectionAttempts();
503          if (bindResult.getResultCode() != ResultCode.SUCCESS)
504          {
505            Debug.debugConnectionPool(Level.SEVERE, this, c,
506                 "Failed to authenticate a new pooled connection", le);
507          }
508          else
509          {
510            Debug.debugConnectionPool(Level.SEVERE, this, c,
511                 "A new pooled connection failed its post-authentication " +
512                      "health check",
513                 le);
514          }
515          c.setDisconnectInfo(DisconnectType.BIND_FAILED, null, le);
516          c.setClosed();
517        }
518        catch (final Exception e)
519        {
520          Debug.debugException(e);
521        }
522
523        throw le;
524      }
525    }
526
527
528    // Invoke post-authentication post-connect processing.
529    if (postConnectProcessor != null)
530    {
531      try
532      {
533        postConnectProcessor.processPostAuthenticatedConnection(c);
534      }
535      catch (final Exception e)
536      {
537        Debug.debugException(e);
538        try
539        {
540          poolStatistics.incrementNumFailedConnectionAttempts();
541          Debug.debugConnectionPool(Level.SEVERE, this, c,
542               "Exception in post-authentication post-connect processing", e);
543          c.setDisconnectInfo(DisconnectType.POOL_CREATION_FAILURE, null, e);
544          c.setClosed();
545        }
546        catch (final Exception e2)
547        {
548          Debug.debugException(e2);
549        }
550
551        if (e instanceof LDAPException)
552        {
553          throw ((LDAPException) e);
554        }
555        else
556        {
557          throw new LDAPException(ResultCode.CONNECT_ERROR,
558               ERR_POOL_POST_CONNECT_ERROR.get(
559                    StaticUtils.getExceptionMessage(e)),
560               e);
561        }
562      }
563    }
564
565
566    // Get the pooled schema if appropriate.
567    if (opts.usePooledSchema())
568    {
569      final long currentTime = System.currentTimeMillis();
570      if ((pooledSchema == null) || (currentTime > pooledSchema.getFirst()))
571      {
572        try
573        {
574          final Schema schema = c.getSchema();
575          if (schema != null)
576          {
577            c.setCachedSchema(schema);
578
579            final long timeout = opts.getPooledSchemaTimeoutMillis();
580            if ((timeout <= 0L) || (currentTime + timeout <= 0L))
581            {
582              pooledSchema = new ObjectPair<>(Long.MAX_VALUE, schema);
583            }
584            else
585            {
586              pooledSchema = new ObjectPair<>((currentTime+timeout), schema);
587            }
588          }
589        }
590        catch (final Exception e)
591        {
592          Debug.debugException(e);
593
594          // There was a problem retrieving the schema from the server, but if
595          // we have an earlier copy then we can assume it's still valid.
596          if (pooledSchema != null)
597          {
598            c.setCachedSchema(pooledSchema.getSecond());
599          }
600        }
601      }
602      else
603      {
604        c.setCachedSchema(pooledSchema.getSecond());
605      }
606    }
607
608
609    // Finish setting up the connection.
610    c.setConnectionPoolName(connectionPoolName);
611    poolStatistics.incrementNumSuccessfulConnectionAttempts();
612    Debug.debugConnectionPool(Level.INFO, this, c,
613         "Successfully created a new pooled connection", null);
614
615    return c;
616  }
617
618
619
620  /**
621   * {@inheritDoc}
622   */
623  @Override()
624  public void close()
625  {
626    close(true, 1);
627  }
628
629
630
631  /**
632   * {@inheritDoc}
633   */
634  @Override()
635  public void close(final boolean unbind, final int numThreads)
636  {
637    try
638    {
639      final boolean healthCheckThreadAlreadySignaled = closed;
640      closed = true;
641      healthCheckThread.stopRunning(! healthCheckThreadAlreadySignaled);
642
643      try
644      {
645        serverSet.shutDown();
646      }
647      catch (final Exception e)
648      {
649        Debug.debugException(e);
650      }
651
652      if (numThreads > 1)
653      {
654        final ArrayList<LDAPConnection> connList =
655             new ArrayList<>(connections.size());
656        final Iterator<LDAPConnection> iterator =
657             connections.values().iterator();
658        while (iterator.hasNext())
659        {
660          connList.add(iterator.next());
661          iterator.remove();
662        }
663
664        if (! connList.isEmpty())
665        {
666          final ParallelPoolCloser closer =
667               new ParallelPoolCloser(connList, unbind, numThreads);
668          closer.closeConnections();
669        }
670      }
671      else
672      {
673        final Iterator<Map.Entry<Thread,LDAPConnection>> iterator =
674             connections.entrySet().iterator();
675        while (iterator.hasNext())
676        {
677          final LDAPConnection conn = iterator.next().getValue();
678          iterator.remove();
679
680          poolStatistics.incrementNumConnectionsClosedUnneeded();
681          Debug.debugConnectionPool(Level.INFO, this, conn,
682               "Closed a connection as part of closing the connection pool",
683               null);
684          conn.setDisconnectInfo(DisconnectType.POOL_CLOSED, null, null);
685          if (unbind)
686          {
687            conn.terminate(null);
688          }
689          else
690          {
691            conn.setClosed();
692          }
693        }
694      }
695    }
696    finally
697    {
698      Debug.debugConnectionPool(Level.INFO, this, null,
699           "Closed the connection pool", null);
700    }
701  }
702
703
704
705  /**
706   * {@inheritDoc}
707   */
708  @Override()
709  public boolean isClosed()
710  {
711    return closed;
712  }
713
714
715
716  /**
717   * Processes a simple bind using a connection from this connection pool, and
718   * then reverts that authentication by re-binding as the same user used to
719   * authenticate new connections.  If new connections are unauthenticated, then
720   * the subsequent bind will be an anonymous simple bind.  This method attempts
721   * to ensure that processing the provided bind operation does not have a
722   * lasting impact the authentication state of the connection used to process
723   * it.
724   * <BR><BR>
725   * If the second bind attempt (the one used to restore the authentication
726   * identity) fails, the connection will be closed as defunct so that a new
727   * connection will be created to take its place.
728   *
729   * @param  bindDN    The bind DN for the simple bind request.
730   * @param  password  The password for the simple bind request.
731   * @param  controls  The optional set of controls for the simple bind request.
732   *
733   * @return  The result of processing the provided bind operation.
734   *
735   * @throws  LDAPException  If the server rejects the bind request, or if a
736   *                         problem occurs while sending the request or reading
737   *                         the response.
738   */
739  @NotNull()
740  public BindResult bindAndRevertAuthentication(@Nullable final String bindDN,
741                         @Nullable final String password,
742                         @Nullable final Control... controls)
743         throws LDAPException
744  {
745    return bindAndRevertAuthentication(
746         new SimpleBindRequest(bindDN, password, controls));
747  }
748
749
750
751  /**
752   * Processes the provided bind request using a connection from this connection
753   * pool, and then reverts that authentication by re-binding as the same user
754   * used to authenticate new connections.  If new connections are
755   * unauthenticated, then the subsequent bind will be an anonymous simple bind.
756   * This method attempts to ensure that processing the provided bind operation
757   * does not have a lasting impact the authentication state of the connection
758   * used to process it.
759   * <BR><BR>
760   * If the second bind attempt (the one used to restore the authentication
761   * identity) fails, the connection will be closed as defunct so that a new
762   * connection will be created to take its place.
763   *
764   * @param  bindRequest  The bind request to be processed.  It must not be
765   *                      {@code null}.
766   *
767   * @return  The result of processing the provided bind operation.
768   *
769   * @throws  LDAPException  If the server rejects the bind request, or if a
770   *                         problem occurs while sending the request or reading
771   *                         the response.
772   */
773  @NotNull()
774  public BindResult bindAndRevertAuthentication(
775                         @NotNull final BindRequest bindRequest)
776         throws LDAPException
777  {
778    LDAPConnection conn = getConnection();
779
780    try
781    {
782      final BindResult result = conn.bind(bindRequest);
783      releaseAndReAuthenticateConnection(conn);
784      return result;
785    }
786    catch (final Throwable t)
787    {
788      Debug.debugException(t);
789
790      if (t instanceof LDAPException)
791      {
792        final LDAPException le = (LDAPException) t;
793
794        boolean shouldThrow;
795        try
796        {
797          healthCheck.ensureConnectionValidAfterException(conn, le);
798
799          // The above call will throw an exception if the connection doesn't
800          // seem to be valid, so if we've gotten here then we should assume
801          // that it is valid and we will pass the exception onto the client
802          // without retrying the operation.
803          releaseAndReAuthenticateConnection(conn);
804          shouldThrow = true;
805        }
806        catch (final Exception e)
807        {
808          Debug.debugException(e);
809
810          // This implies that the connection is not valid.  If the pool is
811          // configured to re-try bind operations on a newly-established
812          // connection, then that will be done later in this method.
813          // Otherwise, release the connection as defunct and pass the bind
814          // exception onto the client.
815          if (! getOperationTypesToRetryDueToInvalidConnections().contains(
816                     OperationType.BIND))
817          {
818            releaseDefunctConnection(conn);
819            shouldThrow = true;
820          }
821          else
822          {
823            shouldThrow = false;
824          }
825        }
826
827        if (shouldThrow)
828        {
829          throw le;
830        }
831      }
832      else
833      {
834        releaseDefunctConnection(conn);
835        StaticUtils.rethrowIfError(t);
836        throw new LDAPException(ResultCode.LOCAL_ERROR,
837             ERR_POOL_OP_EXCEPTION.get(StaticUtils.getExceptionMessage(t)), t);
838      }
839    }
840
841
842    // If we've gotten here, then the bind operation should be re-tried on a
843    // newly-established connection.
844    conn = replaceDefunctConnection(conn);
845
846    try
847    {
848      final BindResult result = conn.bind(bindRequest);
849      releaseAndReAuthenticateConnection(conn);
850      return result;
851    }
852    catch (final Throwable t)
853    {
854      Debug.debugException(t);
855
856      if (t instanceof LDAPException)
857      {
858        final LDAPException le = (LDAPException) t;
859
860        try
861        {
862          healthCheck.ensureConnectionValidAfterException(conn, le);
863          releaseAndReAuthenticateConnection(conn);
864        }
865        catch (final Exception e)
866        {
867          Debug.debugException(e);
868          releaseDefunctConnection(conn);
869        }
870
871        throw le;
872      }
873      else
874      {
875        releaseDefunctConnection(conn);
876        StaticUtils.rethrowIfError(t);
877        throw new LDAPException(ResultCode.LOCAL_ERROR,
878             ERR_POOL_OP_EXCEPTION.get(StaticUtils.getExceptionMessage(t)), t);
879      }
880    }
881  }
882
883
884
885  /**
886   * {@inheritDoc}
887   */
888  @Override()
889  @NotNull()
890  public LDAPConnection getConnection()
891         throws LDAPException
892  {
893    final Thread t = Thread.currentThread();
894    LDAPConnection conn = connections.get(t);
895
896    if (closed)
897    {
898      if (conn != null)
899      {
900        conn.terminate(null);
901        connections.remove(t);
902      }
903
904      poolStatistics.incrementNumFailedCheckouts();
905      Debug.debugConnectionPool(Level.SEVERE, this, null,
906           "Failed to get a connection to a closed connection pool", null);
907      throw new LDAPException(ResultCode.CONNECT_ERROR,
908                              ERR_POOL_CLOSED.get());
909    }
910
911    boolean created = false;
912    if ((conn == null) || (! conn.isConnected()))
913    {
914      conn = createConnection();
915      connections.put(t, conn);
916      created = true;
917    }
918
919    try
920    {
921      healthCheck.ensureConnectionValidForCheckout(conn);
922      if (created)
923      {
924        poolStatistics.incrementNumSuccessfulCheckoutsNewConnection();
925        Debug.debugConnectionPool(Level.INFO, this, conn,
926             "Checked out a newly created pooled connection", null);
927      }
928      else
929      {
930        poolStatistics.incrementNumSuccessfulCheckoutsWithoutWaiting();
931        Debug.debugConnectionPool(Level.INFO, this, conn,
932             "Checked out an existing pooled connection", null);
933      }
934      return conn;
935    }
936    catch (final LDAPException le)
937    {
938      Debug.debugException(le);
939
940      conn.setClosed();
941      connections.remove(t);
942
943      if (created)
944      {
945        poolStatistics.incrementNumFailedCheckouts();
946        Debug.debugConnectionPool(Level.SEVERE, this, conn,
947             "Failed to check out a connection because a newly created " +
948                  "connection failed the checkout health check",
949             le);
950        throw le;
951      }
952    }
953
954    try
955    {
956      conn = createConnection();
957      healthCheck.ensureConnectionValidForCheckout(conn);
958      connections.put(t, conn);
959      poolStatistics.incrementNumSuccessfulCheckoutsNewConnection();
960      Debug.debugConnectionPool(Level.INFO, this, conn,
961           "Checked out a newly created pooled connection", null);
962      return conn;
963    }
964    catch (final LDAPException le)
965    {
966      Debug.debugException(le);
967
968      poolStatistics.incrementNumFailedCheckouts();
969      if (conn == null)
970      {
971        Debug.debugConnectionPool(Level.SEVERE, this, conn,
972             "Unable to check out a connection because an error occurred " +
973                  "while establishing the connection",
974             le);
975      }
976      else
977      {
978        Debug.debugConnectionPool(Level.SEVERE, this, conn,
979             "Unable to check out a newly created connection because it " +
980                  "failed the checkout health check",
981             le);
982        conn.setClosed();
983      }
984
985      throw le;
986    }
987  }
988
989
990
991  /**
992   * {@inheritDoc}
993   */
994  @Override()
995  public void releaseConnection(@NotNull final LDAPConnection connection)
996  {
997    if (connection == null)
998    {
999      return;
1000    }
1001
1002    connection.setConnectionPoolName(connectionPoolName);
1003    if (connectionIsExpired(connection))
1004    {
1005      try
1006      {
1007        final LDAPConnection newConnection = createConnection();
1008        connections.put(Thread.currentThread(), newConnection);
1009
1010        connection.setDisconnectInfo(DisconnectType.POOLED_CONNECTION_EXPIRED,
1011             null, null);
1012        connection.terminate(null);
1013        poolStatistics.incrementNumConnectionsClosedExpired();
1014        Debug.debugConnectionPool(Level.WARNING, this, connection,
1015             "Closing a released connection because it is expired", null);
1016        lastExpiredDisconnectTime = System.currentTimeMillis();
1017      }
1018      catch (final LDAPException le)
1019      {
1020        Debug.debugException(le);
1021      }
1022    }
1023
1024    try
1025    {
1026      healthCheck.ensureConnectionValidForRelease(connection);
1027    }
1028    catch (final LDAPException le)
1029    {
1030      releaseDefunctConnection(connection);
1031      return;
1032    }
1033
1034    poolStatistics.incrementNumReleasedValid();
1035    Debug.debugConnectionPool(Level.INFO, this, connection,
1036         "Released a connection back to the pool", null);
1037
1038    if (closed)
1039    {
1040      close();
1041    }
1042  }
1043
1044
1045
1046  /**
1047   * Performs a bind on the provided connection before releasing it back to the
1048   * pool, so that it will be authenticated as the same user as
1049   * newly-established connections.  If newly-established connections are
1050   * unauthenticated, then this method will perform an anonymous simple bind to
1051   * ensure that the resulting connection is unauthenticated.
1052   *
1053   * Releases the provided connection back to this pool.
1054   *
1055   * @param  connection  The connection to be released back to the pool after
1056   *                     being re-authenticated.
1057   */
1058  public void releaseAndReAuthenticateConnection(
1059                   @NotNull final LDAPConnection connection)
1060  {
1061    if (connection == null)
1062    {
1063      return;
1064    }
1065
1066    try
1067    {
1068      BindResult bindResult;
1069      try
1070      {
1071        if (bindRequest == null)
1072        {
1073          bindResult = connection.bind("", "");
1074        }
1075        else
1076        {
1077          bindResult = connection.bind(bindRequest.duplicate());
1078        }
1079      }
1080      catch (final LDAPBindException lbe)
1081      {
1082        Debug.debugException(lbe);
1083        bindResult = lbe.getBindResult();
1084      }
1085
1086      try
1087      {
1088        healthCheck.ensureConnectionValidAfterAuthentication(connection,
1089             bindResult);
1090        if (bindResult.getResultCode() != ResultCode.SUCCESS)
1091        {
1092          throw new LDAPBindException(bindResult);
1093        }
1094      }
1095      catch (final LDAPException le)
1096      {
1097        Debug.debugException(le);
1098
1099        try
1100        {
1101          connection.setDisconnectInfo(DisconnectType.BIND_FAILED, null, le);
1102          connection.terminate(null);
1103          releaseDefunctConnection(connection);
1104        }
1105        catch (final Exception e)
1106        {
1107          Debug.debugException(e);
1108        }
1109
1110        throw le;
1111      }
1112
1113      releaseConnection(connection);
1114    }
1115    catch (final Exception e)
1116    {
1117      Debug.debugException(e);
1118      releaseDefunctConnection(connection);
1119    }
1120  }
1121
1122
1123
1124  /**
1125   * {@inheritDoc}
1126   */
1127  @Override()
1128  public void releaseDefunctConnection(@NotNull final LDAPConnection connection)
1129  {
1130    if (connection == null)
1131    {
1132      return;
1133    }
1134
1135    connection.setConnectionPoolName(connectionPoolName);
1136    poolStatistics.incrementNumConnectionsClosedDefunct();
1137    Debug.debugConnectionPool(Level.WARNING, this, connection,
1138         "Releasing a defunct connection", null);
1139    handleDefunctConnection(connection);
1140  }
1141
1142
1143
1144  /**
1145   * Performs the real work of terminating a defunct connection and replacing it
1146   * with a new connection if possible.
1147   *
1148   * @param  connection  The defunct connection to be replaced.
1149   */
1150  private void handleDefunctConnection(@NotNull final LDAPConnection connection)
1151  {
1152    final Thread t = Thread.currentThread();
1153
1154    connection.setDisconnectInfo(DisconnectType.POOLED_CONNECTION_DEFUNCT, null,
1155                                 null);
1156    connection.setClosed();
1157    connections.remove(t);
1158
1159    if (closed)
1160    {
1161      return;
1162    }
1163
1164    try
1165    {
1166      final LDAPConnection conn = createConnection();
1167      connections.put(t, conn);
1168    }
1169    catch (final LDAPException le)
1170    {
1171      Debug.debugException(le);
1172    }
1173  }
1174
1175
1176
1177  /**
1178   * {@inheritDoc}
1179   */
1180  @Override()
1181  @NotNull()
1182  public LDAPConnection replaceDefunctConnection(
1183                             @NotNull final LDAPConnection connection)
1184         throws LDAPException
1185  {
1186    poolStatistics.incrementNumConnectionsClosedDefunct();
1187    Debug.debugConnectionPool(Level.WARNING, this, connection,
1188         "Releasing a defunct connection that is to be replaced", null);
1189    connection.setDisconnectInfo(DisconnectType.POOLED_CONNECTION_DEFUNCT, null,
1190                                 null);
1191    connection.setClosed();
1192    connections.remove(Thread.currentThread(), connection);
1193
1194    if (closed)
1195    {
1196      throw new LDAPException(ResultCode.CONNECT_ERROR, ERR_POOL_CLOSED.get());
1197    }
1198
1199    final LDAPConnection newConnection = createConnection();
1200    connections.put(Thread.currentThread(), newConnection);
1201    return newConnection;
1202  }
1203
1204
1205
1206  /**
1207   * {@inheritDoc}
1208   */
1209  @Override()
1210  @NotNull()
1211  public Set<OperationType> getOperationTypesToRetryDueToInvalidConnections()
1212  {
1213    return retryOperationTypes.get();
1214  }
1215
1216
1217
1218  /**
1219   * {@inheritDoc}
1220   */
1221  @Override()
1222  public void setRetryFailedOperationsDueToInvalidConnections(
1223                   @Nullable final Set<OperationType> operationTypes)
1224  {
1225    if ((operationTypes == null) || operationTypes.isEmpty())
1226    {
1227      retryOperationTypes.set(
1228           Collections.unmodifiableSet(EnumSet.noneOf(OperationType.class)));
1229    }
1230    else
1231    {
1232      final EnumSet<OperationType> s = EnumSet.noneOf(OperationType.class);
1233      s.addAll(operationTypes);
1234      retryOperationTypes.set(Collections.unmodifiableSet(s));
1235    }
1236  }
1237
1238
1239
1240  /**
1241   * Indicates whether the provided connection should be considered expired.
1242   *
1243   * @param  connection  The connection for which to make the determination.
1244   *
1245   * @return  {@code true} if the provided connection should be considered
1246   *          expired, or {@code false} if not.
1247   */
1248  private boolean connectionIsExpired(@NotNull final LDAPConnection connection)
1249  {
1250    // If connection expiration is not enabled, then there is nothing to do.
1251    if (maxConnectionAge <= 0L)
1252    {
1253      return false;
1254    }
1255
1256    // If there is a minimum disconnect interval, then make sure that we have
1257    // not closed another expired connection too recently.
1258    final long currentTime = System.currentTimeMillis();
1259    if ((currentTime - lastExpiredDisconnectTime) < minDisconnectInterval)
1260    {
1261      return false;
1262    }
1263
1264    // Get the age of the connection and see if it is expired.
1265    final long connectionAge = currentTime - connection.getConnectTime();
1266    return (connectionAge > maxConnectionAge);
1267  }
1268
1269
1270
1271  /**
1272   * Specifies the bind request that will be used to authenticate subsequent new
1273   * connections that are established by this connection pool.  The
1274   * authentication state for existing connections will not be altered unless
1275   * one of the {@code bindAndRevertAuthentication} or
1276   * {@code releaseAndReAuthenticateConnection} methods are invoked on those
1277   * connections.
1278   *
1279   * @param  bindRequest  The bind request that will be used to authenticate new
1280   *                      connections that are established by this pool, or
1281   *                      that will be applied to existing connections via the
1282   *                      {@code bindAndRevertAuthentication} or
1283   *                      {@code releaseAndReAuthenticateConnection} method.  It
1284   *                      may be {@code null} if new connections should be
1285   *                      unauthenticated.
1286   */
1287  public void setBindRequest(@Nullable final BindRequest bindRequest)
1288  {
1289    this.bindRequest = bindRequest;
1290  }
1291
1292
1293
1294  /**
1295   * Retrieves the server set that should be used to establish new connections
1296   * for use in this connection pool.
1297   *
1298   * @return  The server set that should be used to establish new connections
1299   *          for use in this connection pool.
1300   */
1301  @NotNull()
1302  public ServerSet getServerSet()
1303  {
1304    return serverSet;
1305  }
1306
1307
1308
1309  /**
1310   * Specifies the server set that should be used to establish new connections
1311   * for use in this connection pool.  Existing connections will not be
1312   * affected.
1313   *
1314   * @param  serverSet  The server set that should be used to establish new
1315   *                    connections for use in this connection pool.  It must
1316   *                    not be {@code null}.
1317   */
1318  public void setServerSet(@NotNull final ServerSet serverSet)
1319  {
1320    Validator.ensureNotNull(serverSet);
1321    this.serverSet = serverSet;
1322  }
1323
1324
1325
1326  /**
1327   * {@inheritDoc}
1328   */
1329  @Override()
1330  @Nullable()
1331  public String getConnectionPoolName()
1332  {
1333    return connectionPoolName;
1334  }
1335
1336
1337
1338  /**
1339   * {@inheritDoc}
1340   */
1341  @Override()
1342  public void setConnectionPoolName(@Nullable final String connectionPoolName)
1343  {
1344    this.connectionPoolName = connectionPoolName;
1345  }
1346
1347
1348
1349  /**
1350   * Retrieves the maximum length of time in milliseconds that a connection in
1351   * this pool may be established before it is closed and replaced with another
1352   * connection.
1353   *
1354   * @return  The maximum length of time in milliseconds that a connection in
1355   *          this pool may be established before it is closed and replaced with
1356   *          another connection, or {@code 0L} if no maximum age should be
1357   *          enforced.
1358   */
1359  public long getMaxConnectionAgeMillis()
1360  {
1361    return maxConnectionAge;
1362  }
1363
1364
1365
1366  /**
1367   * Specifies the maximum length of time in milliseconds that a connection in
1368   * this pool may be established before it should be closed and replaced with
1369   * another connection.
1370   *
1371   * @param  maxConnectionAge  The maximum length of time in milliseconds that a
1372   *                           connection in this pool may be established before
1373   *                           it should be closed and replaced with another
1374   *                           connection.  A value of zero indicates that no
1375   *                           maximum age should be enforced.
1376   */
1377  public void setMaxConnectionAgeMillis(final long maxConnectionAge)
1378  {
1379    if (maxConnectionAge > 0L)
1380    {
1381      this.maxConnectionAge = maxConnectionAge;
1382    }
1383    else
1384    {
1385      this.maxConnectionAge = 0L;
1386    }
1387  }
1388
1389
1390
1391  /**
1392   * Retrieves the minimum length of time in milliseconds that should pass
1393   * between connections closed because they have been established for longer
1394   * than the maximum connection age.
1395   *
1396   * @return  The minimum length of time in milliseconds that should pass
1397   *          between connections closed because they have been established for
1398   *          longer than the maximum connection age, or {@code 0L} if expired
1399   *          connections may be closed as quickly as they are identified.
1400   */
1401  public long getMinDisconnectIntervalMillis()
1402  {
1403    return minDisconnectInterval;
1404  }
1405
1406
1407
1408  /**
1409   * Specifies the minimum length of time in milliseconds that should pass
1410   * between connections closed because they have been established for longer
1411   * than the maximum connection age.
1412   *
1413   * @param  minDisconnectInterval  The minimum length of time in milliseconds
1414   *                                that should pass between connections closed
1415   *                                because they have been established for
1416   *                                longer than the maximum connection age.  A
1417   *                                value less than or equal to zero indicates
1418   *                                that no minimum time should be enforced.
1419   */
1420  public void setMinDisconnectIntervalMillis(final long minDisconnectInterval)
1421  {
1422    if (minDisconnectInterval > 0)
1423    {
1424      this.minDisconnectInterval = minDisconnectInterval;
1425    }
1426    else
1427    {
1428      this.minDisconnectInterval = 0L;
1429    }
1430  }
1431
1432
1433
1434  /**
1435   * {@inheritDoc}
1436   */
1437  @Override()
1438  @NotNull()
1439  public LDAPConnectionPoolHealthCheck getHealthCheck()
1440  {
1441    return healthCheck;
1442  }
1443
1444
1445
1446  /**
1447   * Sets the health check implementation for this connection pool.
1448   *
1449   * @param  healthCheck  The health check implementation for this connection
1450   *                      pool.  It must not be {@code null}.
1451   */
1452  public void setHealthCheck(
1453                   @NotNull final LDAPConnectionPoolHealthCheck healthCheck)
1454  {
1455    Validator.ensureNotNull(healthCheck);
1456    this.healthCheck = healthCheck;
1457  }
1458
1459
1460
1461  /**
1462   * {@inheritDoc}
1463   */
1464  @Override()
1465  public long getHealthCheckIntervalMillis()
1466  {
1467    return healthCheckInterval;
1468  }
1469
1470
1471
1472  /**
1473   * {@inheritDoc}
1474   */
1475  @Override()
1476  public void setHealthCheckIntervalMillis(final long healthCheckInterval)
1477  {
1478    Validator.ensureTrue(healthCheckInterval > 0L,
1479         "LDAPConnectionPool.healthCheckInterval must be greater than 0.");
1480    this.healthCheckInterval = healthCheckInterval;
1481    healthCheckThread.wakeUp();
1482  }
1483
1484
1485
1486  /**
1487   * {@inheritDoc}
1488   */
1489  @Override()
1490  protected void doHealthCheck()
1491  {
1492    final Iterator<Map.Entry<Thread,LDAPConnection>> iterator =
1493         connections.entrySet().iterator();
1494    while (iterator.hasNext())
1495    {
1496      final Map.Entry<Thread,LDAPConnection> e = iterator.next();
1497      final Thread                           t = e.getKey();
1498      final LDAPConnection                   c = e.getValue();
1499
1500      if (! t.isAlive())
1501      {
1502        c.setDisconnectInfo(DisconnectType.POOLED_CONNECTION_UNNEEDED, null,
1503                            null);
1504        c.terminate(null);
1505        iterator.remove();
1506      }
1507    }
1508  }
1509
1510
1511
1512  /**
1513   * {@inheritDoc}
1514   */
1515  @Override()
1516  public int getCurrentAvailableConnections()
1517  {
1518    return -1;
1519  }
1520
1521
1522
1523  /**
1524   * {@inheritDoc}
1525   */
1526  @Override()
1527  public int getMaximumAvailableConnections()
1528  {
1529    return -1;
1530  }
1531
1532
1533
1534  /**
1535   * {@inheritDoc}
1536   */
1537  @Override()
1538  @NotNull()
1539  public LDAPConnectionPoolStatistics getConnectionPoolStatistics()
1540  {
1541    return poolStatistics;
1542  }
1543
1544
1545
1546  /**
1547   * Closes this connection pool in the event that it becomes unreferenced.
1548   *
1549   * @throws  Throwable  If an unexpected problem occurs.
1550   */
1551  @Override()
1552  protected void finalize()
1553            throws Throwable
1554  {
1555    super.finalize();
1556
1557    close();
1558  }
1559
1560
1561
1562  /**
1563   * {@inheritDoc}
1564   */
1565  @Override()
1566  public void toString(@NotNull final StringBuilder buffer)
1567  {
1568    buffer.append("LDAPThreadLocalConnectionPool(");
1569
1570    final String name = connectionPoolName;
1571    if (name != null)
1572    {
1573      buffer.append("name='");
1574      buffer.append(name);
1575      buffer.append("', ");
1576    }
1577
1578    buffer.append("serverSet=");
1579    serverSet.toString(buffer);
1580    buffer.append(')');
1581  }
1582}