Many resources are needed to download a project. Please understand that we have to compensate our server costs. Thank you in advance. Project price only 1 $
You can buy this project and download/modify it how often you want.
/*
* 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;
}
}