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