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