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