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