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