001/* 002 * Copyright 2008-2018 Ping Identity Corporation 003 * All Rights Reserved. 004 */ 005/* 006 * Copyright (C) 2008-2018 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.LinkedHashSet; 032import java.util.List; 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 LinkedHashSet<String> derefAllowedValues = new LinkedHashSet<>(4); 568 derefAllowedValues.add("never"); 569 derefAllowedValues.add("always"); 570 derefAllowedValues.add("search"); 571 derefAllowedValues.add("find"); 572 description = "The alias dereferencing policy to use for search " + 573 "requests. The value should be one of 'never', 'always', " + 574 "'search', or 'find'. If this is not provided, then a " + 575 "default value of 'never' will be used."; 576 dereferencePolicy = new StringArgument(null, "dereferencePolicy", false, 1, 577 "{never|always|search|find}", description, derefAllowedValues, 578 "never"); 579 dereferencePolicy.setArgumentGroupName("Search Arguments"); 580 dereferencePolicy.addLongIdentifier("dereference-policy", true); 581 parser.addArgument(dereferencePolicy); 582 583 584 description = "Indicates that serve should only include the names of the " + 585 "attributes contained in matching entries rather than both " + 586 "names and values."; 587 typesOnly = new BooleanArgument(null, "typesOnly", 1, description); 588 typesOnly.setArgumentGroupName("Search Arguments"); 589 typesOnly.addLongIdentifier("types-only", true); 590 parser.addArgument(typesOnly); 591 592 593 description = "The filter to use for the searches. It may be a simple " + 594 "filter or a value pattern to specify a range of filters " + 595 "(e.g., \"(uid=user.[1-1000])\"). See " + 596 ValuePattern.PUBLIC_JAVADOC_URL + " for complete details " + 597 "about the value pattern syntax. This must be provided."; 598 filter = new StringArgument('f', "filter", true, 1, "{filter}", 599 description); 600 filter.setArgumentGroupName("Search Arguments"); 601 parser.addArgument(filter); 602 603 604 description = "The name of an attribute to include in entries returned " + 605 "from the searches. Multiple attributes may be requested " + 606 "by providing this argument multiple times. If no request " + 607 "attributes are provided, then the entries returned will " + 608 "include all user attributes."; 609 attributes = new StringArgument('A', "attribute", false, 0, "{name}", 610 description); 611 attributes.setArgumentGroupName("Search Arguments"); 612 parser.addArgument(attributes); 613 614 615 description = "Indicates that search requests should include the " + 616 "assertion request control with the specified filter."; 617 assertionFilter = new FilterArgument(null, "assertionFilter", false, 1, 618 "{filter}", description); 619 assertionFilter.setArgumentGroupName("Request Control Arguments"); 620 assertionFilter.addLongIdentifier("assertion-filter", true); 621 parser.addArgument(assertionFilter); 622 623 624 description = "Indicates that search requests should include the simple " + 625 "paged results control with the specified page size."; 626 simplePageSize = new IntegerArgument(null, "simplePageSize", false, 1, 627 "{size}", description, 1, 628 Integer.MAX_VALUE); 629 simplePageSize.setArgumentGroupName("Request Control Arguments"); 630 simplePageSize.addLongIdentifier("simple-page-size", true); 631 parser.addArgument(simplePageSize); 632 633 634 description = "Indicates that search requests should include the " + 635 "server-side sort request control with the specified sort " + 636 "order. This should be a comma-delimited list in which " + 637 "each item is an attribute name, optionally preceded by a " + 638 "plus or minus sign (to indicate ascending or descending " + 639 "order; where ascending order is the default), and " + 640 "optionally followed by a colon and the name or OID of " + 641 "the desired ordering matching rule (if this is not " + 642 "provided, the the attribute type's default ordering " + 643 "rule will be used)."; 644 sortOrder = new StringArgument(null, "sortOrder", false, 1, "{sortOrder}", 645 description); 646 sortOrder.setArgumentGroupName("Request Control Arguments"); 647 sortOrder.addLongIdentifier("sort-order", true); 648 parser.addArgument(sortOrder); 649 650 651 description = "Indicates that the proxied authorization control (as " + 652 "defined in RFC 4370) should be used to request that " + 653 "operations be processed using an alternate authorization " + 654 "identity. This may be a simple authorization ID or it " + 655 "may be a value pattern to specify a range of " + 656 "identities. See " + ValuePattern.PUBLIC_JAVADOC_URL + 657 " for complete details about the value pattern syntax."; 658 proxyAs = new StringArgument('Y', "proxyAs", false, 1, "{authzID}", 659 description); 660 proxyAs.setArgumentGroupName("Request Control Arguments"); 661 proxyAs.addLongIdentifier("proxy-as", true); 662 parser.addArgument(proxyAs); 663 664 665 description = "Indicates that search requests should include the " + 666 "specified request control. This may be provided multiple " + 667 "times to include multiple request controls."; 668 control = new ControlArgument('J', "control", false, 0, null, description); 669 control.setArgumentGroupName("Request Control Arguments"); 670 parser.addArgument(control); 671 672 673 description = "The number of threads to use to perform the searches. If " + 674 "this is not provided, then a default of one thread will " + 675 "be used."; 676 numThreads = new IntegerArgument('t', "numThreads", true, 1, "{num}", 677 description, 1, Integer.MAX_VALUE, 1); 678 numThreads.setArgumentGroupName("Rate Management Arguments"); 679 numThreads.addLongIdentifier("num-threads", true); 680 parser.addArgument(numThreads); 681 682 683 description = "The length of time in seconds between output lines. If " + 684 "this is not provided, then a default interval of five " + 685 "seconds will be used."; 686 collectionInterval = new IntegerArgument('i', "intervalDuration", true, 1, 687 "{num}", description, 1, 688 Integer.MAX_VALUE, 5); 689 collectionInterval.setArgumentGroupName("Rate Management Arguments"); 690 collectionInterval.addLongIdentifier("interval-duration", true); 691 parser.addArgument(collectionInterval); 692 693 694 description = "The maximum number of intervals for which to run. If " + 695 "this is not provided, then the tool will run until it is " + 696 "interrupted."; 697 numIntervals = new IntegerArgument('I', "numIntervals", true, 1, "{num}", 698 description, 1, Integer.MAX_VALUE, 699 Integer.MAX_VALUE); 700 numIntervals.setArgumentGroupName("Rate Management Arguments"); 701 numIntervals.addLongIdentifier("num-intervals", true); 702 parser.addArgument(numIntervals); 703 704 description = "The number of search iterations that should be processed " + 705 "on a connection before that connection is closed and " + 706 "replaced with a newly-established (and authenticated, if " + 707 "appropriate) connection. If this is not provided, then " + 708 "connections will not be periodically closed and " + 709 "re-established."; 710 iterationsBeforeReconnect = new IntegerArgument(null, 711 "iterationsBeforeReconnect", false, 1, "{num}", description, 0); 712 iterationsBeforeReconnect.setArgumentGroupName("Rate Management Arguments"); 713 iterationsBeforeReconnect.addLongIdentifier("iterations-before-reconnect", 714 true); 715 parser.addArgument(iterationsBeforeReconnect); 716 717 description = "The target number of searches to perform per second. It " + 718 "is still necessary to specify a sufficient number of " + 719 "threads for achieving this rate. If neither this option " + 720 "nor --variableRateData is provided, then the tool will " + 721 "run at the maximum rate for the specified number of " + 722 "threads."; 723 ratePerSecond = new IntegerArgument('r', "ratePerSecond", false, 1, 724 "{searches-per-second}", description, 725 1, Integer.MAX_VALUE); 726 ratePerSecond.setArgumentGroupName("Rate Management Arguments"); 727 ratePerSecond.addLongIdentifier("rate-per-second", true); 728 parser.addArgument(ratePerSecond); 729 730 final String variableRateDataArgName = "variableRateData"; 731 final String generateSampleRateFileArgName = "generateSampleRateFile"; 732 description = RateAdjustor.getVariableRateDataArgumentDescription( 733 generateSampleRateFileArgName); 734 variableRateData = new FileArgument(null, variableRateDataArgName, false, 1, 735 "{path}", description, true, true, true, 736 false); 737 variableRateData.setArgumentGroupName("Rate Management Arguments"); 738 variableRateData.addLongIdentifier("variable-rate-data", true); 739 parser.addArgument(variableRateData); 740 741 description = RateAdjustor.getGenerateSampleVariableRateFileDescription( 742 variableRateDataArgName); 743 sampleRateFile = new FileArgument(null, generateSampleRateFileArgName, 744 false, 1, "{path}", description, false, 745 true, true, false); 746 sampleRateFile.setArgumentGroupName("Rate Management Arguments"); 747 sampleRateFile.addLongIdentifier("generate-sample-rate-file", true); 748 sampleRateFile.setUsageArgument(true); 749 parser.addArgument(sampleRateFile); 750 parser.addExclusiveArgumentSet(variableRateData, sampleRateFile); 751 752 description = "The number of intervals to complete before beginning " + 753 "overall statistics collection. Specifying a nonzero " + 754 "number of warm-up intervals gives the client and server " + 755 "a chance to warm up without skewing performance results."; 756 warmUpIntervals = new IntegerArgument(null, "warmUpIntervals", true, 1, 757 "{num}", description, 0, Integer.MAX_VALUE, 0); 758 warmUpIntervals.setArgumentGroupName("Rate Management Arguments"); 759 warmUpIntervals.addLongIdentifier("warm-up-intervals", true); 760 parser.addArgument(warmUpIntervals); 761 762 description = "Indicates the format to use for timestamps included in " + 763 "the output. A value of 'none' indicates that no " + 764 "timestamps should be included. A value of 'with-date' " + 765 "indicates that both the date and the time should be " + 766 "included. A value of 'without-date' indicates that only " + 767 "the time should be included."; 768 final LinkedHashSet<String> allowedFormats = new LinkedHashSet<>(3); 769 allowedFormats.add("none"); 770 allowedFormats.add("with-date"); 771 allowedFormats.add("without-date"); 772 timestampFormat = new StringArgument(null, "timestampFormat", true, 1, 773 "{format}", description, allowedFormats, "none"); 774 timestampFormat.addLongIdentifier("timestamp-format", true); 775 parser.addArgument(timestampFormat); 776 777 description = "Indicates that the client should operate in asynchronous " + 778 "mode, in which it will not be necessary to wait for a " + 779 "response to a previous request before sending the next " + 780 "request. Either the '--ratePerSecond' or the " + 781 "'--maxOutstandingRequests' argument must be provided to " + 782 "limit the number of outstanding requests."; 783 asynchronousMode = new BooleanArgument('a', "asynchronous", description); 784 parser.addArgument(asynchronousMode); 785 786 description = "Specifies the maximum number of outstanding requests " + 787 "that should be allowed when operating in asynchronous mode."; 788 maxOutstandingRequests = new IntegerArgument('O', "maxOutstandingRequests", 789 false, 1, "{num}", description, 1, Integer.MAX_VALUE, (Integer) null); 790 maxOutstandingRequests.addLongIdentifier("max-outstanding-requests", true); 791 parser.addArgument(maxOutstandingRequests); 792 793 description = "Indicates that information about the result codes for " + 794 "failed operations should not be displayed."; 795 suppressErrors = new BooleanArgument(null, 796 "suppressErrorResultCodes", 1, description); 797 suppressErrors.addLongIdentifier("suppress-error-result-codes", true); 798 parser.addArgument(suppressErrors); 799 800 description = "Generate output in CSV format rather than a " + 801 "display-friendly format"; 802 csvFormat = new BooleanArgument('c', "csv", 1, description); 803 parser.addArgument(csvFormat); 804 805 description = "Specifies the seed to use for the random number generator."; 806 randomSeed = new IntegerArgument('R', "randomSeed", false, 1, "{value}", 807 description); 808 randomSeed.addLongIdentifier("random-seed", true); 809 parser.addArgument(randomSeed); 810 811 812 parser.addDependentArgumentSet(asynchronousMode, ratePerSecond, 813 maxOutstandingRequests); 814 parser.addDependentArgumentSet(maxOutstandingRequests, asynchronousMode); 815 816 parser.addExclusiveArgumentSet(asynchronousMode, simplePageSize); 817 } 818 819 820 821 /** 822 * Indicates whether this tool supports creating connections to multiple 823 * servers. If it is to support multiple servers, then the "--hostname" and 824 * "--port" arguments will be allowed to be provided multiple times, and 825 * will be required to be provided the same number of times. The same type of 826 * communication security and bind credentials will be used for all servers. 827 * 828 * @return {@code true} if this tool supports creating connections to 829 * multiple servers, or {@code false} if not. 830 */ 831 @Override() 832 protected boolean supportsMultipleServers() 833 { 834 return true; 835 } 836 837 838 839 /** 840 * Retrieves the connection options that should be used for connections 841 * created for use with this tool. 842 * 843 * @return The connection options that should be used for connections created 844 * for use with this tool. 845 */ 846 @Override() 847 public LDAPConnectionOptions getConnectionOptions() 848 { 849 final LDAPConnectionOptions options = new LDAPConnectionOptions(); 850 options.setUseSynchronousMode(! asynchronousMode.isPresent()); 851 return options; 852 } 853 854 855 856 /** 857 * Performs the actual processing for this tool. In this case, it gets a 858 * connection to the directory server and uses it to perform the requested 859 * searches. 860 * 861 * @return The result code for the processing that was performed. 862 */ 863 @Override() 864 public ResultCode doToolProcessing() 865 { 866 runningThread = Thread.currentThread(); 867 868 try 869 { 870 return doToolProcessingInternal(); 871 } 872 finally 873 { 874 runningThread = null; 875 } 876 } 877 878 879 880 /** 881 * Performs the actual processing for this tool. In this case, it gets a 882 * connection to the directory server and uses it to perform the requested 883 * searches. 884 * 885 * @return The result code for the processing that was performed. 886 */ 887 private ResultCode doToolProcessingInternal() 888 { 889 // If the sample rate file argument was specified, then generate the sample 890 // variable rate data file and return. 891 if (sampleRateFile.isPresent()) 892 { 893 try 894 { 895 RateAdjustor.writeSampleVariableRateFile(sampleRateFile.getValue()); 896 return ResultCode.SUCCESS; 897 } 898 catch (final Exception e) 899 { 900 Debug.debugException(e); 901 err("An error occurred while trying to write sample variable data " + 902 "rate file '", sampleRateFile.getValue().getAbsolutePath(), 903 "': ", StaticUtils.getExceptionMessage(e)); 904 return ResultCode.LOCAL_ERROR; 905 } 906 } 907 908 909 // Determine the random seed to use. 910 final Long seed; 911 if (randomSeed.isPresent()) 912 { 913 seed = Long.valueOf(randomSeed.getValue()); 914 } 915 else 916 { 917 seed = null; 918 } 919 920 // Create value patterns for the base DN, filter, and proxied authorization 921 // DN. 922 final ValuePattern dnPattern; 923 try 924 { 925 dnPattern = new ValuePattern(baseDN.getValue(), seed); 926 } 927 catch (final ParseException pe) 928 { 929 Debug.debugException(pe); 930 err("Unable to parse the base DN value pattern: ", pe.getMessage()); 931 return ResultCode.PARAM_ERROR; 932 } 933 934 final ValuePattern filterPattern; 935 try 936 { 937 filterPattern = new ValuePattern(filter.getValue(), seed); 938 } 939 catch (final ParseException pe) 940 { 941 Debug.debugException(pe); 942 err("Unable to parse the filter pattern: ", pe.getMessage()); 943 return ResultCode.PARAM_ERROR; 944 } 945 946 final ValuePattern authzIDPattern; 947 if (proxyAs.isPresent()) 948 { 949 try 950 { 951 authzIDPattern = new ValuePattern(proxyAs.getValue(), seed); 952 } 953 catch (final ParseException pe) 954 { 955 Debug.debugException(pe); 956 err("Unable to parse the proxied authorization pattern: ", 957 pe.getMessage()); 958 return ResultCode.PARAM_ERROR; 959 } 960 } 961 else 962 { 963 authzIDPattern = null; 964 } 965 966 967 // Get the alias dereference policy to use. 968 final DereferencePolicy derefPolicy; 969 final String derefValue = 970 StaticUtils.toLowerCase(dereferencePolicy.getValue()); 971 if (derefValue.equals("always")) 972 { 973 derefPolicy = DereferencePolicy.ALWAYS; 974 } 975 else if (derefValue.equals("search")) 976 { 977 derefPolicy = DereferencePolicy.SEARCHING; 978 } 979 else if (derefValue.equals("find")) 980 { 981 derefPolicy = DereferencePolicy.FINDING; 982 } 983 else 984 { 985 derefPolicy = DereferencePolicy.NEVER; 986 } 987 988 989 // Get the set of controls to include in search requests. 990 final ArrayList<Control> controlList = new ArrayList<>(5); 991 if (assertionFilter.isPresent()) 992 { 993 controlList.add(new AssertionRequestControl(assertionFilter.getValue())); 994 } 995 996 if (sortOrder.isPresent()) 997 { 998 final ArrayList<SortKey> sortKeys = new ArrayList<>(5); 999 final StringTokenizer tokenizer = 1000 new StringTokenizer(sortOrder.getValue(), ","); 1001 while (tokenizer.hasMoreTokens()) 1002 { 1003 String token = tokenizer.nextToken().trim(); 1004 1005 final boolean ascending; 1006 if (token.startsWith("+")) 1007 { 1008 ascending = true; 1009 token = token.substring(1); 1010 } 1011 else if (token.startsWith("-")) 1012 { 1013 ascending = false; 1014 token = token.substring(1); 1015 } 1016 else 1017 { 1018 ascending = true; 1019 } 1020 1021 final String attributeName; 1022 final String matchingRuleID; 1023 final int colonPos = token.indexOf(':'); 1024 if (colonPos < 0) 1025 { 1026 attributeName = token; 1027 matchingRuleID = null; 1028 } 1029 else 1030 { 1031 attributeName = token.substring(0, colonPos); 1032 matchingRuleID = token.substring(colonPos+1); 1033 } 1034 1035 sortKeys.add(new SortKey(attributeName, matchingRuleID, (! ascending))); 1036 } 1037 1038 controlList.add(new ServerSideSortRequestControl(sortKeys)); 1039 } 1040 1041 if (control.isPresent()) 1042 { 1043 controlList.addAll(control.getValues()); 1044 } 1045 1046 1047 // Get the attributes to return. 1048 final String[] attrs; 1049 if (attributes.isPresent()) 1050 { 1051 final List<String> attrList = attributes.getValues(); 1052 attrs = new String[attrList.size()]; 1053 attrList.toArray(attrs); 1054 } 1055 else 1056 { 1057 attrs = StaticUtils.NO_STRINGS; 1058 } 1059 1060 1061 // If the --ratePerSecond option was specified, then limit the rate 1062 // accordingly. 1063 FixedRateBarrier fixedRateBarrier = null; 1064 if (ratePerSecond.isPresent() || variableRateData.isPresent()) 1065 { 1066 // We might not have a rate per second if --variableRateData is specified. 1067 // The rate typically doesn't matter except when we have warm-up 1068 // intervals. In this case, we'll run at the max rate. 1069 final int intervalSeconds = collectionInterval.getValue(); 1070 final int ratePerInterval = 1071 (ratePerSecond.getValue() == null) 1072 ? Integer.MAX_VALUE 1073 : ratePerSecond.getValue() * intervalSeconds; 1074 fixedRateBarrier = 1075 new FixedRateBarrier(1000L * intervalSeconds, ratePerInterval); 1076 } 1077 1078 1079 // If --variableRateData was specified, then initialize a RateAdjustor. 1080 RateAdjustor rateAdjustor = null; 1081 if (variableRateData.isPresent()) 1082 { 1083 try 1084 { 1085 rateAdjustor = RateAdjustor.newInstance(fixedRateBarrier, 1086 ratePerSecond.getValue(), variableRateData.getValue()); 1087 } 1088 catch (final IOException | IllegalArgumentException e) 1089 { 1090 Debug.debugException(e); 1091 err("Initializing the variable rates failed: " + e.getMessage()); 1092 return ResultCode.PARAM_ERROR; 1093 } 1094 } 1095 1096 1097 // If the --maxOutstandingRequests option was specified, then create the 1098 // semaphore used to enforce that limit. 1099 final Semaphore asyncSemaphore; 1100 if (maxOutstandingRequests.isPresent()) 1101 { 1102 asyncSemaphore = new Semaphore(maxOutstandingRequests.getValue()); 1103 } 1104 else 1105 { 1106 asyncSemaphore = null; 1107 } 1108 1109 1110 // Determine whether to include timestamps in the output and if so what 1111 // format should be used for them. 1112 final boolean includeTimestamp; 1113 final String timeFormat; 1114 if (timestampFormat.getValue().equalsIgnoreCase("with-date")) 1115 { 1116 includeTimestamp = true; 1117 timeFormat = "dd/MM/yyyy HH:mm:ss"; 1118 } 1119 else if (timestampFormat.getValue().equalsIgnoreCase("without-date")) 1120 { 1121 includeTimestamp = true; 1122 timeFormat = "HH:mm:ss"; 1123 } 1124 else 1125 { 1126 includeTimestamp = false; 1127 timeFormat = null; 1128 } 1129 1130 1131 // Determine whether any warm-up intervals should be run. 1132 final long totalIntervals; 1133 final boolean warmUp; 1134 int remainingWarmUpIntervals = warmUpIntervals.getValue(); 1135 if (remainingWarmUpIntervals > 0) 1136 { 1137 warmUp = true; 1138 totalIntervals = 0L + numIntervals.getValue() + remainingWarmUpIntervals; 1139 } 1140 else 1141 { 1142 warmUp = true; 1143 totalIntervals = 0L + numIntervals.getValue(); 1144 } 1145 1146 1147 // Create the table that will be used to format the output. 1148 final OutputFormat outputFormat; 1149 if (csvFormat.isPresent()) 1150 { 1151 outputFormat = OutputFormat.CSV; 1152 } 1153 else 1154 { 1155 outputFormat = OutputFormat.COLUMNS; 1156 } 1157 1158 final ColumnFormatter formatter = new ColumnFormatter(includeTimestamp, 1159 timeFormat, outputFormat, " ", 1160 new FormattableColumn(12, HorizontalAlignment.RIGHT, "Recent", 1161 "Searches/Sec"), 1162 new FormattableColumn(12, HorizontalAlignment.RIGHT, "Recent", 1163 "Avg Dur ms"), 1164 new FormattableColumn(12, HorizontalAlignment.RIGHT, "Recent", 1165 "Entries/Srch"), 1166 new FormattableColumn(12, HorizontalAlignment.RIGHT, "Recent", 1167 "Errors/Sec"), 1168 new FormattableColumn(12, HorizontalAlignment.RIGHT, "Overall", 1169 "Searches/Sec"), 1170 new FormattableColumn(12, HorizontalAlignment.RIGHT, "Overall", 1171 "Avg Dur ms")); 1172 1173 1174 // Create values to use for statistics collection. 1175 final AtomicLong searchCounter = new AtomicLong(0L); 1176 final AtomicLong entryCounter = new AtomicLong(0L); 1177 final AtomicLong errorCounter = new AtomicLong(0L); 1178 final AtomicLong searchDurations = new AtomicLong(0L); 1179 final ResultCodeCounter rcCounter = new ResultCodeCounter(); 1180 1181 1182 // Determine the length of each interval in milliseconds. 1183 final long intervalMillis = 1000L * collectionInterval.getValue(); 1184 1185 1186 // Create the threads to use for the searches. 1187 final CyclicBarrier barrier = new CyclicBarrier(numThreads.getValue() + 1); 1188 final SearchRateThread[] threads = 1189 new SearchRateThread[numThreads.getValue()]; 1190 for (int i=0; i < threads.length; i++) 1191 { 1192 final LDAPConnection connection; 1193 try 1194 { 1195 connection = getConnection(); 1196 } 1197 catch (final LDAPException le) 1198 { 1199 Debug.debugException(le); 1200 err("Unable to connect to the directory server: ", 1201 StaticUtils.getExceptionMessage(le)); 1202 return le.getResultCode(); 1203 } 1204 1205 threads[i] = new SearchRateThread(this, i, connection, 1206 asynchronousMode.isPresent(), dnPattern, scopeArg.getValue(), 1207 derefPolicy, sizeLimit.getValue(), timeLimitSeconds.getValue(), 1208 typesOnly.isPresent(), filterPattern, attrs, authzIDPattern, 1209 simplePageSize.getValue(), controlList, 1210 iterationsBeforeReconnect.getValue(), barrier, searchCounter, 1211 entryCounter, searchDurations, errorCounter, rcCounter, 1212 fixedRateBarrier, asyncSemaphore); 1213 threads[i].start(); 1214 } 1215 1216 1217 // Display the table header. 1218 for (final String headerLine : formatter.getHeaderLines(true)) 1219 { 1220 out(headerLine); 1221 } 1222 1223 1224 // Start the RateAdjustor before the threads so that the initial value is 1225 // in place before any load is generated unless we're doing a warm-up in 1226 // which case, we'll start it after the warm-up is complete. 1227 if ((rateAdjustor != null) && (remainingWarmUpIntervals <= 0)) 1228 { 1229 rateAdjustor.start(); 1230 } 1231 1232 1233 // Indicate that the threads can start running. 1234 try 1235 { 1236 barrier.await(); 1237 } 1238 catch (final Exception e) 1239 { 1240 Debug.debugException(e); 1241 } 1242 1243 long overallStartTime = System.nanoTime(); 1244 long nextIntervalStartTime = System.currentTimeMillis() + intervalMillis; 1245 1246 1247 boolean setOverallStartTime = false; 1248 long lastDuration = 0L; 1249 long lastNumEntries = 0L; 1250 long lastNumErrors = 0L; 1251 long lastNumSearches = 0L; 1252 long lastEndTime = System.nanoTime(); 1253 for (long i=0; i < totalIntervals; i++) 1254 { 1255 if (rateAdjustor != null) 1256 { 1257 if (! rateAdjustor.isAlive()) 1258 { 1259 out("All of the rates in " + variableRateData.getValue().getName() + 1260 " have been completed."); 1261 break; 1262 } 1263 } 1264 1265 final long startTimeMillis = System.currentTimeMillis(); 1266 final long sleepTimeMillis = nextIntervalStartTime - startTimeMillis; 1267 nextIntervalStartTime += intervalMillis; 1268 if (sleepTimeMillis > 0) 1269 { 1270 sleeper.sleep(sleepTimeMillis); 1271 } 1272 1273 if (stopRequested.get()) 1274 { 1275 break; 1276 } 1277 1278 final long endTime = System.nanoTime(); 1279 final long intervalDuration = endTime - lastEndTime; 1280 1281 final long numSearches; 1282 final long numEntries; 1283 final long numErrors; 1284 final long totalDuration; 1285 if (warmUp && (remainingWarmUpIntervals > 0)) 1286 { 1287 numSearches = searchCounter.getAndSet(0L); 1288 numEntries = entryCounter.getAndSet(0L); 1289 numErrors = errorCounter.getAndSet(0L); 1290 totalDuration = searchDurations.getAndSet(0L); 1291 } 1292 else 1293 { 1294 numSearches = searchCounter.get(); 1295 numEntries = entryCounter.get(); 1296 numErrors = errorCounter.get(); 1297 totalDuration = searchDurations.get(); 1298 } 1299 1300 final long recentNumSearches = numSearches - lastNumSearches; 1301 final long recentNumEntries = numEntries - lastNumEntries; 1302 final long recentNumErrors = numErrors - lastNumErrors; 1303 final long recentDuration = totalDuration - lastDuration; 1304 1305 final double numSeconds = intervalDuration / 1_000_000_000.0d; 1306 final double recentSearchRate = recentNumSearches / numSeconds; 1307 final double recentErrorRate = recentNumErrors / numSeconds; 1308 1309 final double recentAvgDuration; 1310 final double recentEntriesPerSearch; 1311 if (recentNumSearches > 0L) 1312 { 1313 recentEntriesPerSearch = 1.0d * recentNumEntries / recentNumSearches; 1314 recentAvgDuration = 1315 1.0d * recentDuration / recentNumSearches / 1_000_000; 1316 } 1317 else 1318 { 1319 recentEntriesPerSearch = 0.0d; 1320 recentAvgDuration = 0.0d; 1321 } 1322 1323 1324 if (warmUp && (remainingWarmUpIntervals > 0)) 1325 { 1326 out(formatter.formatRow(recentSearchRate, recentAvgDuration, 1327 recentEntriesPerSearch, recentErrorRate, "warming up", 1328 "warming up")); 1329 1330 remainingWarmUpIntervals--; 1331 if (remainingWarmUpIntervals == 0) 1332 { 1333 out("Warm-up completed. Beginning overall statistics collection."); 1334 setOverallStartTime = true; 1335 if (rateAdjustor != null) 1336 { 1337 rateAdjustor.start(); 1338 } 1339 } 1340 } 1341 else 1342 { 1343 if (setOverallStartTime) 1344 { 1345 overallStartTime = lastEndTime; 1346 setOverallStartTime = false; 1347 } 1348 1349 final double numOverallSeconds = 1350 (endTime - overallStartTime) / 1_000_000_000.0d; 1351 final double overallSearchRate = numSearches / numOverallSeconds; 1352 1353 final double overallAvgDuration; 1354 if (numSearches > 0L) 1355 { 1356 overallAvgDuration = 1.0d * totalDuration / numSearches / 1_000_000; 1357 } 1358 else 1359 { 1360 overallAvgDuration = 0.0d; 1361 } 1362 1363 out(formatter.formatRow(recentSearchRate, recentAvgDuration, 1364 recentEntriesPerSearch, recentErrorRate, overallSearchRate, 1365 overallAvgDuration)); 1366 1367 lastNumSearches = numSearches; 1368 lastNumEntries = numEntries; 1369 lastNumErrors = numErrors; 1370 lastDuration = totalDuration; 1371 } 1372 1373 final List<ObjectPair<ResultCode,Long>> rcCounts = 1374 rcCounter.getCounts(true); 1375 if ((! suppressErrors.isPresent()) && (! rcCounts.isEmpty())) 1376 { 1377 err("\tError Results:"); 1378 for (final ObjectPair<ResultCode,Long> p : rcCounts) 1379 { 1380 err("\t", p.getFirst().getName(), ": ", p.getSecond()); 1381 } 1382 } 1383 1384 lastEndTime = endTime; 1385 } 1386 1387 1388 // Shut down the RateAdjustor if we have one. 1389 if (rateAdjustor != null) 1390 { 1391 rateAdjustor.shutDown(); 1392 } 1393 1394 1395 // Stop all of the threads. 1396 ResultCode resultCode = ResultCode.SUCCESS; 1397 for (final SearchRateThread t : threads) 1398 { 1399 t.signalShutdown(); 1400 } 1401 for (final SearchRateThread t : threads) 1402 { 1403 final ResultCode r = t.waitForShutdown(); 1404 if (resultCode == ResultCode.SUCCESS) 1405 { 1406 resultCode = r; 1407 } 1408 } 1409 1410 return resultCode; 1411 } 1412 1413 1414 1415 /** 1416 * Requests that this tool stop running. This method will attempt to wait 1417 * for all threads to complete before returning control to the caller. 1418 */ 1419 public void stopRunning() 1420 { 1421 stopRequested.set(true); 1422 sleeper.wakeup(); 1423 1424 final Thread t = runningThread; 1425 if (t != null) 1426 { 1427 try 1428 { 1429 t.join(); 1430 } 1431 catch (final Exception e) 1432 { 1433 Debug.debugException(e); 1434 1435 if (e instanceof InterruptedException) 1436 { 1437 Thread.currentThread().interrupt(); 1438 } 1439 } 1440 } 1441 } 1442 1443 1444 1445 /** 1446 * Retrieves the maximum number of outstanding requests that may be in 1447 * progress at any time, if appropriate. 1448 * 1449 * @return The maximum number of outstanding requests that may be in progress 1450 * at any time, or -1 if the tool was not configured to perform 1451 * asynchronous searches with a maximum number of outstanding 1452 * requests. 1453 */ 1454 int getMaxOutstandingRequests() 1455 { 1456 if (maxOutstandingRequests.isPresent()) 1457 { 1458 return maxOutstandingRequests.getValue(); 1459 } 1460 else 1461 { 1462 return -1; 1463 } 1464 } 1465 1466 1467 1468 /** 1469 * {@inheritDoc} 1470 */ 1471 @Override() 1472 public LinkedHashMap<String[],String> getExampleUsages() 1473 { 1474 final LinkedHashMap<String[],String> examples = new LinkedHashMap<>(2); 1475 1476 String[] args = 1477 { 1478 "--hostname", "server.example.com", 1479 "--port", "389", 1480 "--bindDN", "uid=admin,dc=example,dc=com", 1481 "--bindPassword", "password", 1482 "--baseDN", "dc=example,dc=com", 1483 "--scope", "sub", 1484 "--filter", "(uid=user.[1-1000000])", 1485 "--attribute", "givenName", 1486 "--attribute", "sn", 1487 "--attribute", "mail", 1488 "--numThreads", "10" 1489 }; 1490 String description = 1491 "Test search performance by searching randomly across a set " + 1492 "of one million users located below 'dc=example,dc=com' with ten " + 1493 "concurrent threads. The entries returned to the client will " + 1494 "include the givenName, sn, and mail attributes."; 1495 examples.put(args, description); 1496 1497 args = new String[] 1498 { 1499 "--generateSampleRateFile", "variable-rate-data.txt" 1500 }; 1501 description = 1502 "Generate a sample variable rate definition file that may be used " + 1503 "in conjunction with the --variableRateData argument. The sample " + 1504 "file will include comments that describe the format for data to be " + 1505 "included in this file."; 1506 examples.put(args, description); 1507 1508 return examples; 1509 } 1510}