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