com.unboundid.ldap.sdk.examples.SearchRate Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of unboundid-ldapsdk Show documentation
Show all versions of unboundid-ldapsdk Show documentation
The UnboundID LDAP SDK for Java is a fast, comprehensive, and easy-to-use
Java API for communicating with LDAP directory servers and performing
related tasks like reading and writing LDIF, encoding and decoding data
using base64 and ASN.1 BER, and performing secure communication. This
package contains the Standard Edition of the LDAP SDK, which is a
complete, general-purpose library for communicating with LDAPv3 directory
servers.
/*
* Copyright 2008-2022 Ping Identity Corporation
* All Rights Reserved.
*/
/*
* Copyright 2008-2022 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-2022 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;
}
}