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}