UnboundID Server SDK

Ping Identity
UnboundID Server SDK Documentation

ExampleLDAPHealthCheck.java

/*
 * CDDL HEADER START
 *
 * The contents of this file are subject to the terms of the
 * Common Development and Distribution License, Version 1.0 only
 * (the "License").  You may not use this file except in compliance
 * with the License.
 *
 * You can obtain a copy of the license at
 * docs/licenses/cddl.txt
 * or http://www.opensource.org/licenses/cddl1.php.
 * See the License for the specific language governing permissions
 * and limitations under the License.
 *
 * When distributing Covered Code, include this CDDL HEADER in each
 * file and include the License file at
 * docs/licenses/cddl.txt.  If applicable,
 * add the following below this CDDL HEADER, with the fields enclosed
 * by brackets "[]" replaced with your own identifying information:
 *      Portions Copyright [yyyy] [name of copyright owner]
 *
 * CDDL HEADER END
 *
 *
 *      Portions Copyright 2010-2024 Ping Identity Corporation
 */
package com.unboundid.directory.sdk.examples;



import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;

import com.unboundid.directory.sdk.proxy.api.LDAPHealthCheck;
import com.unboundid.directory.sdk.proxy.config.LDAPHealthCheckConfig;
import com.unboundid.directory.sdk.proxy.types.BackendServer;
import com.unboundid.directory.sdk.proxy.types.CompletedProxyOperationContext;
import com.unboundid.directory.sdk.proxy.types.HealthCheckResult;
import com.unboundid.directory.sdk.proxy.types.HealthCheckState;
import com.unboundid.directory.sdk.proxy.types.ProxyServerContext;
import com.unboundid.ldap.sdk.Filter;
import com.unboundid.ldap.sdk.LDAPConnection;
import com.unboundid.ldap.sdk.LDAPException;
import com.unboundid.ldap.sdk.LDAPSearchException;
import com.unboundid.ldap.sdk.ResultCode;
import com.unboundid.ldap.sdk.SearchRequest;
import com.unboundid.ldap.sdk.SearchResult;
import com.unboundid.ldap.sdk.SearchScope;
import com.unboundid.util.StaticUtils;
import com.unboundid.util.args.ArgumentException;
import com.unboundid.util.args.ArgumentParser;
import com.unboundid.util.args.DNArgument;
import com.unboundid.util.args.DurationArgument;



/**
 * This class provides a simple example of an LDAP health check which simply
 * attempts to retrieve a specified entry from the backend server.  The length
 * of time required to retrieve the entry will be used to help determine the
 * health check score.  It has the following configuration arguments:
 * <UL>
 *   <LI>entry-dn -- The DN of the entry to retrieve.</LI>
 *   <LI>max-available-response-time -- The maximum search duration to consider
 *       a server available.  Any duration longer than this will cause the
 *       server to be considered either degraded or unavailable.</LI>
 *   <LI>max-degraded-response-time -- The maximum search duration to consider a
 *       server degraded.  Any duration longer than this will cause the server
 *       to be considered unavailable.</LI>
 * </UL>
 */
public final class ExampleLDAPHealthCheck
       extends LDAPHealthCheck
{
  /**
   * The name of the argument that will be used to specify the DN of the entry
   * to retrieve.
   */
  private static final String ARG_NAME_ENTRY_DN = "entry-dn";



  /**
   * The name of the argument that will be used to specify the maximum available
   * response time.
   */
  private static final String ARG_NAME_MAX_AVAILABLE_DURATION =
       "max-available-response-time";



  /**
   * The name of the argument that will be used to specify the maximum degraded
   * response time.
   */
  private static final String ARG_NAME_MAX_DEGRADED_DURATION =
       "max-degraded-response-time";



  // The maximum available duration.
  private volatile long maxAvailableDuration;

  // The maximum degraded duration.
  private volatile long maxDegradedDuration;

  // The server context for the server in which this extension is running.
  private ProxyServerContext serverContext;

  // The DN of the entry to retrieve.
  private volatile String entryDN;



  /**
   * Creates a new instance of this LDAP health check.  All LDAP health check
   * implementations must include a default constructor, but any initialization
   * should generally be done in the {@code initializeLDAPHealthCheck} method.
   */
  public ExampleLDAPHealthCheck()
  {
    // No implementation required.
  }



  /**
   * Retrieves a human-readable name for this extension.
   *
   * @return  A human-readable name for this extension.
   */
  @Override()
  public String getExtensionName()
  {
    return "Example LDAP Health Check";
  }



  /**
   * Retrieves a human-readable description for this extension.  Each element
   * of the array that is returned will be considered a separate paragraph in
   * generated documentation.
   *
   * @return  A human-readable description for this extension, or {@code null}
   *          or an empty array if no description should be available.
   */
  @Override()
  public String[] getExtensionDescription()
  {
    return new String[]
    {
      "This LDAP health check serves as an example that may be used to " +
           "demonstrate the process for creating a third-party health " +
           "check.  It will perform a search in order to retrieve a " +
           "entry from a backend server, and any problem encountered while " +
           "attempting to retrieve that entry will cause the server to be " +
           "considered unavailable.  If the entry is retrieved successfully, " +
           "then the length of time required to retrieve it may be used to " +
           "classify the server as either available or degraded, and it will " +
           "also be used to generate the score that can help rank the server " +
           "relative to other servers with the same state."
    };
  }



  /**
   * Updates the provided argument parser to define any configuration arguments
   * which may be used by this LDAP health check.  The argument parser may also
   * be updated to define relationships between arguments (e.g., to specify
   * required, exclusive, or dependent argument sets).
   *
   * @param  parser  The argument parser to be updated with the configuration
   *                 arguments which may be used by this LDAP health check.
   *
   * @throws  ArgumentException  If a problem is encountered while updating the
   *                             provided argument parser.
   */
  @Override()
  public void defineConfigArguments(final ArgumentParser parser)
         throws ArgumentException
  {
    // Add an argument that allows you to specify the target entry.
    Character shortIdentifier = null;
    String    longIdentifier  = ARG_NAME_ENTRY_DN;
    boolean   required        = true;
    int       maxOccurrences  = 1;
    String    placeholder     = "{dn}";
    String    description     = "The DN of the entry to retrieve during the " +
         "course of health check processing.";

    parser.addArgument(new DNArgument(shortIdentifier, longIdentifier,
         required, maxOccurrences, placeholder, description));


    // Add an argument that allows you to specify the maximum available
    // duration.
    shortIdentifier = null;
    longIdentifier  = ARG_NAME_MAX_AVAILABLE_DURATION;
    required        = true;
    placeholder     = "{duration}";
    description     = "The maximum length of time that a health check search " +
         "may take for a server to be considered available.  The value " +
         "should consist of an integer followed by a time unit (e.g., " +
         "'10 ms').";

    Long     defaultValue    = null;
    TimeUnit defaultUnit     = null;
    Long     lowerBoundValue = 1L;
    TimeUnit lowerBoundUnit  = TimeUnit.MILLISECONDS;
    Long     upperBoundValue = null;
    TimeUnit upperBoundUnit  = null;

    parser.addArgument(new DurationArgument(shortIdentifier, longIdentifier,
         required, placeholder, description, defaultValue, defaultUnit,
         lowerBoundValue, lowerBoundUnit, upperBoundValue, upperBoundUnit));


    // Add an argument that allows you to specify the maximum degraded
    // duration.
    shortIdentifier = null;
    longIdentifier  = ARG_NAME_MAX_DEGRADED_DURATION;
    required        = true;
    placeholder     = "{duration}";
    description     = "The maximum length of time that a health check search " +
         "may take for a server to be considered degraded.  The value " +
         "should consist of an integer followed by a time unit (e.g., " +
         "'10 ms').";
    defaultValue    = null;
    defaultUnit     = null;
    lowerBoundValue = 2L;
    lowerBoundUnit  = TimeUnit.MILLISECONDS;
    upperBoundValue = null;
    upperBoundUnit  = null;

    parser.addArgument(new DurationArgument(shortIdentifier, longIdentifier,
         required, placeholder, description, defaultValue, defaultUnit,
         lowerBoundValue, lowerBoundUnit, upperBoundValue, upperBoundUnit));
  }



  /**
   * Initializes this LDAP health check.
   *
   * @param  serverContext  A handle to the server context for the server in
   *                        which this extension is running.
   * @param  config         The general configuration for this LDAP health
   *                        check.
   * @param  parser         The argument parser which has been initialized from
   *                        the configuration for this LDAP health check.
   *
   * @throws  LDAPException  If a problem occurs while initializing this LDAP
   *                         health check.
   */
  @Override()
  public void initializeLDAPHealthCheck(final ProxyServerContext serverContext,
                                        final LDAPHealthCheckConfig config,
                                        final ArgumentParser parser)
         throws LDAPException
  {
    serverContext.debugInfo("Beginning LDAP health check initialization");

    this.serverContext = serverContext;

    // Get the target entry DN.
    final DNArgument dnArg =
         (DNArgument) parser.getNamedArgument(ARG_NAME_ENTRY_DN);
    entryDN = dnArg.getValue().toString();

    // Get the maximum available response time.
    final DurationArgument maxAvailableArg =
         (DurationArgument)
         parser.getNamedArgument(ARG_NAME_MAX_AVAILABLE_DURATION);
    maxAvailableDuration = maxAvailableArg.getValue(TimeUnit.MILLISECONDS);

    // Get the maximum degraded response time.
    final DurationArgument maxDegradedArg =
         (DurationArgument)
         parser.getNamedArgument(ARG_NAME_MAX_DEGRADED_DURATION);
    maxDegradedDuration = maxDegradedArg.getValue(TimeUnit.MILLISECONDS);


    // The maximum available response time must be less than or equal to the
    // maximum degraded response time.
    if (maxAvailableDuration > maxDegradedDuration)
    {
      throw new LDAPException(ResultCode.PARAM_ERROR,
           "The maximum available duration must be less than or equal to the " +
                "maximum degraded duration.");
    }
  }



  /**
   * Indicates whether the configuration contained in the provided argument
   * parser represents a valid configuration for this extension.
   *
   * @param  config               The general configuration for this LDAP health
   *                              check.
   * @param  parser               The argument parser which has been initialized
   *                              with the proposed configuration.
   * @param  unacceptableReasons  A list that can be updated with reasons that
   *                              the proposed configuration is not acceptable.
   *
   * @return  {@code true} if the proposed configuration is acceptable, or
   *          {@code false} if not.
   */
  @Override()
  public boolean isConfigurationAcceptable(final LDAPHealthCheckConfig config,
                      final ArgumentParser parser,
                      final List<String> unacceptableReasons)
  {
    boolean acceptable = true;

    // The maximum available response time must be less than or equal to the
    // maximum degraded response time.
    final DurationArgument maxAvailableArg =
         (DurationArgument)
         parser.getNamedArgument(ARG_NAME_MAX_AVAILABLE_DURATION);
    final long maxAvailable = maxAvailableArg.getValue(TimeUnit.MILLISECONDS);

    final DurationArgument maxDegradedArg =
         (DurationArgument)
         parser.getNamedArgument(ARG_NAME_MAX_DEGRADED_DURATION);
    final long maxDegraded = maxDegradedArg.getValue(TimeUnit.MILLISECONDS);

    if (maxAvailable > maxDegraded)
    {
      unacceptableReasons.add("The maximum available duration must be less " +
           "than or equal to the maximum degraded duration.");
      acceptable = false;
    }


    return acceptable;
  }



  /**
   * Attempts to apply the configuration contained in the provided argument
   * parser.
   *
   * @param  config                The general configuration for this LDAP
   *                               health check.
   * @param  parser                The argument parser which has been
   *                               initialized with the new configuration.
   * @param  adminActionsRequired  A list that can be updated with information
   *                               about any administrative actions that may be
   *                               required before one or more of the
   *                               configuration changes will be applied.
   * @param  messages              A list that can be updated with information
   *                               about the result of applying the new
   *                               configuration.
   *
   * @return  A result code that provides information about the result of
   *          attempting to apply the configuration change.
   */
  @Override()
  public ResultCode applyConfiguration(final LDAPHealthCheckConfig config,
                                       final ArgumentParser parser,
                                       final List<String> adminActionsRequired,
                                       final List<String> messages)
  {
    // Get the target entry DN.
    final DNArgument dnArg =
         (DNArgument) parser.getNamedArgument(ARG_NAME_ENTRY_DN);
    final String newDN = dnArg.getValue().toString();

    // Get the maximum available response time.
    final DurationArgument maxAvailableArg =
         (DurationArgument)
         parser.getNamedArgument(ARG_NAME_MAX_AVAILABLE_DURATION);
    final long newAvailable = maxAvailableArg.getValue(TimeUnit.MILLISECONDS);

    // Get the maximum degraded response time.
    final DurationArgument maxDegradedArg =
         (DurationArgument)
         parser.getNamedArgument(ARG_NAME_MAX_DEGRADED_DURATION);
    final long newDegraded = maxDegradedArg.getValue(TimeUnit.MILLISECONDS);


    entryDN              = newDN;
    maxAvailableDuration = newAvailable;
    maxDegradedDuration  = newDegraded;

    return ResultCode.SUCCESS;
  }



  /**
   * Performs any cleanup which may be necessary when this LDAP health check is
   * to be taken out of service.
   */
  @Override()
  public void finalizeLDAPHealthCheck()
  {
    // No finalization is required.
  }



  /**
   * Attempts to determine the health of the provided LDAP external server whose
   * last health check result indicated that the server had a state of
   * AVAILABLE.  This method may be periodically invoked for AVAILABLE servers
   * to determine whether their state has changed.
   *
   * @param  backendServer  A handle to the LDAP external server whose health is
   *                        to be assessed.
   * @param  connection     A connection that may be used to communicate with
   *                        the server in the course of performing the
   *                        evaluation.  The health check should not do anything
   *                        which may alter the state of this connection.
   *
   * @return  Information about the result of the health check.
   */
  @Override()
  public HealthCheckResult checkAvailableServer(
                                final BackendServer backendServer,
                                final LDAPConnection connection)
  {
    return checkServer(connection);
  }



  /**
   * Attempts to determine the health of the provided LDAP external server whose
   * last health check result indicated that the server had a state of DEGRADED.
   * This method may be periodically invoked for DEGRADED servers to determine
   * whether their state has changed.
   *
   * @param  backendServer  A handle to the LDAP external server whose health is
   *                        to be assessed.
   * @param  connection     A connection that may be used to communicate with
   *                        the server in the course of performing the
   *                        evaluation.  The health check should not do anything
   *                        which may alter the state of this connection.
   *
   * @return  Information about the result of the health check.
   */
  @Override()
  public HealthCheckResult checkDegradedServer(
                                final BackendServer backendServer,
                                final LDAPConnection connection)
  {
    return checkServer(connection);
  }



  /**
   * Attempts to determine the health of the provided LDAP external server whose
   * last health check result indicated that the server had a state of
   * UNAVAILABLE.  This method may be periodically invoked for UNAVAILABLE
   * servers to determine whether their state has changed.
   *
   * @param  backendServer  A handle to the LDAP external server whose health is
   *                        to be assessed.
   * @param  connection     A connection that may be used to communicate with
   *                        the server in the course of performing the
   *                        evaluation.  The health check should not do anything
   *                        which may alter the state of this connection.
   *
   * @return  Information about the result of the health check.
   */
  @Override()
  public HealthCheckResult checkUnavailableServer(
                                final BackendServer backendServer,
                                final LDAPConnection connection)
  {
    return checkServer(connection);
  }



  /**
   * Attempts to determine the health of the provided LDAP external server in
   * which an attempted operation did not complete successfully.
   *
   * @param  operationContext  A handle to the operation context for the
   *                           operation that failed.
   * @param  exception         The exception caught when attempting to process
   *                           the associated operation in the backend server.
   * @param  backendServer     A handle to the backend server in which the
   *                           operation was processed.
   *
   * @return  Information about the result of the health check.
   */
  @Override()
  public HealthCheckResult checkFailedOperation(
              final CompletedProxyOperationContext operationContext,
              final LDAPException exception,
              final BackendServer backendServer)
  {
    // Look at the result code to see if it indicates that the server might not
    // be available.
    if (exception.getResultCode().isConnectionUsable())
    {
      // The result code indicates that the connection is probably usable, so
      // we'll just return whatever the last known result was.
      return backendServer.getHealthCheckResult();
    }


    // The server might not be usable.  See if we can establish a connection to
    // it.
    final LDAPConnection connection;
    try
    {
      connection = backendServer.createNewConnection(null,
           "Example Health Check for failed operation " +
                operationContext.toString());
    }
    catch (final Exception e)
    {
      // We can't establish a connection, so we have to consider the server
      // unavailable.
      serverContext.debugCaught(e);
      return serverContext.createHealthCheckResult(
           HealthCheckState.UNAVAILABLE, 0,
           "Unable to establish a connection to the server:  " +
                StaticUtils.getExceptionMessage(e));
    }


    // Use the connection to perform the health check.
    try
    {
      return checkServer(connection);
    }
    finally
    {
      connection.close();
    }
  }



  /**
   * Performs a search to assess the health of the backend server using the
   * given connection.
   *
   * @param  connection  The connection to use to communicate with the server.
   *
   * @return  The health check result representing the assessed health of the
   *          server.
   */
  private HealthCheckResult checkServer(final LDAPConnection connection)
  {
    // Create local copies of the config variables.
    final String dn = entryDN;
    final long maxA = maxAvailableDuration;
    final long maxD = maxDegradedDuration;


    // Construct a search request to use for the health check.
    final SearchRequest searchRequest = new SearchRequest(dn, SearchScope.BASE,
         Filter.createPresenceFilter("objectClass"),
         SearchRequest.NO_ATTRIBUTES);
    searchRequest.setResponseTimeoutMillis(maxDegradedDuration);


    // Get the start time.
    final long startTime = System.currentTimeMillis();


    // Perform a search to retrieve the target entry
    SearchResult searchResult;
    try
    {
      searchResult = connection.search(searchRequest);
    }
    catch (final LDAPSearchException lse)
    {
      serverContext.debugCaught(lse);
      searchResult = lse.getSearchResult();
    }


    // Get the stop time.
    final long stopTime = System.currentTimeMillis();


    // If the result code is anything other than success, then we'll consider
    // the server unavailable.
    if (searchResult.getResultCode() != ResultCode.SUCCESS)
    {
      return serverContext.createHealthCheckResult(
           HealthCheckState.UNAVAILABLE, 0,
           "Example health check search returned a non-success result of " +
                searchResult.toString());
    }


    // Figure out how long the search took and use that to determine the state
    // and score to use for the server.
    final long elapsedTime = stopTime - startTime;
    if (elapsedTime <= maxA)
    {
      final int score = (int) Math.round(10.0d - (10.0d * elapsedTime / maxA));
      return serverContext.createHealthCheckResult(
           HealthCheckState.AVAILABLE, score);
    }
    else if (elapsedTime <= maxD)
    {
      final int score = (int) Math.round(10.0d - (10.0d * elapsedTime / maxD));
      return serverContext.createHealthCheckResult(
           HealthCheckState.DEGRADED, score,
           "Example health check duration exceeded the maximum available " +
                "response time of " + maxA + "ms");
    }
    else
    {
      return serverContext.createHealthCheckResult(
           HealthCheckState.UNAVAILABLE, 0,
           "Example health check duration exceeded the maximum degraded " +
                "response time of " + maxD + "ms");
    }
  }



  /**
   * Retrieves a map containing examples of configurations that may be used for
   * this extension.  The map key should be a list of sample arguments, and the
   * corresponding value should be a description of the behavior that will be
   * exhibited by the extension when used with that configuration.
   *
   * @return  A map containing examples of configurations that may be used for
   *          this extension.  It may be {@code null} or empty if there should
   *          not be any example argument sets.
   */
  @Override()
  public Map<List<String>,String> getExamplesArgumentSets()
  {
    final LinkedHashMap<List<String>,String> exampleMap =
         new LinkedHashMap<List<String>,String>(1);

    exampleMap.put(
         Arrays.asList(
              ARG_NAME_ENTRY_DN + "=dc=example,dc=com",
              ARG_NAME_MAX_AVAILABLE_DURATION + "=500ms",
              ARG_NAME_MAX_DEGRADED_DURATION + "=5s"),
         "Ensure that the 'dc=example,dc=com' entry can be retrieved.  If " +
              "the time required to retrieve the entry is no more than " +
              "500ms, then the server will be considered available.  If the " +
              "time required to retrieve the entry is between 500ms and 5s, " +
              "then the server will be considered degraded.  If the entry " +
              "cannot be retrieved, or if it takes more than 5s to retrieve " +
              "it, then the server will be considered unavailable.");

    return exampleMap;
  }



  /**
   * Appends a string representation of this LDAP health check to the provided
   * buffer.
   *
   * @param  buffer  The buffer to which the string representation should be
   *                 appended.
   */
  @Override()
  public void toString(final StringBuilder buffer)
  {
    buffer.append("ExampleHealthCheck(dn='");
    buffer.append(entryDN);
    buffer.append("', maxAvailableResponseTime=");
    buffer.append(maxAvailableDuration);
    buffer.append("ms, maxDegradedResponseTime=");
    buffer.append(maxDegradedDuration);
    buffer.append("ms)");
  }
}