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