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