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