001/* 002 * Copyright 2013-2024 Ping Identity Corporation 003 * All Rights Reserved. 004 */ 005/* 006 * Copyright 2013-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) 2013-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.Iterator; 043import java.util.LinkedHashMap; 044import java.util.List; 045import java.util.Map; 046import java.util.TreeMap; 047import java.util.concurrent.atomic.AtomicLong; 048import javax.net.SocketFactory; 049 050import com.unboundid.util.Debug; 051import com.unboundid.util.NotMutable; 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 060 061 062/** 063 * This class provides a server set implementation that will establish a 064 * connection to the server with the fewest established connections previously 065 * created by the same server set instance. If there are multiple servers that 066 * share the fewest number of established connections, the first one in the list 067 * will be chosen. If a server is unavailable when an attempt is made to 068 * establish a connection to it, then the connection will be established to the 069 * available server with the next fewest number of established connections. 070 * <BR><BR> 071 * This server set implementation has the ability to maintain a temporary 072 * blacklist of servers that have been recently found to be unavailable or 073 * unsuitable for use. If an attempt to establish or authenticate a 074 * connection fails, if post-connect processing fails for that connection, or if 075 * health checking indicates that the connection is not suitable, then that 076 * server may be placed on the blacklist so that it will only be tried as a last 077 * resort after all non-blacklisted servers have been attempted. The blacklist 078 * will be checked at regular intervals to determine whether a server should be 079 * re-instated to availability. 080 * <BR><BR> 081 * Note that this server set implementation is primarily intended for use with 082 * connection pools, but is also suitable for cases in which standalone 083 * connections are created as long as there will not be any attempt to close the 084 * connections when they are re-established. It is not suitable for use in 085 * connections that may be re-established one or more times after being closed. 086 * <BR><BR> 087 * <H2>Example</H2> 088 * The following example demonstrates the process for creating a fewest 089 * connections server set that may be used to establish connections to either of 090 * two servers. 091 * <PRE> 092 * // Create arrays with the addresses and ports of the directory server 093 * // instances. 094 * String[] addresses = 095 * { 096 * server1Address, 097 * server2Address 098 * }; 099 * int[] ports = 100 * { 101 * server1Port, 102 * server2Port 103 * }; 104 * 105 * // Create the server set using the address and port arrays. 106 * FewestConnectionsServerSet fewestConnectionsSet = 107 * new FewestConnectionsServerSet(addresses, ports); 108 * 109 * // Verify that we can establish a single connection using the server set. 110 * LDAPConnection connection = fewestConnectionsSet.getConnection(); 111 * RootDSE rootDSEFromConnection = connection.getRootDSE(); 112 * connection.close(); 113 * 114 * // Verify that we can establish a connection pool using the server set. 115 * SimpleBindRequest bindRequest = 116 * new SimpleBindRequest("uid=pool.user,dc=example,dc=com", "password"); 117 * LDAPConnectionPool pool = 118 * new LDAPConnectionPool(fewestConnectionsSet, bindRequest, 10); 119 * RootDSE rootDSEFromPool = pool.getRootDSE(); 120 * pool.close(); 121 * </PRE> 122 */ 123@NotMutable() 124@ThreadSafety(level=ThreadSafetyLevel.COMPLETELY_THREADSAFE) 125public final class FewestConnectionsServerSet 126 extends ServerSet 127{ 128 /** 129 * The name of a system property that can be used to override the default 130 * blacklist check interval, in milliseconds. 131 */ 132 @NotNull static final String 133 PROPERTY_DEFAULT_BLACKLIST_CHECK_INTERVAL_MILLIS = 134 FewestConnectionsServerSet.class.getName() + 135 ".defaultBlacklistCheckIntervalMillis"; 136 137 138 139 // The bind request to use to authenticate connections created by this 140 // server set. 141 @Nullable private final BindRequest bindRequest; 142 143 // The set of connection options to use for new connections. 144 @NotNull private final LDAPConnectionOptions connectionOptions; 145 146 // A map with the number of connections currently established for each server. 147 @NotNull private final Map<ObjectPair<String,Integer>,AtomicLong> 148 connectionCountsByServer; 149 150 // The post-connect processor to invoke against connections created by this 151 // server set. 152 @Nullable private final PostConnectProcessor postConnectProcessor; 153 154 // The blacklist manager for this server set. 155 @Nullable private final ServerSetBlacklistManager blacklistManager; 156 157 // The socket factory to use to establish connections. 158 @NotNull private final SocketFactory socketFactory; 159 160 161 162 /** 163 * Creates a new fewest connections server set with the specified set of 164 * directory server addresses and port numbers. It will use the default 165 * socket factory provided by the JVM to create the underlying sockets. 166 * 167 * @param addresses The addresses of the directory servers to which the 168 * connections should be established. It must not be 169 * {@code null} or empty. 170 * @param ports The ports of the directory servers to which the 171 * connections should be established. It must not be 172 * {@code null}, and it must have the same number of 173 * elements as the {@code addresses} array. The order of 174 * elements in the {@code addresses} array must correspond 175 * to the order of elements in the {@code ports} array. 176 */ 177 public FewestConnectionsServerSet(@NotNull final String[] addresses, 178 @NotNull final int[] ports) 179 { 180 this(addresses, ports, null, null); 181 } 182 183 184 185 /** 186 * Creates a new fewest connections server set with the specified set of 187 * directory server addresses and port numbers. It will use the default 188 * socket factory provided by the JVM to create the underlying sockets. 189 * 190 * @param addresses The addresses of the directory servers to which 191 * the connections should be established. It must 192 * not be {@code null} or empty. 193 * @param ports The ports of the directory servers to which the 194 * connections should be established. It must not 195 * be {@code null}, and it must have the same 196 * number of elements as the {@code addresses} 197 * array. The order of elements in the 198 * {@code addresses} array must correspond to the 199 * order of elements in the {@code ports} array. 200 * @param connectionOptions The set of connection options to use for the 201 * underlying connections. 202 */ 203 public FewestConnectionsServerSet(@NotNull final String[] addresses, 204 @NotNull final int[] ports, 205 @Nullable final LDAPConnectionOptions connectionOptions) 206 { 207 this(addresses, ports, null, connectionOptions); 208 } 209 210 211 212 /** 213 * Creates a new fewest connections server set with the specified set of 214 * directory server addresses and port numbers. It will use the provided 215 * socket factory to create the underlying sockets. 216 * 217 * @param addresses The addresses of the directory servers to which the 218 * connections should be established. It must not be 219 * {@code null} or empty. 220 * @param ports The ports of the directory servers to which the 221 * connections should be established. It must not be 222 * {@code null}, and it must have the same number of 223 * elements as the {@code addresses} array. The order 224 * of elements in the {@code addresses} array must 225 * correspond to the order of elements in the 226 * {@code ports} array. 227 * @param socketFactory The socket factory to use to create the underlying 228 * connections. 229 */ 230 public FewestConnectionsServerSet(@NotNull final String[] addresses, 231 @NotNull final int[] ports, 232 @Nullable final SocketFactory socketFactory) 233 { 234 this(addresses, ports, socketFactory, null); 235 } 236 237 238 239 /** 240 * Creates a new fewest connections server set with the specified set of 241 * directory server addresses and port numbers. It will use the provided 242 * socket factory to create the underlying sockets. 243 * 244 * @param addresses The addresses of the directory servers to which 245 * the connections should be established. It must 246 * not be {@code null} or empty. 247 * @param ports The ports of the directory servers to which the 248 * connections should be established. It must not 249 * be {@code null}, and it must have the same 250 * number of elements as the {@code addresses} 251 * array. The order of elements in the 252 * {@code addresses} array must correspond to the 253 * order of elements in the {@code ports} array. 254 * @param socketFactory The socket factory to use to create the 255 * underlying connections. 256 * @param connectionOptions The set of connection options to use for the 257 * underlying connections. 258 */ 259 public FewestConnectionsServerSet(@NotNull final String[] addresses, 260 @NotNull final int[] ports, 261 @Nullable final SocketFactory socketFactory, 262 @Nullable final LDAPConnectionOptions connectionOptions) 263 { 264 this(addresses, ports, socketFactory, connectionOptions, null, null); 265 } 266 267 268 269 /** 270 * Creates a new fewest connections server set with the specified set of 271 * directory server addresses and port numbers. It will use the provided 272 * socket factory to create the underlying sockets. 273 * 274 * @param addresses The addresses of the directory servers to 275 * which the connections should be established. 276 * It must not be {@code null} or empty. 277 * @param ports The ports of the directory servers to which 278 * the connections should be established. It 279 * must not be {@code null}, and it must have 280 * the same number of elements as the 281 * {@code addresses} array. The order of 282 * elements in the {@code addresses} array must 283 * correspond to the order of elements in the 284 * {@code ports} array. 285 * @param socketFactory The socket factory to use to create the 286 * underlying connections. 287 * @param connectionOptions The set of connection options to use for the 288 * underlying connections. 289 * @param bindRequest The bind request that should be used to 290 * authenticate newly established connections. 291 * It may be {@code null} if this server set 292 * should not perform any authentication. 293 * @param postConnectProcessor The post-connect processor that should be 294 * invoked on newly established connections. It 295 * may be {@code null} if this server set should 296 * not perform any post-connect processing. 297 */ 298 public FewestConnectionsServerSet(@NotNull final String[] addresses, 299 @NotNull final int[] ports, 300 @Nullable final SocketFactory socketFactory, 301 @Nullable final LDAPConnectionOptions connectionOptions, 302 @Nullable final BindRequest bindRequest, 303 @Nullable final PostConnectProcessor postConnectProcessor) 304 { 305 this(addresses, ports, socketFactory, connectionOptions, bindRequest, 306 postConnectProcessor, getDefaultBlacklistCheckIntervalMillis()); 307 } 308 309 310 311 /** 312 * Creates a new fewest connections server set with the specified set of 313 * directory server addresses and port numbers. It will use the provided 314 * socket factory to create the underlying sockets. 315 * 316 * @param addresses The addresses of the directory 317 * servers to which the connections 318 * should be established. It must not 319 * be {@code null} or empty. 320 * @param ports The ports of the directory servers to 321 * which the connections should be 322 * established. It must not be 323 * {@code null}, and it must have the 324 * same number of elements as the 325 * {@code addresses} array. The order 326 * of elements in the {@code addresses} 327 * array must correspond to the order of 328 * elements in the {@code ports} array. 329 * @param socketFactory The socket factory to use to create 330 * the underlying connections. 331 * @param connectionOptions The set of connection options to use 332 * for the underlying connections. 333 * @param bindRequest The bind request that should be used 334 * to authenticate newly established 335 * connections. It may be {@code null} 336 * if this server set should not perform 337 * any authentication. 338 * @param postConnectProcessor The post-connect processor that 339 * should be invoked on newly 340 * established connections. It may be 341 * {@code null} if this server set 342 * should not perform any post-connect 343 * processing. 344 * @param blacklistCheckIntervalMillis The length of time in milliseconds 345 * between checks of servers on the 346 * blacklist to determine whether they 347 * are once again suitable for use. A 348 * value that is less than or equal to 349 * zero indicates that no blacklist 350 * should be maintained. 351 */ 352 public FewestConnectionsServerSet(@NotNull final String[] addresses, 353 @NotNull final int[] ports, 354 @Nullable final SocketFactory socketFactory, 355 @Nullable final LDAPConnectionOptions connectionOptions, 356 @Nullable final BindRequest bindRequest, 357 @Nullable final PostConnectProcessor postConnectProcessor, 358 final long blacklistCheckIntervalMillis) 359 { 360 Validator.ensureNotNull(addresses, ports); 361 Validator.ensureTrue(addresses.length > 0, 362 "FewestConnectionsServerSet.addresses must not be empty."); 363 Validator.ensureTrue(addresses.length == ports.length, 364 "FewestConnectionsServerSet addresses and ports arrays must be " + 365 "the same size."); 366 367 final LinkedHashMap<ObjectPair<String,Integer>,AtomicLong> m = 368 new LinkedHashMap<>(StaticUtils.computeMapCapacity(ports.length)); 369 for (int i=0; i < addresses.length; i++) 370 { 371 m.put(new ObjectPair<>(addresses[i], ports[i]), new AtomicLong(0L)); 372 } 373 374 connectionCountsByServer = Collections.unmodifiableMap(m); 375 376 this.bindRequest = bindRequest; 377 this.postConnectProcessor = postConnectProcessor; 378 379 if (socketFactory == null) 380 { 381 this.socketFactory = SocketFactory.getDefault(); 382 } 383 else 384 { 385 this.socketFactory = socketFactory; 386 } 387 388 if (connectionOptions == null) 389 { 390 this.connectionOptions = new LDAPConnectionOptions(); 391 } 392 else 393 { 394 this.connectionOptions = connectionOptions; 395 } 396 397 if (blacklistCheckIntervalMillis > 0L) 398 { 399 blacklistManager = new ServerSetBlacklistManager(this, socketFactory, 400 connectionOptions, bindRequest, postConnectProcessor, 401 blacklistCheckIntervalMillis); 402 } 403 else 404 { 405 blacklistManager = null; 406 } 407 } 408 409 410 411 /** 412 * Retrieves the default blacklist check interval (in milliseconds that should 413 * be used if it is not specified. 414 * 415 * @return The default blacklist check interval (in milliseconds that should 416 * be used if it is not specified. 417 */ 418 private static long getDefaultBlacklistCheckIntervalMillis() 419 { 420 final String propertyValue = StaticUtils.getSystemProperty( 421 PROPERTY_DEFAULT_BLACKLIST_CHECK_INTERVAL_MILLIS); 422 if (propertyValue != null) 423 { 424 try 425 { 426 return Long.parseLong(propertyValue); 427 } 428 catch (final Exception e) 429 { 430 Debug.debugException(e); 431 } 432 } 433 434 return 30_000L; 435 } 436 437 438 439 /** 440 * Retrieves the addresses of the directory servers to which the connections 441 * should be established. 442 * 443 * @return The addresses of the directory servers to which the connections 444 * should be established. 445 */ 446 @NotNull() 447 public String[] getAddresses() 448 { 449 int i = 0; 450 final String[] addresses = new String[connectionCountsByServer.size()]; 451 for (final ObjectPair<String,Integer> hostPort : 452 connectionCountsByServer.keySet()) 453 { 454 addresses[i++] = hostPort.getFirst(); 455 } 456 457 return addresses; 458 } 459 460 461 462 /** 463 * Retrieves the ports of the directory servers to which the connections 464 * should be established. 465 * 466 * @return The ports of the directory servers to which the connections should 467 * be established. 468 */ 469 @NotNull() 470 public int[] getPorts() 471 { 472 int i = 0; 473 final int[] ports = new int[connectionCountsByServer.size()]; 474 for (final ObjectPair<String,Integer> hostPort : 475 connectionCountsByServer.keySet()) 476 { 477 ports[i++] = hostPort.getSecond(); 478 } 479 480 return ports; 481 } 482 483 484 485 /** 486 * Retrieves the socket factory that will be used to establish connections. 487 * 488 * @return The socket factory that will be used to establish connections. 489 */ 490 @NotNull() 491 public SocketFactory getSocketFactory() 492 { 493 return socketFactory; 494 } 495 496 497 498 /** 499 * Retrieves the set of connection options that will be used for underlying 500 * connections. 501 * 502 * @return The set of connection options that will be used for underlying 503 * connections. 504 */ 505 @NotNull() 506 public LDAPConnectionOptions getConnectionOptions() 507 { 508 return connectionOptions; 509 } 510 511 512 513 /** 514 * {@inheritDoc} 515 */ 516 @Override() 517 public boolean includesAuthentication() 518 { 519 return (bindRequest != null); 520 } 521 522 523 524 /** 525 * {@inheritDoc} 526 */ 527 @Override() 528 public boolean includesPostConnectProcessing() 529 { 530 return (postConnectProcessor != null); 531 } 532 533 534 535 /** 536 * {@inheritDoc} 537 */ 538 @Override() 539 @NotNull() 540 public LDAPConnection getConnection() 541 throws LDAPException 542 { 543 return getConnection(null); 544 } 545 546 547 548 /** 549 * {@inheritDoc} 550 */ 551 @Override() 552 @NotNull() 553 public LDAPConnection getConnection( 554 @Nullable final LDAPConnectionPoolHealthCheck healthCheck) 555 throws LDAPException 556 { 557 // Organize the servers int lists by increasing numbers of connections. 558 final TreeMap<Long,List<ObjectPair<String,Integer>>> serversByCount = 559 new TreeMap<>(); 560 for (final Map.Entry<ObjectPair<String,Integer>,AtomicLong> e : 561 connectionCountsByServer.entrySet()) 562 { 563 final ObjectPair<String,Integer> hostPort = e.getKey(); 564 final long count = e.getValue().get(); 565 566 List<ObjectPair<String,Integer>> l = serversByCount.get(count); 567 if (l == null) 568 { 569 l = new ArrayList<>(connectionCountsByServer.size()); 570 serversByCount.put(count, l); 571 } 572 l.add(hostPort); 573 } 574 575 576 // Try the servers in order of fewest connections to most. If there are 577 // multiple servers with the same number of connections, then randomize the 578 // order of servers in that list to better spread the load across all of 579 // the servers. 580 LDAPException lastException = null; 581 List<ObjectPair<String,Integer>> blacklistedServers = null; 582 for (final List<ObjectPair<String,Integer>> l : serversByCount.values()) 583 { 584 if (l.size() > 1) 585 { 586 Collections.shuffle(l); 587 } 588 589 for (final ObjectPair<String,Integer> hostPort : l) 590 { 591 if ((blacklistManager != null) && 592 blacklistManager.isBlacklisted(hostPort)) 593 { 594 if (blacklistedServers == null) 595 { 596 blacklistedServers = 597 new ArrayList<>(connectionCountsByServer.size()); 598 } 599 blacklistedServers.add(hostPort); 600 continue; 601 } 602 603 try 604 { 605 final LDAPConnection conn = new LDAPConnection(socketFactory, 606 connectionOptions, hostPort.getFirst(), hostPort.getSecond()); 607 doBindPostConnectAndHealthCheckProcessing(conn, bindRequest, 608 postConnectProcessor, healthCheck); 609 connectionCountsByServer.get(hostPort).incrementAndGet(); 610 associateConnectionWithThisServerSet(conn); 611 return conn; 612 } 613 catch (final LDAPException le) 614 { 615 Debug.debugException(le); 616 lastException = le; 617 if (blacklistManager != null) 618 { 619 blacklistManager.addToBlacklist(hostPort, healthCheck); 620 } 621 } 622 } 623 } 624 625 626 // If we've gotten here, then we couldn't get a connection from a 627 // non-blacklisted server. If there were any blacklisted servers, then try 628 // them as a last resort. 629 if (blacklistedServers != null) 630 { 631 for (final ObjectPair<String,Integer> hostPort : blacklistedServers) 632 { 633 try 634 { 635 final LDAPConnection c = new LDAPConnection(socketFactory, 636 connectionOptions, hostPort.getFirst(), hostPort.getSecond()); 637 doBindPostConnectAndHealthCheckProcessing(c, bindRequest, 638 postConnectProcessor, healthCheck); 639 associateConnectionWithThisServerSet(c); 640 blacklistManager.removeFromBlacklist(hostPort); 641 return c; 642 } 643 catch (final LDAPException e) 644 { 645 Debug.debugException(e); 646 lastException = e; 647 } 648 } 649 } 650 651 652 // If we've gotten here, then we've tried all servers without any success, 653 // so throw the last exception that was encountered. 654 throw lastException; 655 } 656 657 658 659 /** 660 * {@inheritDoc} 661 */ 662 @Override() 663 protected void handleConnectionClosed( 664 @NotNull final LDAPConnection connection, 665 @NotNull final String host, final int port, 666 @NotNull final DisconnectType disconnectType, 667 @Nullable final String message, 668 @Nullable final Throwable cause) 669 { 670 final ObjectPair<String,Integer> hostPort = new ObjectPair<>(host, port); 671 final AtomicLong counter = connectionCountsByServer.get(hostPort); 672 if (counter != null) 673 { 674 final long remainingCount = counter.decrementAndGet(); 675 if (remainingCount < 0L) 676 { 677 // This shouldn't happen. If it does, reset it back to zero. 678 counter.compareAndSet(remainingCount, 0L); 679 } 680 } 681 } 682 683 684 685 /** 686 * Retrieves the blacklist manager for this server set. 687 * 688 * @return The blacklist manager for this server set, or {@code null} if no 689 * blacklist will be maintained. 690 */ 691 @Nullable() 692 public ServerSetBlacklistManager getBlacklistManager() 693 { 694 return blacklistManager; 695 } 696 697 698 699 /** 700 * {@inheritDoc} 701 */ 702 @Override() 703 public void shutDown() 704 { 705 if (blacklistManager != null) 706 { 707 blacklistManager.shutDown(); 708 } 709 } 710 711 712 713 /** 714 * {@inheritDoc} 715 */ 716 @Override() 717 public void toString(@NotNull final StringBuilder buffer) 718 { 719 buffer.append("FewestConnectionsServerSet(servers={"); 720 721 final Iterator<Map.Entry<ObjectPair<String,Integer>,AtomicLong>> 722 cbsIterator = connectionCountsByServer.entrySet().iterator(); 723 while (cbsIterator.hasNext()) 724 { 725 final Map.Entry<ObjectPair<String,Integer>,AtomicLong> e = 726 cbsIterator.next(); 727 final ObjectPair<String,Integer> hostPort = e.getKey(); 728 final long count = e.getValue().get(); 729 730 buffer.append('\''); 731 buffer.append(hostPort.getFirst()); 732 buffer.append(':'); 733 buffer.append(hostPort.getSecond()); 734 buffer.append("':"); 735 buffer.append(count); 736 737 if (cbsIterator.hasNext()) 738 { 739 buffer.append(", "); 740 } 741 } 742 743 buffer.append("}, includesAuthentication="); 744 buffer.append(bindRequest != null); 745 buffer.append(", includesPostConnectProcessing="); 746 buffer.append(postConnectProcessor != null); 747 buffer.append(')'); 748 } 749}