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