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