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