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