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