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}