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