All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.unboundid.ldap.sdk.examples.SearchRate Maven / Gradle / Ivy

/*
 * Copyright 2008-2023 Ping Identity Corporation
 * All Rights Reserved.
 */
/*
 * Copyright 2008-2023 Ping Identity Corporation
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
/*
 * Copyright (C) 2008-2023 Ping Identity Corporation
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License (GPLv2 only)
 * or the terms of the GNU Lesser General Public License (LGPLv2.1 only)
 * as published by the Free Software Foundation.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, see .
 */
package com.unboundid.ldap.sdk.examples;



import java.io.IOException;
import java.io.OutputStream;
import java.io.Serializable;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Set;
import java.util.StringTokenizer;
import java.util.concurrent.CyclicBarrier;
import java.util.concurrent.Semaphore;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;

import com.unboundid.ldap.sdk.Control;
import com.unboundid.ldap.sdk.DereferencePolicy;
import com.unboundid.ldap.sdk.LDAPConnection;
import com.unboundid.ldap.sdk.LDAPConnectionOptions;
import com.unboundid.ldap.sdk.LDAPException;
import com.unboundid.ldap.sdk.ResultCode;
import com.unboundid.ldap.sdk.SearchScope;
import com.unboundid.ldap.sdk.Version;
import com.unboundid.ldap.sdk.controls.AssertionRequestControl;
import com.unboundid.ldap.sdk.controls.ServerSideSortRequestControl;
import com.unboundid.ldap.sdk.controls.SortKey;
import com.unboundid.util.ColumnFormatter;
import com.unboundid.util.Debug;
import com.unboundid.util.FixedRateBarrier;
import com.unboundid.util.FormattableColumn;
import com.unboundid.util.HorizontalAlignment;
import com.unboundid.util.LDAPCommandLineTool;
import com.unboundid.util.NotNull;
import com.unboundid.util.Nullable;
import com.unboundid.util.ObjectPair;
import com.unboundid.util.OutputFormat;
import com.unboundid.util.RateAdjustor;
import com.unboundid.util.ResultCodeCounter;
import com.unboundid.util.StaticUtils;
import com.unboundid.util.ThreadSafety;
import com.unboundid.util.ThreadSafetyLevel;
import com.unboundid.util.WakeableSleeper;
import com.unboundid.util.ValuePattern;
import com.unboundid.util.args.ArgumentException;
import com.unboundid.util.args.ArgumentParser;
import com.unboundid.util.args.BooleanArgument;
import com.unboundid.util.args.ControlArgument;
import com.unboundid.util.args.FileArgument;
import com.unboundid.util.args.FilterArgument;
import com.unboundid.util.args.IntegerArgument;
import com.unboundid.util.args.ScopeArgument;
import com.unboundid.util.args.StringArgument;



/**
 * This class provides a tool that can be used to search an LDAP directory
 * server repeatedly using multiple threads.  It can help provide an estimate of
 * the search performance that a directory server is able to achieve.  Either or
 * both of the base DN and the search filter may be a value pattern as
 * described in the {@link ValuePattern} class.  This makes it possible to
 * search over a range of entries rather than repeatedly performing searches
 * with the same base DN and filter.
 * 

* Some of the APIs demonstrated by this example include: *
    *
  • Argument Parsing (from the {@code com.unboundid.util.args} * package)
  • *
  • LDAP Command-Line Tool (from the {@code com.unboundid.util} * package)
  • *
  • LDAP Communication (from the {@code com.unboundid.ldap.sdk} * package)
  • *
  • Value Patterns (from the {@code com.unboundid.util} package)
  • *
*

* All of the necessary information is provided using command line arguments. * Supported arguments include those allowed by the {@link LDAPCommandLineTool} * class, as well as the following additional arguments: *
    *
  • "-b {baseDN}" or "--baseDN {baseDN}" -- specifies the base DN to use * for the searches. This must be provided. It may be a simple DN, or it * may be a value pattern to express a range of base DNs.
  • *
  • "-s {scope}" or "--scope {scope}" -- specifies the scope to use for the * search. The scope value should be one of "base", "one", "sub", or * "subord". If this isn't specified, then a scope of "sub" will be * used.
  • *
  • "-z {num}" or "--sizeLimit {num}" -- specifies the maximum number of * entries that should be returned in response to each search * request.
  • *
  • "-l {num}" or "--timeLimitSeconds {num}" -- specifies the maximum * length of time, in seconds, that the server should spend processing * each search request.
  • *
  • "--dereferencePolicy {value}" -- specifies the alias dereferencing * policy that should be used for each search request. Allowed values are * "never", "always", "search", and "find".
  • *
  • "--typesOnly" -- indicates that search requests should have the * typesOnly flag set to true, indicating that matching entries should * only include attributes with an attribute description but no * values.
  • *
  • "-f {filter}" or "--filter {filter}" -- specifies the filter to use for * the searches. This must be provided. It may be a simple filter, or it * may be a value pattern to express a range of filters.
  • *
  • "-A {name}" or "--attribute {name}" -- specifies the name of an * attribute that should be included in entries returned from the server. * If this is not provided, then all user attributes will be requested. * This may include special tokens that the server may interpret, like * "1.1" to indicate that no attributes should be returned, "*", for all * user attributes, or "+" for all operational attributes. Multiple * attributes may be requested with multiple instances of this * argument.
  • *
  • "--ldapURL {url}" -- Specifies an LDAP URL that represents the base DN, * scope, filter, and set of requested attributes that should be used for * the search requests. It may be a simple LDAP URL, or it may be a value * pattern to express a range of LDAP URLs. If this argument is provided, * then none of the --baseDN, --scope, --filter, or --attribute arguments * may be used.
  • *
  • "-t {num}" or "--numThreads {num}" -- specifies the number of * concurrent threads to use when performing the searches. If this is not * provided, then a default of one thread will be used.
  • *
  • "-i {sec}" or "--intervalDuration {sec}" -- specifies the length of * time in seconds between lines out output. If this is not provided, * then a default interval duration of five seconds will be used.
  • *
  • "-I {num}" or "--numIntervals {num}" -- specifies the maximum number of * intervals for which to run. If this is not provided, then it will * run forever.
  • *
  • "--iterationsBeforeReconnect {num}" -- specifies the number of search * iterations that should be performed on a connection before that * connection is closed and replaced with a newly-established (and * authenticated, if appropriate) connection.
  • *
  • "-r {searches-per-second}" or "--ratePerSecond {searches-per-second}" * -- specifies the target number of searches to perform per second. It * is still necessary to specify a sufficient number of threads for * achieving this rate. If this option is not provided, then the tool * will run at the maximum rate for the specified number of threads.
  • *
  • "--variableRateData {path}" -- specifies the path to a file containing * information needed to allow the tool to vary the target rate over time. * If this option is not provided, then the tool will either use a fixed * target rate as specified by the "--ratePerSecond" argument, or it will * run at the maximum rate.
  • *
  • "--generateSampleRateFile {path}" -- specifies the path to a file to * which sample data will be written illustrating and describing the * format of the file expected to be used in conjunction with the * "--variableRateData" argument.
  • *
  • "--warmUpIntervals {num}" -- specifies the number of intervals to * complete before beginning overall statistics collection.
  • *
  • "--timestampFormat {format}" -- specifies the format to use for * timestamps included before each output line. The format may be one of * "none" (for no timestamps), "with-date" (to include both the date and * the time), or "without-date" (to include only time time).
  • *
  • "-Y {authzID}" or "--proxyAs {authzID}" -- Use the proxied * authorization v2 control to request that the operation be processed * using an alternate authorization identity. In this case, the bind DN * should be that of a user that has permission to use this control. The * authorization identity may be a value pattern.
  • *
  • "-a" or "--asynchronous" -- Indicates that searches should be performed * in asynchronous mode, in which the client will not wait for a response * to a previous request before sending the next request. Either the * "--ratePerSecond" or "--maxOutstandingRequests" arguments must be * provided to limit the number of outstanding requests.
  • *
  • "-O {num}" or "--maxOutstandingRequests {num}" -- Specifies the maximum * number of outstanding requests that will be allowed in asynchronous * mode.
  • *
  • "--suppressErrorResultCodes" -- Indicates that information about the * result codes for failed operations should not be displayed.
  • *
  • "-c" or "--csv" -- Generate output in CSV format rather than a * display-friendly format.
  • *
*/ @ThreadSafety(level=ThreadSafetyLevel.NOT_THREADSAFE) public final class SearchRate extends LDAPCommandLineTool implements Serializable { /** * The serial version UID for this serializable class. */ private static final long serialVersionUID = 3345838530404592182L; // Indicates whether a request has been made to stop running. @NotNull private final AtomicBoolean stopRequested; // The number of searchrate threads that are currently running. @NotNull private final AtomicInteger runningThreads; // The argument used to indicate whether to operate in asynchronous mode. @Nullable private BooleanArgument asynchronousMode; // The argument used to indicate whether to generate output in CSV format. @Nullable private BooleanArgument csvFormat; // The argument used to indicate whether to suppress information about error // result codes. @Nullable private BooleanArgument suppressErrors; // The argument used to indicate whether to set the typesOnly flag to true in // search requests. @Nullable private BooleanArgument typesOnly; // The argument used to indicate that a generic control should be included in // the request. @Nullable private ControlArgument control; // The argument used to specify a variable rate file. @Nullable private FileArgument sampleRateFile; // The argument used to specify a variable rate file. @Nullable private FileArgument variableRateData; // Indicates that search requests should include the assertion request control // with the specified filter. @Nullable private FilterArgument assertionFilter; // The argument used to specify the collection interval. @Nullable private IntegerArgument collectionInterval; // The argument used to specify the number of search iterations on a // connection before it is closed and re-established. @Nullable private IntegerArgument iterationsBeforeReconnect; // The argument used to specify the maximum number of outstanding asynchronous // requests. @Nullable private IntegerArgument maxOutstandingRequests; // The argument used to specify the number of intervals. @Nullable private IntegerArgument numIntervals; // The argument used to specify the number of threads. @Nullable private IntegerArgument numThreads; // The argument used to specify the seed to use for the random number // generator. @Nullable private IntegerArgument randomSeed; // The target rate of searches per second. @Nullable private IntegerArgument ratePerSecond; // The argument used to indicate that the search should use the simple paged // results control with the specified page size. @Nullable private IntegerArgument simplePageSize; // The argument used to specify the search request size limit. @Nullable private IntegerArgument sizeLimit; // The argument used to specify the search request time limit, in seconds. @Nullable private IntegerArgument timeLimitSeconds; // The number of warm-up intervals to perform. @Nullable private IntegerArgument warmUpIntervals; // The argument used to specify the scope for the searches. @Nullable private ScopeArgument scope; // The argument used to specify the attributes to return. @Nullable private StringArgument attributes; // The argument used to specify the base DNs for the searches. @Nullable private StringArgument baseDN; // The argument used to specify the alias dereferencing policy for the search // requests. @Nullable private StringArgument dereferencePolicy; // The argument used to specify the filters for the searches. @Nullable private StringArgument filter; // The argument used to specify the LDAP URLs for the searches. @Nullable private StringArgument ldapURL; // The argument used to specify the proxied authorization identity. @Nullable private StringArgument proxyAs; // The argument used to request that the server sort the results with the // specified order. @Nullable private StringArgument sortOrder; // The argument used to specify the timestamp format. @Nullable private StringArgument timestampFormat; // A wakeable sleeper that will be used to sleep between reporting intervals. @NotNull private final WakeableSleeper sleeper; /** * Parse the provided command line arguments and make the appropriate set of * changes. * * @param args The command line arguments provided to this program. */ public static void main(@NotNull final String[] args) { final ResultCode resultCode = main(args, System.out, System.err); if (resultCode != ResultCode.SUCCESS) { System.exit(resultCode.intValue()); } } /** * Parse the provided command line arguments and make the appropriate set of * changes. * * @param args The command line arguments provided to this program. * @param outStream The output stream to which standard out should be * written. It may be {@code null} if output should be * suppressed. * @param errStream The output stream to which standard error should be * written. It may be {@code null} if error messages * should be suppressed. * * @return A result code indicating whether the processing was successful. */ @NotNull() public static ResultCode main(@NotNull final String[] args, @Nullable final OutputStream outStream, @Nullable final OutputStream errStream) { final SearchRate searchRate = new SearchRate(outStream, errStream); return searchRate.runTool(args); } /** * Creates a new instance of this tool. * * @param outStream The output stream to which standard out should be * written. It may be {@code null} if output should be * suppressed. * @param errStream The output stream to which standard error should be * written. It may be {@code null} if error messages * should be suppressed. */ public SearchRate(@Nullable final OutputStream outStream, @Nullable final OutputStream errStream) { super(outStream, errStream); stopRequested = new AtomicBoolean(false); runningThreads = new AtomicInteger(0); sleeper = new WakeableSleeper(); } /** * Retrieves the name for this tool. * * @return The name for this tool. */ @Override() @NotNull() public String getToolName() { return "searchrate"; } /** * Retrieves the description for this tool. * * @return The description for this tool. */ @Override() @NotNull() public String getToolDescription() { return "Perform repeated searches against an " + "LDAP directory server."; } /** * Retrieves the version string for this tool. * * @return The version string for this tool. */ @Override() @NotNull() public String getToolVersion() { return Version.NUMERIC_VERSION_STRING; } /** * Indicates whether this tool should provide support for an interactive mode, * in which the tool offers a mode in which the arguments can be provided in * a text-driven menu rather than requiring them to be given on the command * line. If interactive mode is supported, it may be invoked using the * "--interactive" argument. Alternately, if interactive mode is supported * and {@link #defaultsToInteractiveMode()} returns {@code true}, then * interactive mode may be invoked by simply launching the tool without any * arguments. * * @return {@code true} if this tool supports interactive mode, or * {@code false} if not. */ @Override() public boolean supportsInteractiveMode() { return true; } /** * Indicates whether this tool defaults to launching in interactive mode if * the tool is invoked without any command-line arguments. This will only be * used if {@link #supportsInteractiveMode()} returns {@code true}. * * @return {@code true} if this tool defaults to using interactive mode if * launched without any command-line arguments, or {@code false} if * not. */ @Override() public boolean defaultsToInteractiveMode() { return true; } /** * Indicates whether this tool should provide arguments for redirecting output * to a file. If this method returns {@code true}, then the tool will offer * an "--outputFile" argument that will specify the path to a file to which * all standard output and standard error content will be written, and it will * also offer a "--teeToStandardOut" argument that can only be used if the * "--outputFile" argument is present and will cause all output to be written * to both the specified output file and to standard output. * * @return {@code true} if this tool should provide arguments for redirecting * output to a file, or {@code false} if not. */ @Override() protected boolean supportsOutputFile() { return true; } /** * Indicates whether this tool should default to interactively prompting for * the bind password if a password is required but no argument was provided * to indicate how to get the password. * * @return {@code true} if this tool should default to interactively * prompting for the bind password, or {@code false} if not. */ @Override() protected boolean defaultToPromptForBindPassword() { return true; } /** * Indicates whether this tool supports the use of a properties file for * specifying default values for arguments that aren't specified on the * command line. * * @return {@code true} if this tool supports the use of a properties file * for specifying default values for arguments that aren't specified * on the command line, or {@code false} if not. */ @Override() public boolean supportsPropertiesFile() { return true; } /** * Indicates whether the LDAP-specific arguments should include alternate * versions of all long identifiers that consist of multiple words so that * they are available in both camelCase and dash-separated versions. * * @return {@code true} if this tool should provide multiple versions of * long identifiers for LDAP-specific arguments, or {@code false} if * not. */ @Override() protected boolean includeAlternateLongIdentifiers() { return true; } /** * Adds the arguments used by this program that aren't already provided by the * generic {@code LDAPCommandLineTool} framework. * * @param parser The argument parser to which the arguments should be added. * * @throws ArgumentException If a problem occurs while adding the arguments. */ @Override() public void addNonLDAPArguments(@NotNull final ArgumentParser parser) throws ArgumentException { String description = "The base DN to use for the searches. It may be a " + "simple DN or a value pattern to specify a range of DNs (e.g., " + "\"uid=user.[1-1000],ou=People,dc=example,dc=com\"). See " + ValuePattern.PUBLIC_JAVADOC_URL + " for complete details about the " + "value pattern syntax. This argument must not be used in " + "conjunction with the --ldapURL argument."; baseDN = new StringArgument('b', "baseDN", false, 1, "{dn}", description, ""); baseDN.setArgumentGroupName("Search Arguments"); baseDN.addLongIdentifier("base-dn", true); parser.addArgument(baseDN); description = "The scope to use for the searches. It should be 'base', " + "'one', 'sub', or 'subord'. If this is not provided, then a " + "default scope of 'sub' will be used. This argument must not be " + "used in conjunction with the --ldapURL argument."; scope = new ScopeArgument('s', "scope", false, "{scope}", description, SearchScope.SUB); scope.setArgumentGroupName("Search Arguments"); parser.addArgument(scope); description = "The filter to use for the searches. It may be a simple " + "filter or a value pattern to specify a range of filters (e.g., " + "\"(uid=user.[1-1000])\"). See " + ValuePattern.PUBLIC_JAVADOC_URL + " for complete details about the value pattern syntax. Exactly one " + "of this argument and the --ldapURL arguments must be provided."; filter = new StringArgument('f', "filter", false, 1, "{filter}", description); filter.setArgumentGroupName("Search Arguments"); parser.addArgument(filter); description = "The name of an attribute to include in entries returned " + "from the searches. Multiple attributes may be requested by " + "providing this argument multiple times. If no request attributes " + "are provided, then the entries returned will include all user " + "attributes. This argument must not be used in conjunction with " + "the --ldapURL argument."; attributes = new StringArgument('A', "attribute", false, 0, "{name}", description); attributes.setArgumentGroupName("Search Arguments"); parser.addArgument(attributes); description = "An LDAP URL that provides the base DN, scope, filter, and " + "requested attributes to use for the search requests (the address " + "and port components of the URL, if present, will be ignored). It " + "may be a simple LDAP URL or a value pattern to specify a range of " + "URLs. See " + ValuePattern.PUBLIC_JAVADOC_URL + " for complete " + "details about the value pattern syntax. If this argument is " + "provided, then none of the --baseDN, --scope, --filter, or " + "--attribute arguments may be used."; ldapURL = new StringArgument(null, "ldapURL", false, 1, "{url}", description); ldapURL.setArgumentGroupName("Search Arguments"); ldapURL.addLongIdentifier("ldap-url", true); parser.addArgument(ldapURL); description = "The maximum number of entries that the server should " + "return in response to each search request. A value of zero " + "indicates that the client does not wish to impose any limit on " + "the number of entries that are returned (although the server may " + "impose its own limit). If this is not provided, then a default " + "value of zero will be used."; sizeLimit = new IntegerArgument('z', "sizeLimit", false, 1, "{num}", description, 0, Integer.MAX_VALUE, 0); sizeLimit.setArgumentGroupName("Search Arguments"); sizeLimit.addLongIdentifier("size-limit", true); parser.addArgument(sizeLimit); description = "The maximum length of time, in seconds, that the server " + "should spend processing each search request. A value of zero " + "indicates that the client does not wish to impose any limit on the " + "server's processing time (although the server may impose its own " + "limit). If this is not provided, then a default value of zero " + "will be used."; timeLimitSeconds = new IntegerArgument('l', "timeLimitSeconds", false, 1, "{seconds}", description, 0, Integer.MAX_VALUE, 0); timeLimitSeconds.setArgumentGroupName("Search Arguments"); timeLimitSeconds.addLongIdentifier("time-limit-seconds", true); timeLimitSeconds.addLongIdentifier("timeLimit", true); timeLimitSeconds.addLongIdentifier("time-limit", true); parser.addArgument(timeLimitSeconds); final Set derefAllowedValues = StaticUtils.setOf("never", "always", "search", "find"); description = "The alias dereferencing policy to use for search " + "requests. The value should be one of 'never', 'always', 'search', " + "or 'find'. If this is not provided, then a default value of " + "'never' will be used."; dereferencePolicy = new StringArgument(null, "dereferencePolicy", false, 1, "{never|always|search|find}", description, derefAllowedValues, "never"); dereferencePolicy.setArgumentGroupName("Search Arguments"); dereferencePolicy.addLongIdentifier("dereference-policy", true); parser.addArgument(dereferencePolicy); description = "Indicates that server should only include the names of " + "the attributes contained in matching entries rather than both " + "names and values."; typesOnly = new BooleanArgument(null, "typesOnly", 1, description); typesOnly.setArgumentGroupName("Search Arguments"); typesOnly.addLongIdentifier("types-only", true); parser.addArgument(typesOnly); description = "Indicates that search requests should include the " + "assertion request control with the specified filter."; assertionFilter = new FilterArgument(null, "assertionFilter", false, 1, "{filter}", description); assertionFilter.setArgumentGroupName("Request Control Arguments"); assertionFilter.addLongIdentifier("assertion-filter", true); parser.addArgument(assertionFilter); description = "Indicates that search requests should include the simple " + "paged results control with the specified page size."; simplePageSize = new IntegerArgument(null, "simplePageSize", false, 1, "{size}", description, 1, Integer.MAX_VALUE); simplePageSize.setArgumentGroupName("Request Control Arguments"); simplePageSize.addLongIdentifier("simple-page-size", true); parser.addArgument(simplePageSize); description = "Indicates that search requests should include the " + "server-side sort request control with the specified sort order. " + "This should be a comma-delimited list in which each item is an " + "attribute name, optionally preceded by a plus or minus sign (to " + "indicate ascending or descending order; where ascending order is " + "the default), and optionally followed by a colon and the name or " + "OID of the desired ordering matching rule (if this is not " + "provided, the the attribute type's default ordering rule will be " + "used)."; sortOrder = new StringArgument(null, "sortOrder", false, 1, "{sortOrder}", description); sortOrder.setArgumentGroupName("Request Control Arguments"); sortOrder.addLongIdentifier("sort-order", true); parser.addArgument(sortOrder); description = "Indicates that the proxied authorization control (as " + "defined in RFC 4370) should be used to request that operations be " + "processed using an alternate authorization identity. This may be " + "a simple authorization ID or it may be a value pattern to specify " + "a range of identities. See " + ValuePattern.PUBLIC_JAVADOC_URL + " for complete details about the value pattern syntax."; proxyAs = new StringArgument('Y', "proxyAs", false, 1, "{authzID}", description); proxyAs.setArgumentGroupName("Request Control Arguments"); proxyAs.addLongIdentifier("proxy-as", true); parser.addArgument(proxyAs); description = "Indicates that search requests should include the " + "specified request control. This may be provided multiple times to " + "include multiple request controls."; control = new ControlArgument('J', "control", false, 0, null, description); control.setArgumentGroupName("Request Control Arguments"); parser.addArgument(control); description = "The number of threads to use to perform the searches. If " + "this is not provided, then a default of one thread will be used."; numThreads = new IntegerArgument('t', "numThreads", true, 1, "{num}", description, 1, Integer.MAX_VALUE, 1); numThreads.setArgumentGroupName("Rate Management Arguments"); numThreads.addLongIdentifier("num-threads", true); parser.addArgument(numThreads); description = "The length of time in seconds between output lines. If " + "this is not provided, then a default interval of five seconds will " + "be used."; collectionInterval = new IntegerArgument('i', "intervalDuration", true, 1, "{num}", description, 1, Integer.MAX_VALUE, 5); collectionInterval.setArgumentGroupName("Rate Management Arguments"); collectionInterval.addLongIdentifier("interval-duration", true); parser.addArgument(collectionInterval); description = "The maximum number of intervals for which to run. If " + "this is not provided, then the tool will run until it is " + "interrupted."; numIntervals = new IntegerArgument('I', "numIntervals", true, 1, "{num}", description, 1, Integer.MAX_VALUE, Integer.MAX_VALUE); numIntervals.setArgumentGroupName("Rate Management Arguments"); numIntervals.addLongIdentifier("num-intervals", true); parser.addArgument(numIntervals); description = "The number of search iterations that should be processed " + "on a connection before that connection is closed and replaced with " + "a newly-established (and authenticated, if appropriate) " + "connection. If this is not provided, then connections will not " + "be periodically closed and re-established."; iterationsBeforeReconnect = new IntegerArgument(null, "iterationsBeforeReconnect", false, 1, "{num}", description, 0); iterationsBeforeReconnect.setArgumentGroupName("Rate Management Arguments"); iterationsBeforeReconnect.addLongIdentifier("iterations-before-reconnect", true); parser.addArgument(iterationsBeforeReconnect); description = "The target number of searches to perform per second. It " + "is still necessary to specify a sufficient number of threads for " + "achieving this rate. If neither this option nor " + "--variableRateData is provided, then the tool will run at the " + "maximum rate for the specified number of threads."; ratePerSecond = new IntegerArgument('r', "ratePerSecond", false, 1, "{searches-per-second}", description, 1, Integer.MAX_VALUE); ratePerSecond.setArgumentGroupName("Rate Management Arguments"); ratePerSecond.addLongIdentifier("rate-per-second", true); parser.addArgument(ratePerSecond); final String variableRateDataArgName = "variableRateData"; final String generateSampleRateFileArgName = "generateSampleRateFile"; description = RateAdjustor.getVariableRateDataArgumentDescription( generateSampleRateFileArgName); variableRateData = new FileArgument(null, variableRateDataArgName, false, 1, "{path}", description, true, true, true, false); variableRateData.setArgumentGroupName("Rate Management Arguments"); variableRateData.addLongIdentifier("variable-rate-data", true); parser.addArgument(variableRateData); description = RateAdjustor.getGenerateSampleVariableRateFileDescription( variableRateDataArgName); sampleRateFile = new FileArgument(null, generateSampleRateFileArgName, false, 1, "{path}", description, false, true, true, false); sampleRateFile.setArgumentGroupName("Rate Management Arguments"); sampleRateFile.addLongIdentifier("generate-sample-rate-file", true); sampleRateFile.setUsageArgument(true); parser.addArgument(sampleRateFile); parser.addExclusiveArgumentSet(variableRateData, sampleRateFile); description = "The number of intervals to complete before beginning " + "overall statistics collection. Specifying a nonzero number of " + "warm-up intervals gives the client and server a chance to warm up " + "without skewing performance results."; warmUpIntervals = new IntegerArgument(null, "warmUpIntervals", true, 1, "{num}", description, 0, Integer.MAX_VALUE, 0); warmUpIntervals.setArgumentGroupName("Rate Management Arguments"); warmUpIntervals.addLongIdentifier("warm-up-intervals", true); parser.addArgument(warmUpIntervals); description = "Indicates the format to use for timestamps included in " + "the output. A value of 'none' indicates that no timestamps should " + "be included. A value of 'with-date' indicates that both the date " + "and the time should be included. A value of 'without-date' " + "indicates that only the time should be included."; final Set allowedFormats = StaticUtils.setOf("none", "with-date", "without-date"); timestampFormat = new StringArgument(null, "timestampFormat", true, 1, "{format}", description, allowedFormats, "none"); timestampFormat.addLongIdentifier("timestamp-format", true); parser.addArgument(timestampFormat); description = "Indicates that the client should operate in asynchronous " + "mode, in which it will not be necessary to wait for a response to " + "a previous request before sending the next request. Either the " + "'--ratePerSecond' or the '--maxOutstandingRequests' argument must " + "be provided to limit the number of outstanding requests."; asynchronousMode = new BooleanArgument('a', "asynchronous", description); parser.addArgument(asynchronousMode); description = "Specifies the maximum number of outstanding requests " + "that should be allowed when operating in asynchronous mode."; maxOutstandingRequests = new IntegerArgument('O', "maxOutstandingRequests", false, 1, "{num}", description, 1, Integer.MAX_VALUE, (Integer) null); maxOutstandingRequests.addLongIdentifier("max-outstanding-requests", true); parser.addArgument(maxOutstandingRequests); description = "Indicates that information about the result codes for " + "failed operations should not be displayed."; suppressErrors = new BooleanArgument(null, "suppressErrorResultCodes", 1, description); suppressErrors.addLongIdentifier("suppress-error-result-codes", true); parser.addArgument(suppressErrors); description = "Generate output in CSV format rather than a " + "display-friendly format"; csvFormat = new BooleanArgument('c', "csv", 1, description); parser.addArgument(csvFormat); description = "Specifies the seed to use for the random number generator."; randomSeed = new IntegerArgument('R', "randomSeed", false, 1, "{value}", description); randomSeed.addLongIdentifier("random-seed", true); parser.addArgument(randomSeed); parser.addExclusiveArgumentSet(baseDN, ldapURL); parser.addExclusiveArgumentSet(scope, ldapURL); parser.addExclusiveArgumentSet(filter, ldapURL); parser.addExclusiveArgumentSet(attributes, ldapURL); parser.addRequiredArgumentSet(filter, ldapURL); parser.addDependentArgumentSet(asynchronousMode, ratePerSecond, maxOutstandingRequests); parser.addDependentArgumentSet(maxOutstandingRequests, asynchronousMode); parser.addExclusiveArgumentSet(asynchronousMode, simplePageSize); } /** * Indicates whether this tool supports creating connections to multiple * servers. If it is to support multiple servers, then the "--hostname" and * "--port" arguments will be allowed to be provided multiple times, and * will be required to be provided the same number of times. The same type of * communication security and bind credentials will be used for all servers. * * @return {@code true} if this tool supports creating connections to * multiple servers, or {@code false} if not. */ @Override() protected boolean supportsMultipleServers() { return true; } /** * Retrieves the connection options that should be used for connections * created for use with this tool. * * @return The connection options that should be used for connections created * for use with this tool. */ @Override() @NotNull() public LDAPConnectionOptions getConnectionOptions() { final LDAPConnectionOptions options = new LDAPConnectionOptions(); options.setUseSynchronousMode(! asynchronousMode.isPresent()); return options; } /** * Performs the actual processing for this tool. In this case, it gets a * connection to the directory server and uses it to perform the requested * searches. * * @return The result code for the processing that was performed. */ @Override() @NotNull() public ResultCode doToolProcessing() { // If the sample rate file argument was specified, then generate the sample // variable rate data file and return. if (sampleRateFile.isPresent()) { try { RateAdjustor.writeSampleVariableRateFile(sampleRateFile.getValue()); return ResultCode.SUCCESS; } catch (final Exception e) { Debug.debugException(e); err("An error occurred while trying to write sample variable data " + "rate file '", sampleRateFile.getValue().getAbsolutePath(), "': ", StaticUtils.getExceptionMessage(e)); return ResultCode.LOCAL_ERROR; } } // Determine the random seed to use. final Long seed; if (randomSeed.isPresent()) { seed = Long.valueOf(randomSeed.getValue()); } else { seed = null; } // Create value patterns for the base DN, filter, LDAP URL, and proxied // authorization DN. final ValuePattern dnPattern; try { if (baseDN.getNumOccurrences() > 0) { dnPattern = new ValuePattern(baseDN.getValue(), seed); } else if (ldapURL.isPresent()) { dnPattern = null; } else { dnPattern = new ValuePattern("", seed); } } catch (final ParseException pe) { Debug.debugException(pe); err("Unable to parse the base DN value pattern: ", pe.getMessage()); return ResultCode.PARAM_ERROR; } final ValuePattern filterPattern; try { if (filter.isPresent()) { filterPattern = new ValuePattern(filter.getValue(), seed); } else { filterPattern = null; } } catch (final ParseException pe) { Debug.debugException(pe); err("Unable to parse the filter pattern: ", pe.getMessage()); return ResultCode.PARAM_ERROR; } final ValuePattern ldapURLPattern; try { if (ldapURL.isPresent()) { ldapURLPattern = new ValuePattern(ldapURL.getValue(), seed); } else { ldapURLPattern = null; } } catch (final ParseException pe) { Debug.debugException(pe); err("Unable to parse the LDAP URL pattern: ", pe.getMessage()); return ResultCode.PARAM_ERROR; } final ValuePattern authzIDPattern; if (proxyAs.isPresent()) { try { authzIDPattern = new ValuePattern(proxyAs.getValue(), seed); } catch (final ParseException pe) { Debug.debugException(pe); err("Unable to parse the proxied authorization pattern: ", pe.getMessage()); return ResultCode.PARAM_ERROR; } } else { authzIDPattern = null; } // Get the alias dereference policy to use. final DereferencePolicy derefPolicy; final String derefValue = StaticUtils.toLowerCase(dereferencePolicy.getValue()); if (derefValue.equals("always")) { derefPolicy = DereferencePolicy.ALWAYS; } else if (derefValue.equals("search")) { derefPolicy = DereferencePolicy.SEARCHING; } else if (derefValue.equals("find")) { derefPolicy = DereferencePolicy.FINDING; } else { derefPolicy = DereferencePolicy.NEVER; } // Get the set of controls to include in search requests. final ArrayList controlList = new ArrayList<>(5); if (assertionFilter.isPresent()) { controlList.add(new AssertionRequestControl(assertionFilter.getValue())); } if (sortOrder.isPresent()) { final ArrayList sortKeys = new ArrayList<>(5); final StringTokenizer tokenizer = new StringTokenizer(sortOrder.getValue(), ","); while (tokenizer.hasMoreTokens()) { String token = tokenizer.nextToken().trim(); final boolean ascending; if (token.startsWith("+")) { ascending = true; token = token.substring(1); } else if (token.startsWith("-")) { ascending = false; token = token.substring(1); } else { ascending = true; } final String attributeName; final String matchingRuleID; final int colonPos = token.indexOf(':'); if (colonPos < 0) { attributeName = token; matchingRuleID = null; } else { attributeName = token.substring(0, colonPos); matchingRuleID = token.substring(colonPos+1); } sortKeys.add(new SortKey(attributeName, matchingRuleID, (! ascending))); } controlList.add(new ServerSideSortRequestControl(sortKeys)); } if (control.isPresent()) { controlList.addAll(control.getValues()); } // Get the attributes to return. final String[] attrs; if (attributes.isPresent()) { final List attrList = attributes.getValues(); attrs = new String[attrList.size()]; attrList.toArray(attrs); } else { attrs = StaticUtils.NO_STRINGS; } // If the --ratePerSecond option was specified, then limit the rate // accordingly. FixedRateBarrier fixedRateBarrier = null; if (ratePerSecond.isPresent() || variableRateData.isPresent()) { // We might not have a rate per second if --variableRateData is specified. // The rate typically doesn't matter except when we have warm-up // intervals. In this case, we'll run at the max rate. final int intervalSeconds = collectionInterval.getValue(); final int ratePerInterval = (ratePerSecond.getValue() == null) ? Integer.MAX_VALUE : ratePerSecond.getValue() * intervalSeconds; fixedRateBarrier = new FixedRateBarrier(1000L * intervalSeconds, ratePerInterval); } // If --variableRateData was specified, then initialize a RateAdjustor. RateAdjustor rateAdjustor = null; if (variableRateData.isPresent()) { try { rateAdjustor = RateAdjustor.newInstance(fixedRateBarrier, ratePerSecond.getValue(), variableRateData.getValue()); } catch (final IOException | IllegalArgumentException e) { Debug.debugException(e); err("Initializing the variable rates failed: " + e.getMessage()); return ResultCode.PARAM_ERROR; } } // If the --maxOutstandingRequests option was specified, then create the // semaphore used to enforce that limit. final Semaphore asyncSemaphore; if (maxOutstandingRequests.isPresent()) { asyncSemaphore = new Semaphore(maxOutstandingRequests.getValue()); } else { asyncSemaphore = null; } // Determine whether to include timestamps in the output and if so what // format should be used for them. final boolean includeTimestamp; final String timeFormat; if (timestampFormat.getValue().equalsIgnoreCase("with-date")) { includeTimestamp = true; timeFormat = "dd/MM/yyyy HH:mm:ss"; } else if (timestampFormat.getValue().equalsIgnoreCase("without-date")) { includeTimestamp = true; timeFormat = "HH:mm:ss"; } else { includeTimestamp = false; timeFormat = null; } // Determine whether any warm-up intervals should be run. final long totalIntervals; final boolean warmUp; int remainingWarmUpIntervals = warmUpIntervals.getValue(); if (remainingWarmUpIntervals > 0) { warmUp = true; totalIntervals = 0L + numIntervals.getValue() + remainingWarmUpIntervals; } else { warmUp = true; totalIntervals = 0L + numIntervals.getValue(); } // Create the table that will be used to format the output. final OutputFormat outputFormat; if (csvFormat.isPresent()) { outputFormat = OutputFormat.CSV; } else { outputFormat = OutputFormat.COLUMNS; } final ColumnFormatter formatter = new ColumnFormatter(includeTimestamp, timeFormat, outputFormat, " ", new FormattableColumn(12, HorizontalAlignment.RIGHT, "Recent", "Searches/Sec"), new FormattableColumn(12, HorizontalAlignment.RIGHT, "Recent", "Avg Dur ms"), new FormattableColumn(12, HorizontalAlignment.RIGHT, "Recent", "Entries/Srch"), new FormattableColumn(12, HorizontalAlignment.RIGHT, "Recent", "Errors/Sec"), new FormattableColumn(12, HorizontalAlignment.RIGHT, "Overall", "Searches/Sec"), new FormattableColumn(12, HorizontalAlignment.RIGHT, "Overall", "Avg Dur ms")); // Create values to use for statistics collection. final AtomicLong searchCounter = new AtomicLong(0L); final AtomicLong entryCounter = new AtomicLong(0L); final AtomicLong errorCounter = new AtomicLong(0L); final AtomicLong searchDurations = new AtomicLong(0L); final ResultCodeCounter rcCounter = new ResultCodeCounter(); // Determine the length of each interval in milliseconds. final long intervalMillis = 1000L * collectionInterval.getValue(); // Create the threads to use for the searches. final CyclicBarrier barrier = new CyclicBarrier(numThreads.getValue() + 1); final SearchRateThread[] threads = new SearchRateThread[numThreads.getValue()]; for (int i=0; i < threads.length; i++) { final LDAPConnection connection; try { connection = getConnection(); } catch (final LDAPException le) { Debug.debugException(le); err("Unable to connect to the directory server: ", StaticUtils.getExceptionMessage(le)); return le.getResultCode(); } threads[i] = new SearchRateThread(this, i, connection, asynchronousMode.isPresent(), dnPattern, scope.getValue(), derefPolicy, sizeLimit.getValue(), timeLimitSeconds.getValue(), typesOnly.isPresent(), filterPattern, attrs, ldapURLPattern, authzIDPattern, simplePageSize.getValue(), controlList, iterationsBeforeReconnect.getValue(), runningThreads, barrier, searchCounter, entryCounter, searchDurations, errorCounter, rcCounter, fixedRateBarrier, asyncSemaphore); threads[i].start(); } // Display the table header. for (final String headerLine : formatter.getHeaderLines(true)) { out(headerLine); } // Start the RateAdjustor before the threads so that the initial value is // in place before any load is generated unless we're doing a warm-up in // which case, we'll start it after the warm-up is complete. if ((rateAdjustor != null) && (remainingWarmUpIntervals <= 0)) { rateAdjustor.start(); } // Indicate that the threads can start running. try { barrier.await(); } catch (final Exception e) { Debug.debugException(e); } long overallStartTime = System.nanoTime(); long nextIntervalStartTime = System.currentTimeMillis() + intervalMillis; boolean setOverallStartTime = false; long lastDuration = 0L; long lastNumEntries = 0L; long lastNumErrors = 0L; long lastNumSearches = 0L; long lastEndTime = System.nanoTime(); for (long i=0; i < totalIntervals; i++) { if (rateAdjustor != null) { if (! rateAdjustor.isAlive()) { out("All of the rates in " + variableRateData.getValue().getName() + " have been completed."); break; } } final long startTimeMillis = System.currentTimeMillis(); final long sleepTimeMillis = nextIntervalStartTime - startTimeMillis; nextIntervalStartTime += intervalMillis; if (sleepTimeMillis > 0) { sleeper.sleep(sleepTimeMillis); } if (stopRequested.get()) { break; } final long endTime = System.nanoTime(); final long intervalDuration = endTime - lastEndTime; final long numSearches; final long numEntries; final long numErrors; final long totalDuration; if (warmUp && (remainingWarmUpIntervals > 0)) { numSearches = searchCounter.getAndSet(0L); numEntries = entryCounter.getAndSet(0L); numErrors = errorCounter.getAndSet(0L); totalDuration = searchDurations.getAndSet(0L); } else { numSearches = searchCounter.get(); numEntries = entryCounter.get(); numErrors = errorCounter.get(); totalDuration = searchDurations.get(); } final long recentNumSearches = numSearches - lastNumSearches; final long recentNumEntries = numEntries - lastNumEntries; final long recentNumErrors = numErrors - lastNumErrors; final long recentDuration = totalDuration - lastDuration; final double numSeconds = intervalDuration / 1_000_000_000.0d; final double recentSearchRate = recentNumSearches / numSeconds; final double recentErrorRate = recentNumErrors / numSeconds; final double recentAvgDuration; final double recentEntriesPerSearch; if (recentNumSearches > 0L) { recentEntriesPerSearch = 1.0d * recentNumEntries / recentNumSearches; recentAvgDuration = 1.0d * recentDuration / recentNumSearches / 1_000_000; } else { recentEntriesPerSearch = 0.0d; recentAvgDuration = 0.0d; } if (warmUp && (remainingWarmUpIntervals > 0)) { out(formatter.formatRow(recentSearchRate, recentAvgDuration, recentEntriesPerSearch, recentErrorRate, "warming up", "warming up")); remainingWarmUpIntervals--; if (remainingWarmUpIntervals == 0) { out("Warm-up completed. Beginning overall statistics collection."); setOverallStartTime = true; if (rateAdjustor != null) { rateAdjustor.start(); } } } else { if (setOverallStartTime) { overallStartTime = lastEndTime; setOverallStartTime = false; } final double numOverallSeconds = (endTime - overallStartTime) / 1_000_000_000.0d; final double overallSearchRate = numSearches / numOverallSeconds; final double overallAvgDuration; if (numSearches > 0L) { overallAvgDuration = 1.0d * totalDuration / numSearches / 1_000_000; } else { overallAvgDuration = 0.0d; } out(formatter.formatRow(recentSearchRate, recentAvgDuration, recentEntriesPerSearch, recentErrorRate, overallSearchRate, overallAvgDuration)); lastNumSearches = numSearches; lastNumEntries = numEntries; lastNumErrors = numErrors; lastDuration = totalDuration; } final List> rcCounts = rcCounter.getCounts(true); if ((! suppressErrors.isPresent()) && (! rcCounts.isEmpty())) { err("\tError Results:"); for (final ObjectPair p : rcCounts) { err("\t", p.getFirst().getName(), ": ", p.getSecond()); } } lastEndTime = endTime; } // Shut down the RateAdjustor if we have one. if (rateAdjustor != null) { rateAdjustor.shutDown(); } // Stop all of the threads. ResultCode resultCode = ResultCode.SUCCESS; for (final SearchRateThread t : threads) { t.signalShutdown(); } for (final SearchRateThread t : threads) { final ResultCode r = t.waitForShutdown(); if (resultCode == ResultCode.SUCCESS) { resultCode = r; } } return resultCode; } /** * Requests that this tool stop running. This method will attempt to wait * for all threads to complete before returning control to the caller. */ public void stopRunning() { stopRequested.set(true); sleeper.wakeup(); while (true) { final int stillRunning = runningThreads.get(); if (stillRunning <= 0) { break; } else { try { Thread.sleep(1L); } catch (final Exception e) {} } } } /** * Retrieves the maximum number of outstanding requests that may be in * progress at any time, if appropriate. * * @return The maximum number of outstanding requests that may be in progress * at any time, or -1 if the tool was not configured to perform * asynchronous searches with a maximum number of outstanding * requests. */ int getMaxOutstandingRequests() { if (maxOutstandingRequests.isPresent()) { return maxOutstandingRequests.getValue(); } else { return -1; } } /** * {@inheritDoc} */ @Override() @NotNull() public LinkedHashMap getExampleUsages() { final LinkedHashMap examples = new LinkedHashMap<>(StaticUtils.computeMapCapacity(2)); String[] args = { "--hostname", "server.example.com", "--port", "389", "--bindDN", "uid=admin,dc=example,dc=com", "--bindPassword", "password", "--baseDN", "dc=example,dc=com", "--scope", "sub", "--filter", "(uid=user.[1-1000000])", "--attribute", "givenName", "--attribute", "sn", "--attribute", "mail", "--numThreads", "10" }; String description = "Test search performance by searching randomly across a set " + "of one million users located below 'dc=example,dc=com' with ten " + "concurrent threads. The entries returned to the client will " + "include the givenName, sn, and mail attributes."; examples.put(args, description); args = new String[] { "--generateSampleRateFile", "variable-rate-data.txt" }; description = "Generate a sample variable rate definition file that may be used " + "in conjunction with the --variableRateData argument. The sample " + "file will include comments that describe the format for data to be " + "included in this file."; examples.put(args, description); return examples; } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy