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