001/*
002 * Copyright 2009-2020 Ping Identity Corporation
003 * All Rights Reserved.
004 */
005/*
006 * Copyright 2009-2020 Ping Identity Corporation
007 *
008 * Licensed under the Apache License, Version 2.0 (the "License");
009 * you may not use this file except in compliance with the License.
010 * You may obtain a copy of the License at
011 *
012 *    http://www.apache.org/licenses/LICENSE-2.0
013 *
014 * Unless required by applicable law or agreed to in writing, software
015 * distributed under the License is distributed on an "AS IS" BASIS,
016 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
017 * See the License for the specific language governing permissions and
018 * limitations under the License.
019 */
020/*
021 * Copyright (C) 2009-2020 Ping Identity Corporation
022 *
023 * This program is free software; you can redistribute it and/or modify
024 * it under the terms of the GNU General Public License (GPLv2 only)
025 * or the terms of the GNU Lesser General Public License (LGPLv2.1 only)
026 * as published by the Free Software Foundation.
027 *
028 * This program is distributed in the hope that it will be useful,
029 * but WITHOUT ANY WARRANTY; without even the implied warranty of
030 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
031 * GNU General Public License for more details.
032 *
033 * You should have received a copy of the GNU General Public License
034 * along with this program; if not, see <http://www.gnu.org/licenses>.
035 */
036package com.unboundid.ldap.sdk;
037
038
039
040import java.util.ArrayList;
041import java.util.Collections;
042import java.util.EnumSet;
043import java.util.Iterator;
044import java.util.Map;
045import java.util.Set;
046import java.util.concurrent.ConcurrentHashMap;
047import java.util.concurrent.atomic.AtomicReference;
048import java.util.logging.Level;
049
050import com.unboundid.ldap.sdk.schema.Schema;
051import com.unboundid.util.Debug;
052import com.unboundid.util.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      if (numThreads > 1)
644      {
645        final ArrayList<LDAPConnection> connList =
646             new ArrayList<>(connections.size());
647        final Iterator<LDAPConnection> iterator =
648             connections.values().iterator();
649        while (iterator.hasNext())
650        {
651          connList.add(iterator.next());
652          iterator.remove();
653        }
654
655        if (! connList.isEmpty())
656        {
657          final ParallelPoolCloser closer =
658               new ParallelPoolCloser(connList, unbind, numThreads);
659          closer.closeConnections();
660        }
661      }
662      else
663      {
664        final Iterator<Map.Entry<Thread,LDAPConnection>> iterator =
665             connections.entrySet().iterator();
666        while (iterator.hasNext())
667        {
668          final LDAPConnection conn = iterator.next().getValue();
669          iterator.remove();
670
671          poolStatistics.incrementNumConnectionsClosedUnneeded();
672          Debug.debugConnectionPool(Level.INFO, this, conn,
673               "Closed a connection as part of closing the connection pool",
674               null);
675          conn.setDisconnectInfo(DisconnectType.POOL_CLOSED, null, null);
676          if (unbind)
677          {
678            conn.terminate(null);
679          }
680          else
681          {
682            conn.setClosed();
683          }
684        }
685      }
686    }
687    finally
688    {
689      Debug.debugConnectionPool(Level.INFO, this, null,
690           "Closed the connection pool", null);
691    }
692  }
693
694
695
696  /**
697   * {@inheritDoc}
698   */
699  @Override()
700  public boolean isClosed()
701  {
702    return closed;
703  }
704
705
706
707  /**
708   * Processes a simple bind using a connection from this connection pool, and
709   * then reverts that authentication by re-binding as the same user used to
710   * authenticate new connections.  If new connections are unauthenticated, then
711   * the subsequent bind will be an anonymous simple bind.  This method attempts
712   * to ensure that processing the provided bind operation does not have a
713   * lasting impact the authentication state of the connection used to process
714   * it.
715   * <BR><BR>
716   * If the second bind attempt (the one used to restore the authentication
717   * identity) fails, the connection will be closed as defunct so that a new
718   * connection will be created to take its place.
719   *
720   * @param  bindDN    The bind DN for the simple bind request.
721   * @param  password  The password for the simple bind request.
722   * @param  controls  The optional set of controls for the simple bind request.
723   *
724   * @return  The result of processing the provided bind operation.
725   *
726   * @throws  LDAPException  If the server rejects the bind request, or if a
727   *                         problem occurs while sending the request or reading
728   *                         the response.
729   */
730  @NotNull()
731  public BindResult bindAndRevertAuthentication(@Nullable final String bindDN,
732                         @Nullable final String password,
733                         @Nullable final Control... controls)
734         throws LDAPException
735  {
736    return bindAndRevertAuthentication(
737         new SimpleBindRequest(bindDN, password, controls));
738  }
739
740
741
742  /**
743   * Processes the provided bind request using a connection from this connection
744   * pool, and then reverts that authentication by re-binding as the same user
745   * used to authenticate new connections.  If new connections are
746   * unauthenticated, then the subsequent bind will be an anonymous simple bind.
747   * This method attempts to ensure that processing the provided bind operation
748   * does not have a lasting impact the authentication state of the connection
749   * used to process it.
750   * <BR><BR>
751   * If the second bind attempt (the one used to restore the authentication
752   * identity) fails, the connection will be closed as defunct so that a new
753   * connection will be created to take its place.
754   *
755   * @param  bindRequest  The bind request to be processed.  It must not be
756   *                      {@code null}.
757   *
758   * @return  The result of processing the provided bind operation.
759   *
760   * @throws  LDAPException  If the server rejects the bind request, or if a
761   *                         problem occurs while sending the request or reading
762   *                         the response.
763   */
764  @NotNull()
765  public BindResult bindAndRevertAuthentication(
766                         @NotNull final BindRequest bindRequest)
767         throws LDAPException
768  {
769    LDAPConnection conn = getConnection();
770
771    try
772    {
773      final BindResult result = conn.bind(bindRequest);
774      releaseAndReAuthenticateConnection(conn);
775      return result;
776    }
777    catch (final Throwable t)
778    {
779      Debug.debugException(t);
780
781      if (t instanceof LDAPException)
782      {
783        final LDAPException le = (LDAPException) t;
784
785        boolean shouldThrow;
786        try
787        {
788          healthCheck.ensureConnectionValidAfterException(conn, le);
789
790          // The above call will throw an exception if the connection doesn't
791          // seem to be valid, so if we've gotten here then we should assume
792          // that it is valid and we will pass the exception onto the client
793          // without retrying the operation.
794          releaseAndReAuthenticateConnection(conn);
795          shouldThrow = true;
796        }
797        catch (final Exception e)
798        {
799          Debug.debugException(e);
800
801          // This implies that the connection is not valid.  If the pool is
802          // configured to re-try bind operations on a newly-established
803          // connection, then that will be done later in this method.
804          // Otherwise, release the connection as defunct and pass the bind
805          // exception onto the client.
806          if (! getOperationTypesToRetryDueToInvalidConnections().contains(
807                     OperationType.BIND))
808          {
809            releaseDefunctConnection(conn);
810            shouldThrow = true;
811          }
812          else
813          {
814            shouldThrow = false;
815          }
816        }
817
818        if (shouldThrow)
819        {
820          throw le;
821        }
822      }
823      else
824      {
825        releaseDefunctConnection(conn);
826        StaticUtils.rethrowIfError(t);
827        throw new LDAPException(ResultCode.LOCAL_ERROR,
828             ERR_POOL_OP_EXCEPTION.get(StaticUtils.getExceptionMessage(t)), t);
829      }
830    }
831
832
833    // If we've gotten here, then the bind operation should be re-tried on a
834    // newly-established connection.
835    conn = replaceDefunctConnection(conn);
836
837    try
838    {
839      final BindResult result = conn.bind(bindRequest);
840      releaseAndReAuthenticateConnection(conn);
841      return result;
842    }
843    catch (final Throwable t)
844    {
845      Debug.debugException(t);
846
847      if (t instanceof LDAPException)
848      {
849        final LDAPException le = (LDAPException) t;
850
851        try
852        {
853          healthCheck.ensureConnectionValidAfterException(conn, le);
854          releaseAndReAuthenticateConnection(conn);
855        }
856        catch (final Exception e)
857        {
858          Debug.debugException(e);
859          releaseDefunctConnection(conn);
860        }
861
862        throw le;
863      }
864      else
865      {
866        releaseDefunctConnection(conn);
867        StaticUtils.rethrowIfError(t);
868        throw new LDAPException(ResultCode.LOCAL_ERROR,
869             ERR_POOL_OP_EXCEPTION.get(StaticUtils.getExceptionMessage(t)), t);
870      }
871    }
872  }
873
874
875
876  /**
877   * {@inheritDoc}
878   */
879  @Override()
880  @NotNull()
881  public LDAPConnection getConnection()
882         throws LDAPException
883  {
884    final Thread t = Thread.currentThread();
885    LDAPConnection conn = connections.get(t);
886
887    if (closed)
888    {
889      if (conn != null)
890      {
891        conn.terminate(null);
892        connections.remove(t);
893      }
894
895      poolStatistics.incrementNumFailedCheckouts();
896      Debug.debugConnectionPool(Level.SEVERE, this, null,
897           "Failed to get a connection to a closed connection pool", null);
898      throw new LDAPException(ResultCode.CONNECT_ERROR,
899                              ERR_POOL_CLOSED.get());
900    }
901
902    boolean created = false;
903    if ((conn == null) || (! conn.isConnected()))
904    {
905      conn = createConnection();
906      connections.put(t, conn);
907      created = true;
908    }
909
910    try
911    {
912      healthCheck.ensureConnectionValidForCheckout(conn);
913      if (created)
914      {
915        poolStatistics.incrementNumSuccessfulCheckoutsNewConnection();
916        Debug.debugConnectionPool(Level.INFO, this, conn,
917             "Checked out a newly created pooled connection", null);
918      }
919      else
920      {
921        poolStatistics.incrementNumSuccessfulCheckoutsWithoutWaiting();
922        Debug.debugConnectionPool(Level.INFO, this, conn,
923             "Checked out an existing pooled connection", null);
924      }
925      return conn;
926    }
927    catch (final LDAPException le)
928    {
929      Debug.debugException(le);
930
931      conn.setClosed();
932      connections.remove(t);
933
934      if (created)
935      {
936        poolStatistics.incrementNumFailedCheckouts();
937        Debug.debugConnectionPool(Level.SEVERE, this, conn,
938             "Failed to check out a connection because a newly created " +
939                  "connection failed the checkout health check",
940             le);
941        throw le;
942      }
943    }
944
945    try
946    {
947      conn = createConnection();
948      healthCheck.ensureConnectionValidForCheckout(conn);
949      connections.put(t, conn);
950      poolStatistics.incrementNumSuccessfulCheckoutsNewConnection();
951      Debug.debugConnectionPool(Level.INFO, this, conn,
952           "Checked out a newly created pooled connection", null);
953      return conn;
954    }
955    catch (final LDAPException le)
956    {
957      Debug.debugException(le);
958
959      poolStatistics.incrementNumFailedCheckouts();
960      if (conn == null)
961      {
962        Debug.debugConnectionPool(Level.SEVERE, this, conn,
963             "Unable to check out a connection because an error occurred " +
964                  "while establishing the connection",
965             le);
966      }
967      else
968      {
969        Debug.debugConnectionPool(Level.SEVERE, this, conn,
970             "Unable to check out a newly created connection because it " +
971                  "failed the checkout health check",
972             le);
973        conn.setClosed();
974      }
975
976      throw le;
977    }
978  }
979
980
981
982  /**
983   * {@inheritDoc}
984   */
985  @Override()
986  public void releaseConnection(@NotNull final LDAPConnection connection)
987  {
988    if (connection == null)
989    {
990      return;
991    }
992
993    connection.setConnectionPoolName(connectionPoolName);
994    if (connectionIsExpired(connection))
995    {
996      try
997      {
998        final LDAPConnection newConnection = createConnection();
999        connections.put(Thread.currentThread(), newConnection);
1000
1001        connection.setDisconnectInfo(DisconnectType.POOLED_CONNECTION_EXPIRED,
1002             null, null);
1003        connection.terminate(null);
1004        poolStatistics.incrementNumConnectionsClosedExpired();
1005        Debug.debugConnectionPool(Level.WARNING, this, connection,
1006             "Closing a released connection because it is expired", null);
1007        lastExpiredDisconnectTime = System.currentTimeMillis();
1008      }
1009      catch (final LDAPException le)
1010      {
1011        Debug.debugException(le);
1012      }
1013    }
1014
1015    try
1016    {
1017      healthCheck.ensureConnectionValidForRelease(connection);
1018    }
1019    catch (final LDAPException le)
1020    {
1021      releaseDefunctConnection(connection);
1022      return;
1023    }
1024
1025    poolStatistics.incrementNumReleasedValid();
1026    Debug.debugConnectionPool(Level.INFO, this, connection,
1027         "Released a connection back to the pool", null);
1028
1029    if (closed)
1030    {
1031      close();
1032    }
1033  }
1034
1035
1036
1037  /**
1038   * Performs a bind on the provided connection before releasing it back to the
1039   * pool, so that it will be authenticated as the same user as
1040   * newly-established connections.  If newly-established connections are
1041   * unauthenticated, then this method will perform an anonymous simple bind to
1042   * ensure that the resulting connection is unauthenticated.
1043   *
1044   * Releases the provided connection back to this pool.
1045   *
1046   * @param  connection  The connection to be released back to the pool after
1047   *                     being re-authenticated.
1048   */
1049  public void releaseAndReAuthenticateConnection(
1050                   @NotNull final LDAPConnection connection)
1051  {
1052    if (connection == null)
1053    {
1054      return;
1055    }
1056
1057    try
1058    {
1059      BindResult bindResult;
1060      try
1061      {
1062        if (bindRequest == null)
1063        {
1064          bindResult = connection.bind("", "");
1065        }
1066        else
1067        {
1068          bindResult = connection.bind(bindRequest.duplicate());
1069        }
1070      }
1071      catch (final LDAPBindException lbe)
1072      {
1073        Debug.debugException(lbe);
1074        bindResult = lbe.getBindResult();
1075      }
1076
1077      try
1078      {
1079        healthCheck.ensureConnectionValidAfterAuthentication(connection,
1080             bindResult);
1081        if (bindResult.getResultCode() != ResultCode.SUCCESS)
1082        {
1083          throw new LDAPBindException(bindResult);
1084        }
1085      }
1086      catch (final LDAPException le)
1087      {
1088        Debug.debugException(le);
1089
1090        try
1091        {
1092          connection.setDisconnectInfo(DisconnectType.BIND_FAILED, null, le);
1093          connection.terminate(null);
1094          releaseDefunctConnection(connection);
1095        }
1096        catch (final Exception e)
1097        {
1098          Debug.debugException(e);
1099        }
1100
1101        throw le;
1102      }
1103
1104      releaseConnection(connection);
1105    }
1106    catch (final Exception e)
1107    {
1108      Debug.debugException(e);
1109      releaseDefunctConnection(connection);
1110    }
1111  }
1112
1113
1114
1115  /**
1116   * {@inheritDoc}
1117   */
1118  @Override()
1119  public void releaseDefunctConnection(@NotNull final LDAPConnection connection)
1120  {
1121    if (connection == null)
1122    {
1123      return;
1124    }
1125
1126    connection.setConnectionPoolName(connectionPoolName);
1127    poolStatistics.incrementNumConnectionsClosedDefunct();
1128    Debug.debugConnectionPool(Level.WARNING, this, connection,
1129         "Releasing a defunct connection", null);
1130    handleDefunctConnection(connection);
1131  }
1132
1133
1134
1135  /**
1136   * Performs the real work of terminating a defunct connection and replacing it
1137   * with a new connection if possible.
1138   *
1139   * @param  connection  The defunct connection to be replaced.
1140   */
1141  private void handleDefunctConnection(@NotNull final LDAPConnection connection)
1142  {
1143    final Thread t = Thread.currentThread();
1144
1145    connection.setDisconnectInfo(DisconnectType.POOLED_CONNECTION_DEFUNCT, null,
1146                                 null);
1147    connection.setClosed();
1148    connections.remove(t);
1149
1150    if (closed)
1151    {
1152      return;
1153    }
1154
1155    try
1156    {
1157      final LDAPConnection conn = createConnection();
1158      connections.put(t, conn);
1159    }
1160    catch (final LDAPException le)
1161    {
1162      Debug.debugException(le);
1163    }
1164  }
1165
1166
1167
1168  /**
1169   * {@inheritDoc}
1170   */
1171  @Override()
1172  @NotNull()
1173  public LDAPConnection replaceDefunctConnection(
1174                             @NotNull final LDAPConnection connection)
1175         throws LDAPException
1176  {
1177    poolStatistics.incrementNumConnectionsClosedDefunct();
1178    Debug.debugConnectionPool(Level.WARNING, this, connection,
1179         "Releasing a defunct connection that is to be replaced", null);
1180    connection.setDisconnectInfo(DisconnectType.POOLED_CONNECTION_DEFUNCT, null,
1181                                 null);
1182    connection.setClosed();
1183    connections.remove(Thread.currentThread(), connection);
1184
1185    if (closed)
1186    {
1187      throw new LDAPException(ResultCode.CONNECT_ERROR, ERR_POOL_CLOSED.get());
1188    }
1189
1190    final LDAPConnection newConnection = createConnection();
1191    connections.put(Thread.currentThread(), newConnection);
1192    return newConnection;
1193  }
1194
1195
1196
1197  /**
1198   * {@inheritDoc}
1199   */
1200  @Override()
1201  @NotNull()
1202  public Set<OperationType> getOperationTypesToRetryDueToInvalidConnections()
1203  {
1204    return retryOperationTypes.get();
1205  }
1206
1207
1208
1209  /**
1210   * {@inheritDoc}
1211   */
1212  @Override()
1213  public void setRetryFailedOperationsDueToInvalidConnections(
1214                   @Nullable final Set<OperationType> operationTypes)
1215  {
1216    if ((operationTypes == null) || operationTypes.isEmpty())
1217    {
1218      retryOperationTypes.set(
1219           Collections.unmodifiableSet(EnumSet.noneOf(OperationType.class)));
1220    }
1221    else
1222    {
1223      final EnumSet<OperationType> s = EnumSet.noneOf(OperationType.class);
1224      s.addAll(operationTypes);
1225      retryOperationTypes.set(Collections.unmodifiableSet(s));
1226    }
1227  }
1228
1229
1230
1231  /**
1232   * Indicates whether the provided connection should be considered expired.
1233   *
1234   * @param  connection  The connection for which to make the determination.
1235   *
1236   * @return  {@code true} if the provided connection should be considered
1237   *          expired, or {@code false} if not.
1238   */
1239  private boolean connectionIsExpired(@NotNull final LDAPConnection connection)
1240  {
1241    // If connection expiration is not enabled, then there is nothing to do.
1242    if (maxConnectionAge <= 0L)
1243    {
1244      return false;
1245    }
1246
1247    // If there is a minimum disconnect interval, then make sure that we have
1248    // not closed another expired connection too recently.
1249    final long currentTime = System.currentTimeMillis();
1250    if ((currentTime - lastExpiredDisconnectTime) < minDisconnectInterval)
1251    {
1252      return false;
1253    }
1254
1255    // Get the age of the connection and see if it is expired.
1256    final long connectionAge = currentTime - connection.getConnectTime();
1257    return (connectionAge > maxConnectionAge);
1258  }
1259
1260
1261
1262  /**
1263   * Specifies the bind request that will be used to authenticate subsequent new
1264   * connections that are established by this connection pool.  The
1265   * authentication state for existing connections will not be altered unless
1266   * one of the {@code bindAndRevertAuthentication} or
1267   * {@code releaseAndReAuthenticateConnection} methods are invoked on those
1268   * connections.
1269   *
1270   * @param  bindRequest  The bind request that will be used to authenticate new
1271   *                      connections that are established by this pool, or
1272   *                      that will be applied to existing connections via the
1273   *                      {@code bindAndRevertAuthentication} or
1274   *                      {@code releaseAndReAuthenticateConnection} method.  It
1275   *                      may be {@code null} if new connections should be
1276   *                      unauthenticated.
1277   */
1278  public void setBindRequest(@Nullable final BindRequest bindRequest)
1279  {
1280    this.bindRequest = bindRequest;
1281  }
1282
1283
1284
1285  /**
1286   * Specifies the server set that should be used to establish new connections
1287   * for use in this connection pool.  Existing connections will not be
1288   * affected.
1289   *
1290   * @param  serverSet  The server set that should be used to establish new
1291   *                    connections for use in this connection pool.  It must
1292   *                    not be {@code null}.
1293   */
1294  public void setServerSet(@NotNull final ServerSet serverSet)
1295  {
1296    Validator.ensureNotNull(serverSet);
1297    this.serverSet = serverSet;
1298  }
1299
1300
1301
1302  /**
1303   * {@inheritDoc}
1304   */
1305  @Override()
1306  @Nullable()
1307  public String getConnectionPoolName()
1308  {
1309    return connectionPoolName;
1310  }
1311
1312
1313
1314  /**
1315   * {@inheritDoc}
1316   */
1317  @Override()
1318  public void setConnectionPoolName(@Nullable final String connectionPoolName)
1319  {
1320    this.connectionPoolName = connectionPoolName;
1321  }
1322
1323
1324
1325  /**
1326   * Retrieves the maximum length of time in milliseconds that a connection in
1327   * this pool may be established before it is closed and replaced with another
1328   * connection.
1329   *
1330   * @return  The maximum length of time in milliseconds that a connection in
1331   *          this pool may be established before it is closed and replaced with
1332   *          another connection, or {@code 0L} if no maximum age should be
1333   *          enforced.
1334   */
1335  public long getMaxConnectionAgeMillis()
1336  {
1337    return maxConnectionAge;
1338  }
1339
1340
1341
1342  /**
1343   * Specifies the maximum length of time in milliseconds that a connection in
1344   * this pool may be established before it should be closed and replaced with
1345   * another connection.
1346   *
1347   * @param  maxConnectionAge  The maximum length of time in milliseconds that a
1348   *                           connection in this pool may be established before
1349   *                           it should be closed and replaced with another
1350   *                           connection.  A value of zero indicates that no
1351   *                           maximum age should be enforced.
1352   */
1353  public void setMaxConnectionAgeMillis(final long maxConnectionAge)
1354  {
1355    if (maxConnectionAge > 0L)
1356    {
1357      this.maxConnectionAge = maxConnectionAge;
1358    }
1359    else
1360    {
1361      this.maxConnectionAge = 0L;
1362    }
1363  }
1364
1365
1366
1367  /**
1368   * Retrieves the minimum length of time in milliseconds that should pass
1369   * between connections closed because they have been established for longer
1370   * than the maximum connection age.
1371   *
1372   * @return  The minimum length of time in milliseconds that should pass
1373   *          between connections closed because they have been established for
1374   *          longer than the maximum connection age, or {@code 0L} if expired
1375   *          connections may be closed as quickly as they are identified.
1376   */
1377  public long getMinDisconnectIntervalMillis()
1378  {
1379    return minDisconnectInterval;
1380  }
1381
1382
1383
1384  /**
1385   * Specifies the minimum length of time in milliseconds that should pass
1386   * between connections closed because they have been established for longer
1387   * than the maximum connection age.
1388   *
1389   * @param  minDisconnectInterval  The minimum length of time in milliseconds
1390   *                                that should pass between connections closed
1391   *                                because they have been established for
1392   *                                longer than the maximum connection age.  A
1393   *                                value less than or equal to zero indicates
1394   *                                that no minimum time should be enforced.
1395   */
1396  public void setMinDisconnectIntervalMillis(final long minDisconnectInterval)
1397  {
1398    if (minDisconnectInterval > 0)
1399    {
1400      this.minDisconnectInterval = minDisconnectInterval;
1401    }
1402    else
1403    {
1404      this.minDisconnectInterval = 0L;
1405    }
1406  }
1407
1408
1409
1410  /**
1411   * {@inheritDoc}
1412   */
1413  @Override()
1414  @NotNull()
1415  public LDAPConnectionPoolHealthCheck getHealthCheck()
1416  {
1417    return healthCheck;
1418  }
1419
1420
1421
1422  /**
1423   * Sets the health check implementation for this connection pool.
1424   *
1425   * @param  healthCheck  The health check implementation for this connection
1426   *                      pool.  It must not be {@code null}.
1427   */
1428  public void setHealthCheck(
1429                   @NotNull final LDAPConnectionPoolHealthCheck healthCheck)
1430  {
1431    Validator.ensureNotNull(healthCheck);
1432    this.healthCheck = healthCheck;
1433  }
1434
1435
1436
1437  /**
1438   * {@inheritDoc}
1439   */
1440  @Override()
1441  public long getHealthCheckIntervalMillis()
1442  {
1443    return healthCheckInterval;
1444  }
1445
1446
1447
1448  /**
1449   * {@inheritDoc}
1450   */
1451  @Override()
1452  public void setHealthCheckIntervalMillis(final long healthCheckInterval)
1453  {
1454    Validator.ensureTrue(healthCheckInterval > 0L,
1455         "LDAPConnectionPool.healthCheckInterval must be greater than 0.");
1456    this.healthCheckInterval = healthCheckInterval;
1457    healthCheckThread.wakeUp();
1458  }
1459
1460
1461
1462  /**
1463   * {@inheritDoc}
1464   */
1465  @Override()
1466  protected void doHealthCheck()
1467  {
1468    final Iterator<Map.Entry<Thread,LDAPConnection>> iterator =
1469         connections.entrySet().iterator();
1470    while (iterator.hasNext())
1471    {
1472      final Map.Entry<Thread,LDAPConnection> e = iterator.next();
1473      final Thread                           t = e.getKey();
1474      final LDAPConnection                   c = e.getValue();
1475
1476      if (! t.isAlive())
1477      {
1478        c.setDisconnectInfo(DisconnectType.POOLED_CONNECTION_UNNEEDED, null,
1479                            null);
1480        c.terminate(null);
1481        iterator.remove();
1482      }
1483    }
1484  }
1485
1486
1487
1488  /**
1489   * {@inheritDoc}
1490   */
1491  @Override()
1492  public int getCurrentAvailableConnections()
1493  {
1494    return -1;
1495  }
1496
1497
1498
1499  /**
1500   * {@inheritDoc}
1501   */
1502  @Override()
1503  public int getMaximumAvailableConnections()
1504  {
1505    return -1;
1506  }
1507
1508
1509
1510  /**
1511   * {@inheritDoc}
1512   */
1513  @Override()
1514  @NotNull()
1515  public LDAPConnectionPoolStatistics getConnectionPoolStatistics()
1516  {
1517    return poolStatistics;
1518  }
1519
1520
1521
1522  /**
1523   * Closes this connection pool in the event that it becomes unreferenced.
1524   *
1525   * @throws  Throwable  If an unexpected problem occurs.
1526   */
1527  @Override()
1528  protected void finalize()
1529            throws Throwable
1530  {
1531    super.finalize();
1532
1533    close();
1534  }
1535
1536
1537
1538  /**
1539   * {@inheritDoc}
1540   */
1541  @Override()
1542  public void toString(@NotNull final StringBuilder buffer)
1543  {
1544    buffer.append("LDAPThreadLocalConnectionPool(");
1545
1546    final String name = connectionPoolName;
1547    if (name != null)
1548    {
1549      buffer.append("name='");
1550      buffer.append(name);
1551      buffer.append("', ");
1552    }
1553
1554    buffer.append("serverSet=");
1555    serverSet.toString(buffer);
1556    buffer.append(')');
1557  }
1558}