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