/*
* 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
*
* Copyright 2014-2016 UnboundID Corp.
*/
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>
* <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;
}
}
}
|