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