001/* 002 * Copyright 2024 Ping Identity Corporation 003 * All Rights Reserved. 004 */ 005/* 006 * Copyright 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) 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.unboundidds; 037 038 039 040import java.io.Serializable; 041import java.util.Date; 042 043import com.unboundid.ldap.sdk.BindResult; 044import com.unboundid.ldap.sdk.DereferencePolicy; 045import com.unboundid.ldap.sdk.DisconnectType; 046import com.unboundid.ldap.sdk.Filter; 047import com.unboundid.ldap.sdk.LDAPConnection; 048import com.unboundid.ldap.sdk.LDAPConnectionPoolHealthCheck; 049import com.unboundid.ldap.sdk.LDAPException; 050import com.unboundid.ldap.sdk.ResultCode; 051import com.unboundid.ldap.sdk.SearchRequest; 052import com.unboundid.ldap.sdk.SearchResultEntry; 053import com.unboundid.ldap.sdk.SearchScope; 054import com.unboundid.util.Debug; 055import com.unboundid.util.NotMutable; 056import com.unboundid.util.NotNull; 057import com.unboundid.util.Nullable; 058import com.unboundid.util.StaticUtils; 059import com.unboundid.util.ThreadSafety; 060import com.unboundid.util.ThreadSafetyLevel; 061import com.unboundid.util.Validator; 062 063import static com.unboundid.ldap.sdk.unboundidds.UnboundIDDSMessages.*; 064 065 066 067/** 068 * This class provides an LDAP connection pool health check implementation that 069 * can be used to examine the replication backlog (reflecting changes that have 070 * been made in other replicas but have not yet been applied in the local 071 * instance) of a Ping Identity Directory Server instance. It can consider both 072 * the number of changes in the replication backlog and the age of the oldest 073 * outstanding change. 074 * <BR> 075 * <BLOCKQUOTE> 076 * <B>NOTE:</B> This class, and other classes within the 077 * {@code com.unboundid.ldap.sdk.unboundidds} package structure, are only 078 * supported for use against Ping Identity, UnboundID, and 079 * Nokia/Alcatel-Lucent 8661 server products. These classes provide support 080 * for proprietary functionality or for external specifications that are not 081 * considered stable or mature enough to be guaranteed to work in an 082 * interoperable way with other types of LDAP servers. 083 * </BLOCKQUOTE> 084 */ 085@NotMutable() 086@ThreadSafety(level=ThreadSafetyLevel.COMPLETELY_THREADSAFE) 087public final class ReplicationBacklogLDAPConnectionPoolHealthCheck 088 extends LDAPConnectionPoolHealthCheck 089 implements Serializable 090{ 091 /** 092 * The default maximum response time value in milliseconds, which is set to 093 * 5,000 milliseconds or 5 seconds. 094 */ 095 private static final long DEFAULT_MAX_RESPONSE_TIME_MILLIS = 5_000L; 096 097 098 099 /** 100 * The name of the attribute used to specify the base DN for the target 101 * replication domain. 102 */ 103 @NotNull() 104 private static final String BASE_DN_ATTRIBUTE_NAME = "base-dn"; 105 106 107 108 /** 109 * The name of the attribute used to specify the number of changes currently 110 * in the replication backlog. 111 */ 112 @NotNull() 113 private static final String BACKLOG_COUNT_ATTRIBUTE_NAME = 114 "replication-backlog"; 115 116 117 118 /** 119 * The name of the attribute used to specify the time that the oldest change 120 * in the replication backlog was first applied to another instance. 121 */ 122 @NotNull() 123 private static final String OLDEST_BACKLOG_CHANGE_TIME_ATTRIBUTE_NAME = 124 "age-of-oldest-backlog-change"; 125 126 127 128 /** 129 * The name of the object class used for replica monitor entries. 130 */ 131 @NotNull() 132 private static final String REPLICA_MONITOR_ENTRY_OBJECT_CLASS_NAME = 133 "ds-replica-monitor-entry"; 134 135 136 137 /** 138 * The serial version UID for this serializable class. 139 */ 140 private static final long serialVersionUID = -2201740505566813382L; 141 142 143 144 // Indicates whether to invoke the test after a connection has been 145 // authenticated. 146 private final boolean invokeAfterAuthentication; 147 148 // Indicates whether to invoke the test during background health checks. 149 private final boolean invokeForBackgroundChecks; 150 151 // Indicates whether to invoke the test when checking out a connection. 152 private final boolean invokeOnCheckout; 153 154 // Indicates whether to invoke the test when creating a new connection. 155 private final boolean invokeOnCreate; 156 157 // Indicates whether to invoke the test whenever an exception is encountered 158 // when using the connection. 159 private final boolean invokeOnException; 160 161 // Indicates whether to invoke the test when releasing a connection. 162 private final boolean invokeOnRelease; 163 164 // The maximum allowed age, in milliseconds, of any change in the replication 165 // backlog. 166 @Nullable private final Long maxAllowedBacklogAgeMillis; 167 168 // The maximum allowed number of changes iun the replication backlog. 169 @Nullable private final Long maxAllowedBacklogCount; 170 171 // The maximum response time value in milliseconds. 172 private final long maxResponseTimeMillis; 173 174 // The search request that will be used to retrieve the monitor entry. 175 @NotNull private final SearchRequest searchRequest; 176 177 // The base DN for the target replication domain. 178 @NotNull private final String baseDN; 179 180 181 182 /** 183 * Creates a new instance of this LDAP connection pool health check with the 184 * provided information. 185 * 186 * @param invokeOnCreate 187 * Indicates whether to test for the existence of the target 188 * entry whenever a new connection is created for use in the 189 * pool. Note that this check will be performed immediately 190 * after the connection has been established and before any 191 * attempt has been made to authenticate that connection. 192 * @param invokeAfterAuthentication 193 * Indicates whether to test for the existence of the target 194 * entry immediately after a connection has been authenticated. 195 * This includes immediately after a newly-created connection 196 * has been authenticated, after a call to the connection pool's 197 * {@code bindAndRevertAuthentication} method, and after a call 198 * to the connection pool's 199 * {@code releaseAndReAuthenticateConnection} method. Note that 200 * even if this is {@code true}, the health check will only be 201 * performed if the provided bind result indicates that the bind 202 * was successful. 203 * @param invokeOnCheckout 204 * Indicates whether to test for the existence of the target 205 * entry immediately before a connection is checked out of the 206 * pool. 207 * @param invokeOnRelease 208 * Indicates whether to test for the existence of the target 209 * entry immediately after a connection has been released back 210 * to the pool. 211 * @param invokeForBackgroundChecks 212 * Indicates whether to test for the existence of the target 213 * entry during periodic background health checks. 214 * @param invokeOnException 215 * Indicates whether to test for the existence of the target 216 * entry if an exception is encountered when using the 217 * connection. 218 * @param maxResponseTimeMillis 219 * The maximum length of time, in milliseconds, to wait for the 220 * monitor entry to be retrieved. If the monitor entry cannot be 221 * retrieved within this length of time, the health check will 222 * fail. If the provided value is less than or equal to zero, 223 * then a default timeout of 5,000 milliseconds (5 seconds) will 224 * be used. 225 * @param baseDN 226 * The base DN for the target replication domain. This is 227 * typically the base DN for the backend containing the 228 * replicated data. It must not be {@code null}. 229 * @param maxAllowedBacklogCount 230 * The maximum number of changes that may be contained in the 231 * replication backlog before a server will be considered 232 * unavailable. This may be {@code null} if the backlog is to 233 * be evaluated only based on the age of the oldest outstanding 234 * change, but at least one of {@code maxAllowedBacklogCount} and 235 * {@code maxAllowedBacklogAgeMillis} must be specified. 236 * @param maxAllowedBacklogAgeMillis 237 * The maximum length of time, in milliseconds, that a change may 238 * be contained in the replication backlog before a server will 239 * be considered unavailable. This may be {@code null} if the 240 * backlog is to be evaluated only based on the number of 241 * outstanding changes, but at least one of 242 * {@code maxAllowedBacklogCount} and 243 * {@code maxAllowedBacklogAgeMillis} must be specified. 244 */ 245 public ReplicationBacklogLDAPConnectionPoolHealthCheck( 246 final boolean invokeOnCreate, 247 final boolean invokeAfterAuthentication, 248 final boolean invokeOnCheckout, 249 final boolean invokeOnRelease, 250 final boolean invokeForBackgroundChecks, 251 final boolean invokeOnException, 252 final long maxResponseTimeMillis, 253 @NotNull final String baseDN, 254 @Nullable final Long maxAllowedBacklogCount, 255 @Nullable final Long maxAllowedBacklogAgeMillis) 256 { 257 Validator.ensureNotNullWithMessage(baseDN, 258 "ReplicationBacklogLDAPConnectionPoolHealthCheck.baseDN must not be " + 259 "null."); 260 261 if (maxAllowedBacklogCount == null) 262 { 263 if (maxAllowedBacklogAgeMillis == null) 264 { 265 Validator.violation("At least one of maxAllowedBacklogCount or " + 266 "maxAllowedBacklogAgeMillis must be non-null for the " + 267 "ReplicationBacklogLDAPConnectionPoolHealthCheck"); 268 } 269 } 270 else 271 { 272 Validator.ensureTrue((maxAllowedBacklogCount > 0L), 273 "If specified, ReplicationBacklogLDAPConnectionPoolHealthCheck." + 274 "maxAllowedBacklogCount must be greater than zero."); 275 } 276 277 if (maxAllowedBacklogAgeMillis != null) 278 { 279 Validator.ensureTrue((maxAllowedBacklogAgeMillis > 0L), 280 "If specified, ReplicationBacklogLDAPConnectionPoolHealthCheck." + 281 "maxAllowedBacklogAgeMillis must be greater than zero."); 282 } 283 284 this.invokeOnCreate = invokeOnCreate; 285 this.invokeAfterAuthentication = invokeAfterAuthentication; 286 this.invokeOnCheckout = invokeOnCheckout; 287 this.invokeOnRelease = invokeOnRelease; 288 this.invokeForBackgroundChecks = invokeForBackgroundChecks; 289 this.invokeOnException = invokeOnException; 290 this.baseDN = baseDN; 291 this.maxAllowedBacklogCount = maxAllowedBacklogCount; 292 this.maxAllowedBacklogAgeMillis = maxAllowedBacklogAgeMillis; 293 294 if (maxResponseTimeMillis > 0L) 295 { 296 this.maxResponseTimeMillis = maxResponseTimeMillis; 297 } 298 else 299 { 300 this.maxResponseTimeMillis = DEFAULT_MAX_RESPONSE_TIME_MILLIS; 301 } 302 303 int timeLimitSeconds = (int) (this.maxResponseTimeMillis / 1_000L); 304 if ((this.maxResponseTimeMillis % 1_000L) != 0L) 305 { 306 timeLimitSeconds++; 307 } 308 309 final Filter filter = Filter.createANDFilter( 310 Filter.createEqualityFilter("objectClass", 311 REPLICA_MONITOR_ENTRY_OBJECT_CLASS_NAME), 312 Filter.createEqualityFilter(BASE_DN_ATTRIBUTE_NAME, baseDN)); 313 searchRequest = new SearchRequest("cn=monitor", SearchScope.SUB, 314 DereferencePolicy.NEVER, 1, timeLimitSeconds, false, filter, 315 BACKLOG_COUNT_ATTRIBUTE_NAME, 316 OLDEST_BACKLOG_CHANGE_TIME_ATTRIBUTE_NAME); 317 searchRequest.setResponseTimeoutMillis(this.maxResponseTimeMillis); 318 } 319 320 321 322 /** 323 * {@inheritDoc} 324 */ 325 @Override() 326 public void ensureNewConnectionValid(@NotNull final LDAPConnection connection) 327 throws LDAPException 328 { 329 if (invokeOnCreate) 330 { 331 checkReplicationBacklog(connection); 332 } 333 } 334 335 336 337 /** 338 * {@inheritDoc} 339 */ 340 @Override() 341 public void ensureConnectionValidAfterAuthentication( 342 @NotNull final LDAPConnection connection, 343 @NotNull final BindResult bindResult) 344 throws LDAPException 345 { 346 if (invokeAfterAuthentication && 347 (bindResult.getResultCode() == ResultCode.SUCCESS)) 348 { 349 checkReplicationBacklog(connection); 350 } 351 } 352 353 354 355 /** 356 * {@inheritDoc} 357 */ 358 @Override() 359 public void ensureConnectionValidForCheckout( 360 @NotNull final LDAPConnection connection) 361 throws LDAPException 362 { 363 if (invokeOnCheckout) 364 { 365 checkReplicationBacklog(connection); 366 } 367 } 368 369 370 371 /** 372 * {@inheritDoc} 373 */ 374 @Override() 375 public void ensureConnectionValidForRelease( 376 @NotNull final LDAPConnection connection) 377 throws LDAPException 378 { 379 if (invokeOnRelease) 380 { 381 checkReplicationBacklog(connection); 382 } 383 } 384 385 386 387 /** 388 * {@inheritDoc} 389 */ 390 @Override() 391 public void ensureConnectionValidForContinuedUse( 392 @NotNull final LDAPConnection connection) 393 throws LDAPException 394 { 395 if (invokeForBackgroundChecks) 396 { 397 checkReplicationBacklog(connection); 398 } 399 } 400 401 402 403 /** 404 * {@inheritDoc} 405 */ 406 @Override() 407 public void ensureConnectionValidAfterException( 408 @NotNull final LDAPConnection connection, 409 @NotNull final LDAPException exception) 410 throws LDAPException 411 { 412 if (invokeOnException && 413 (! ResultCode.isConnectionUsable(exception.getResultCode()))) 414 { 415 checkReplicationBacklog(connection); 416 } 417 } 418 419 420 421 /** 422 * Indicates whether this health check will check the replication backlog 423 * whenever a new connection is created. 424 * 425 * @return {@code true} if this health check will check the replication 426 * backlog whenever a new connection is created, or {@code false} if 427 * not. 428 */ 429 public boolean invokeOnCreate() 430 { 431 return invokeOnCreate; 432 } 433 434 435 436 /** 437 * Indicates whether this health check will check the replication backlog 438 * after a connection has been authenticated, including after authenticating a 439 * newly-created connection, as well as after calls to the connection pool's 440 * {@code bindAndRevertAuthentication} and 441 * {@code releaseAndReAuthenticateConnection} methods. 442 * 443 * @return {@code true} if this health check will check the replication 444 * backlog whenever a connection has been authenticated, or 445 * {@code false} if not. 446 */ 447 public boolean invokeAfterAuthentication() 448 { 449 return invokeAfterAuthentication; 450 } 451 452 453 454 /** 455 * Indicates whether this health check will check the replication backlog 456 * whenever a connection is to be checked out for use. 457 * 458 * @return {@code true} if this health check will check the replication 459 * backlog whenever a connection is to be checked out, or 460 * {@code false} if not. 461 */ 462 public boolean invokeOnCheckout() 463 { 464 return invokeOnCheckout; 465 } 466 467 468 469 /** 470 * Indicates whether this health check will check the replication backlog 471 * whenever a connection is to be released back to the pool. 472 * 473 * @return {@code true} if this health check will check the replication 474 * backlog whenever a connection is to be released, or {@code false} 475 * if not. 476 */ 477 public boolean invokeOnRelease() 478 { 479 return invokeOnRelease; 480 } 481 482 483 484 /** 485 * Indicates whether this health check will check the replication backlog 486 * during periodic background health checks. 487 * 488 * @return {@code true} if this health check will check the replication 489 * backlog during periodic background health checks, or {@code false} 490 * if not. 491 */ 492 public boolean invokeForBackgroundChecks() 493 { 494 return invokeForBackgroundChecks; 495 } 496 497 498 499 /** 500 * Indicates whether this health check will check the replication backlog if 501 * an exception is caught while processing an operation on a connection. 502 * 503 * @return {@code true} if this health check will check the replication 504 * backlog whenever an exception is caught, or {@code false} if not. 505 */ 506 public boolean invokeOnException() 507 { 508 return invokeOnException; 509 } 510 511 512 513 /** 514 * Retrieves the maximum length of time in milliseconds that this health 515 * check should wait for the target monitor entry to be returned. 516 * 517 * @return The maximum length of time in milliseconds that this health check 518 * should wait for the target monitor entry to be returned. 519 */ 520 public long getMaxResponseTimeMillis() 521 { 522 return maxResponseTimeMillis; 523 } 524 525 526 527 /** 528 * Retrieves the base DN for the target replication domain. 529 * 530 * @return The base DN for the target replication domain. 531 */ 532 @NotNull() 533 public String getBaseDN() 534 { 535 return baseDN; 536 } 537 538 539 540 /** 541 * Retrieves the maximum number of changes that may be contained in the 542 * replication backlog before a server will be considered unavailable. 543 * 544 * @return The maximum number of changes that may be contained in the 545 * replication backlog before a server will be considered 546 * unavailable, or {@code null} if the backlog will be evaluated only 547 * based on the age of the oldest outstanding change. 548 */ 549 @Nullable() 550 public Long getMaxAllowedBacklogCount() 551 { 552 return maxAllowedBacklogCount; 553 } 554 555 556 557 /** 558 * Retrieves the maximum length of time, in milliseconds, that a change may be 559 * contained in the replication backlog before a server will be considered 560 * unavailable. 561 * 562 * @return The maximum length of time, in milliseconds, that a change may be 563 * contained in the replication backlog before a server will be 564 * considered unavailable, or {@code null} if the backlog will be 565 * evaluated only based on the number of outstanding changes. 566 */ 567 @Nullable() 568 public Long getMaxAllowedBacklogAgeMillis() 569 { 570 return maxAllowedBacklogAgeMillis; 571 } 572 573 574 575 /** 576 * Retrieves the replica monitor entry for the target base DN and uses it to 577 * determine the size and age of the replication backlog. If the server has 578 * too many outstanding changes, if the oldest change is too old, or if a 579 * problem occurs while attempting to make the determination, then an 580 * exception will be thrown. 581 * 582 * @param conn The connection to be checked. 583 * 584 * @throws LDAPException If a problem occurs while trying to retrieve the 585 * target monitor entry, if it cannot be retrieved in 586 * an acceptable length of time, or if the server has 587 * an unacceptable replication backlog. 588 */ 589 private void checkReplicationBacklog(@NotNull final LDAPConnection conn) 590 throws LDAPException 591 { 592 final SearchResultEntry monitorEntry; 593 try 594 { 595 monitorEntry = conn.searchForEntry(searchRequest.duplicate()); 596 } 597 catch (final LDAPException e) 598 { 599 Debug.debugException(e); 600 601 final String message = 602 ERR_REPLICATION_BACKLOG_HEALTH_CHECK_ERROR_GETTING_MONITOR_ENTRY.get( 603 baseDN, conn.getHostPort(), StaticUtils.getExceptionMessage(e)); 604 conn.setDisconnectInfo(DisconnectType.POOLED_CONNECTION_DEFUNCT, message, 605 e); 606 throw new LDAPException(e.getResultCode(), message, e); 607 } 608 609 610 if (monitorEntry == null) 611 { 612 // If no monitor entry was returned, then we'll assume that the backlog 613 // is acceptable. 614 return; 615 } 616 617 618 if (maxAllowedBacklogCount != null) 619 { 620 final Long currentBacklogCount = 621 monitorEntry.getAttributeValueAsLong(BACKLOG_COUNT_ATTRIBUTE_NAME); 622 if ((currentBacklogCount != null) && 623 (currentBacklogCount > maxAllowedBacklogCount)) 624 { 625 final String message = 626 ERR_REPLICATION_BACKLOG_HEALTH_CHECK_COUNT_EXCEEDED.get( 627 currentBacklogCount, baseDN, conn.getHostPort(), 628 maxAllowedBacklogCount); 629 conn.setDisconnectInfo(DisconnectType.POOLED_CONNECTION_DEFUNCT, 630 message, null); 631 throw new LDAPException(ResultCode.UNAVAILABLE, message); 632 } 633 } 634 635 636 if (maxAllowedBacklogAgeMillis != null) 637 { 638 final Date oldestChangeDate = monitorEntry.getAttributeValueAsDate( 639 OLDEST_BACKLOG_CHANGE_TIME_ATTRIBUTE_NAME); 640 if (oldestChangeDate != null) 641 { 642 final long oldestChangeAgeMillis = 643 System.currentTimeMillis() - oldestChangeDate.getTime(); 644 if (oldestChangeAgeMillis > maxAllowedBacklogAgeMillis) 645 { 646 final String message = 647 ERR_REPLICATION_BACKLOG_HEALTH_CHECK_AGE_EXCEEDED.get( 648 baseDN, conn.getHostPort(), 649 StaticUtils.millisToHumanReadableDuration( 650 oldestChangeAgeMillis), 651 StaticUtils.millisToHumanReadableDuration( 652 maxAllowedBacklogAgeMillis)); 653 conn.setDisconnectInfo(DisconnectType.POOLED_CONNECTION_DEFUNCT, 654 message, null); 655 throw new LDAPException(ResultCode.UNAVAILABLE, message); 656 } 657 } 658 } 659 } 660 661 662 663 /** 664 * {@inheritDoc} 665 */ 666 @Override() 667 public void toString(@NotNull final StringBuilder buffer) 668 { 669 buffer.append("ReplicationBacklogLDAPConnectionPoolHealthCheck("); 670 buffer.append("invokeOnCreate="); 671 buffer.append(invokeOnCreate); 672 buffer.append(", invokeAfterAuthentication="); 673 buffer.append(invokeAfterAuthentication); 674 buffer.append(", invokeOnCheckout="); 675 buffer.append(invokeOnCheckout); 676 buffer.append(", invokeOnRelease="); 677 buffer.append(invokeOnRelease); 678 buffer.append(", invokeForBackgroundChecks="); 679 buffer.append(invokeForBackgroundChecks); 680 buffer.append(", invokeOnException="); 681 buffer.append(invokeOnException); 682 buffer.append(", maxResponseTimeMillis="); 683 buffer.append(maxResponseTimeMillis); 684 buffer.append(", baseDN='"); 685 buffer.append(baseDN); 686 buffer.append('\''); 687 688 if (maxAllowedBacklogCount != null) 689 { 690 buffer.append(", maxAllowedBacklogCount="); 691 buffer.append(maxAllowedBacklogCount); 692 } 693 694 if (maxAllowedBacklogAgeMillis != null) 695 { 696 buffer.append(", maxAllowedBacklogAgeMillis="); 697 buffer.append(maxAllowedBacklogAgeMillis); 698 } 699 700 buffer.append(')'); 701 } 702}