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