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