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.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 Set<String> allowedFormats =
798         StaticUtils.setOf("none", "with-date", "without-date");
799    timestampFormat = new StringArgument(null, "timestampFormat", true, 1,
800         "{format}", description, allowedFormats, "none");
801    timestampFormat.addLongIdentifier("timestamp-format", true);
802    parser.addArgument(timestampFormat);
803
804    description = "Indicates that information about the result codes for " +
805                  "failed operations should not be displayed.";
806    suppressErrors = new BooleanArgument(null,
807         "suppressErrorResultCodes", 1, description);
808    suppressErrors.addLongIdentifier("suppress-error-result-codes", true);
809    parser.addArgument(suppressErrors);
810
811    description = "Generate output in CSV format rather than a " +
812                  "display-friendly format";
813    csvFormat = new BooleanArgument('c', "csv", 1, description);
814    parser.addArgument(csvFormat);
815
816    description = "Specifies the seed to use for the random number generator.";
817    randomSeed = new IntegerArgument('R', "randomSeed", false, 1, "{value}",
818         description);
819    randomSeed.addLongIdentifier("random-seed", true);
820    parser.addArgument(randomSeed);
821  }
822
823
824
825  /**
826   * Indicates whether this tool supports creating connections to multiple
827   * servers.  If it is to support multiple servers, then the "--hostname" and
828   * "--port" arguments will be allowed to be provided multiple times, and
829   * will be required to be provided the same number of times.  The same type of
830   * communication security and bind credentials will be used for all servers.
831   *
832   * @return  {@code true} if this tool supports creating connections to
833   *          multiple servers, or {@code false} if not.
834   */
835  @Override()
836  protected boolean supportsMultipleServers()
837  {
838    return true;
839  }
840
841
842
843  /**
844   * Retrieves the connection options that should be used for connections
845   * created for use with this tool.
846   *
847   * @return  The connection options that should be used for connections created
848   *          for use with this tool.
849   */
850  @Override()
851  public LDAPConnectionOptions getConnectionOptions()
852  {
853    final LDAPConnectionOptions options = new LDAPConnectionOptions();
854    options.setUseSynchronousMode(true);
855    return options;
856  }
857
858
859
860  /**
861   * Performs the actual processing for this tool.  In this case, it gets a
862   * connection to the directory server and uses it to perform the requested
863   * searches.
864   *
865   * @return  The result code for the processing that was performed.
866   */
867  @Override()
868  public ResultCode doToolProcessing()
869  {
870    runningThread = Thread.currentThread();
871
872    try
873    {
874      return doToolProcessingInternal();
875    }
876    finally
877    {
878      runningThread = null;
879    }
880  }
881
882
883
884  /**
885   * Performs the actual processing for this tool.  In this case, it gets a
886   * connection to the directory server and uses it to perform the requested
887   * searches.
888   *
889   * @return  The result code for the processing that was performed.
890   */
891  private ResultCode doToolProcessingInternal()
892  {
893    // If the sample rate file argument was specified, then generate the sample
894    // variable rate data file and return.
895    if (sampleRateFile.isPresent())
896    {
897      try
898      {
899        RateAdjustor.writeSampleVariableRateFile(sampleRateFile.getValue());
900        return ResultCode.SUCCESS;
901      }
902      catch (final Exception e)
903      {
904        Debug.debugException(e);
905        err("An error occurred while trying to write sample variable data " +
906             "rate file '", sampleRateFile.getValue().getAbsolutePath(),
907             "':  ", StaticUtils.getExceptionMessage(e));
908        return ResultCode.LOCAL_ERROR;
909      }
910    }
911
912
913    // Determine the random seed to use.
914    final Long seed;
915    if (randomSeed.isPresent())
916    {
917      seed = Long.valueOf(randomSeed.getValue());
918    }
919    else
920    {
921      seed = null;
922    }
923
924    // Create value patterns for the base DN, filter, and proxied authorization
925    // DN.
926    final ValuePattern dnPattern;
927    try
928    {
929      dnPattern = new ValuePattern(baseDN.getValue(), seed);
930    }
931    catch (final ParseException pe)
932    {
933      Debug.debugException(pe);
934      err("Unable to parse the base DN value pattern:  ", pe.getMessage());
935      return ResultCode.PARAM_ERROR;
936    }
937
938    final ValuePattern filterPattern;
939    try
940    {
941      filterPattern = new ValuePattern(filter.getValue(), seed);
942    }
943    catch (final ParseException pe)
944    {
945      Debug.debugException(pe);
946      err("Unable to parse the filter pattern:  ", pe.getMessage());
947      return ResultCode.PARAM_ERROR;
948    }
949
950    final ValuePattern authzIDPattern;
951    if (proxyAs.isPresent())
952    {
953      try
954      {
955        authzIDPattern = new ValuePattern(proxyAs.getValue(), seed);
956      }
957      catch (final ParseException pe)
958      {
959        Debug.debugException(pe);
960        err("Unable to parse the proxied authorization pattern:  ",
961            pe.getMessage());
962        return ResultCode.PARAM_ERROR;
963      }
964    }
965    else
966    {
967      authzIDPattern = null;
968    }
969
970
971    // Get the set of controls to include in search requests.
972    final ArrayList<Control> searchControls = new ArrayList<>(5);
973    if (searchAssertionFilter.isPresent())
974    {
975      searchControls.add(new AssertionRequestControl(
976           searchAssertionFilter.getValue()));
977    }
978
979    if (searchControl.isPresent())
980    {
981      searchControls.addAll(searchControl.getValues());
982    }
983
984
985    // Get the set of controls to include in modify requests.
986    final ArrayList<Control> modifyControls = new ArrayList<>(5);
987    if (modifyAssertionFilter.isPresent())
988    {
989      modifyControls.add(new AssertionRequestControl(
990           modifyAssertionFilter.getValue()));
991    }
992
993    if (permissiveModify.isPresent())
994    {
995      modifyControls.add(new PermissiveModifyRequestControl());
996    }
997
998    if (preReadAttribute.isPresent())
999    {
1000      final List<String> attrList = preReadAttribute.getValues();
1001      final String[] attrArray = new String[attrList.size()];
1002      attrList.toArray(attrArray);
1003      modifyControls.add(new PreReadRequestControl(attrArray));
1004    }
1005
1006    if (postReadAttribute.isPresent())
1007    {
1008      final List<String> attrList = postReadAttribute.getValues();
1009      final String[] attrArray = new String[attrList.size()];
1010      attrList.toArray(attrArray);
1011      modifyControls.add(new PostReadRequestControl(attrArray));
1012    }
1013
1014    if (modifyControl.isPresent())
1015    {
1016      modifyControls.addAll(modifyControl.getValues());
1017    }
1018
1019
1020    // Get the attributes to return.
1021    final String[] returnAttrs;
1022    if (returnAttributes.isPresent())
1023    {
1024      final List<String> attrList = returnAttributes.getValues();
1025      returnAttrs = new String[attrList.size()];
1026      attrList.toArray(returnAttrs);
1027    }
1028    else
1029    {
1030      returnAttrs = StaticUtils.NO_STRINGS;
1031    }
1032
1033
1034    // Get the names of the attributes to modify.
1035    final String[] modAttrs = new String[modifyAttributes.getValues().size()];
1036    modifyAttributes.getValues().toArray(modAttrs);
1037
1038
1039    // Get the character set as a byte array.
1040    final byte[] charSet = StaticUtils.getBytes(characterSet.getValue());
1041
1042
1043    // If the --ratePerSecond option was specified, then limit the rate
1044    // accordingly.
1045    FixedRateBarrier fixedRateBarrier = null;
1046    if (ratePerSecond.isPresent() || variableRateData.isPresent())
1047    {
1048      // We might not have a rate per second if --variableRateData is specified.
1049      // The rate typically doesn't matter except when we have warm-up
1050      // intervals.  In this case, we'll run at the max rate.
1051      final int intervalSeconds = collectionInterval.getValue();
1052      final int ratePerInterval =
1053           (ratePerSecond.getValue() == null)
1054           ? Integer.MAX_VALUE
1055           : ratePerSecond.getValue() * intervalSeconds;
1056      fixedRateBarrier =
1057           new FixedRateBarrier(1000L * intervalSeconds, ratePerInterval);
1058    }
1059
1060
1061    // If --variableRateData was specified, then initialize a RateAdjustor.
1062    RateAdjustor rateAdjustor = null;
1063    if (variableRateData.isPresent())
1064    {
1065      try
1066      {
1067        rateAdjustor = RateAdjustor.newInstance(fixedRateBarrier,
1068             ratePerSecond.getValue(), variableRateData.getValue());
1069      }
1070      catch (final IOException | IllegalArgumentException e)
1071      {
1072        Debug.debugException(e);
1073        err("Initializing the variable rates failed: " + e.getMessage());
1074        return ResultCode.PARAM_ERROR;
1075      }
1076    }
1077
1078
1079    // Determine whether to include timestamps in the output and if so what
1080    // format should be used for them.
1081    final boolean includeTimestamp;
1082    final String timeFormat;
1083    if (timestampFormat.getValue().equalsIgnoreCase("with-date"))
1084    {
1085      includeTimestamp = true;
1086      timeFormat       = "dd/MM/yyyy HH:mm:ss";
1087    }
1088    else if (timestampFormat.getValue().equalsIgnoreCase("without-date"))
1089    {
1090      includeTimestamp = true;
1091      timeFormat       = "HH:mm:ss";
1092    }
1093    else
1094    {
1095      includeTimestamp = false;
1096      timeFormat       = null;
1097    }
1098
1099
1100    // Determine whether any warm-up intervals should be run.
1101    final long totalIntervals;
1102    final boolean warmUp;
1103    int remainingWarmUpIntervals = warmUpIntervals.getValue();
1104    if (remainingWarmUpIntervals > 0)
1105    {
1106      warmUp = true;
1107      totalIntervals = 0L + numIntervals.getValue() + remainingWarmUpIntervals;
1108    }
1109    else
1110    {
1111      warmUp = true;
1112      totalIntervals = 0L + numIntervals.getValue();
1113    }
1114
1115
1116    // Create the table that will be used to format the output.
1117    final OutputFormat outputFormat;
1118    if (csvFormat.isPresent())
1119    {
1120      outputFormat = OutputFormat.CSV;
1121    }
1122    else
1123    {
1124      outputFormat = OutputFormat.COLUMNS;
1125    }
1126
1127    final ColumnFormatter formatter = new ColumnFormatter(includeTimestamp,
1128         timeFormat, outputFormat, " ",
1129         new FormattableColumn(12, HorizontalAlignment.RIGHT, "Recent",
1130                  "Searches/Sec"),
1131         new FormattableColumn(12, HorizontalAlignment.RIGHT, "Recent",
1132                  "Srch Dur ms"),
1133         new FormattableColumn(12, HorizontalAlignment.RIGHT, "Recent",
1134                  "Mods/Sec"),
1135         new FormattableColumn(12, HorizontalAlignment.RIGHT, "Recent",
1136                  "Mod Dur ms"),
1137         new FormattableColumn(12, HorizontalAlignment.RIGHT, "Recent",
1138                  "Errors/Sec"),
1139         new FormattableColumn(12, HorizontalAlignment.RIGHT, "Overall",
1140                  "Searches/Sec"),
1141         new FormattableColumn(12, HorizontalAlignment.RIGHT, "Overall",
1142                  "Srch Dur ms"),
1143         new FormattableColumn(12, HorizontalAlignment.RIGHT, "Overall",
1144                  "Mods/Sec"),
1145         new FormattableColumn(12, HorizontalAlignment.RIGHT, "Overall",
1146                  "Mod Dur ms"));
1147
1148
1149    // Create values to use for statistics collection.
1150    final AtomicLong        searchCounter   = new AtomicLong(0L);
1151    final AtomicLong        errorCounter    = new AtomicLong(0L);
1152    final AtomicLong        modCounter      = new AtomicLong(0L);
1153    final AtomicLong        modDurations    = new AtomicLong(0L);
1154    final AtomicLong        searchDurations = new AtomicLong(0L);
1155    final ResultCodeCounter rcCounter       = new ResultCodeCounter();
1156
1157
1158    // Determine the length of each interval in milliseconds.
1159    final long intervalMillis = 1000L * collectionInterval.getValue();
1160
1161
1162    // Create the threads to use for the searches.
1163    final Random random = new Random();
1164    final CyclicBarrier barrier = new CyclicBarrier(numThreads.getValue() + 1);
1165    final SearchAndModRateThread[] threads =
1166         new SearchAndModRateThread[numThreads.getValue()];
1167    for (int i=0; i < threads.length; i++)
1168    {
1169      final LDAPConnection connection;
1170      try
1171      {
1172        connection = getConnection();
1173      }
1174      catch (final LDAPException le)
1175      {
1176        Debug.debugException(le);
1177        err("Unable to connect to the directory server:  ",
1178            StaticUtils.getExceptionMessage(le));
1179        return le.getResultCode();
1180      }
1181
1182      threads[i] = new SearchAndModRateThread(this, i, connection, dnPattern,
1183           scopeArg.getValue(), filterPattern, returnAttrs, modAttrs,
1184           valueLength.getValue(), charSet, authzIDPattern,
1185           simplePageSize.getValue(), searchControls, modifyControls,
1186           iterationsBeforeReconnect.getValue(), random.nextLong(), barrier,
1187           searchCounter, modCounter, searchDurations, modDurations,
1188           errorCounter, rcCounter, fixedRateBarrier);
1189      threads[i].start();
1190    }
1191
1192
1193    // Display the table header.
1194    for (final String headerLine : formatter.getHeaderLines(true))
1195    {
1196      out(headerLine);
1197    }
1198
1199
1200    // Start the RateAdjustor before the threads so that the initial value is
1201    // in place before any load is generated unless we're doing a warm-up in
1202    // which case, we'll start it after the warm-up is complete.
1203    if ((rateAdjustor != null) && (remainingWarmUpIntervals <= 0))
1204    {
1205      rateAdjustor.start();
1206    }
1207
1208
1209    // Indicate that the threads can start running.
1210    try
1211    {
1212      barrier.await();
1213    }
1214    catch (final Exception e)
1215    {
1216      Debug.debugException(e);
1217    }
1218
1219    long overallStartTime = System.nanoTime();
1220    long nextIntervalStartTime = System.currentTimeMillis() + intervalMillis;
1221
1222
1223    boolean setOverallStartTime = false;
1224    long    lastSearchDuration  = 0L;
1225    long    lastModDuration     = 0L;
1226    long    lastNumErrors       = 0L;
1227    long    lastNumSearches     = 0L;
1228    long    lastNumMods          = 0L;
1229    long    lastEndTime         = System.nanoTime();
1230    for (long i=0; i < totalIntervals; i++)
1231    {
1232      if (rateAdjustor != null)
1233      {
1234        if (! rateAdjustor.isAlive())
1235        {
1236          out("All of the rates in " + variableRateData.getValue().getName() +
1237              " have been completed.");
1238          break;
1239        }
1240      }
1241
1242      final long startTimeMillis = System.currentTimeMillis();
1243      final long sleepTimeMillis = nextIntervalStartTime - startTimeMillis;
1244      nextIntervalStartTime += intervalMillis;
1245      if (sleepTimeMillis > 0)
1246      {
1247        sleeper.sleep(sleepTimeMillis);
1248      }
1249
1250      if (stopRequested.get())
1251      {
1252        break;
1253      }
1254
1255      final long endTime          = System.nanoTime();
1256      final long intervalDuration = endTime - lastEndTime;
1257
1258      final long numSearches;
1259      final long numMods;
1260      final long numErrors;
1261      final long totalSearchDuration;
1262      final long totalModDuration;
1263      if (warmUp && (remainingWarmUpIntervals > 0))
1264      {
1265        numSearches         = searchCounter.getAndSet(0L);
1266        numMods             = modCounter.getAndSet(0L);
1267        numErrors           = errorCounter.getAndSet(0L);
1268        totalSearchDuration = searchDurations.getAndSet(0L);
1269        totalModDuration    = modDurations.getAndSet(0L);
1270      }
1271      else
1272      {
1273        numSearches         = searchCounter.get();
1274        numMods             = modCounter.get();
1275        numErrors           = errorCounter.get();
1276        totalSearchDuration = searchDurations.get();
1277        totalModDuration    = modDurations.get();
1278      }
1279
1280      final long recentNumSearches = numSearches - lastNumSearches;
1281      final long recentNumMods = numMods - lastNumMods;
1282      final long recentNumErrors = numErrors - lastNumErrors;
1283      final long recentSearchDuration =
1284           totalSearchDuration - lastSearchDuration;
1285      final long recentModDuration = totalModDuration - lastModDuration;
1286
1287      final double numSeconds = intervalDuration / 1_000_000_000.0d;
1288      final double recentSearchRate = recentNumSearches / numSeconds;
1289      final double recentModRate = recentNumMods / numSeconds;
1290      final double recentErrorRate  = recentNumErrors / numSeconds;
1291
1292      final double recentAvgSearchDuration;
1293      if (recentNumSearches > 0L)
1294      {
1295        recentAvgSearchDuration =
1296             1.0d * recentSearchDuration / recentNumSearches / 1_000_000;
1297      }
1298      else
1299      {
1300        recentAvgSearchDuration = 0.0d;
1301      }
1302
1303      final double recentAvgModDuration;
1304      if (recentNumMods > 0L)
1305      {
1306        recentAvgModDuration =
1307             1.0d * recentModDuration / recentNumMods / 1_000_000;
1308      }
1309      else
1310      {
1311        recentAvgModDuration = 0.0d;
1312      }
1313
1314      if (warmUp && (remainingWarmUpIntervals > 0))
1315      {
1316        out(formatter.formatRow(recentSearchRate, recentAvgSearchDuration,
1317             recentModRate, recentAvgModDuration, recentErrorRate, "warming up",
1318             "warming up", "warming up", "warming up"));
1319
1320        remainingWarmUpIntervals--;
1321        if (remainingWarmUpIntervals == 0)
1322        {
1323          out("Warm-up completed.  Beginning overall statistics collection.");
1324          setOverallStartTime = true;
1325          if (rateAdjustor != null)
1326          {
1327            rateAdjustor.start();
1328          }
1329        }
1330      }
1331      else
1332      {
1333        if (setOverallStartTime)
1334        {
1335          overallStartTime    = lastEndTime;
1336          setOverallStartTime = false;
1337        }
1338
1339        final double numOverallSeconds =
1340             (endTime - overallStartTime) / 1_000_000_000.0d;
1341        final double overallSearchRate = numSearches / numOverallSeconds;
1342        final double overallModRate = numMods / numOverallSeconds;
1343
1344        final double overallAvgSearchDuration;
1345        if (numSearches > 0L)
1346        {
1347          overallAvgSearchDuration =
1348               1.0d * totalSearchDuration / numSearches / 1_000_000;
1349        }
1350        else
1351        {
1352          overallAvgSearchDuration = 0.0d;
1353        }
1354
1355        final double overallAvgModDuration;
1356        if (numMods > 0L)
1357        {
1358          overallAvgModDuration =
1359               1.0d * totalModDuration / numMods / 1_000_000;
1360        }
1361        else
1362        {
1363          overallAvgModDuration = 0.0d;
1364        }
1365
1366        out(formatter.formatRow(recentSearchRate, recentAvgSearchDuration,
1367             recentModRate, recentAvgModDuration, recentErrorRate,
1368             overallSearchRate, overallAvgSearchDuration, overallModRate,
1369             overallAvgModDuration));
1370
1371        lastNumSearches    = numSearches;
1372        lastNumMods        = numMods;
1373        lastNumErrors      = numErrors;
1374        lastSearchDuration = totalSearchDuration;
1375        lastModDuration    = totalModDuration;
1376      }
1377
1378      final List<ObjectPair<ResultCode,Long>> rcCounts =
1379           rcCounter.getCounts(true);
1380      if ((! suppressErrors.isPresent()) && (! rcCounts.isEmpty()))
1381      {
1382        err("\tError Results:");
1383        for (final ObjectPair<ResultCode,Long> p : rcCounts)
1384        {
1385          err("\t", p.getFirst().getName(), ":  ", p.getSecond());
1386        }
1387      }
1388
1389      lastEndTime = endTime;
1390    }
1391
1392
1393    // Shut down the RateAdjustor if we have one.
1394    if (rateAdjustor != null)
1395    {
1396      rateAdjustor.shutDown();
1397    }
1398
1399    // Stop all of the threads.
1400    ResultCode resultCode = ResultCode.SUCCESS;
1401    for (final SearchAndModRateThread t : threads)
1402    {
1403      final ResultCode r = t.stopRunning();
1404      if (resultCode == ResultCode.SUCCESS)
1405      {
1406        resultCode = r;
1407      }
1408    }
1409
1410    return resultCode;
1411  }
1412
1413
1414
1415  /**
1416   * Requests that this tool stop running.  This method will attempt to wait
1417   * for all threads to complete before returning control to the caller.
1418   */
1419  public void stopRunning()
1420  {
1421    stopRequested.set(true);
1422    sleeper.wakeup();
1423
1424    final Thread t = runningThread;
1425    if (t != null)
1426    {
1427      try
1428      {
1429        t.join();
1430      }
1431      catch (final Exception e)
1432      {
1433        Debug.debugException(e);
1434
1435        if (e instanceof InterruptedException)
1436        {
1437          Thread.currentThread().interrupt();
1438        }
1439      }
1440    }
1441  }
1442
1443
1444
1445  /**
1446   * {@inheritDoc}
1447   */
1448  @Override()
1449  public LinkedHashMap<String[],String> getExampleUsages()
1450  {
1451    final LinkedHashMap<String[],String> examples =
1452         new LinkedHashMap<>(StaticUtils.computeMapCapacity(2));
1453
1454    String[] args =
1455    {
1456      "--hostname", "server.example.com",
1457      "--port", "389",
1458      "--bindDN", "uid=admin,dc=example,dc=com",
1459      "--bindPassword", "password",
1460      "--baseDN", "dc=example,dc=com",
1461      "--scope", "sub",
1462      "--filter", "(uid=user.[1-1000000])",
1463      "--attribute", "givenName",
1464      "--attribute", "sn",
1465      "--attribute", "mail",
1466      "--modifyAttribute", "description",
1467      "--valueLength", "10",
1468      "--characterSet", "abcdefghijklmnopqrstuvwxyz0123456789",
1469      "--numThreads", "10"
1470    };
1471    String description =
1472         "Test search and modify performance by searching randomly across a " +
1473         "set of one million users located below 'dc=example,dc=com' with " +
1474         "ten concurrent threads.  The entries returned to the client will " +
1475         "include the givenName, sn, and mail attributes, and the " +
1476         "description attribute of each entry returned will be replaced " +
1477         "with a string of ten randomly-selected alphanumeric characters.";
1478    examples.put(args, description);
1479
1480    args = new String[]
1481    {
1482      "--generateSampleRateFile", "variable-rate-data.txt"
1483    };
1484    description =
1485         "Generate a sample variable rate definition file that may be used " +
1486         "in conjunction with the --variableRateData argument.  The sample " +
1487         "file will include comments that describe the format for data to be " +
1488         "included in this file.";
1489    examples.put(args, description);
1490
1491    return examples;
1492  }
1493}