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