UnboundID Server SDK

Ping Identity
UnboundID Server SDK Documentation

ExampleOverloadHandlerPlugin.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 2014-2023 Ping Identity Corporation
 */
package com.unboundid.directory.sdk.examples;

import com.unboundid.directory.sdk.common.api.MonitorProvider;
import com.unboundid.directory.sdk.common.operation.AddRequest;
import com.unboundid.directory.sdk.common.operation.AddResult;
import com.unboundid.directory.sdk.common.operation.DeleteRequest;
import com.unboundid.directory.sdk.common.operation.DeleteResult;
import com.unboundid.directory.sdk.common.operation.ModifyDNRequest;
import com.unboundid.directory.sdk.common.operation.ModifyDNResult;
import com.unboundid.directory.sdk.common.operation.ModifyRequest;
import com.unboundid.directory.sdk.common.operation.ModifyResult;
import com.unboundid.directory.sdk.common.operation.UpdatableAddRequest;
import com.unboundid.directory.sdk.common.operation.UpdatableAddResult;
import com.unboundid.directory.sdk.common.operation.UpdatableBindResult;
import com.unboundid.directory.sdk.common.operation.UpdatableCompareRequest;
import com.unboundid.directory.sdk.common.operation.UpdatableCompareResult;
import com.unboundid.directory.sdk.common.operation.UpdatableDeleteRequest;
import com.unboundid.directory.sdk.common.operation.UpdatableDeleteResult;
import com.unboundid.directory.sdk.common.operation.UpdatableExtendedRequest;
import com.unboundid.directory.sdk.common.operation.UpdatableExtendedResult;
import com.unboundid.directory.sdk.common.operation.UpdatableGenericResult;
import com.unboundid.directory.sdk.common.operation.UpdatableModifyDNRequest;
import com.unboundid.directory.sdk.common.operation.UpdatableModifyDNResult;
import com.unboundid.directory.sdk.common.operation.UpdatableModifyRequest;
import com.unboundid.directory.sdk.common.operation.UpdatableModifyResult;
import com.unboundid.directory.sdk.common.operation.UpdatableSearchRequest;
import com.unboundid.directory.sdk.common.operation.UpdatableSearchResult;
import com.unboundid.directory.sdk.common.operation.UpdatableSimpleBindRequest;
import com.unboundid.directory.sdk.common.types.ActiveOperationContext;
import com.unboundid.directory.sdk.common.types.ActiveSearchOperationContext;
import com.unboundid.directory.sdk.common.types.CompletedOperationContext;
import com.unboundid.directory.sdk.common.types.Entry;
import com.unboundid.directory.sdk.common.types.InternalConnection;
import com.unboundid.directory.sdk.common.types.RegisteredMonitorProvider;
import com.unboundid.directory.sdk.ds.api.ChangeListener;
import com.unboundid.directory.sdk.ds.api.Plugin;
import com.unboundid.directory.sdk.ds.config.PluginConfig;
import com.unboundid.directory.sdk.ds.types.DirectoryServerContext;
import com.unboundid.directory.sdk.ds.types.PreParsePluginResult;
import com.unboundid.directory.sdk.ds.types.RegisteredChangeListener;
import com.unboundid.directory.sdk.common.types.AlarmSeverity;
import com.unboundid.ldap.sdk.Attribute;
import com.unboundid.ldap.sdk.ChangeType;
import com.unboundid.ldap.sdk.Filter;
import com.unboundid.ldap.sdk.LDAPException;
import com.unboundid.ldap.sdk.LDAPSearchException;
import com.unboundid.ldap.sdk.ResultCode;
import com.unboundid.ldap.sdk.SearchResult;
import com.unboundid.ldap.sdk.SearchResultEntry;
import com.unboundid.ldap.sdk.SearchScope;
import com.unboundid.util.args.ArgumentException;
import com.unboundid.util.args.ArgumentParser;
import com.unboundid.util.args.IntegerArgument;
import com.unboundid.util.args.StringArgument;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.ConcurrentSkipListMap;
import java.util.concurrent.atomic.AtomicLong;

/**
 * This class provides an example overload handler plugin that will demonstrate
 * how to control LDAP traffic based on the current severity level of a
 * specified gauge. The assumption is that client traffic is causing the server
 * to be overloaded, and rejecting some of the traffic will help with the
 * overload condition.
 * <p>
 * The plugin supports the following configuration properties.
 * <ul>
 *   <li>
 *     gauge-name -- The name of the gauge whose severity level will be used to
 *     control incoming client requests. This is a mandatory property and
 *     omitting it is an error.
 *   </li>
 *   <li>
 *     drop-percent-critical -- The total percentage of requests to drop when
 *     the gauge's current severity level is 'critical'. If not specified, no
 *     default value will be assumed, i.e. the server will not drop any traffic
 *     for this severity level.
 *   </li>
 *   <li>
 *     drop-percent-major -- The total percentage of requests to drop when
 *     the gauge's current severity level is 'major'. If not specified, no
 *     default value will be assumed, i.e. the server will not drop any traffic
 *     for this severity level.
 *   </li>
 *   <li>
 *     drop-percent-minor -- The total percentage of requests to drop when
 *     the gauge's current severity level is 'minor'. If not specified, no
 *     default value will be assumed, i.e. the server will not drop any traffic
 *     for this severity level.
 *   </li>
 *   <li>
 *     drop-percent-warning -- The total percentage of requests to drop when
 *     the gauge's current severity level is 'warning'. If not specified, no
 *     default value will be assumed, i.e. the server will not drop any traffic
 *     for this severity level.
 *   </li>
 * </ul>
 * <p>
 * The plugin will register a listener to the server's "cn=alarms" backend so
 * that it is notified of changes in the alarm backend. When there is a change
 * in the alarm backend, it will read the current severity level for the gauge
 * from the alarm backend and save it internally. For each incoming LDAP
 * operation, the plugin will drop or accept it based on the configured
 * percentage of requests to drop for that severity.
 * </p>
 * <p>
 * The plugin will also register a monitor provider that will provide
 * information about the total number of requests dropped for each severity
 * level.
 * </p>
 * The plugin may be created and enabled by running the following dsconfig
 * command:
 * <p>
 *  <code>
 *   dsconfig -n create-plugin
 *   --plugin-name "Example Overload Handler Plugin" --type third-party
 *   --set invoke-for-internal-operations:false
 *   --set plugin-type:preparseadd --set plugin-type:preparsecompare
 *   --set plugin-type:preparsedelete --set plugin-type:preparsemodify
 *   --set plugin-type:preparsemodifydn --set plugin-type:preparsesearch
 *   --set plugin-type:preparsebind --set plugin-type:preparseextended
 *   --set extension-class:com.unboundid.directory.sdk.examples. \
 *          ExampleOverloadHandlerPlugin
 *   --set extension-argument:gauge-name='CPU Usage (Percent)'
 *   --set extension-argument:drop-percent-critical=60
 *   --set extension-argument:drop-percent-major=50
 *   --set enabled:true
 *  </code>
 * </p>
 * The gauge to be used by the plugin, if not already enabled, may be enabled
 * by running the following dsconfig command.
 * <p>
 *   <code>
 *     dsconfig -n set-gauge-prop --gauge-name 'CPU Usage (Percent)'
 *     --set enabled:true
 *   </code>
 * </p>
 */
public final class ExampleOverloadHandlerPlugin
    extends Plugin implements ChangeListener {

  // The argument name for the gauge name property
  private static final String ARG_GAUGE_NAME = "gauge-name";

  // The argument name for the total percentage of requests to drop when the
  // gauge is at critical severity.
  private static final String ARG_DROP_PERCENT_CRITICAL =
      "drop-percent-critical";

  // The argument name for the total percentage of requests to drop when the
  // gauge is at major severity.
  private static final String ARG_DROP_PERCENT_MAJOR = "drop-percent-major";

  // The argument name for the total percentage of requests to drop when the
  // gauge is at minor severity.
  private static final String ARG_DROP_PERCENT_MINOR = "drop-percent-minor";

  // The argument name for the total percentage of requests to drop when the
  // gauge is at warning severity.
  private static final String ARG_DROP_PERCENT_WARNING = "drop-percent-warning";

  // The base DN for gauge definitions.
  private static final String GAUGES_BASE_DN = "cn=Gauges,cn=config";

  // The base DN of the alarm backend.
  private static final String ALARM_BACKEND_BASE_DN = "cn=alarms";

  // The name of the standard attribute that is used to hold common names,
  private static final String ATTR_COMMON_NAME = "cn";

  // The name of the standard attribute that is used to specify whether a
  // configuration object is enabled or not.
  private static final String ATTR_ENABLED = "ds-cfg-enabled";

  // The name of the attribute that is used to specify the severity of an alarm
  // in the alarm backend.
  private static final String ATTR_ALARM_SEVERITY = "ds-alarm-severity";

  // The name of the attribute that is used to specify the condition of an
  // alarm in the alarm backend.
  private static final String ATTR_ALARM_CONDITION = "ds-alarm-condition";

  // A pre-parse plugin result that will be returned for requests that should
  // be rejected.
  private static final PreParsePluginResult REJECT_REQUEST_RESULT =
      new PreParsePluginResult(
          false,  // Connection terminated
          false,  // Continue pre-parse plugin processing
          true,   // Send response immediately
          true);  // Skip core processing

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

  // The name of the gauge whose severity level is used to control incoming
  // LDAP traffic.
  private volatile String gaugeName;

  // Configured percentage of requests to drop per severity.
  private volatile Map<AlarmSeverity, Integer> percentToDrop =
      new HashMap<AlarmSeverity, Integer>();

  // The current severity level of the gauge.
  private volatile AlarmSeverity currentSeverity;

  // Request handler for incoming client requests, and monitor provider
  // providing information about the total number of dropped requests for each
  // severity.
  private volatile PreParseRequestHandler preParseRequestHandler;

  // Registered monitor provider, used for de-registration upon changes to
  // configuration.
  private RegisteredMonitorProvider registeredMonitorProvider;

  // The registered change listener for the alarm backend, used for
  // de-registration upon changes to configuration.
  private RegisteredChangeListener registeredChangeListener;

  // Flag indicating whether the plugin has been finalized.
  private volatile boolean finalized = false;

  /**
   * Creates a new instance of this plugin. All plugin implementations must
   * include a default constructor, but any initialization should generally be
   * done in the {@code initializePlugin} method.
   */
  public ExampleOverloadHandlerPlugin() {
    // 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 Overload Handler Plugin";
  }

  /**
   * 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 plugin serves as an example that may be used to demonstrate how " +
          "to control incoming LDAP traffic based on the current overload " +
          "level of a specified gauge."
    };
  }

  /**
   * Updates the provided argument parser to define any configuration arguments
   * which may be used by this plugin. 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 plugin.
   *
   * @throws  com.unboundid.util.args.ArgumentException
   *                If a problem is encountered while updating the provided
   *                argument parser.
   */
  @Override()
  public void defineConfigArguments(final ArgumentParser parser)
         throws ArgumentException {

    parser.addArgument(new StringArgument(
        null,                                           // short identifier
        ARG_GAUGE_NAME,                                 // long identifier
        true,                                           // required
        1,                                              // max occurrences
        "{gauge-name}",                                 // place holder
        "The name of the gauge whose severity level " + // description
            "will be used to control incoming " +
            "client requests."
    ));

    parser.addArgument(new IntegerArgument(
        null,                                           // short identifier
        ARG_DROP_PERCENT_CRITICAL,                      // long identifier
        false,                                          // required
        1,                                              // max occurrences
        "{drop-percent-critical}",                      // place holder
        "The total percentage of requests to drop "  +  // description
            "when the gauge's current severity is "  +
            "'critical'.",
        0,                                              // lower bound
        100                                             // upper bound
    ));

    parser.addArgument(new IntegerArgument(
        null,                                           // short identifier
        ARG_DROP_PERCENT_MAJOR,                         // long identifier
        false,                                          // required
        1,                                              // max occurrences
        "{drop-percent-major}",                         // place holder
        "The total percentage of requests to drop "  +  // description
            "when the gauge's current severity is "  +
            "'major'.",
        0,                                              // lower bound
        100                                             // upper bound
    ));

    parser.addArgument(new IntegerArgument(
        null,                                           // short identifier
        ARG_DROP_PERCENT_MINOR,                         // long identifier
        false,                                          // required
        1,                                              // max occurrences
        "{drop-percent-minor}",                         // place holder
        "The total percentage of requests to drop "  +  // description
            "when the gauge's current severity is "  +
            "'minor'.",
        0,                                              // lower bound
        100                                             // upper bound
    ));

    parser.addArgument(new IntegerArgument(
        null,                                           // short identifier
        ARG_DROP_PERCENT_WARNING,                       // long identifier
        false,                                          // required
        1,                                              // max occurrences
        "{drop-percent-warning}",                       // place holder
        "The total percentage of requests to drop "  +  // description
            "when the gauge's current severity is "  +
            "'warning'.",
        0,                                              // lower bound
        100                                             // upper bound
    ));
  }

  /**
   * Initializes this plugin.
   *
   * @param  serverContext  A handle to the server context for the server in
   *                        which this extension is running.
   * @param  config         The general configuration for this plugin.
   * @param  parser         The argument parser which has been initialized from
   *                        the configuration for this plugin.
   *
   * @throws  LDAPException  If a problem occurs while initializing this plugin.
   */
  @Override()
  public void initializePlugin(
          final DirectoryServerContext serverContext,
          final PluginConfig config,
          final ArgumentParser parser) throws LDAPException {

    serverContext.debugInfo("Beginning plugin initialization");
    final List<String> messages = new ArrayList<String>();
    initialize(config, parser, messages);
  }

  /**
   * Performs any cleanup which may be necessary when this plugin is to be taken
   * out of service.
   */
  @Override
  public void finalizePlugin() {
    if (registeredChangeListener != null) {
      serverContext.deregisterChangeListener(registeredChangeListener);
    }
    if (registeredMonitorProvider != null) {
      serverContext.deregisterMonitorProvider(registeredMonitorProvider);
    }
    finalized = true;
  }

  /**
   * Indicates whether the configuration contained in the provided argument
   * parser represents a valid configuration for this extension.
   *
   * @param  config               The general configuration for this plugin.
   * @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 PluginConfig config,
          final ArgumentParser parser,
          final List<String> unacceptableReasons) {

    final DirectoryServerContext ctx = config.getServerContext();
    final String name = getGaugeName(ctx, parser, unacceptableReasons);
    return name != null && parseAndValidateDropPercentages(
        new HashMap<AlarmSeverity, Integer>(), parser, unacceptableReasons);
  }

  /**
   * Attempts to apply the configuration contained in the provided argument
   * parser.
   *
   * @param  config                The general configuration for this plugin.
   * @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 PluginConfig config,
          final ArgumentParser parser,
          final List<String> adminActionsRequired,
          final List<String> messages) {
    serverContext.debugInfo("Beginning to apply configuration");
    try {
      initialize(config, parser, messages);
    }
    catch (final LDAPException le) {
      config.getServerContext().debugCaught(le);
      messages.add(le.getMessage());
      return le.getResultCode();
    }
    return ResultCode.SUCCESS;
  }

  /**
   * Performs any processing which may be necessary before the server starts
   * processing for an add request.
   *
   * @param  operationContext  The context for the add operation.
   * @param  request           The add request to be processed. It may be
   *                           altered if desired.
   * @param  result            The result that will be returned to the client
   *                           if the plugin result indicates that processing
   *                           on the operation should be interrupted. It may
   *                           be altered if desired.
   *
   * @return  Information about the result of the plugin processing.
   */
  @Override()
  public PreParsePluginResult doPreParse(
            final ActiveOperationContext operationContext,
            final UpdatableAddRequest request,
            final UpdatableAddResult result) {
    return doPreParse(operationContext, result);
  }

  /**
   * Performs any processing which may be necessary before the server starts
   * processing for a compare request.
   *
   * @param  operationContext  The context for the compare operation.
   * @param  request           The compare request to be processed. It may be
   *                           altered if desired.
   * @param  result            The result that will be returned to the client
   *                           if the plugin result indicates that processing
   *                           on the operation should be interrupted. It may
   *                           be altered if desired.
   *
   * @return  Information about the result of the plugin processing.
   */
  @Override()
  public PreParsePluginResult doPreParse(
            final ActiveOperationContext operationContext,
            final UpdatableCompareRequest request,
            final UpdatableCompareResult result) {
    return doPreParse(operationContext, result);
  }

  /**
   * Performs any processing which may be necessary before the server starts
   * processing for a modify request.
   *
   * @param  operationContext  The context for the modify operation.
   * @param  request           The modify request to be processed. It may be
   *                           altered if desired.
   * @param  result            The result that will be returned to the client
   *                           if the plugin result indicates that processing
   *                           on the operation should be interrupted. It may
   *                           be altered if desired.
   *
   * @return  Information about the result of the plugin processing.
   */
  @Override()
  public PreParsePluginResult doPreParse(
              final ActiveOperationContext operationContext,
              final UpdatableModifyRequest request,
              final UpdatableModifyResult result) {
    return doPreParse(operationContext, result);
  }

  /**
   * Performs any processing which may be necessary before the server starts
   * processing for a modify DN request.
   *
   * @param  operationContext  The context for the modify DN operation.
   * @param  request           The modify DN request to be processed. It may be
   *                           altered if desired.
   * @param  result            The result that will be returned to the client
   *                           if the plugin result indicates that processing
   *                           on the operation should be interrupted. It may
   *                           be altered if desired.
   *
   * @return  Information about the result of the plugin processing.
   */
  @Override()
  public PreParsePluginResult doPreParse(
              final ActiveOperationContext operationContext,
              final UpdatableModifyDNRequest request,
              final UpdatableModifyDNResult result) {
    return doPreParse(operationContext, result);
  }

  /**
   * Performs any processing which may be necessary before the server starts
   * processing for a search request.
   *
   * @param  operationContext  The context for the search operation.
   * @param  request           The search request to be processed. It may be
   *                           altered if desired.
   * @param  result            The result that will be returned to the client
   *                           if the plugin result indicates that processing
   *                           on the operation should be interrupted. It may
   *                           be altered if desired.
   * @return  Information about the result of the plugin processing.
   */
  @Override()
  public PreParsePluginResult doPreParse(
              final ActiveSearchOperationContext operationContext,
              final UpdatableSearchRequest request,
              final UpdatableSearchResult result) {
    return doPreParse(operationContext, result);
  }

  /**
   * Performs any processing which may be necessary before the server starts
   * processing for a simple bind request.
   *
   * @param  operationContext  The context for the bind operation.
   * @param  request           The bind request to be processed. It may be
   *                           altered if desired.
   * @param  result            The result that will be returned to the client
   *                           if the plugin result indicates that processing
   *                           on the operation should be interrupted. It may
   *                           be altered if desired.
   *
   * @return  Information about the result of the plugin processing.
   */
  public PreParsePluginResult doPreParse(
      final ActiveOperationContext operationContext,
      final UpdatableSimpleBindRequest request,
      final UpdatableBindResult result) {
    return doPreParse(operationContext, result);
  }

  /**
   * Performs any processing which may be necessary before the server starts
   * processing for a delete request. This will be invoked only for delete
   * operations requested directly by clients, but not for delete operations
   * received from another server via replication.
   *
   * @param  operationContext  The context for the delete operation.
   * @param  request           The bind request to be processed. It may be
   *                           altered if desired.
   * @param  result            The result that will be returned to the client
   *                           if the plugin result indicates that processing
   *                           on the operation should be interrupted. It may
   *                           be altered if desired.
   *
   * @return  Information about the result of the plugin processing.
   */
  public PreParsePluginResult doPreParse(
      final ActiveOperationContext operationContext,
      final UpdatableDeleteRequest request,
      final UpdatableDeleteResult result) {
    return doPreParse(operationContext, result);
  }

  /**
   * Performs any processing which may be necessary before the server starts
   * processing for an extended request.
   *
   * @param  operationContext  The context for the extended operation.
   * @param  request           The bind request to be processed. It may be
   *                           altered if desired.
   * @param  result            The result that will be returned to the client
   *                           if the plugin result indicates that processing
   *                           on the operation should be interrupted. It may
   *                           be altered if desired.
   *
   * @return  Information about the result of the plugin processing.
   */
  public PreParsePluginResult doPreParse(
      final ActiveOperationContext operationContext,
      final UpdatableExtendedRequest request,
      final UpdatableExtendedResult result) {
    return doPreParse(operationContext, result);
  }

  /**
   * Delegate the request to the  pre-parse request handler if it has been
   * initialized. Otherwise, just accept the request.
   *
   * @param  operationContext
   *                 The context for the LDAP operation.
   * @param  result  The result that will be returned to the client if the
   *                 plugin result indicates that processing on the operation
   *                 should be interrupted. It may be altered if desired.
   *
   * @return Information about the result of the plugin processing.
   */
  private PreParsePluginResult doPreParse(
      final ActiveOperationContext operationContext,
      final UpdatableGenericResult result) {
    // Do not ever reject an administrative operation, for example, an
    // invocation of the server status tool should never be dropped.
    if (operationContext.isAdministrativeOperation()) {
      return PreParsePluginResult.SUCCESS;
    }
    return (preParseRequestHandler == null)
        ? PreParsePluginResult.SUCCESS
        : preParseRequestHandler.handleRequest(result);
  }

  /**
   * 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_GAUGE_NAME + "=CPU Usage (Percent)",
            ARG_DROP_PERCENT_CRITICAL + "=60",
            ARG_DROP_PERCENT_MAJOR + "=50",
            ARG_DROP_PERCENT_MINOR + "=40",
            ARG_DROP_PERCENT_WARNING + "=30"),

        "Monitor the 'CPU Usage (Percent)' gauge. If the gauge's current " +
            "severity is determined to be abnormal, then the configured " +
            "percentage of requests for that severity will be dropped. In " +
            "this configuration, if the gauge is currently at 'critical' " +
            "severity, then the server will drop 60% of the requests.");

    return exampleMap;
  }

  /**
   * Validate that the gauge with the specified name exists and is enabled. If
   * validation passes, then the gauge name will be returned. Otherwise,
   * {@code null} will be returned.
   *
   * @param serverContext   A handle to the server context for the server in
   *                        which this plugin is running.
   * @param parser          The argument parser with the configuration for this
   *                        plugin.
   * @param unacceptableReasons
   *                        A list that can be updated with reasons that
   *                        the proposed configuration is not acceptable.
   * @return                Name of the gauge if valid, or {@code null} if not.
   */
  private String getGaugeName(final DirectoryServerContext serverContext,
                              final ArgumentParser parser,
                              final List<String> unacceptableReasons) {

    final StringArgument gaugeNameArg =
        (StringArgument) parser.getNamedArgument(ARG_GAUGE_NAME);

    final String name = gaugeNameArg.getValue();

    try {
      final InternalConnection conn = serverContext.getInternalRootConnection();
      final Filter searchFilter = Filter.createEqualityFilter(
          ATTR_COMMON_NAME, name);

      final SearchResult result =
          conn.search(GAUGES_BASE_DN, SearchScope.ONE, searchFilter,
              ATTR_ENABLED);

      if (result.getResultCode() != ResultCode.SUCCESS) {
        unacceptableReasons.add(String.format(
            "LDAP search for gauge with name '%s' was unsuccessful",
            name));
        return null;
      }

      if (result.getEntryCount() == 0) {
        unacceptableReasons.add(String.format(
            "No gauge with name '%s' found", name));
        return null;
      }

      boolean enabled = false;
      for (final SearchResultEntry entry : result.getSearchEntries()) {
        if (entry.getAttributeValueAsBoolean(ATTR_ENABLED)) {
          enabled = true;
          break;
        }
      }

      if (!enabled) {
        unacceptableReasons.add(String.format(
            "Gauge '%s' is not enabled", name));
        return null;
      }
    }
    catch (final LDAPSearchException lse) {
      serverContext.debugCaught(lse);
      unacceptableReasons.add(lse.getMessage());
      return null;
    }

    return name;
  }

  /**
   * Parse the configured percentage of requests to drop for each severity
   * level, and validate that the drop percentage for a severity is greater
   * than or equal to the drop percentage for all of the severities lower than
   * that severity.
   *
   * @param dropPercentages  The map to be updated with the parsed drop
   *                         percentages per severity.
   * @param parser           The argument parser with the configuration for
   *                         this plugin.
   * @param invalidReasons   A list that can be updated with reasons for why
   *                         the configuration is invalid, if it is invalid.
   *
   * @return                 {@code true} if the configuration is valid.
   */
  private boolean parseAndValidateDropPercentages(
      final Map<AlarmSeverity, Integer> dropPercentages,
      final ArgumentParser parser,
      final List<String> invalidReasons) {

    parseDropPercentages(dropPercentages, parser, AlarmSeverity.CRITICAL,
        ARG_DROP_PERCENT_CRITICAL);
    parseDropPercentages(dropPercentages, parser, AlarmSeverity.MAJOR,
        ARG_DROP_PERCENT_MAJOR);
    parseDropPercentages(dropPercentages, parser, AlarmSeverity.MINOR,
        ARG_DROP_PERCENT_MINOR);
    parseDropPercentages(dropPercentages, parser, AlarmSeverity.WARNING,
        ARG_DROP_PERCENT_WARNING);

    for (final AlarmSeverity severity : AlarmSeverity.values()) {
      final Integer percent = dropPercentages.get(severity);
      if (percent == null) {
        continue;
      }
      // Compare this severity with higher severities and verify that the drop
      // percentage, if configured, is not greater than the ones for the higher
      // severities.
      for (final AlarmSeverity other : AlarmSeverity.values()) {
        if (severity.compareTo(other) >= 0) {
          continue;
        }
        final Integer otherPercent = dropPercentages.get(other);
        if (otherPercent == null) {
          continue;
        }
        if (percent > otherPercent) {
          invalidReasons.add(String.format("Drop percentage for severity " +
              "%s is %d, which is higher than the drop percentage of %d " +
              "for severity %s", severity, percent, otherPercent, other));
        }
      }
    }

    return invalidReasons.isEmpty();
  }

  /**
   * Parse the configured percentage of requests to drop for the specified
   * severity level.
   *
   * @param dropPercentages  The map to be updated with the parsed drop
   *                         percentages per severity.
   * @param parser           The argument parser with the configuration for
   *                         this plugin.
   * @param severity         The alarm severity.
   * @param propertyName     The property name to parse for the alarm severity.
   */
  private void parseDropPercentages(
      final Map<AlarmSeverity, Integer> dropPercentages,
      final ArgumentParser parser,
      final AlarmSeverity severity,
      final String propertyName) {

    final IntegerArgument dropPercentArg =
        (IntegerArgument) parser.getNamedArgument(propertyName);

    if (dropPercentArg != null && dropPercentArg.getValue() != null) {
      dropPercentages.put(severity, dropPercentArg.getValue());
    }
  }

  /**
   * Indicates that an add operation has been processed within the alarm
   * backend in the server. Read the current severity of the gauge from the
   * alarm entry and update the cached copy of it.
   *
   * @param  operationContext  The context for the add operation.
   * @param  addRequest        Information about the request for the add
   *                           operation that was processed.
   * @param  addResult         Information about the result for the add
   *                           operation that was processed.
   * @param  entry             The entry that was added to the server.
   */
  @Override
  public void addOperationProcessed(
      final CompletedOperationContext operationContext,
      final AddRequest addRequest,
      final AddResult addResult,
      final Entry entry) {
    readAndUpdateCurrentSeverity(entry);
  }

  /**
   * Indicates that a modify operation has been processed within the alarm
   * backend in the server. Read the current severity of the gauge from the
   * alarm entry and update the cached copy of it.
   *
   * @param  operationContext  The context for the modify operation.
   * @param  modifyRequest     Information about the request for the modify
   *                           operation that was processed.
   * @param  modifyResult      Information about the result for the modify
   *                           operation that was processed.
   * @param  oldEntry          The entry as it appeared before the change was
   *                           processed.
   * @param  newEntry          The entry as it appeared immediately after the
   *                           change was processed.
   */
  @Override
  public void modifyOperationProcessed(
      final CompletedOperationContext operationContext,
      final ModifyRequest modifyRequest,
      final ModifyResult modifyResult,
      final Entry oldEntry,
      final Entry newEntry) {
    readAndUpdateCurrentSeverity(newEntry);
  }

  /**
   * {@inheritDoc}
   */
  @Override
  public void deleteOperationProcessed(
      final CompletedOperationContext operationContext,
      final DeleteRequest deleteRequest,
      final DeleteResult deleteResult,
      final Entry entry) {
    // If the alarm entry for the gauge has been deleted, then that means that
    // the gauge is no longer overloaded. So reset the current severity.
    final List<Attribute> conditions = entry.getAttribute(ATTR_ALARM_CONDITION);
    if (conditions != null && !conditions.isEmpty()) {
      final Attribute condition = conditions.get(0);
      if (condition.hasValue() && condition.getValue().equals(gaugeName)) {
        updateCurrentSeverity(null);
      }
    }
  }

  /**
   * {@inheritDoc}
   */
  @Override
  public void modifyDNOperationProcessed(
      final CompletedOperationContext operationContext,
      final ModifyDNRequest modifyDNRequest,
      final ModifyDNResult modifyDNResult,
      final Entry oldEntry,
      final Entry newEntry) {
    // Nothing to do - not registered for modify DN changes
  }

  /**
   * Read the current severity from the alarm entry and save it in a member
   * variable.
   *
   * @param entry   The alarm entry. May be {@code null}.
   */
  private void readAndUpdateCurrentSeverity(final Entry entry) {
    final AlarmSeverity severity = readCurrentSeverity(entry);
    updateCurrentSeverity(severity);
  }

  /**
   * Read the current severity from the alarm entry.
   *
   * @param entry   The alarm entry. May be {@code null}.
   *
   * @return        The current severity read from the alarm entry. May be
   *                {@code null}.
   */
  private AlarmSeverity readCurrentSeverity(final Entry entry) {
    AlarmSeverity severity = null;
    if (entry != null) {
      final List<Attribute> attributes =
          entry.getAttribute(ATTR_ALARM_SEVERITY);

      if (attributes != null && !attributes.isEmpty()) {
        severity = AlarmSeverity.forDisplayName(attributes.get(0).getValue());
      }
    }
    return severity;
  }

  /**
   * Read the current severity of the input gauge from the alarm backend.
   *
   * @return  The alarm severity of the input gauge. May be {@code null} if
   *          the gauge is not currently overloaded.
   *
   * @throws LDAPException  If a problem occurs while searching the alarm
   *                        backend for the gauge's current severity.
   */
  private AlarmSeverity readCurrentSeverity() throws LDAPException {
    final Filter filter = Filter.createEqualityFilter(ATTR_ALARM_CONDITION,
        gaugeName);

    final InternalConnection conn = serverContext.getInternalRootConnection();
    final SearchResult r = conn.search(ALARM_BACKEND_BASE_DN, SearchScope.SUB,
        filter, ATTR_ALARM_SEVERITY);

    if (r.getResultCode() != ResultCode.SUCCESS) {
      throw new LDAPException(r.getResultCode());
    }

    for (final SearchResultEntry entry : r.getSearchEntries()) {
      final String severity = entry.getAttributeValue(ATTR_ALARM_SEVERITY);
      if (severity != null) {
        return AlarmSeverity.forDisplayName(severity);
      }
    }

    return null;
  }

  /**
   * Update the current severity read from the alarm backend for the gauge.
   *
   * @param severity  The current severity of the gauge.
   */
  private void updateCurrentSeverity(final AlarmSeverity severity) {
    currentSeverity = severity;
  }

  /**
   * Attempts to apply the configuration contained in the provided argument
   * parser.
   *
   * @param  config                The general configuration for this plugin.
   * @param  parser                The argument parser which has been
   *                               initialized with the new configuration.
   * @param  messages              A list that can be updated with information
   *                               about the result of applying the new
   *                               configuration.
   *
   * @throws  LDAPException  If a problem occurs while initializing this plugin.
   */
  private void initialize(
      final PluginConfig config,
      final ArgumentParser parser,
      final List<String> messages) throws LDAPException {

    if (finalized) {
      serverContext.debugInfo("Not initializing - plugin has been finalized.");
      return;
    }

    serverContext = config.getServerContext();

    // Update configuration.
    gaugeName = getGaugeName(serverContext, parser, messages);

    percentToDrop.clear();
    parseAndValidateDropPercentages(percentToDrop, parser, messages);

    // Register a monitor provider for monitoring information about the
    // number of dropped requests per severity level.
    preParseRequestHandler = new PreParseRequestHandler();

    if (registeredMonitorProvider != null) {
      serverContext.deregisterMonitorProvider(registeredMonitorProvider);
    }
    registeredMonitorProvider = serverContext.registerMonitorProvider(
        preParseRequestHandler, config);

    // Register for add/delete/modify changes in the alarm backend for the
    // gauge.
    final Set<ChangeType> changeTypes = new HashSet<ChangeType>();
    changeTypes.add(ChangeType.ADD);
    changeTypes.add(ChangeType.DELETE);
    changeTypes.add(ChangeType.MODIFY);

    final List<String> baseDNs = Arrays.asList(ALARM_BACKEND_BASE_DN);
    final Filter alarmConditionFilter = Filter.createEqualityFilter(
        ATTR_ALARM_CONDITION, gaugeName);

    if (registeredChangeListener != null) {
      serverContext.deregisterChangeListener(registeredChangeListener);
    }
    registeredChangeListener = serverContext.registerChangeListener(this,
       changeTypes, baseDNs, alarmConditionFilter);

    final AlarmSeverity severity = readCurrentSeverity();
    updateCurrentSeverity(severity);
  }

  /**
   * A pre-parse request handler for client requests and a monitor provider
   * that provides information about the total number of requests dropped
   * for each severity level.
   */
  private class PreParseRequestHandler extends MonitorProvider {
    // The prefix to use for the monitor attribute name.
    private static final String MONITOR_ATTRIBUTE_PREFIX =
        "total-dropped-requests-";

    // Percentage accumulator for each severity.
    private Map<AlarmSeverity, PercentageAccumulator>
        accumulatorPerSeverity =
            new HashMap<AlarmSeverity, PercentageAccumulator>();

    // Total number of dropped requests for each severity.
    private ConcurrentMap<AlarmSeverity, AtomicLong> totalDroppedRequests =
        new ConcurrentSkipListMap<AlarmSeverity, AtomicLong>();

    /**
     * Constructor.
     */
    PreParseRequestHandler() {
      for (final AlarmSeverity severity : AlarmSeverity.values()) {
        final Integer toDropPercent = percentToDrop.get(severity);
        if (toDropPercent != null) {
          accumulatorPerSeverity.put(severity,
              new PercentageAccumulator(toDropPercent));
        }
      }
    }

    /**
     * Determine whether or not to drop the request based on configured
     * drop percentages for each severity level.
     *
     * @param  result  The result that will be returned to the client if the
     *                 plugin result indicates that processing on the operation
     *                 should be interrupted. It may be altered if desired.
     *
     * @return Information about the result of the plugin processing.
     */
    PreParsePluginResult handleRequest(final UpdatableGenericResult result) {
      // Defer to the accumulator to decide whether or not to drop the
      // request.
      final PercentageAccumulator accumulator = accumulatorPerSeverity.get(
          currentSeverity);

      if (accumulator == null || !accumulator.drop()) {
        return PreParsePluginResult.SUCCESS;
      }

      final String message = String.format(
          "Server is too busy to accept the request because the gauge " +
              "'%s' is currently at severity '%s' and the handler is " +
              "configured to drop %d percent of requests at that severity.",
          gaugeName, currentSeverity, accumulator.getDropPercentage());

      result.setResultCode(ResultCode.BUSY);
      result.setDiagnosticMessage(message);

      AtomicLong previous = totalDroppedRequests.get(currentSeverity);
      if (previous == null) {
        previous = totalDroppedRequests.putIfAbsent(currentSeverity,
            new AtomicLong(1L));
      }
      if (previous != null) {
        previous.incrementAndGet();
      }

      return REJECT_REQUEST_RESULT;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public String getExtensionName() {
      return getClass().getName();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public String[] getExtensionDescription() {
      return new String[] {
          "The gauge overload monitor provider provides information about " +
              "the total number of client requests dropped for each " +
              "severity while the gauge being monitored has a non-normal " +
              "severity."
      };
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public String getMonitorInstanceName() {
      return "Example Overload Handler Plugin";
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public String getMonitorObjectClass() {
      return "example-overload-handler-plugin-entry";
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public List<Attribute> getMonitorAttributes() {
      final List<Attribute> attrs = new ArrayList<Attribute>(1);
      for (final Map.Entry<AlarmSeverity, AtomicLong> entry :
          totalDroppedRequests.entrySet()) {
        attrs.add(new Attribute(
            MONITOR_ATTRIBUTE_PREFIX + entry.getKey().getDisplayName(),
            String.valueOf(entry.getValue())
        ));
      }
      return attrs;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public long getUpdateIntervalMillis() {
      return 0L;
    }
  }

  /**
   * A percentage accumulator that is used to decide whether or not to drop
   * the next request. The accumulator is designed to evenly spread out the
   * requests that will be dropped. For example, if the drop percentage is 20,
   * then, for the first 20 requests, requests 5, 10, 15, 20 and 25 will be
   * dropped. For the next 30 requests, requests 30, 35, 40, 45, 50, 55 and 60
   * will be dropped, and so on.
   */
  public static class PercentageAccumulator {
    // The percentage of requests to drop.
    private final int dropPercentage;

    // The percentage accumulator.
    private final AtomicLong accumulator = new AtomicLong();

    /**
     * Constructor.
     *
     * @param dropPercentage  The percentage of requests to drop.
     */
    public PercentageAccumulator(final int dropPercentage) {
      this.dropPercentage = dropPercentage;
    }

    /**
     * Determines if the next request should be dropped.
     *
     * @return  {@code true}, if the next request should to be dropped.
     */
    public boolean drop() {
      // Each operation adds dropPercentage to the accumulator. When the
      // accumulator transitions across a multiple of 100 (e.g. from 280 to
      // 320), then that operation is dropped.
      final long before = accumulator.getAndAdd(dropPercentage);
      final long after = before + dropPercentage;
      return ((before / 100) != (after / 100));
    }

    /**
     * Returns the configured drop percentage.
     *
     * @return  The configured drop percentage.
     */
    public int getDropPercentage() {
      return dropPercentage;
    }
  }
}