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}