/* * 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> * 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; } } }