001    /*
002     * Copyright 2008-2013 UnboundID Corp.
003     * All Rights Reserved.
004     */
005    /*
006     * Copyright (C) 2008-2013 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.OutputStream;
026    import java.io.Serializable;
027    import java.text.ParseException;
028    import java.util.LinkedHashMap;
029    import java.util.LinkedHashSet;
030    import java.util.List;
031    import java.util.concurrent.CyclicBarrier;
032    import java.util.concurrent.Semaphore;
033    import java.util.concurrent.atomic.AtomicLong;
034    
035    import com.unboundid.ldap.sdk.LDAPConnection;
036    import com.unboundid.ldap.sdk.LDAPConnectionOptions;
037    import com.unboundid.ldap.sdk.LDAPException;
038    import com.unboundid.ldap.sdk.ResultCode;
039    import com.unboundid.ldap.sdk.SearchScope;
040    import com.unboundid.ldap.sdk.Version;
041    import com.unboundid.util.ColumnFormatter;
042    import com.unboundid.util.FixedRateBarrier;
043    import com.unboundid.util.FormattableColumn;
044    import com.unboundid.util.HorizontalAlignment;
045    import com.unboundid.util.LDAPCommandLineTool;
046    import com.unboundid.util.ObjectPair;
047    import com.unboundid.util.OutputFormat;
048    import com.unboundid.util.ResultCodeCounter;
049    import com.unboundid.util.ThreadSafety;
050    import com.unboundid.util.ThreadSafetyLevel;
051    import com.unboundid.util.ValuePattern;
052    import com.unboundid.util.args.ArgumentException;
053    import com.unboundid.util.args.ArgumentParser;
054    import com.unboundid.util.args.BooleanArgument;
055    import com.unboundid.util.args.IntegerArgument;
056    import com.unboundid.util.args.ScopeArgument;
057    import com.unboundid.util.args.StringArgument;
058    
059    import static com.unboundid.util.StaticUtils.*;
060    
061    
062    
063    /**
064     * This class provides a tool that can be used to search an LDAP directory
065     * server repeatedly using multiple threads.  It can help provide an estimate of
066     * the search performance that a directory server is able to achieve.  Either or
067     * both of the base DN and the search filter may be a value pattern as
068     * described in the {@link ValuePattern} class.  This makes it possible to
069     * search over a range of entries rather than repeatedly performing searches
070     * with the same base DN and filter.
071     * <BR><BR>
072     * Some of the APIs demonstrated by this example include:
073     * <UL>
074     *   <LI>Argument Parsing (from the {@code com.unboundid.util.args}
075     *       package)</LI>
076     *   <LI>LDAP Command-Line Tool (from the {@code com.unboundid.util}
077     *       package)</LI>
078     *   <LI>LDAP Communication (from the {@code com.unboundid.ldap.sdk}
079     *       package)</LI>
080     *   <LI>Value Patterns (from the {@code com.unboundid.util} package)</LI>
081     * </UL>
082     * <BR><BR>
083     * All of the necessary information is provided using command line arguments.
084     * Supported arguments include those allowed by the {@link LDAPCommandLineTool}
085     * class, as well as the following additional arguments:
086     * <UL>
087     *   <LI>"-b {baseDN}" or "--baseDN {baseDN}" -- specifies the base DN to use
088     *       for the searches.  This must be provided.  It may be a simple DN, or it
089     *       may be a value pattern to express a range of base DNs.</LI>
090     *   <LI>"-s {scope}" or "--scope {scope}" -- specifies the scope to use for the
091     *       search.  The scope value should be one of "base", "one", "sub", or
092     *       "subord".  If this isn't specified, then a scope of "sub" will be
093     *       used.</LI>
094     *   <LI>"-f {filter}" or "--filter {filter}" -- specifies the filter to use for
095     *       the searches.  This must be provided.  It may be a simple filter, or it
096     *       may be a value pattern to express a range of filters.</LI>
097     *   <LI>"-A {name}" or "--attribute {name}" -- specifies the name of an
098     *       attribute that should be included in entries returned from the server.
099     *       If this is not provided, then all user attributes will be requested.
100     *       This may include special tokens that the server may interpret, like
101     *       "1.1" to indicate that no attributes should be returned, "*", for all
102     *       user attributes, or "+" for all operational attributes.  Multiple
103     *       attributes may be requested with multiple instances of this
104     *       argument.</LI>
105     *   <LI>"-t {num}" or "--numThreads {num}" -- specifies the number of
106     *       concurrent threads to use when performing the searches.  If this is not
107     *       provided, then a default of one thread will be used.</LI>
108     *   <LI>"-i {sec}" or "--intervalDuration {sec}" -- specifies the length of
109     *       time in seconds between lines out output.  If this is not provided,
110     *       then a default interval duration of five seconds will be used.</LI>
111     *   <LI>"-I {num}" or "--numIntervals {num}" -- specifies the maximum number of
112     *       intervals for which to run.  If this is not provided, then it will
113     *       run forever.</LI>
114     *   <LI>"--iterationsBeforeReconnect {num}" -- specifies the number of search
115     *       iterations that should be performed on a connection before that
116     *       connection is closed and replaced with a newly-established (and
117     *       authenticated, if appropriate) connection.</LI>
118     *   <LI>"-r {searches-per-second}" or "--ratePerSecond {searches-per-second}"
119     *       -- specifies the target number of searches to perform per second.  It
120     *       is still necessary to specify a sufficient number of threads for
121     *       achieving this rate.  If this option is not provided, then the tool
122     *       will run at the maximum rate for the specified number of threads.</LI>
123     *   <LI>"--warmUpIntervals {num}" -- specifies the number of intervals to
124     *       complete before beginning overall statistics collection.</LI>
125     *   <LI>"--timestampFormat {format}" -- specifies the format to use for
126     *       timestamps included before each output line.  The format may be one of
127     *       "none" (for no timestamps), "with-date" (to include both the date and
128     *       the time), or "without-date" (to include only time time).</LI>
129     *   <LI>"-Y {authzID}" or "--proxyAs {authzID}" -- Use the proxied
130     *       authorization v2 control to request that the operation be processed
131     *       using an alternate authorization identity.  In this case, the bind DN
132     *       should be that of a user that has permission to use this control.  The
133     *       authorization identity may be a value pattern.</LI>
134     *   <LI>"-a" or "--asynchronous" -- Indicates that searches should be performed
135     *       in asynchronous mode, in which the client will not wait for a response
136     *       to a previous request before sending the next request.  Either the
137     *       "--ratePerSecond" or "--maxOutstandingRequests" arguments must be
138     *       provided to limit the number of outstanding requests.</LI>
139     *   <LI>"-O {num}" or "--maxOutstandingRequests {num}" -- Specifies the maximum
140     *       number of outstanding requests that will be allowed in asynchronous
141     *       mode.</LI>
142     *   <LI>"--suppressErrorResultCodes" -- Indicates that information about the
143     *       result codes for failed operations should not be displayed.</LI>
144     *   <LI>"-c" or "--csv" -- Generate output in CSV format rather than a
145     *       display-friendly format.</LI>
146     * </UL>
147     */
148    @ThreadSafety(level=ThreadSafetyLevel.NOT_THREADSAFE)
149    public final class SearchRate
150           extends LDAPCommandLineTool
151           implements Serializable
152    {
153      /**
154       * The serial version UID for this serializable class.
155       */
156      private static final long serialVersionUID = 3345838530404592182L;
157    
158    
159    
160      // The argument used to indicate whether to operate in asynchronous mode.
161      private BooleanArgument asynchronousMode;
162    
163      // The argument used to indicate whether to generate output in CSV format.
164      private BooleanArgument csvFormat;
165    
166      // The argument used to indicate whether to suppress information about error
167      // result codes.
168      private BooleanArgument suppressErrors;
169    
170      // The argument used to specify the collection interval.
171      private IntegerArgument collectionInterval;
172    
173      // The argument used to specify the number of search iterations on a
174      // connection before it is closed and re-established.
175      private IntegerArgument iterationsBeforeReconnect;
176    
177      // The argument used to specify the maximum number of outstanding asynchronous
178      // requests.
179      private IntegerArgument maxOutstandingRequests;
180    
181      // The argument used to specify the number of intervals.
182      private IntegerArgument numIntervals;
183    
184      // The argument used to specify the number of threads.
185      private IntegerArgument numThreads;
186    
187      // The argument used to specify the seed to use for the random number
188      // generator.
189      private IntegerArgument randomSeed;
190    
191      // The target rate of searches per second.
192      private IntegerArgument ratePerSecond;
193    
194      // The number of warm-up intervals to perform.
195      private IntegerArgument warmUpIntervals;
196    
197      // The argument used to specify the scope for the searches.
198      private ScopeArgument scopeArg;
199    
200      // The argument used to specify the attributes to return.
201      private StringArgument attributes;
202    
203      // The argument used to specify the base DNs for the searches.
204      private StringArgument baseDN;
205    
206      // The argument used to specify the filters for the searches.
207      private StringArgument filter;
208    
209      // The argument used to specify the proxied authorization identity.
210      private StringArgument proxyAs;
211    
212      // The argument used to specify the timestamp format.
213      private StringArgument timestampFormat;
214    
215    
216    
217      /**
218       * Parse the provided command line arguments and make the appropriate set of
219       * changes.
220       *
221       * @param  args  The command line arguments provided to this program.
222       */
223      public static void main(final String[] args)
224      {
225        final ResultCode resultCode = main(args, System.out, System.err);
226        if (resultCode != ResultCode.SUCCESS)
227        {
228          System.exit(resultCode.intValue());
229        }
230      }
231    
232    
233    
234      /**
235       * Parse the provided command line arguments and make the appropriate set of
236       * changes.
237       *
238       * @param  args       The command line arguments provided to this program.
239       * @param  outStream  The output stream to which standard out should be
240       *                    written.  It may be {@code null} if output should be
241       *                    suppressed.
242       * @param  errStream  The output stream to which standard error should be
243       *                    written.  It may be {@code null} if error messages
244       *                    should be suppressed.
245       *
246       * @return  A result code indicating whether the processing was successful.
247       */
248      public static ResultCode main(final String[] args,
249                                    final OutputStream outStream,
250                                    final OutputStream errStream)
251      {
252        final SearchRate searchRate = new SearchRate(outStream, errStream);
253        return searchRate.runTool(args);
254      }
255    
256    
257    
258      /**
259       * Creates a new instance of this tool.
260       *
261       * @param  outStream  The output stream to which standard out should be
262       *                    written.  It may be {@code null} if output should be
263       *                    suppressed.
264       * @param  errStream  The output stream to which standard error should be
265       *                    written.  It may be {@code null} if error messages
266       *                    should be suppressed.
267       */
268      public SearchRate(final OutputStream outStream, final OutputStream errStream)
269      {
270        super(outStream, errStream);
271      }
272    
273    
274    
275      /**
276       * Retrieves the name for this tool.
277       *
278       * @return  The name for this tool.
279       */
280      @Override()
281      public String getToolName()
282      {
283        return "searchrate";
284      }
285    
286    
287    
288      /**
289       * Retrieves the description for this tool.
290       *
291       * @return  The description for this tool.
292       */
293      @Override()
294      public String getToolDescription()
295      {
296        return "Perform repeated searches against an " +
297               "LDAP directory server.";
298      }
299    
300    
301    
302      /**
303       * Retrieves the version string for this tool.
304       *
305       * @return  The version string for this tool.
306       */
307      @Override()
308      public String getToolVersion()
309      {
310        return Version.NUMERIC_VERSION_STRING;
311      }
312    
313    
314    
315      /**
316       * Adds the arguments used by this program that aren't already provided by the
317       * generic {@code LDAPCommandLineTool} framework.
318       *
319       * @param  parser  The argument parser to which the arguments should be added.
320       *
321       * @throws  ArgumentException  If a problem occurs while adding the arguments.
322       */
323      @Override()
324      public void addNonLDAPArguments(final ArgumentParser parser)
325             throws ArgumentException
326      {
327        String description = "The base DN to use for the searches.  It may be a " +
328             "simple DN or a value pattern to specify a range of DNs (e.g., " +
329             "\"uid=user.[1-1000],ou=People,dc=example,dc=com\").  This must be " +
330             "provided.";
331        baseDN = new StringArgument('b', "baseDN", true, 1, "{dn}", description);
332        parser.addArgument(baseDN);
333    
334    
335        description = "The scope to use for the searches.  It should be 'base', " +
336                      "'one', 'sub', or 'subord'.  If this is not provided, then " +
337                      "a default scope of 'sub' will be used.";
338        scopeArg = new ScopeArgument('s', "scope", false, "{scope}", description,
339                                     SearchScope.SUB);
340        parser.addArgument(scopeArg);
341    
342    
343        description = "The filter to use for the searches.  It may be a simple " +
344                      "filter or a value pattern to specify a range of filters " +
345                      "(e.g., \"(uid=user.[1-1000])\").  This must be provided.";
346        filter = new StringArgument('f', "filter", true, 1, "{filter}",
347                                    description);
348        parser.addArgument(filter);
349    
350    
351        description = "The name of an attribute to include in entries returned " +
352                      "from the searches.  Multiple attributes may be requested " +
353                      "by providing this argument multiple times.  If no request " +
354                      "attributes are provided, then the entries returned will " +
355                      "include all user attributes.";
356        attributes = new StringArgument('A', "attribute", false, 0, "{name}",
357                                        description);
358        parser.addArgument(attributes);
359    
360    
361        description = "The number of threads to use to perform the searches.  If " +
362                      "this is not provided, then a default of one thread will " +
363                      "be used.";
364        numThreads = new IntegerArgument('t', "numThreads", true, 1, "{num}",
365                                         description, 1, Integer.MAX_VALUE, 1);
366        parser.addArgument(numThreads);
367    
368    
369        description = "The length of time in seconds between output lines.  If " +
370                      "this is not provided, then a default interval of five " +
371                      "seconds will be used.";
372        collectionInterval = new IntegerArgument('i', "intervalDuration", true, 1,
373                                                 "{num}", description, 1,
374                                                 Integer.MAX_VALUE, 5);
375        parser.addArgument(collectionInterval);
376    
377    
378        description = "The maximum number of intervals for which to run.  If " +
379                      "this is not provided, then the tool will run until it is " +
380                      "interrupted.";
381        numIntervals = new IntegerArgument('I', "numIntervals", true, 1, "{num}",
382                                           description, 1, Integer.MAX_VALUE,
383                                           Integer.MAX_VALUE);
384        parser.addArgument(numIntervals);
385    
386        description = "The number of search iterations that should be processed " +
387                      "on a connection before that connection is closed and " +
388                      "replaced with a newly-established (and authenticated, if " +
389                      "appropriate) connection.  If this is not provided, then " +
390                      "connections will not be periodically closed and " +
391                      "re-established.";
392        iterationsBeforeReconnect = new IntegerArgument(null,
393             "iterationsBeforeReconnect", false, 1, "{num}", description, 0);
394        parser.addArgument(iterationsBeforeReconnect);
395    
396        description = "The target number of searches to perform per second.  It " +
397                      "is still necessary to specify a sufficient number of " +
398                      "threads for achieving this rate.  If this option is not " +
399                      "provided, then the tool will run at the maximum rate for " +
400                      "the specified number of threads.";
401        ratePerSecond = new IntegerArgument('r', "ratePerSecond", false, 1,
402                                            "{searches-per-second}", description,
403                                            1, Integer.MAX_VALUE);
404        parser.addArgument(ratePerSecond);
405    
406        description = "The number of intervals to complete before beginning " +
407                      "overall statistics collection.  Specifying a nonzero " +
408                      "number of warm-up intervals gives the client and server " +
409                      "a chance to warm up without skewing performance results.";
410        warmUpIntervals = new IntegerArgument(null, "warmUpIntervals", true, 1,
411             "{num}", description, 0, Integer.MAX_VALUE, 0);
412        parser.addArgument(warmUpIntervals);
413    
414        description = "Indicates the format to use for timestamps included in " +
415                      "the output.  A value of 'none' indicates that no " +
416                      "timestamps should be included.  A value of 'with-date' " +
417                      "indicates that both the date and the time should be " +
418                      "included.  A value of 'without-date' indicates that only " +
419                      "the time should be included.";
420        final LinkedHashSet<String> allowedFormats = new LinkedHashSet<String>(3);
421        allowedFormats.add("none");
422        allowedFormats.add("with-date");
423        allowedFormats.add("without-date");
424        timestampFormat = new StringArgument(null, "timestampFormat", true, 1,
425             "{format}", description, allowedFormats, "none");
426        parser.addArgument(timestampFormat);
427    
428        description = "Indicates that the proxied authorization control (as " +
429                      "defined in RFC 4370) should be used to request that " +
430                      "operations be processed using an alternate authorization " +
431                      "identity.";
432        proxyAs = new StringArgument('Y', "proxyAs", false, 1, "{authzID}",
433                                     description);
434        parser.addArgument(proxyAs);
435    
436        description = "Indicates that the client should operate in asynchronous " +
437                      "mode, in which it will not be necessary to wait for a " +
438                      "response to a previous request before sending the next " +
439                      "request.  Either the '--ratePerSecond' or the " +
440                      "'--maxOutstandingRequests' argument must be provided to " +
441                      "limit the number of outstanding requests.";
442        asynchronousMode = new BooleanArgument('a', "asynchronous", description);
443        parser.addArgument(asynchronousMode);
444    
445        description = "Specifies the maximum number of outstanding requests " +
446                      "that should be allowed when operating in asynchronous mode.";
447        maxOutstandingRequests = new IntegerArgument('O', "maxOutstandingRequests",
448             false, 1, "{num}", description, 1, Integer.MAX_VALUE, (Integer) null);
449        parser.addArgument(maxOutstandingRequests);
450    
451        description = "Indicates that information about the result codes for " +
452                      "failed operations should not be displayed.";
453        suppressErrors = new BooleanArgument(null,
454             "suppressErrorResultCodes", 1, description);
455        parser.addArgument(suppressErrors);
456    
457        description = "Generate output in CSV format rather than a " +
458                      "display-friendly format";
459        csvFormat = new BooleanArgument('c', "csv", 1, description);
460        parser.addArgument(csvFormat);
461    
462        description = "Specifies the seed to use for the random number generator.";
463        randomSeed = new IntegerArgument('R', "randomSeed", false, 1, "{value}",
464             description);
465        parser.addArgument(randomSeed);
466    
467    
468        parser.addDependentArgumentSet(asynchronousMode, ratePerSecond,
469             maxOutstandingRequests);
470        parser.addDependentArgumentSet(maxOutstandingRequests, asynchronousMode);
471      }
472    
473    
474    
475      /**
476       * Indicates whether this tool supports creating connections to multiple
477       * servers.  If it is to support multiple servers, then the "--hostname" and
478       * "--port" arguments will be allowed to be provided multiple times, and
479       * will be required to be provided the same number of times.  The same type of
480       * communication security and bind credentials will be used for all servers.
481       *
482       * @return  {@code true} if this tool supports creating connections to
483       *          multiple servers, or {@code false} if not.
484       */
485      @Override()
486      protected boolean supportsMultipleServers()
487      {
488        return true;
489      }
490    
491    
492    
493      /**
494       * Retrieves the connection options that should be used for connections
495       * created for use with this tool.
496       *
497       * @return  The connection options that should be used for connections created
498       *          for use with this tool.
499       */
500      @Override()
501      public LDAPConnectionOptions getConnectionOptions()
502      {
503        final LDAPConnectionOptions options = new LDAPConnectionOptions();
504        options.setAutoReconnect(true);
505        options.setUseSynchronousMode(! asynchronousMode.isPresent());
506        return options;
507      }
508    
509    
510    
511      /**
512       * Performs the actual processing for this tool.  In this case, it gets a
513       * connection to the directory server and uses it to perform the requested
514       * searches.
515       *
516       * @return  The result code for the processing that was performed.
517       */
518      @Override()
519      public ResultCode doToolProcessing()
520      {
521        // Determine the random seed to use.
522        final Long seed;
523        if (randomSeed.isPresent())
524        {
525          seed = Long.valueOf(randomSeed.getValue());
526        }
527        else
528        {
529          seed = null;
530        }
531    
532        // Create value patterns for the base DN, filter, and proxied authorization
533        // DN.
534        final ValuePattern dnPattern;
535        try
536        {
537          dnPattern = new ValuePattern(baseDN.getValue(), seed);
538        }
539        catch (ParseException pe)
540        {
541          err("Unable to parse the base DN value pattern:  ", pe.getMessage());
542          return ResultCode.PARAM_ERROR;
543        }
544    
545        final ValuePattern filterPattern;
546        try
547        {
548          filterPattern = new ValuePattern(filter.getValue(), seed);
549        }
550        catch (ParseException pe)
551        {
552          err("Unable to parse the filter pattern:  ", pe.getMessage());
553          return ResultCode.PARAM_ERROR;
554        }
555    
556        final ValuePattern authzIDPattern;
557        if (proxyAs.isPresent())
558        {
559          try
560          {
561            authzIDPattern = new ValuePattern(proxyAs.getValue(), seed);
562          }
563          catch (ParseException pe)
564          {
565            err("Unable to parse the proxied authorization pattern:  ",
566                pe.getMessage());
567            return ResultCode.PARAM_ERROR;
568          }
569        }
570        else
571        {
572          authzIDPattern = null;
573        }
574    
575    
576        // Get the attributes to return.
577        final String[] attrs;
578        if (attributes.isPresent())
579        {
580          final List<String> attrList = attributes.getValues();
581          attrs = new String[attrList.size()];
582          attrList.toArray(attrs);
583        }
584        else
585        {
586          attrs = NO_STRINGS;
587        }
588    
589    
590        // If the --ratePerSecond option was specified, then limit the rate
591        // accordingly.
592        FixedRateBarrier fixedRateBarrier = null;
593        if (ratePerSecond.isPresent())
594        {
595          final int intervalSeconds = collectionInterval.getValue();
596          final int ratePerInterval = ratePerSecond.getValue() * intervalSeconds;
597    
598          fixedRateBarrier =
599               new FixedRateBarrier(1000L * intervalSeconds, ratePerInterval);
600        }
601    
602    
603        // If the --maxOutstandingRequests option was specified, then create the
604        // semaphore used to enforce that limit.
605        final Semaphore asyncSemaphore;
606        if (maxOutstandingRequests.isPresent())
607        {
608          asyncSemaphore = new Semaphore(maxOutstandingRequests.getValue());
609        }
610        else
611        {
612          asyncSemaphore = null;
613        }
614    
615    
616        // Determine whether to include timestamps in the output and if so what
617        // format should be used for them.
618        final boolean includeTimestamp;
619        final String timeFormat;
620        if (timestampFormat.getValue().equalsIgnoreCase("with-date"))
621        {
622          includeTimestamp = true;
623          timeFormat       = "dd/MM/yyyy HH:mm:ss";
624        }
625        else if (timestampFormat.getValue().equalsIgnoreCase("without-date"))
626        {
627          includeTimestamp = true;
628          timeFormat       = "HH:mm:ss";
629        }
630        else
631        {
632          includeTimestamp = false;
633          timeFormat       = null;
634        }
635    
636    
637        // Determine whether any warm-up intervals should be run.
638        final long totalIntervals;
639        final boolean warmUp;
640        int remainingWarmUpIntervals = warmUpIntervals.getValue();
641        if (remainingWarmUpIntervals > 0)
642        {
643          warmUp = true;
644          totalIntervals = 0L + numIntervals.getValue() + remainingWarmUpIntervals;
645        }
646        else
647        {
648          warmUp = true;
649          totalIntervals = 0L + numIntervals.getValue();
650        }
651    
652    
653        // Create the table that will be used to format the output.
654        final OutputFormat outputFormat;
655        if (csvFormat.isPresent())
656        {
657          outputFormat = OutputFormat.CSV;
658        }
659        else
660        {
661          outputFormat = OutputFormat.COLUMNS;
662        }
663    
664        final ColumnFormatter formatter = new ColumnFormatter(includeTimestamp,
665             timeFormat, outputFormat, " ",
666             new FormattableColumn(12, HorizontalAlignment.RIGHT, "Recent",
667                      "Searches/Sec"),
668             new FormattableColumn(12, HorizontalAlignment.RIGHT, "Recent",
669                      "Avg Dur ms"),
670             new FormattableColumn(12, HorizontalAlignment.RIGHT, "Recent",
671                      "Entries/Srch"),
672             new FormattableColumn(12, HorizontalAlignment.RIGHT, "Recent",
673                      "Errors/Sec"),
674             new FormattableColumn(12, HorizontalAlignment.RIGHT, "Overall",
675                      "Searches/Sec"),
676             new FormattableColumn(12, HorizontalAlignment.RIGHT, "Overall",
677                      "Avg Dur ms"));
678    
679    
680        // Create values to use for statistics collection.
681        final AtomicLong        searchCounter   = new AtomicLong(0L);
682        final AtomicLong        entryCounter    = new AtomicLong(0L);
683        final AtomicLong        errorCounter    = new AtomicLong(0L);
684        final AtomicLong        searchDurations = new AtomicLong(0L);
685        final ResultCodeCounter rcCounter       = new ResultCodeCounter();
686    
687    
688        // Determine the length of each interval in milliseconds.
689        final long intervalMillis = 1000L * collectionInterval.getValue();
690    
691    
692        // Create the threads to use for the searches.
693        final CyclicBarrier barrier = new CyclicBarrier(numThreads.getValue() + 1);
694        final SearchRateThread[] threads =
695             new SearchRateThread[numThreads.getValue()];
696        for (int i=0; i < threads.length; i++)
697        {
698          final LDAPConnection connection;
699          try
700          {
701            connection = getConnection();
702          }
703          catch (LDAPException le)
704          {
705            err("Unable to connect to the directory server:  ",
706                getExceptionMessage(le));
707            return le.getResultCode();
708          }
709    
710          threads[i] = new SearchRateThread(this, i, connection,
711               asynchronousMode.isPresent(), dnPattern, scopeArg.getValue(),
712               filterPattern, attrs, authzIDPattern,
713               iterationsBeforeReconnect.getValue(), barrier, searchCounter,
714               entryCounter, searchDurations, errorCounter, rcCounter,
715               fixedRateBarrier, asyncSemaphore);
716          threads[i].start();
717        }
718    
719    
720        // Display the table header.
721        for (final String headerLine : formatter.getHeaderLines(true))
722        {
723          out(headerLine);
724        }
725    
726    
727        // Indicate that the threads can start running.
728        try
729        {
730          barrier.await();
731        } catch (Exception e) {}
732        long overallStartTime = System.nanoTime();
733        long nextIntervalStartTime = System.currentTimeMillis() + intervalMillis;
734    
735    
736        boolean setOverallStartTime = false;
737        long    lastDuration        = 0L;
738        long    lastNumEntries      = 0L;
739        long    lastNumErrors       = 0L;
740        long    lastNumSearches     = 0L;
741        long    lastEndTime         = System.nanoTime();
742        for (long i=0; i < totalIntervals; i++)
743        {
744          final long startTimeMillis = System.currentTimeMillis();
745          final long sleepTimeMillis = nextIntervalStartTime - startTimeMillis;
746          nextIntervalStartTime += intervalMillis;
747          try
748          {
749            if (sleepTimeMillis > 0)
750            {
751              Thread.sleep(sleepTimeMillis);
752            }
753          } catch (Exception e) {}
754    
755          final long endTime          = System.nanoTime();
756          final long intervalDuration = endTime - lastEndTime;
757    
758          final long numSearches;
759          final long numEntries;
760          final long numErrors;
761          final long totalDuration;
762          if (warmUp && (remainingWarmUpIntervals > 0))
763          {
764            numSearches   = searchCounter.getAndSet(0L);
765            numEntries    = entryCounter.getAndSet(0L);
766            numErrors     = errorCounter.getAndSet(0L);
767            totalDuration = searchDurations.getAndSet(0L);
768          }
769          else
770          {
771            numSearches   = searchCounter.get();
772            numEntries    = entryCounter.get();
773            numErrors     = errorCounter.get();
774            totalDuration = searchDurations.get();
775          }
776    
777          final long recentNumSearches = numSearches - lastNumSearches;
778          final long recentNumEntries = numEntries - lastNumEntries;
779          final long recentNumErrors = numErrors - lastNumErrors;
780          final long recentDuration = totalDuration - lastDuration;
781    
782          final double numSeconds = intervalDuration / 1000000000.0d;
783          final double recentSearchRate = recentNumSearches / numSeconds;
784          final double recentErrorRate  = recentNumErrors / numSeconds;
785    
786          final double recentAvgDuration;
787          final double recentEntriesPerSearch;
788          if (recentNumSearches > 0L)
789          {
790            recentEntriesPerSearch = 1.0d * recentNumEntries / recentNumSearches;
791            recentAvgDuration = 1.0d * recentDuration / recentNumSearches / 1000000;
792          }
793          else
794          {
795            recentEntriesPerSearch = 0.0d;
796            recentAvgDuration = 0.0d;
797          }
798    
799    
800          if (warmUp && (remainingWarmUpIntervals > 0))
801          {
802            out(formatter.formatRow(recentSearchRate, recentAvgDuration,
803                 recentEntriesPerSearch, recentErrorRate, "warming up",
804                 "warming up"));
805    
806            remainingWarmUpIntervals--;
807            if (remainingWarmUpIntervals == 0)
808            {
809              out("Warm-up completed.  Beginning overall statistics collection.");
810              setOverallStartTime = true;
811            }
812          }
813          else
814          {
815            if (setOverallStartTime)
816            {
817              overallStartTime    = lastEndTime;
818              setOverallStartTime = false;
819            }
820    
821            final double numOverallSeconds =
822                 (endTime - overallStartTime) / 1000000000.0d;
823            final double overallSearchRate = numSearches / numOverallSeconds;
824    
825            final double overallAvgDuration;
826            if (numSearches > 0L)
827            {
828              overallAvgDuration = 1.0d * totalDuration / numSearches / 1000000;
829            }
830            else
831            {
832              overallAvgDuration = 0.0d;
833            }
834    
835            out(formatter.formatRow(recentSearchRate, recentAvgDuration,
836                 recentEntriesPerSearch, recentErrorRate, overallSearchRate,
837                 overallAvgDuration));
838    
839            lastNumSearches = numSearches;
840            lastNumEntries  = numEntries;
841            lastNumErrors   = numErrors;
842            lastDuration    = totalDuration;
843          }
844    
845          final List<ObjectPair<ResultCode,Long>> rcCounts =
846               rcCounter.getCounts(true);
847          if ((! suppressErrors.isPresent()) && (! rcCounts.isEmpty()))
848          {
849            err("\tError Results:");
850            for (final ObjectPair<ResultCode,Long> p : rcCounts)
851            {
852              err("\t", p.getFirst().getName(), ":  ", p.getSecond());
853            }
854          }
855    
856          lastEndTime = endTime;
857        }
858    
859    
860        // Stop all of the threads.
861        ResultCode resultCode = ResultCode.SUCCESS;
862        for (final SearchRateThread t : threads)
863        {
864          t.signalShutdown();
865        }
866        for (final SearchRateThread t : threads)
867        {
868          final ResultCode r = t.waitForShutdown();
869          if (resultCode == ResultCode.SUCCESS)
870          {
871            resultCode = r;
872          }
873        }
874    
875        return resultCode;
876      }
877    
878    
879    
880      /**
881       * Retrieves the maximum number of outstanding requests that may be in
882       * progress at any time, if appropriate.
883       *
884       * @return  The maximum number of outstanding requests that may be in progress
885       *          at any time, or -1 if the tool was not configured to perform
886       *          asynchronous searches with a maximum number of outstanding
887       *          requests.
888       */
889      int getMaxOutstandingRequests()
890      {
891        if (maxOutstandingRequests.isPresent())
892        {
893          return maxOutstandingRequests.getValue();
894        }
895        else
896        {
897          return -1;
898        }
899      }
900    
901    
902    
903      /**
904       * {@inheritDoc}
905       */
906      @Override()
907      public LinkedHashMap<String[],String> getExampleUsages()
908      {
909        final LinkedHashMap<String[],String> examples =
910             new LinkedHashMap<String[],String>();
911    
912        final String[] args =
913        {
914          "--hostname", "server.example.com",
915          "--port", "389",
916          "--bindDN", "uid=admin,dc=example,dc=com",
917          "--bindPassword", "password",
918          "--baseDN", "dc=example,dc=com",
919          "--scope", "sub",
920          "--filter", "(uid=user.[1-1000000])",
921          "--attribute", "givenName",
922          "--attribute", "sn",
923          "--attribute", "mail",
924          "--numThreads", "10"
925        };
926        final String description =
927             "Test search performance by searching randomly across a set " +
928             "of one million users located below 'dc=example,dc=com' with ten " +
929             "concurrent threads.  The entries returned to the client will " +
930             "include the givenName, sn, and mail attributes.";
931        examples.put(args, description);
932    
933        return examples;
934      }
935    }