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