001/*
002 * Copyright 2008-2024 Ping Identity Corporation
003 * All Rights Reserved.
004 */
005/*
006 * Copyright 2008-2024 Ping Identity Corporation
007 *
008 * Licensed under the Apache License, Version 2.0 (the "License");
009 * you may not use this file except in compliance with the License.
010 * You may obtain a copy of the License at
011 *
012 *    http://www.apache.org/licenses/LICENSE-2.0
013 *
014 * Unless required by applicable law or agreed to in writing, software
015 * distributed under the License is distributed on an "AS IS" BASIS,
016 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
017 * See the License for the specific language governing permissions and
018 * limitations under the License.
019 */
020/*
021 * Copyright (C) 2008-2024 Ping Identity Corporation
022 *
023 * This program is free software; you can redistribute it and/or modify
024 * it under the terms of the GNU General Public License (GPLv2 only)
025 * or the terms of the GNU Lesser General Public License (LGPLv2.1 only)
026 * as published by the Free Software Foundation.
027 *
028 * This program is distributed in the hope that it will be useful,
029 * but WITHOUT ANY WARRANTY; without even the implied warranty of
030 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
031 * GNU General Public License for more details.
032 *
033 * You should have received a copy of the GNU General Public License
034 * along with this program; if not, see <http://www.gnu.org/licenses>.
035 */
036package com.unboundid.ldap.sdk.examples;
037
038
039
040import java.io.IOException;
041import java.io.OutputStream;
042import java.io.Serializable;
043import java.text.ParseException;
044import java.util.ArrayList;
045import java.util.LinkedHashMap;
046import java.util.List;
047import java.util.Set;
048import java.util.StringTokenizer;
049import java.util.concurrent.CyclicBarrier;
050import java.util.concurrent.Semaphore;
051import java.util.concurrent.atomic.AtomicBoolean;
052import java.util.concurrent.atomic.AtomicInteger;
053import java.util.concurrent.atomic.AtomicLong;
054
055import com.unboundid.ldap.sdk.Control;
056import com.unboundid.ldap.sdk.DereferencePolicy;
057import com.unboundid.ldap.sdk.LDAPConnection;
058import com.unboundid.ldap.sdk.LDAPConnectionOptions;
059import com.unboundid.ldap.sdk.LDAPException;
060import com.unboundid.ldap.sdk.ResultCode;
061import com.unboundid.ldap.sdk.SearchScope;
062import com.unboundid.ldap.sdk.Version;
063import com.unboundid.ldap.sdk.controls.AssertionRequestControl;
064import com.unboundid.ldap.sdk.controls.ServerSideSortRequestControl;
065import com.unboundid.ldap.sdk.controls.SortKey;
066import com.unboundid.util.ColumnFormatter;
067import com.unboundid.util.Debug;
068import com.unboundid.util.FixedRateBarrier;
069import com.unboundid.util.FormattableColumn;
070import com.unboundid.util.HorizontalAlignment;
071import com.unboundid.util.LDAPCommandLineTool;
072import com.unboundid.util.NotNull;
073import com.unboundid.util.Nullable;
074import com.unboundid.util.ObjectPair;
075import com.unboundid.util.OutputFormat;
076import com.unboundid.util.RateAdjustor;
077import com.unboundid.util.ResultCodeCounter;
078import com.unboundid.util.StaticUtils;
079import com.unboundid.util.ThreadSafety;
080import com.unboundid.util.ThreadSafetyLevel;
081import com.unboundid.util.WakeableSleeper;
082import com.unboundid.util.ValuePattern;
083import com.unboundid.util.args.ArgumentException;
084import com.unboundid.util.args.ArgumentParser;
085import com.unboundid.util.args.BooleanArgument;
086import com.unboundid.util.args.ControlArgument;
087import com.unboundid.util.args.FileArgument;
088import com.unboundid.util.args.FilterArgument;
089import com.unboundid.util.args.IntegerArgument;
090import com.unboundid.util.args.ScopeArgument;
091import com.unboundid.util.args.StringArgument;
092
093
094
095/**
096 * This class provides a tool that can be used to search an LDAP directory
097 * server repeatedly using multiple threads.  It can help provide an estimate of
098 * the search performance that a directory server is able to achieve.  Either or
099 * both of the base DN and the search filter may be a value pattern as
100 * described in the {@link ValuePattern} class.  This makes it possible to
101 * search over a range of entries rather than repeatedly performing searches
102 * with the same base DN and filter.
103 * <BR><BR>
104 * Some of the APIs demonstrated by this example include:
105 * <UL>
106 *   <LI>Argument Parsing (from the {@code com.unboundid.util.args}
107 *       package)</LI>
108 *   <LI>LDAP Command-Line Tool (from the {@code com.unboundid.util}
109 *       package)</LI>
110 *   <LI>LDAP Communication (from the {@code com.unboundid.ldap.sdk}
111 *       package)</LI>
112 *   <LI>Value Patterns (from the {@code com.unboundid.util} package)</LI>
113 * </UL>
114 * <BR><BR>
115 * All of the necessary information is provided using command line arguments.
116 * Supported arguments include those allowed by the {@link LDAPCommandLineTool}
117 * class, as well as the following additional arguments:
118 * <UL>
119 *   <LI>"-b {baseDN}" or "--baseDN {baseDN}" -- specifies the base DN to use
120 *       for the searches.  This must be provided.  It may be a simple DN, or it
121 *       may be a value pattern to express a range of base DNs.</LI>
122 *   <LI>"-s {scope}" or "--scope {scope}" -- specifies the scope to use for the
123 *       search.  The scope value should be one of "base", "one", "sub", or
124 *       "subord".  If this isn't specified, then a scope of "sub" will be
125 *       used.</LI>
126 *   <LI>"-z {num}" or "--sizeLimit {num}" -- specifies the maximum number of
127 *       entries that should be returned in response to each search
128 *       request.</LI>
129 *   <LI>"-l {num}" or "--timeLimitSeconds {num}" -- specifies the maximum
130 *       length of time, in seconds, that the server should spend processing
131 *       each search request.</LI>
132 *   <LI>"--dereferencePolicy {value}" -- specifies the alias dereferencing
133 *       policy that should be used for each search request.  Allowed values are
134 *       "never", "always", "search", and "find".</LI>
135 *   <LI>"--typesOnly" -- indicates that search requests should have the
136 *       typesOnly flag set to true, indicating that matching entries should
137 *       only include attributes with an attribute description but no
138 *       values.</LI>
139 *   <LI>"-f {filter}" or "--filter {filter}" -- specifies the filter to use for
140 *       the searches.  This must be provided.  It may be a simple filter, or it
141 *       may be a value pattern to express a range of filters.</LI>
142 *   <LI>"-A {name}" or "--attribute {name}" -- specifies the name of an
143 *       attribute that should be included in entries returned from the server.
144 *       If this is not provided, then all user attributes will be requested.
145 *       This may include special tokens that the server may interpret, like
146 *       "1.1" to indicate that no attributes should be returned, "*", for all
147 *       user attributes, or "+" for all operational attributes.  Multiple
148 *       attributes may be requested with multiple instances of this
149 *       argument.</LI>
150 *   <LI>"--ldapURL {url}" -- Specifies an LDAP URL that represents the base DN,
151 *       scope, filter, and set of requested attributes that should be used for
152 *       the search requests.  It may be a simple LDAP URL, or it may be a value
153 *       pattern to express a range of LDAP URLs.  If this argument is provided,
154 *       then none of the --baseDN, --scope, --filter, or --attribute arguments
155 *       may be used.</LI>
156 *   <LI>"-t {num}" or "--numThreads {num}" -- specifies the number of
157 *       concurrent threads to use when performing the searches.  If this is not
158 *       provided, then a default of one thread will be used.</LI>
159 *   <LI>"-i {sec}" or "--intervalDuration {sec}" -- specifies the length of
160 *       time in seconds between lines out output.  If this is not provided,
161 *       then a default interval duration of five seconds will be used.</LI>
162 *   <LI>"-I {num}" or "--numIntervals {num}" -- specifies the maximum number of
163 *       intervals for which to run.  If this is not provided, then it will
164 *       run forever.</LI>
165 *   <LI>"--iterationsBeforeReconnect {num}" -- specifies the number of search
166 *       iterations that should be performed on a connection before that
167 *       connection is closed and replaced with a newly-established (and
168 *       authenticated, if appropriate) connection.</LI>
169 *   <LI>"-r {searches-per-second}" or "--ratePerSecond {searches-per-second}"
170 *       -- specifies the target number of searches to perform per second.  It
171 *       is still necessary to specify a sufficient number of threads for
172 *       achieving this rate.  If this option is not provided, then the tool
173 *       will run at the maximum rate for the specified number of threads.</LI>
174 *   <LI>"--variableRateData {path}" -- specifies the path to a file containing
175 *       information needed to allow the tool to vary the target rate over time.
176 *       If this option is not provided, then the tool will either use a fixed
177 *       target rate as specified by the "--ratePerSecond" argument, or it will
178 *       run at the maximum rate.</LI>
179 *   <LI>"--generateSampleRateFile {path}" -- specifies the path to a file to
180 *       which sample data will be written illustrating and describing the
181 *       format of the file expected to be used in conjunction with the
182 *       "--variableRateData" argument.</LI>
183 *   <LI>"--warmUpIntervals {num}" -- specifies the number of intervals to
184 *       complete before beginning overall statistics collection.</LI>
185 *   <LI>"--timestampFormat {format}" -- specifies the format to use for
186 *       timestamps included before each output line.  The format may be one of
187 *       "none" (for no timestamps), "with-date" (to include both the date and
188 *       the time), or "without-date" (to include only time time).</LI>
189 *   <LI>"-Y {authzID}" or "--proxyAs {authzID}" -- Use the proxied
190 *       authorization v2 control to request that the operation be processed
191 *       using an alternate authorization identity.  In this case, the bind DN
192 *       should be that of a user that has permission to use this control.  The
193 *       authorization identity may be a value pattern.</LI>
194 *   <LI>"-a" or "--asynchronous" -- Indicates that searches should be performed
195 *       in asynchronous mode, in which the client will not wait for a response
196 *       to a previous request before sending the next request.  Either the
197 *       "--ratePerSecond" or "--maxOutstandingRequests" arguments must be
198 *       provided to limit the number of outstanding requests.</LI>
199 *   <LI>"-O {num}" or "--maxOutstandingRequests {num}" -- Specifies the maximum
200 *       number of outstanding requests that will be allowed in asynchronous
201 *       mode.</LI>
202 *   <LI>"--suppressErrorResultCodes" -- Indicates that information about the
203 *       result codes for failed operations should not be displayed.</LI>
204 *   <LI>"-c" or "--csv" -- Generate output in CSV format rather than a
205 *       display-friendly format.</LI>
206 * </UL>
207 */
208@ThreadSafety(level=ThreadSafetyLevel.NOT_THREADSAFE)
209public final class SearchRate
210       extends LDAPCommandLineTool
211       implements Serializable
212{
213  /**
214   * The serial version UID for this serializable class.
215   */
216  private static final long serialVersionUID = 3345838530404592182L;
217
218
219
220  // Indicates whether a request has been made to stop running.
221  @NotNull private final AtomicBoolean stopRequested;
222
223  // The number of searchrate threads that are currently running.
224  @NotNull private final AtomicInteger runningThreads;
225
226  // The argument used to indicate whether to operate in asynchronous mode.
227  @Nullable private BooleanArgument asynchronousMode;
228
229  // The argument used to indicate whether to generate output in CSV format.
230  @Nullable private BooleanArgument csvFormat;
231
232  // The argument used to indicate whether to suppress information about error
233  // result codes.
234  @Nullable private BooleanArgument suppressErrors;
235
236  // The argument used to indicate whether to set the typesOnly flag to true in
237  // search requests.
238  @Nullable private BooleanArgument typesOnly;
239
240  // The argument used to indicate that a generic control should be included in
241  // the request.
242  @Nullable private ControlArgument control;
243
244  // The argument used to specify a variable rate file.
245  @Nullable private FileArgument sampleRateFile;
246
247  // The argument used to specify a variable rate file.
248  @Nullable private FileArgument variableRateData;
249
250  // Indicates that search requests should include the assertion request control
251  // with the specified filter.
252  @Nullable private FilterArgument assertionFilter;
253
254  // The argument used to specify the collection interval.
255  @Nullable private IntegerArgument collectionInterval;
256
257  // The argument used to specify the number of search iterations on a
258  // connection before it is closed and re-established.
259  @Nullable private IntegerArgument iterationsBeforeReconnect;
260
261  // The argument used to specify the maximum number of outstanding asynchronous
262  // requests.
263  @Nullable private IntegerArgument maxOutstandingRequests;
264
265  // The argument used to specify the number of intervals.
266  @Nullable private IntegerArgument numIntervals;
267
268  // The argument used to specify the number of threads.
269  @Nullable private IntegerArgument numThreads;
270
271  // The argument used to specify the seed to use for the random number
272  // generator.
273  @Nullable private IntegerArgument randomSeed;
274
275  // The target rate of searches per second.
276  @Nullable private IntegerArgument ratePerSecond;
277
278  // The argument used to indicate that the search should use the simple paged
279  // results control with the specified page size.
280  @Nullable private IntegerArgument simplePageSize;
281
282  // The argument used to specify the search request size limit.
283  @Nullable private IntegerArgument sizeLimit;
284
285  // The argument used to specify the search request time limit, in seconds.
286  @Nullable private IntegerArgument timeLimitSeconds;
287
288  // The number of warm-up intervals to perform.
289  @Nullable private IntegerArgument warmUpIntervals;
290
291  // The argument used to specify the scope for the searches.
292  @Nullable private ScopeArgument scope;
293
294  // The argument used to specify the attributes to return.
295  @Nullable private StringArgument attributes;
296
297  // The argument used to specify the base DNs for the searches.
298  @Nullable private StringArgument baseDN;
299
300  // The argument used to specify the alias dereferencing policy for the search
301  // requests.
302  @Nullable private StringArgument dereferencePolicy;
303
304  // The argument used to specify the filters for the searches.
305  @Nullable private StringArgument filter;
306
307  // The argument used to specify the LDAP URLs for the searches.
308  @Nullable private StringArgument ldapURL;
309
310  // The argument used to specify the proxied authorization identity.
311  @Nullable private StringArgument proxyAs;
312
313  // The argument used to request that the server sort the results with the
314  // specified order.
315  @Nullable private StringArgument sortOrder;
316
317  // The argument used to specify the timestamp format.
318  @Nullable private StringArgument timestampFormat;
319
320  // A wakeable sleeper that will be used to sleep between reporting intervals.
321  @NotNull private final WakeableSleeper sleeper;
322
323
324
325  /**
326   * Parse the provided command line arguments and make the appropriate set of
327   * changes.
328   *
329   * @param  args  The command line arguments provided to this program.
330   */
331  public static void main(@NotNull final String[] args)
332  {
333    final ResultCode resultCode = main(args, System.out, System.err);
334    if (resultCode != ResultCode.SUCCESS)
335    {
336      System.exit(resultCode.intValue());
337    }
338  }
339
340
341
342  /**
343   * Parse the provided command line arguments and make the appropriate set of
344   * changes.
345   *
346   * @param  args       The command line arguments provided to this program.
347   * @param  outStream  The output stream to which standard out should be
348   *                    written.  It may be {@code null} if output should be
349   *                    suppressed.
350   * @param  errStream  The output stream to which standard error should be
351   *                    written.  It may be {@code null} if error messages
352   *                    should be suppressed.
353   *
354   * @return  A result code indicating whether the processing was successful.
355   */
356  @NotNull()
357  public static ResultCode main(@NotNull final String[] args,
358                                @Nullable final OutputStream outStream,
359                                @Nullable final OutputStream errStream)
360  {
361    final SearchRate searchRate = new SearchRate(outStream, errStream);
362    return searchRate.runTool(args);
363  }
364
365
366
367  /**
368   * Creates a new instance of this tool.
369   *
370   * @param  outStream  The output stream to which standard out should be
371   *                    written.  It may be {@code null} if output should be
372   *                    suppressed.
373   * @param  errStream  The output stream to which standard error should be
374   *                    written.  It may be {@code null} if error messages
375   *                    should be suppressed.
376   */
377  public SearchRate(@Nullable final OutputStream outStream,
378                    @Nullable final OutputStream errStream)
379  {
380    super(outStream, errStream);
381
382    stopRequested = new AtomicBoolean(false);
383    runningThreads = new AtomicInteger(0);
384    sleeper = new WakeableSleeper();
385  }
386
387
388
389  /**
390   * Retrieves the name for this tool.
391   *
392   * @return  The name for this tool.
393   */
394  @Override()
395  @NotNull()
396  public String getToolName()
397  {
398    return "searchrate";
399  }
400
401
402
403  /**
404   * Retrieves the description for this tool.
405   *
406   * @return  The description for this tool.
407   */
408  @Override()
409  @NotNull()
410  public String getToolDescription()
411  {
412    return "Perform repeated searches against an " +
413           "LDAP directory server.";
414  }
415
416
417
418  /**
419   * Retrieves the version string for this tool.
420   *
421   * @return  The version string for this tool.
422   */
423  @Override()
424  @NotNull()
425  public String getToolVersion()
426  {
427    return Version.NUMERIC_VERSION_STRING;
428  }
429
430
431
432  /**
433   * Indicates whether this tool should provide support for an interactive mode,
434   * in which the tool offers a mode in which the arguments can be provided in
435   * a text-driven menu rather than requiring them to be given on the command
436   * line.  If interactive mode is supported, it may be invoked using the
437   * "--interactive" argument.  Alternately, if interactive mode is supported
438   * and {@link #defaultsToInteractiveMode()} returns {@code true}, then
439   * interactive mode may be invoked by simply launching the tool without any
440   * arguments.
441   *
442   * @return  {@code true} if this tool supports interactive mode, or
443   *          {@code false} if not.
444   */
445  @Override()
446  public boolean supportsInteractiveMode()
447  {
448    return true;
449  }
450
451
452
453  /**
454   * Indicates whether this tool defaults to launching in interactive mode if
455   * the tool is invoked without any command-line arguments.  This will only be
456   * used if {@link #supportsInteractiveMode()} returns {@code true}.
457   *
458   * @return  {@code true} if this tool defaults to using interactive mode if
459   *          launched without any command-line arguments, or {@code false} if
460   *          not.
461   */
462  @Override()
463  public boolean defaultsToInteractiveMode()
464  {
465    return true;
466  }
467
468
469
470  /**
471   * Indicates whether this tool should provide arguments for redirecting output
472   * to a file.  If this method returns {@code true}, then the tool will offer
473   * an "--outputFile" argument that will specify the path to a file to which
474   * all standard output and standard error content will be written, and it will
475   * also offer a "--teeToStandardOut" argument that can only be used if the
476   * "--outputFile" argument is present and will cause all output to be written
477   * to both the specified output file and to standard output.
478   *
479   * @return  {@code true} if this tool should provide arguments for redirecting
480   *          output to a file, or {@code false} if not.
481   */
482  @Override()
483  protected boolean supportsOutputFile()
484  {
485    return true;
486  }
487
488
489
490  /**
491   * Indicates whether this tool should default to interactively prompting for
492   * the bind password if a password is required but no argument was provided
493   * to indicate how to get the password.
494   *
495   * @return  {@code true} if this tool should default to interactively
496   *          prompting for the bind password, or {@code false} if not.
497   */
498  @Override()
499  protected boolean defaultToPromptForBindPassword()
500  {
501    return true;
502  }
503
504
505
506  /**
507   * Indicates whether this tool supports the use of a properties file for
508   * specifying default values for arguments that aren't specified on the
509   * command line.
510   *
511   * @return  {@code true} if this tool supports the use of a properties file
512   *          for specifying default values for arguments that aren't specified
513   *          on the command line, or {@code false} if not.
514   */
515  @Override()
516  public boolean supportsPropertiesFile()
517  {
518    return true;
519  }
520
521
522
523  /**
524   * Indicates whether the LDAP-specific arguments should include alternate
525   * versions of all long identifiers that consist of multiple words so that
526   * they are available in both camelCase and dash-separated versions.
527   *
528   * @return  {@code true} if this tool should provide multiple versions of
529   *          long identifiers for LDAP-specific arguments, or {@code false} if
530   *          not.
531   */
532  @Override()
533  protected boolean includeAlternateLongIdentifiers()
534  {
535    return true;
536  }
537
538
539
540  /**
541   * Adds the arguments used by this program that aren't already provided by the
542   * generic {@code LDAPCommandLineTool} framework.
543   *
544   * @param  parser  The argument parser to which the arguments should be added.
545   *
546   * @throws  ArgumentException  If a problem occurs while adding the arguments.
547   */
548  @Override()
549  public void addNonLDAPArguments(@NotNull final ArgumentParser parser)
550         throws ArgumentException
551  {
552    String description = "The base DN to use for the searches.  It may be a " +
553         "simple DN or a value pattern to specify a range of DNs (e.g., " +
554         "\"uid=user.[1-1000],ou=People,dc=example,dc=com\").  See " +
555         ValuePattern.PUBLIC_JAVADOC_URL + " for complete details about the " +
556         "value pattern syntax.  This argument must not be used in " +
557         "conjunction with the --ldapURL argument.";
558    baseDN = new StringArgument('b', "baseDN", false, 1, "{dn}", description,
559         "");
560    baseDN.setArgumentGroupName("Search Arguments");
561    baseDN.addLongIdentifier("base-dn", true);
562    parser.addArgument(baseDN);
563
564
565    description = "The scope to use for the searches.  It should be 'base', " +
566         "'one', 'sub', or 'subord'.  If this is not provided, then a " +
567         "default scope of 'sub' will be used.  This argument must not be " +
568         "used in conjunction with the --ldapURL argument.";
569    scope = new ScopeArgument('s', "scope", false, "{scope}", description,
570         SearchScope.SUB);
571    scope.setArgumentGroupName("Search Arguments");
572    parser.addArgument(scope);
573
574
575    description = "The filter to use for the searches.  It may be a simple " +
576         "filter or a value pattern to specify a range of filters (e.g., " +
577         "\"(uid=user.[1-1000])\").  See " + ValuePattern.PUBLIC_JAVADOC_URL +
578         " for complete details about the value pattern syntax.  Exactly one " +
579         "of this argument and the --ldapURL arguments must be provided.";
580    filter = new StringArgument('f', "filter", false, 1, "{filter}",
581         description);
582    filter.setArgumentGroupName("Search Arguments");
583    parser.addArgument(filter);
584
585
586    description = "The name of an attribute to include in entries returned " +
587         "from the searches.  Multiple attributes may be requested by " +
588         "providing this argument multiple times.  If no request attributes " +
589         "are provided, then the entries returned will include all user " +
590         "attributes.  This argument must not be used in conjunction with " +
591         "the --ldapURL argument.";
592    attributes = new StringArgument('A', "attribute", false, 0, "{name}",
593         description);
594    attributes.setArgumentGroupName("Search Arguments");
595    parser.addArgument(attributes);
596
597
598    description = "An LDAP URL that provides the base DN, scope, filter, and " +
599         "requested attributes to use for the search requests (the address " +
600         "and port components of the URL, if present, will be ignored).  It " +
601         "may be a simple LDAP URL or a value pattern to specify a range of " +
602         "URLs.  See " + ValuePattern.PUBLIC_JAVADOC_URL + " for complete " +
603         "details about the value pattern syntax.  If this argument is " +
604         "provided, then none of the --baseDN, --scope, --filter, or " +
605         "--attribute arguments may be used.";
606    ldapURL = new StringArgument(null, "ldapURL", false, 1, "{url}",
607         description);
608    ldapURL.setArgumentGroupName("Search Arguments");
609    ldapURL.addLongIdentifier("ldap-url", true);
610    parser.addArgument(ldapURL);
611
612
613    description = "The maximum number of entries that the server should " +
614         "return in response to each search request.  A value of zero " +
615         "indicates that the client does not wish to impose any limit on " +
616         "the number of entries that are returned (although the server may " +
617         "impose its own limit).  If this is not provided, then a default " +
618         "value of zero will be used.";
619    sizeLimit = new IntegerArgument('z', "sizeLimit", false, 1, "{num}",
620         description, 0, Integer.MAX_VALUE, 0);
621    sizeLimit.setArgumentGroupName("Search Arguments");
622    sizeLimit.addLongIdentifier("size-limit", true);
623    parser.addArgument(sizeLimit);
624
625
626    description = "The maximum length of time, in seconds, that the server " +
627         "should spend processing each search request.  A value of zero " +
628         "indicates that the client does not wish to impose any limit on the " +
629         "server's processing time (although the server may impose its own " +
630         "limit).  If this is not provided, then a default value of zero " +
631         "will be used.";
632    timeLimitSeconds = new IntegerArgument('l', "timeLimitSeconds", false, 1,
633         "{seconds}", description, 0, Integer.MAX_VALUE, 0);
634    timeLimitSeconds.setArgumentGroupName("Search Arguments");
635    timeLimitSeconds.addLongIdentifier("time-limit-seconds", true);
636    timeLimitSeconds.addLongIdentifier("timeLimit", true);
637    timeLimitSeconds.addLongIdentifier("time-limit", true);
638    parser.addArgument(timeLimitSeconds);
639
640
641    final Set<String> derefAllowedValues =
642         StaticUtils.setOf("never", "always", "search", "find");
643    description = "The alias dereferencing policy to use for search " +
644         "requests.  The value should be one of 'never', 'always', 'search', " +
645         "or 'find'.  If this is not provided, then a default value of " +
646         "'never' will be used.";
647    dereferencePolicy = new StringArgument(null, "dereferencePolicy", false, 1,
648         "{never|always|search|find}", description, derefAllowedValues,
649         "never");
650    dereferencePolicy.setArgumentGroupName("Search Arguments");
651    dereferencePolicy.addLongIdentifier("dereference-policy", true);
652    parser.addArgument(dereferencePolicy);
653
654
655    description = "Indicates that server should only include the names of " +
656         "the attributes contained in matching entries rather than both " +
657         "names and values.";
658    typesOnly = new BooleanArgument(null, "typesOnly", 1, description);
659    typesOnly.setArgumentGroupName("Search Arguments");
660    typesOnly.addLongIdentifier("types-only", true);
661    parser.addArgument(typesOnly);
662
663
664    description = "Indicates that search requests should include the " +
665         "assertion request control with the specified filter.";
666    assertionFilter = new FilterArgument(null, "assertionFilter", false, 1,
667         "{filter}", description);
668    assertionFilter.setArgumentGroupName("Request Control Arguments");
669    assertionFilter.addLongIdentifier("assertion-filter", true);
670    parser.addArgument(assertionFilter);
671
672
673    description = "Indicates that search requests should include the simple " +
674         "paged results control with the specified page size.";
675    simplePageSize = new IntegerArgument(null, "simplePageSize", false, 1,
676         "{size}", description, 1, Integer.MAX_VALUE);
677    simplePageSize.setArgumentGroupName("Request Control Arguments");
678    simplePageSize.addLongIdentifier("simple-page-size", true);
679    parser.addArgument(simplePageSize);
680
681
682    description = "Indicates that search requests should include the " +
683         "server-side sort request control with the specified sort order.  " +
684         "This should be a comma-delimited list in which each item is an " +
685         "attribute name, optionally preceded by a plus or minus sign (to " +
686         "indicate ascending or descending order; where ascending order is " +
687         "the default), and optionally followed by a colon and the name or " +
688         "OID of the desired ordering matching rule (if this is not " +
689         "provided, the the attribute type's default ordering rule will be " +
690         "used).";
691    sortOrder = new StringArgument(null, "sortOrder", false, 1, "{sortOrder}",
692         description);
693    sortOrder.setArgumentGroupName("Request Control Arguments");
694    sortOrder.addLongIdentifier("sort-order", true);
695    parser.addArgument(sortOrder);
696
697
698    description = "Indicates that the proxied authorization control (as " +
699         "defined in RFC 4370) should be used to request that operations be " +
700         "processed using an alternate authorization identity.  This may be " +
701         "a simple authorization ID or it may be a value pattern to specify " +
702         "a range of identities.  See " + ValuePattern.PUBLIC_JAVADOC_URL +
703         " for complete details about the value pattern syntax.";
704    proxyAs = new StringArgument('Y', "proxyAs", false, 1, "{authzID}",
705         description);
706    proxyAs.setArgumentGroupName("Request Control Arguments");
707    proxyAs.addLongIdentifier("proxy-as", true);
708    parser.addArgument(proxyAs);
709
710
711    description = "Indicates that search requests should include the " +
712         "specified request control.  This may be provided multiple times to " +
713         "include multiple request controls.";
714    control = new ControlArgument('J', "control", false, 0, null, description);
715    control.setArgumentGroupName("Request Control Arguments");
716    parser.addArgument(control);
717
718
719    description = "The number of threads to use to perform the searches.  If " +
720         "this is not provided, then a default of one thread will be used.";
721    numThreads = new IntegerArgument('t', "numThreads", true, 1, "{num}",
722         description, 1, Integer.MAX_VALUE, 1);
723    numThreads.setArgumentGroupName("Rate Management Arguments");
724    numThreads.addLongIdentifier("num-threads", true);
725    parser.addArgument(numThreads);
726
727
728    description = "The length of time in seconds between output lines.  If " +
729         "this is not provided, then a default interval of five seconds will " +
730         "be used.";
731    collectionInterval = new IntegerArgument('i', "intervalDuration", true, 1,
732         "{num}", description, 1, Integer.MAX_VALUE, 5);
733    collectionInterval.setArgumentGroupName("Rate Management Arguments");
734    collectionInterval.addLongIdentifier("interval-duration", true);
735    parser.addArgument(collectionInterval);
736
737
738    description = "The maximum number of intervals for which to run.  If " +
739         "this is not provided, then the tool will run until it is " +
740         "interrupted.";
741    numIntervals = new IntegerArgument('I', "numIntervals", true, 1, "{num}",
742         description, 1, Integer.MAX_VALUE, Integer.MAX_VALUE);
743    numIntervals.setArgumentGroupName("Rate Management Arguments");
744    numIntervals.addLongIdentifier("num-intervals", true);
745    parser.addArgument(numIntervals);
746
747    description = "The number of search iterations that should be processed " +
748         "on a connection before that connection is closed and replaced with " +
749         "a newly-established (and authenticated, if appropriate) " +
750         "connection.  If this is not provided, then connections will not " +
751         "be periodically closed and re-established.";
752    iterationsBeforeReconnect = new IntegerArgument(null,
753         "iterationsBeforeReconnect", false, 1, "{num}", description, 0);
754    iterationsBeforeReconnect.setArgumentGroupName("Rate Management Arguments");
755    iterationsBeforeReconnect.addLongIdentifier("iterations-before-reconnect",
756         true);
757    parser.addArgument(iterationsBeforeReconnect);
758
759    description = "The target number of searches to perform per second.  It " +
760         "is still necessary to specify a sufficient number of threads for " +
761         "achieving this rate.  If neither this option nor " +
762         "--variableRateData is provided, then the tool will run at the " +
763         "maximum rate for the specified number of threads.";
764    ratePerSecond = new IntegerArgument('r', "ratePerSecond", false, 1,
765         "{searches-per-second}", description, 1, Integer.MAX_VALUE);
766    ratePerSecond.setArgumentGroupName("Rate Management Arguments");
767    ratePerSecond.addLongIdentifier("rate-per-second", true);
768    parser.addArgument(ratePerSecond);
769
770    final String variableRateDataArgName = "variableRateData";
771    final String generateSampleRateFileArgName = "generateSampleRateFile";
772    description = RateAdjustor.getVariableRateDataArgumentDescription(
773         generateSampleRateFileArgName);
774    variableRateData = new FileArgument(null, variableRateDataArgName, false, 1,
775         "{path}", description, true, true, true, false);
776    variableRateData.setArgumentGroupName("Rate Management Arguments");
777    variableRateData.addLongIdentifier("variable-rate-data", true);
778    parser.addArgument(variableRateData);
779
780    description = RateAdjustor.getGenerateSampleVariableRateFileDescription(
781         variableRateDataArgName);
782    sampleRateFile = new FileArgument(null, generateSampleRateFileArgName,
783         false, 1, "{path}", description, false, true, true, false);
784    sampleRateFile.setArgumentGroupName("Rate Management Arguments");
785    sampleRateFile.addLongIdentifier("generate-sample-rate-file", true);
786    sampleRateFile.setUsageArgument(true);
787    parser.addArgument(sampleRateFile);
788    parser.addExclusiveArgumentSet(variableRateData, sampleRateFile);
789
790    description = "The number of intervals to complete before beginning " +
791         "overall statistics collection.  Specifying a nonzero number of " +
792         "warm-up intervals gives the client and server a chance to warm up " +
793         "without skewing performance results.";
794    warmUpIntervals = new IntegerArgument(null, "warmUpIntervals", true, 1,
795         "{num}", description, 0, Integer.MAX_VALUE, 0);
796    warmUpIntervals.setArgumentGroupName("Rate Management Arguments");
797    warmUpIntervals.addLongIdentifier("warm-up-intervals", true);
798    parser.addArgument(warmUpIntervals);
799
800    description = "Indicates the format to use for timestamps included in " +
801         "the output.  A value of 'none' indicates that no timestamps should " +
802         "be included.  A value of 'with-date' indicates that both the date " +
803         "and the time should be included.  A value of 'without-date' " +
804         "indicates that only the time should be included.";
805    final Set<String> allowedFormats =
806         StaticUtils.setOf("none", "with-date", "without-date");
807    timestampFormat = new StringArgument(null, "timestampFormat", true, 1,
808         "{format}", description, allowedFormats, "none");
809    timestampFormat.addLongIdentifier("timestamp-format", true);
810    parser.addArgument(timestampFormat);
811
812    description = "Indicates that the client should operate in asynchronous " +
813         "mode, in which it will not be necessary to wait for a response to " +
814         "a previous request before sending the next request.  Either the " +
815         "'--ratePerSecond' or the '--maxOutstandingRequests' argument must " +
816         "be provided to limit the number of outstanding requests.";
817    asynchronousMode = new BooleanArgument('a', "asynchronous", description);
818    parser.addArgument(asynchronousMode);
819
820    description = "Specifies the maximum number of outstanding requests " +
821         "that should be allowed when operating in asynchronous mode.";
822    maxOutstandingRequests = new IntegerArgument('O', "maxOutstandingRequests",
823         false, 1, "{num}", description, 1, Integer.MAX_VALUE, (Integer) null);
824    maxOutstandingRequests.addLongIdentifier("max-outstanding-requests", true);
825    parser.addArgument(maxOutstandingRequests);
826
827    description = "Indicates that information about the result codes for " +
828         "failed operations should not be displayed.";
829    suppressErrors = new BooleanArgument(null,
830         "suppressErrorResultCodes", 1, description);
831    suppressErrors.addLongIdentifier("suppress-error-result-codes", true);
832    parser.addArgument(suppressErrors);
833
834    description = "Generate output in CSV format rather than a " +
835         "display-friendly format";
836    csvFormat = new BooleanArgument('c', "csv", 1, description);
837    parser.addArgument(csvFormat);
838
839    description = "Specifies the seed to use for the random number generator.";
840    randomSeed = new IntegerArgument('R', "randomSeed", false, 1, "{value}",
841         description);
842    randomSeed.addLongIdentifier("random-seed", true);
843    parser.addArgument(randomSeed);
844
845
846    parser.addExclusiveArgumentSet(baseDN, ldapURL);
847    parser.addExclusiveArgumentSet(scope, ldapURL);
848    parser.addExclusiveArgumentSet(filter, ldapURL);
849    parser.addExclusiveArgumentSet(attributes, ldapURL);
850
851    parser.addRequiredArgumentSet(filter, ldapURL);
852
853    parser.addDependentArgumentSet(asynchronousMode, ratePerSecond,
854         maxOutstandingRequests);
855    parser.addDependentArgumentSet(maxOutstandingRequests, asynchronousMode);
856
857    parser.addExclusiveArgumentSet(asynchronousMode, simplePageSize);
858  }
859
860
861
862  /**
863   * Indicates whether this tool supports creating connections to multiple
864   * servers.  If it is to support multiple servers, then the "--hostname" and
865   * "--port" arguments will be allowed to be provided multiple times, and
866   * will be required to be provided the same number of times.  The same type of
867   * communication security and bind credentials will be used for all servers.
868   *
869   * @return  {@code true} if this tool supports creating connections to
870   *          multiple servers, or {@code false} if not.
871   */
872  @Override()
873  protected boolean supportsMultipleServers()
874  {
875    return true;
876  }
877
878
879
880  /**
881   * Retrieves the connection options that should be used for connections
882   * created for use with this tool.
883   *
884   * @return  The connection options that should be used for connections created
885   *          for use with this tool.
886   */
887  @Override()
888  @NotNull()
889  public LDAPConnectionOptions getConnectionOptions()
890  {
891    final LDAPConnectionOptions options = new LDAPConnectionOptions();
892    options.setUseSynchronousMode(! asynchronousMode.isPresent());
893    return options;
894  }
895
896
897
898  /**
899   * Performs the actual processing for this tool.  In this case, it gets a
900   * connection to the directory server and uses it to perform the requested
901   * searches.
902   *
903   * @return  The result code for the processing that was performed.
904   */
905  @Override()
906  @NotNull()
907  public ResultCode doToolProcessing()
908  {
909    // If the sample rate file argument was specified, then generate the sample
910    // variable rate data file and return.
911    if (sampleRateFile.isPresent())
912    {
913      try
914      {
915        RateAdjustor.writeSampleVariableRateFile(sampleRateFile.getValue());
916        return ResultCode.SUCCESS;
917      }
918      catch (final Exception e)
919      {
920        Debug.debugException(e);
921        err("An error occurred while trying to write sample variable data " +
922             "rate file '", sampleRateFile.getValue().getAbsolutePath(),
923             "':  ", StaticUtils.getExceptionMessage(e));
924        return ResultCode.LOCAL_ERROR;
925      }
926    }
927
928
929    // Determine the random seed to use.
930    final Long seed;
931    if (randomSeed.isPresent())
932    {
933      seed = Long.valueOf(randomSeed.getValue());
934    }
935    else
936    {
937      seed = null;
938    }
939
940    // Create value patterns for the base DN, filter, LDAP URL, and proxied
941    // authorization DN.
942    final ValuePattern dnPattern;
943    try
944    {
945      if (baseDN.getNumOccurrences() > 0)
946      {
947        dnPattern = new ValuePattern(baseDN.getValue(), seed);
948      }
949      else if (ldapURL.isPresent())
950      {
951        dnPattern = null;
952      }
953      else
954      {
955        dnPattern = new ValuePattern("", seed);
956      }
957    }
958    catch (final ParseException pe)
959    {
960      Debug.debugException(pe);
961      err("Unable to parse the base DN value pattern:  ", pe.getMessage());
962      return ResultCode.PARAM_ERROR;
963    }
964
965    final ValuePattern filterPattern;
966    try
967    {
968      if (filter.isPresent())
969      {
970        filterPattern = new ValuePattern(filter.getValue(), seed);
971      }
972      else
973      {
974        filterPattern = null;
975      }
976    }
977    catch (final ParseException pe)
978    {
979      Debug.debugException(pe);
980      err("Unable to parse the filter pattern:  ", pe.getMessage());
981      return ResultCode.PARAM_ERROR;
982    }
983
984    final ValuePattern ldapURLPattern;
985    try
986    {
987      if (ldapURL.isPresent())
988      {
989        ldapURLPattern = new ValuePattern(ldapURL.getValue(), seed);
990      }
991      else
992      {
993        ldapURLPattern = null;
994      }
995    }
996    catch (final ParseException pe)
997    {
998      Debug.debugException(pe);
999      err("Unable to parse the LDAP URL pattern:  ", pe.getMessage());
1000      return ResultCode.PARAM_ERROR;
1001    }
1002
1003    final ValuePattern authzIDPattern;
1004    if (proxyAs.isPresent())
1005    {
1006      try
1007      {
1008        authzIDPattern = new ValuePattern(proxyAs.getValue(), seed);
1009      }
1010      catch (final ParseException pe)
1011      {
1012        Debug.debugException(pe);
1013        err("Unable to parse the proxied authorization pattern:  ",
1014            pe.getMessage());
1015        return ResultCode.PARAM_ERROR;
1016      }
1017    }
1018    else
1019    {
1020      authzIDPattern = null;
1021    }
1022
1023
1024    // Get the alias dereference policy to use.
1025    final DereferencePolicy derefPolicy;
1026    final String derefValue =
1027         StaticUtils.toLowerCase(dereferencePolicy.getValue());
1028    if (derefValue.equals("always"))
1029    {
1030      derefPolicy = DereferencePolicy.ALWAYS;
1031    }
1032    else if (derefValue.equals("search"))
1033    {
1034      derefPolicy = DereferencePolicy.SEARCHING;
1035    }
1036    else if (derefValue.equals("find"))
1037    {
1038      derefPolicy = DereferencePolicy.FINDING;
1039    }
1040    else
1041    {
1042      derefPolicy = DereferencePolicy.NEVER;
1043    }
1044
1045
1046    // Get the set of controls to include in search requests.
1047    final ArrayList<Control> controlList = new ArrayList<>(5);
1048    if (assertionFilter.isPresent())
1049    {
1050      controlList.add(new AssertionRequestControl(assertionFilter.getValue()));
1051    }
1052
1053    if (sortOrder.isPresent())
1054    {
1055      final ArrayList<SortKey> sortKeys = new ArrayList<>(5);
1056      final StringTokenizer tokenizer =
1057           new StringTokenizer(sortOrder.getValue(), ",");
1058      while (tokenizer.hasMoreTokens())
1059      {
1060        String token = tokenizer.nextToken().trim();
1061
1062        final boolean ascending;
1063        if (token.startsWith("+"))
1064        {
1065          ascending = true;
1066          token = token.substring(1);
1067        }
1068        else if (token.startsWith("-"))
1069        {
1070          ascending = false;
1071          token = token.substring(1);
1072        }
1073        else
1074        {
1075          ascending = true;
1076        }
1077
1078        final String attributeName;
1079        final String matchingRuleID;
1080        final int colonPos = token.indexOf(':');
1081        if (colonPos < 0)
1082        {
1083          attributeName = token;
1084          matchingRuleID = null;
1085        }
1086        else
1087        {
1088          attributeName = token.substring(0, colonPos);
1089          matchingRuleID = token.substring(colonPos+1);
1090        }
1091
1092        sortKeys.add(new SortKey(attributeName, matchingRuleID, (! ascending)));
1093      }
1094
1095      controlList.add(new ServerSideSortRequestControl(sortKeys));
1096    }
1097
1098    if (control.isPresent())
1099    {
1100      controlList.addAll(control.getValues());
1101    }
1102
1103
1104    // Get the attributes to return.
1105    final String[] attrs;
1106    if (attributes.isPresent())
1107    {
1108      final List<String> attrList = attributes.getValues();
1109      attrs = new String[attrList.size()];
1110      attrList.toArray(attrs);
1111    }
1112    else
1113    {
1114      attrs = StaticUtils.NO_STRINGS;
1115    }
1116
1117
1118    // If the --ratePerSecond option was specified, then limit the rate
1119    // accordingly.
1120    FixedRateBarrier fixedRateBarrier = null;
1121    if (ratePerSecond.isPresent() || variableRateData.isPresent())
1122    {
1123      // We might not have a rate per second if --variableRateData is specified.
1124      // The rate typically doesn't matter except when we have warm-up
1125      // intervals.  In this case, we'll run at the max rate.
1126      final int intervalSeconds = collectionInterval.getValue();
1127      final int ratePerInterval =
1128           (ratePerSecond.getValue() == null)
1129           ? Integer.MAX_VALUE
1130           : ratePerSecond.getValue() * intervalSeconds;
1131      fixedRateBarrier =
1132           new FixedRateBarrier(1000L * intervalSeconds, ratePerInterval);
1133    }
1134
1135
1136    // If --variableRateData was specified, then initialize a RateAdjustor.
1137    RateAdjustor rateAdjustor = null;
1138    if (variableRateData.isPresent())
1139    {
1140      try
1141      {
1142        rateAdjustor = RateAdjustor.newInstance(fixedRateBarrier,
1143             ratePerSecond.getValue(), variableRateData.getValue());
1144      }
1145      catch (final IOException | IllegalArgumentException e)
1146      {
1147        Debug.debugException(e);
1148        err("Initializing the variable rates failed: " + e.getMessage());
1149        return ResultCode.PARAM_ERROR;
1150      }
1151    }
1152
1153
1154    // If the --maxOutstandingRequests option was specified, then create the
1155    // semaphore used to enforce that limit.
1156    final Semaphore asyncSemaphore;
1157    if (maxOutstandingRequests.isPresent())
1158    {
1159      asyncSemaphore = new Semaphore(maxOutstandingRequests.getValue());
1160    }
1161    else
1162    {
1163      asyncSemaphore = null;
1164    }
1165
1166
1167    // Determine whether to include timestamps in the output and if so what
1168    // format should be used for them.
1169    final boolean includeTimestamp;
1170    final String timeFormat;
1171    if (timestampFormat.getValue().equalsIgnoreCase("with-date"))
1172    {
1173      includeTimestamp = true;
1174      timeFormat       = "dd/MM/yyyy HH:mm:ss";
1175    }
1176    else if (timestampFormat.getValue().equalsIgnoreCase("without-date"))
1177    {
1178      includeTimestamp = true;
1179      timeFormat       = "HH:mm:ss";
1180    }
1181    else
1182    {
1183      includeTimestamp = false;
1184      timeFormat       = null;
1185    }
1186
1187
1188    // Determine whether any warm-up intervals should be run.
1189    final long totalIntervals;
1190    final boolean warmUp;
1191    int remainingWarmUpIntervals = warmUpIntervals.getValue();
1192    if (remainingWarmUpIntervals > 0)
1193    {
1194      warmUp = true;
1195      totalIntervals = 0L + numIntervals.getValue() + remainingWarmUpIntervals;
1196    }
1197    else
1198    {
1199      warmUp = true;
1200      totalIntervals = 0L + numIntervals.getValue();
1201    }
1202
1203
1204    // Create the table that will be used to format the output.
1205    final OutputFormat outputFormat;
1206    if (csvFormat.isPresent())
1207    {
1208      outputFormat = OutputFormat.CSV;
1209    }
1210    else
1211    {
1212      outputFormat = OutputFormat.COLUMNS;
1213    }
1214
1215    final ColumnFormatter formatter = new ColumnFormatter(includeTimestamp,
1216         timeFormat, outputFormat, " ",
1217         new FormattableColumn(12, HorizontalAlignment.RIGHT, "Recent",
1218                  "Searches/Sec"),
1219         new FormattableColumn(12, HorizontalAlignment.RIGHT, "Recent",
1220                  "Avg Dur ms"),
1221         new FormattableColumn(12, HorizontalAlignment.RIGHT, "Recent",
1222                  "Entries/Srch"),
1223         new FormattableColumn(12, HorizontalAlignment.RIGHT, "Recent",
1224                  "Errors/Sec"),
1225         new FormattableColumn(12, HorizontalAlignment.RIGHT, "Overall",
1226                  "Searches/Sec"),
1227         new FormattableColumn(12, HorizontalAlignment.RIGHT, "Overall",
1228                  "Avg Dur ms"));
1229
1230
1231    // Create values to use for statistics collection.
1232    final AtomicLong        searchCounter   = new AtomicLong(0L);
1233    final AtomicLong        entryCounter    = new AtomicLong(0L);
1234    final AtomicLong        errorCounter    = new AtomicLong(0L);
1235    final AtomicLong        searchDurations = new AtomicLong(0L);
1236    final ResultCodeCounter rcCounter       = new ResultCodeCounter();
1237
1238
1239    // Determine the length of each interval in milliseconds.
1240    final long intervalMillis = 1000L * collectionInterval.getValue();
1241
1242
1243    // Create the threads to use for the searches.
1244    final CyclicBarrier barrier = new CyclicBarrier(numThreads.getValue() + 1);
1245    final SearchRateThread[] threads =
1246         new SearchRateThread[numThreads.getValue()];
1247    for (int i=0; i < threads.length; i++)
1248    {
1249      final LDAPConnection connection;
1250      try
1251      {
1252        connection = getConnection();
1253      }
1254      catch (final LDAPException le)
1255      {
1256        Debug.debugException(le);
1257        err("Unable to connect to the directory server:  ",
1258            StaticUtils.getExceptionMessage(le));
1259        return le.getResultCode();
1260      }
1261
1262      threads[i] = new SearchRateThread(this, i, connection,
1263           asynchronousMode.isPresent(), dnPattern, scope.getValue(),
1264           derefPolicy, sizeLimit.getValue(), timeLimitSeconds.getValue(),
1265           typesOnly.isPresent(), filterPattern, attrs, ldapURLPattern,
1266           authzIDPattern, simplePageSize.getValue(), controlList,
1267           iterationsBeforeReconnect.getValue(), runningThreads, barrier,
1268           searchCounter, entryCounter, searchDurations, errorCounter,
1269           rcCounter, fixedRateBarrier, asyncSemaphore);
1270      threads[i].start();
1271    }
1272
1273
1274    // Display the table header.
1275    for (final String headerLine : formatter.getHeaderLines(true))
1276    {
1277      out(headerLine);
1278    }
1279
1280
1281    // Start the RateAdjustor before the threads so that the initial value is
1282    // in place before any load is generated unless we're doing a warm-up in
1283    // which case, we'll start it after the warm-up is complete.
1284    if ((rateAdjustor != null) && (remainingWarmUpIntervals <= 0))
1285    {
1286      rateAdjustor.start();
1287    }
1288
1289
1290    // Indicate that the threads can start running.
1291    try
1292    {
1293      barrier.await();
1294    }
1295    catch (final Exception e)
1296    {
1297      Debug.debugException(e);
1298    }
1299
1300    long overallStartTime = System.nanoTime();
1301    long nextIntervalStartTime = System.currentTimeMillis() + intervalMillis;
1302
1303
1304    boolean setOverallStartTime = false;
1305    long    lastDuration        = 0L;
1306    long    lastNumEntries      = 0L;
1307    long    lastNumErrors       = 0L;
1308    long    lastNumSearches     = 0L;
1309    long    lastEndTime         = System.nanoTime();
1310    for (long i=0; i < totalIntervals; i++)
1311    {
1312      if (rateAdjustor != null)
1313      {
1314        if (! rateAdjustor.isAlive())
1315        {
1316          out("All of the rates in " + variableRateData.getValue().getName() +
1317              " have been completed.");
1318          break;
1319        }
1320      }
1321
1322      final long startTimeMillis = System.currentTimeMillis();
1323      final long sleepTimeMillis = nextIntervalStartTime - startTimeMillis;
1324      nextIntervalStartTime += intervalMillis;
1325      if (sleepTimeMillis > 0)
1326      {
1327        sleeper.sleep(sleepTimeMillis);
1328      }
1329
1330      if (stopRequested.get())
1331      {
1332        break;
1333      }
1334
1335      final long endTime          = System.nanoTime();
1336      final long intervalDuration = endTime - lastEndTime;
1337
1338      final long numSearches;
1339      final long numEntries;
1340      final long numErrors;
1341      final long totalDuration;
1342      if (warmUp && (remainingWarmUpIntervals > 0))
1343      {
1344        numSearches   = searchCounter.getAndSet(0L);
1345        numEntries    = entryCounter.getAndSet(0L);
1346        numErrors     = errorCounter.getAndSet(0L);
1347        totalDuration = searchDurations.getAndSet(0L);
1348      }
1349      else
1350      {
1351        numSearches   = searchCounter.get();
1352        numEntries    = entryCounter.get();
1353        numErrors     = errorCounter.get();
1354        totalDuration = searchDurations.get();
1355      }
1356
1357      final long recentNumSearches = numSearches - lastNumSearches;
1358      final long recentNumEntries = numEntries - lastNumEntries;
1359      final long recentNumErrors = numErrors - lastNumErrors;
1360      final long recentDuration = totalDuration - lastDuration;
1361
1362      final double numSeconds = intervalDuration / 1_000_000_000.0d;
1363      final double recentSearchRate = recentNumSearches / numSeconds;
1364      final double recentErrorRate  = recentNumErrors / numSeconds;
1365
1366      final double recentAvgDuration;
1367      final double recentEntriesPerSearch;
1368      if (recentNumSearches > 0L)
1369      {
1370        recentEntriesPerSearch = 1.0d * recentNumEntries / recentNumSearches;
1371        recentAvgDuration =
1372             1.0d * recentDuration / recentNumSearches / 1_000_000;
1373      }
1374      else
1375      {
1376        recentEntriesPerSearch = 0.0d;
1377        recentAvgDuration = 0.0d;
1378      }
1379
1380
1381      if (warmUp && (remainingWarmUpIntervals > 0))
1382      {
1383        out(formatter.formatRow(recentSearchRate, recentAvgDuration,
1384             recentEntriesPerSearch, recentErrorRate, "warming up",
1385             "warming up"));
1386
1387        remainingWarmUpIntervals--;
1388        if (remainingWarmUpIntervals == 0)
1389        {
1390          out("Warm-up completed.  Beginning overall statistics collection.");
1391          setOverallStartTime = true;
1392          if (rateAdjustor != null)
1393          {
1394            rateAdjustor.start();
1395          }
1396        }
1397      }
1398      else
1399      {
1400        if (setOverallStartTime)
1401        {
1402          overallStartTime    = lastEndTime;
1403          setOverallStartTime = false;
1404        }
1405
1406        final double numOverallSeconds =
1407             (endTime - overallStartTime) / 1_000_000_000.0d;
1408        final double overallSearchRate = numSearches / numOverallSeconds;
1409
1410        final double overallAvgDuration;
1411        if (numSearches > 0L)
1412        {
1413          overallAvgDuration = 1.0d * totalDuration / numSearches / 1_000_000;
1414        }
1415        else
1416        {
1417          overallAvgDuration = 0.0d;
1418        }
1419
1420        out(formatter.formatRow(recentSearchRate, recentAvgDuration,
1421             recentEntriesPerSearch, recentErrorRate, overallSearchRate,
1422             overallAvgDuration));
1423
1424        lastNumSearches = numSearches;
1425        lastNumEntries  = numEntries;
1426        lastNumErrors   = numErrors;
1427        lastDuration    = totalDuration;
1428      }
1429
1430      final List<ObjectPair<ResultCode,Long>> rcCounts =
1431           rcCounter.getCounts(true);
1432      if ((! suppressErrors.isPresent()) && (! rcCounts.isEmpty()))
1433      {
1434        err("\tError Results:");
1435        for (final ObjectPair<ResultCode,Long> p : rcCounts)
1436        {
1437          err("\t", p.getFirst().getName(), ":  ", p.getSecond());
1438        }
1439      }
1440
1441      lastEndTime = endTime;
1442    }
1443
1444
1445    // Shut down the RateAdjustor if we have one.
1446    if (rateAdjustor != null)
1447    {
1448      rateAdjustor.shutDown();
1449    }
1450
1451
1452    // Stop all of the threads.
1453    ResultCode resultCode = ResultCode.SUCCESS;
1454    for (final SearchRateThread t : threads)
1455    {
1456      t.signalShutdown();
1457    }
1458    for (final SearchRateThread t : threads)
1459    {
1460      final ResultCode r = t.waitForShutdown();
1461      if (resultCode == ResultCode.SUCCESS)
1462      {
1463        resultCode = r;
1464      }
1465    }
1466
1467    return resultCode;
1468  }
1469
1470
1471
1472  /**
1473   * Requests that this tool stop running.  This method will attempt to wait
1474   * for all threads to complete before returning control to the caller.
1475   */
1476  public void stopRunning()
1477  {
1478    stopRequested.set(true);
1479    sleeper.wakeup();
1480
1481    while (true)
1482    {
1483      final int stillRunning = runningThreads.get();
1484      if (stillRunning <= 0)
1485      {
1486        break;
1487      }
1488      else
1489      {
1490        try
1491        {
1492          Thread.sleep(1L);
1493        } catch (final Exception e) {}
1494      }
1495    }
1496  }
1497
1498
1499
1500  /**
1501   * Retrieves the maximum number of outstanding requests that may be in
1502   * progress at any time, if appropriate.
1503   *
1504   * @return  The maximum number of outstanding requests that may be in progress
1505   *          at any time, or -1 if the tool was not configured to perform
1506   *          asynchronous searches with a maximum number of outstanding
1507   *          requests.
1508   */
1509  int getMaxOutstandingRequests()
1510  {
1511    if (maxOutstandingRequests.isPresent())
1512    {
1513      return maxOutstandingRequests.getValue();
1514    }
1515    else
1516    {
1517      return -1;
1518    }
1519  }
1520
1521
1522
1523  /**
1524   * {@inheritDoc}
1525   */
1526  @Override()
1527  @NotNull()
1528  public LinkedHashMap<String[],String> getExampleUsages()
1529  {
1530    final LinkedHashMap<String[],String> examples =
1531         new LinkedHashMap<>(StaticUtils.computeMapCapacity(2));
1532
1533    String[] args =
1534    {
1535      "--hostname", "server.example.com",
1536      "--port", "389",
1537      "--bindDN", "uid=admin,dc=example,dc=com",
1538      "--bindPassword", "password",
1539      "--baseDN", "dc=example,dc=com",
1540      "--scope", "sub",
1541      "--filter", "(uid=user.[1-1000000])",
1542      "--attribute", "givenName",
1543      "--attribute", "sn",
1544      "--attribute", "mail",
1545      "--numThreads", "10"
1546    };
1547    String description =
1548         "Test search performance by searching randomly across a set " +
1549         "of one million users located below 'dc=example,dc=com' with ten " +
1550         "concurrent threads.  The entries returned to the client will " +
1551         "include the givenName, sn, and mail attributes.";
1552    examples.put(args, description);
1553
1554    args = new String[]
1555    {
1556      "--generateSampleRateFile", "variable-rate-data.txt"
1557    };
1558    description =
1559         "Generate a sample variable rate definition file that may be used " +
1560         "in conjunction with the --variableRateData argument.  The sample " +
1561         "file will include comments that describe the format for data to be " +
1562         "included in this file.";
1563    examples.put(args, description);
1564
1565    return examples;
1566  }
1567}