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}