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