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}