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