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}