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