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