001/*
002 * Copyright 2019-2024 Ping Identity Corporation
003 * All Rights Reserved.
004 */
005/*
006 * Copyright 2019-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) 2019-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.Collections;
041import java.util.HashSet;
042import java.util.Iterator;
043import java.util.Map;
044import java.util.Set;
045import java.util.Timer;
046import java.util.concurrent.ConcurrentHashMap;
047import java.util.concurrent.atomic.AtomicReference;
048import javax.net.SocketFactory;
049
050import com.unboundid.util.Debug;
051import com.unboundid.util.Mutable;
052import com.unboundid.util.NotNull;
053import com.unboundid.util.Nullable;
054import com.unboundid.util.ObjectPair;
055import com.unboundid.util.StaticUtils;
056import com.unboundid.util.ThreadSafety;
057import com.unboundid.util.ThreadSafetyLevel;
058import com.unboundid.util.Validator;
059
060
061
062/**
063 * This class provides a mechanism for maintaining a blacklist of servers that
064 * have recently been found to be unacceptable for use by a server set.  Server
065 * sets that use this class can temporarily avoid trying to access servers that
066 * may be experiencing problems.
067 */
068@Mutable()
069@ThreadSafety(level=ThreadSafetyLevel.COMPLETELY_THREADSAFE)
070public final class ServerSetBlacklistManager
071{
072  // A reference to a timer that is used to periodically check the status of
073  // blacklisted servers.
074  @NotNull private final AtomicReference<Timer> timerReference;
075
076  // The bind request to use to authenticate newly created connections.
077  @Nullable private final BindRequest bindRequest;
078
079  // The connection options to use when creating connections.
080  @NotNull private final LDAPConnectionOptions connectionOptions;
081
082  // The length of time, in milliseconds, between checks to determine whether
083  // a server should be removed from the blacklist.
084  private final long checkIntervalMillis;
085
086  // A map of currently blacklisted servers.
087  @NotNull private final Map<ObjectPair<String,Integer>,
088       LDAPConnectionPoolHealthCheck> blacklistedServers;
089
090  // The post-connect processor to use for newly created connections.
091  @Nullable private final PostConnectProcessor postConnectProcessor;
092
093  // The socket factory to use when creating connections.
094  @NotNull private final SocketFactory socketFactory;
095
096  // A string representation of the associated server set.
097  @NotNull private final String serverSetString;
098
099
100
101  /**
102   * Creates a new server set blacklist manager with the provided information.
103   *
104   * @param  serverSet             The server set with which this blacklist
105   *                               manager is associated.
106   * @param  socketFactory         An optional socket factory to use when
107   *                               creating connections.  If this is
108   *                               {@code null}, a default socket factory will
109   *                               be used.
110   * @param  connectionOptions     An optional set of connection options to use
111   *                               when creating connections.  If this is
112   *                               {@code null}, a default set of connection
113   *                               options will be used.
114   * @param  bindRequest           An optional bind request to use to
115   *                               authenticate connections that are
116   *                               established.  It may be {@code null} if no
117   *                               authentication should be performed.
118   * @param  postConnectProcessor  An optional post-connect processor that
119   *                               should be invoked for any connection that is
120   *                               established.  It may be {@code null} if no
121   *                               post-connect processing should be performed.
122   * @param  checkIntervalMillis   The length of time, in milliseconds, between
123   *                               checks to determine whether a server should
124   *                               be removed from the blacklist.
125   */
126  ServerSetBlacklistManager(@NotNull final ServerSet serverSet,
127       @Nullable final SocketFactory socketFactory,
128       @Nullable final LDAPConnectionOptions connectionOptions,
129       @Nullable final BindRequest bindRequest,
130       @Nullable final PostConnectProcessor postConnectProcessor,
131       final long checkIntervalMillis)
132  {
133    Validator.ensureTrue((checkIntervalMillis > 0L),
134         "ServerSetBlacklistManager.checkIntervalMillis must be greater " +
135              "than zero.");
136    this.checkIntervalMillis = checkIntervalMillis;
137
138    serverSetString = serverSet.toString();
139
140    if (socketFactory == null)
141    {
142      this.socketFactory = SocketFactory.getDefault();
143    }
144    else
145    {
146      this.socketFactory = socketFactory;
147    }
148
149    if (connectionOptions == null)
150    {
151      this.connectionOptions = new LDAPConnectionOptions();
152    }
153    else
154    {
155      this.connectionOptions = connectionOptions;
156    }
157
158    this.bindRequest = bindRequest;
159    this.postConnectProcessor = postConnectProcessor;
160
161    blacklistedServers =
162         new ConcurrentHashMap<>(StaticUtils.computeMapCapacity(10));
163    timerReference = new AtomicReference<>();
164  }
165
166
167
168  /**
169   * Indicates whether the blacklist is currently empty.
170   *
171   * @return  {@code true} if the blacklist is currently empty, or {@code false}
172   *          if it contains at least one server.
173   */
174  public boolean isEmpty()
175  {
176    if (blacklistedServers.isEmpty())
177    {
178      return true;
179    }
180    else
181    {
182      ensureTimerIsRunning();
183      return false;
184    }
185  }
186
187
188
189  /**
190   * Retrieves the number of servers currently on the blacklist.
191   *
192   * @return  The number of servers currently on the blacklist.
193   */
194  public int size()
195  {
196    if (blacklistedServers.isEmpty())
197    {
198      return 0;
199    }
200    else
201    {
202      ensureTimerIsRunning();
203      return blacklistedServers.size();
204    }
205  }
206
207
208
209  /**
210   * Retrieves a list of the servers currently on the blacklist.
211   *
212   * @return  A list of the servers currently on the blacklist.
213   */
214  @NotNull()
215  public Set<ObjectPair<String,Integer>> getBlacklistedServers()
216  {
217    if (! blacklistedServers.isEmpty())
218    {
219      ensureTimerIsRunning();
220    }
221
222    return Collections.unmodifiableSet(
223         new HashSet<>(blacklistedServers.keySet()));
224  }
225
226
227
228  /**
229   * Indicates whether the specified server is currently on the blacklist.
230   *
231   * @param  host  The address of the server for which to make the
232   *               determination.  It must not be {@code null}.
233   * @param  port  The port of the server for which to make the determination.
234   *               It must be between 1 and 65535, inclusive.
235   *
236   * @return  {@code true} if the server is on the blacklist, or {@code false}
237   *          if not.
238   */
239  public boolean isBlacklisted(@NotNull final String host, final int port)
240  {
241    if (blacklistedServers.isEmpty())
242    {
243      return false;
244    }
245    else
246    {
247      ensureTimerIsRunning();
248      return blacklistedServers.containsKey(new ObjectPair<>(host, port));
249    }
250  }
251
252
253
254  /**
255   * Indicates whether the specified server is currently on the blacklist.
256   *
257   * @param  hostPort  An {@code ObjectPair} containing the address and port of
258   *                   the server for which to make the determination.  It must
259   *                   not be {@code null}.
260   *
261   * @return  {@code true} if the server is on the blacklist, or {@code false}
262   *          if not.
263   */
264  public boolean isBlacklisted(
265                      @NotNull final ObjectPair<String,Integer> hostPort)
266  {
267    if (blacklistedServers.isEmpty())
268    {
269      return false;
270    }
271    else
272    {
273      ensureTimerIsRunning();
274      return blacklistedServers.containsKey(hostPort);
275    }
276  }
277
278
279
280  /**
281   * Adds the specified server to the blacklist.
282   *
283   * @param  host         The address of the server to be added.  It must not be
284   *                      {@code null}.
285   * @param  port         The port of the server to be added.  It must be
286   *                      between 1 and 65535, inclusive.
287   * @param  healthCheck  The health check to use for periodic checks to see if
288   *                      the server can be removed from the blacklist.  It may
289   *                      be {@code null} if no health checking is required.
290   */
291  void addToBlacklist(@NotNull final String host, final int port,
292                      @Nullable final LDAPConnectionPoolHealthCheck healthCheck)
293  {
294    addToBlacklist(new ObjectPair<>(host, port), healthCheck);
295  }
296
297
298
299  /**
300   * Adds the specified server to the blacklist.
301   *
302   * @param  hostPort     An {@code ObjectPair} containing the address and port
303   *                      of the server to be added.  It must not be
304   *                      {@code null}.
305   * @param  healthCheck  The health check to use for periodic checks to see if
306   *                      the server can be removed from the blacklist.  It may
307   *                      be {@code null} if no health checking is required.
308   */
309  void addToBlacklist(@NotNull final ObjectPair<String,Integer> hostPort,
310                      @Nullable final LDAPConnectionPoolHealthCheck healthCheck)
311  {
312    if (healthCheck == null)
313    {
314      blacklistedServers.put(hostPort, new LDAPConnectionPoolHealthCheck());
315    }
316    else
317    {
318      blacklistedServers.put(hostPort, healthCheck);
319    }
320    ensureTimerIsRunning();
321  }
322
323
324
325  /**
326   * Removes the specified server from the blacklist.
327   *
328   * @param  host  The address of the server to be removed.  It must not be
329   *               {@code null}.
330   * @param  port  The port of the server to be removed.  It must be between 1
331   *               and 65535, inclusive.
332   */
333  void removeFromBlacklist(@NotNull final String host, final int port)
334  {
335    removeFromBlacklist(new ObjectPair<>(host, port));
336  }
337
338
339
340  /**
341   * Removes the specified server from the blacklist.
342   *
343   * @param  hostPort  An {@code ObjectPair} containing the address and port of
344   *                   the server to be removed.  It must not be {@code null}.
345   */
346  void removeFromBlacklist(@NotNull final ObjectPair<String,Integer> hostPort)
347  {
348    blacklistedServers.remove(hostPort);
349    if (! blacklistedServers.isEmpty())
350    {
351      ensureTimerIsRunning();
352    }
353  }
354
355
356
357  /**
358   * Clears the blacklist.
359   */
360  void clear()
361  {
362    blacklistedServers.clear();
363  }
364
365
366
367  /**
368   * Ensures that there is a timer to periodically check the status of
369   * blacklisted servers.
370   */
371  private synchronized void ensureTimerIsRunning()
372  {
373    Timer timer = timerReference.get();
374    if (timer == null)
375    {
376      timer = new Timer(
377           "ServerSet Blacklist Manager Timer for " + serverSetString, true);
378      timerReference.set(timer);
379
380      timer.scheduleAtFixedRate(new ServerSetBlacklistManagerTimerTask(this),
381           checkIntervalMillis, checkIntervalMillis);
382    }
383  }
384
385
386
387  /**
388   * Checks all blacklisted servers to see if any of them should be removed from
389   * the blacklist.  If there are no servers on the blacklist and the timer is
390   * running, then it will be shut down.
391   */
392  void checkBlacklistedServers()
393  {
394    // Iterate through the blacklist and check each of the servers.  If we find
395    // one that is acceptable, then remove it from the blacklist.
396    final Iterator<Map.Entry<ObjectPair<String,Integer>,
397         LDAPConnectionPoolHealthCheck>> iterator =
398         blacklistedServers.entrySet().iterator();
399    while (iterator.hasNext())
400    {
401      final Map.Entry<ObjectPair<String,Integer>,
402           LDAPConnectionPoolHealthCheck> e = iterator.next();
403      final ObjectPair<String,Integer> hostPort = e.getKey();
404      final LDAPConnectionPoolHealthCheck healthCheck = e.getValue();
405      try (LDAPConnection conn = new LDAPConnection(socketFactory,
406                connectionOptions, hostPort.getFirst(), hostPort.getSecond()))
407      {
408        ServerSet.doBindPostConnectAndHealthCheckProcessing(conn, bindRequest,
409             postConnectProcessor, healthCheck);
410        iterator.remove();
411      }
412      catch (final Exception ex)
413      {
414        Debug.debugException(ex);
415      }
416    }
417
418
419    // If the blacklist is empty, then cancel the timer, if there is one.
420    if (blacklistedServers.isEmpty())
421    {
422      synchronized (this)
423      {
424        if (blacklistedServers.isEmpty())
425        {
426          final Timer timer = timerReference.getAndSet(null);
427          if (timer != null)
428          {
429            timer.cancel();
430            timer.purge();
431          }
432
433          return;
434        }
435      }
436    }
437  }
438
439
440
441  /**
442   * Shuts down the blacklist manager.
443   */
444  public synchronized void shutDown()
445  {
446    final Timer timer = timerReference.getAndSet(null);
447    if (timer != null)
448    {
449      timer.cancel();
450      timer.purge();
451    }
452
453    blacklistedServers.clear();
454  }
455
456
457
458  /**
459   * Retrieves a string representation of this server set blacklist manager.
460   *
461   * @return  A string representation of this server set blacklist manager.
462   */
463  @Override()
464  @NotNull()
465  public String toString()
466  {
467    final StringBuilder buffer = new StringBuilder();
468    toString(buffer);
469    return buffer.toString();
470  }
471
472
473
474  /**
475   * Appends a string representation of this server set blacklist manager to the
476   * provided buffer.
477   *
478   * @param  buffer  The buffer to which the information should be appended.
479   */
480  public void toString(@NotNull final StringBuilder buffer)
481  {
482    buffer.append("ServerSetBlacklistManager(serverSet='");
483    buffer.append(serverSetString);
484    buffer.append("', blacklistedServers={");
485
486    final Iterator<ObjectPair<String,Integer>> iterator =
487         blacklistedServers.keySet().iterator();
488    while (iterator.hasNext())
489    {
490      final ObjectPair<String,Integer> hostPort = iterator.next();
491      buffer.append('\'');
492      buffer.append(hostPort.getFirst());
493      buffer.append(':');
494      buffer.append(hostPort.getSecond());
495      buffer.append('\'');
496
497      if (iterator.hasNext())
498      {
499        buffer.append(',');
500      }
501    }
502
503    buffer.append("}, checkIntervalMillis=");
504    buffer.append(checkIntervalMillis);
505    buffer.append(')');
506  }
507}