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