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