001/* 002 * Copyright 2010-2023 Ping Identity Corporation 003 * All Rights Reserved. 004 */ 005/* 006 * Copyright 2010-2023 Ping Identity Corporation 007 * 008 * Licensed under the Apache License, Version 2.0 (the "License"); 009 * you may not use this file except in compliance with the License. 010 * You may obtain a copy of the License at 011 * 012 * http://www.apache.org/licenses/LICENSE-2.0 013 * 014 * Unless required by applicable law or agreed to in writing, software 015 * distributed under the License is distributed on an "AS IS" BASIS, 016 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 017 * See the License for the specific language governing permissions and 018 * limitations under the License. 019 */ 020/* 021 * Copyright (C) 2010-2023 Ping Identity Corporation 022 * 023 * This program is free software; you can redistribute it and/or modify 024 * it under the terms of the GNU General Public License (GPLv2 only) 025 * or the terms of the GNU Lesser General Public License (LGPLv2.1 only) 026 * as published by the Free Software Foundation. 027 * 028 * This program is distributed in the hope that it will be useful, 029 * but WITHOUT ANY WARRANTY; without even the implied warranty of 030 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 031 * GNU General Public License for more details. 032 * 033 * You should have received a copy of the GNU General Public License 034 * along with this program; if not, see <http://www.gnu.org/licenses>. 035 */ 036package com.unboundid.ldap.sdk.examples; 037 038 039 040import java.io.IOException; 041import java.io.OutputStream; 042import java.io.Serializable; 043import java.text.ParseException; 044import java.util.ArrayList; 045import java.util.LinkedHashMap; 046import java.util.List; 047import java.util.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.NotNull; 072import com.unboundid.util.Nullable; 073import com.unboundid.util.ObjectPair; 074import com.unboundid.util.OutputFormat; 075import com.unboundid.util.RateAdjustor; 076import com.unboundid.util.ResultCodeCounter; 077import com.unboundid.util.StaticUtils; 078import com.unboundid.util.ThreadSafety; 079import com.unboundid.util.ThreadSafetyLevel; 080import com.unboundid.util.ValuePattern; 081import com.unboundid.util.WakeableSleeper; 082import com.unboundid.util.args.ArgumentException; 083import com.unboundid.util.args.ArgumentParser; 084import com.unboundid.util.args.BooleanArgument; 085import com.unboundid.util.args.ControlArgument; 086import com.unboundid.util.args.FileArgument; 087import com.unboundid.util.args.FilterArgument; 088import com.unboundid.util.args.IntegerArgument; 089import com.unboundid.util.args.ScopeArgument; 090import com.unboundid.util.args.StringArgument; 091 092 093 094/** 095 * This class provides a tool that can be used to search an LDAP directory 096 * server repeatedly using multiple threads, and then modify each entry 097 * returned by that server. It can help provide an estimate of the combined 098 * search and modify performance that a directory server is able to achieve. 099 * Either or both of the base DN and the search filter may be a value pattern as 100 * described in the {@link ValuePattern} class. This makes it possible to 101 * search over a range of entries rather than repeatedly performing searches 102 * with the same base DN and filter. 103 * <BR><BR> 104 * Some of the APIs demonstrated by this example include: 105 * <UL> 106 * <LI>Argument Parsing (from the {@code com.unboundid.util.args} 107 * package)</LI> 108 * <LI>LDAP Command-Line Tool (from the {@code com.unboundid.util} 109 * package)</LI> 110 * <LI>LDAP Communication (from the {@code com.unboundid.ldap.sdk} 111 * package)</LI> 112 * <LI>Value Patterns (from the {@code com.unboundid.util} package)</LI> 113 * </UL> 114 * <BR><BR> 115 * All of the necessary information is provided using command line arguments. 116 * Supported arguments include those allowed by the {@link LDAPCommandLineTool} 117 * class, as well as the following additional arguments: 118 * <UL> 119 * <LI>"-b {baseDN}" or "--baseDN {baseDN}" -- specifies the base DN to use 120 * for the searches. This must be provided. It may be a simple DN, or it 121 * may be a value pattern to express a range of base DNs.</LI> 122 * <LI>"-s {scope}" or "--scope {scope}" -- specifies the scope to use for the 123 * search. The scope value should be one of "base", "one", "sub", or 124 * "subord". If this isn't specified, then a scope of "sub" will be 125 * used.</LI> 126 * <LI>"-f {filter}" or "--filter {filter}" -- specifies the filter to use for 127 * the searches. This must be provided. It may be a simple filter, or it 128 * may be a value pattern to express a range of filters.</LI> 129 * <LI>"-A {name}" or "--attribute {name}" -- specifies the name of an 130 * attribute that should be included in entries returned from the server. 131 * If this is not provided, then all user attributes will be requested. 132 * This may include special tokens that the server may interpret, like 133 * "1.1" to indicate that no attributes should be returned, "*", for all 134 * user attributes, or "+" for all operational attributes. Multiple 135 * attributes may be requested with multiple instances of this 136 * argument.</LI> 137 * <LI>"-m {name}" or "--modifyAttribute {name}" -- specifies the name of the 138 * attribute to modify. Multiple attributes may be modified by providing 139 * multiple instances of this argument. At least one attribute must be 140 * provided.</LI> 141 * <LI>"-l {num}" or "--valueLength {num}" -- specifies the length in bytes to 142 * use for the values of the target attributes to modify. If this is not 143 * provided, then a default length of 10 bytes will be used.</LI> 144 * <LI>"-C {chars}" or "--characterSet {chars}" -- specifies the set of 145 * characters that will be used to generate the values to use for the 146 * target attributes to modify. It should only include ASCII characters. 147 * Values will be generated from randomly-selected characters from this 148 * set. If this is not provided, then a default set of lowercase 149 * alphabetic characters will be used.</LI> 150 * <LI>"-t {num}" or "--numThreads {num}" -- specifies the number of 151 * concurrent threads to use when performing the searches. If this is not 152 * provided, then a default of one thread will be used.</LI> 153 * <LI>"-i {sec}" or "--intervalDuration {sec}" -- specifies the length of 154 * time in seconds between lines out output. If this is not provided, 155 * then a default interval duration of five seconds will be used.</LI> 156 * <LI>"-I {num}" or "--numIntervals {num}" -- specifies the maximum number of 157 * intervals for which to run. If this is not provided, then it will 158 * run forever.</LI> 159 * <LI>"--iterationsBeforeReconnect {num}" -- specifies the number of search 160 * iterations that should be performed on a connection before that 161 * connection is closed and replaced with a newly-established (and 162 * authenticated, if appropriate) connection.</LI> 163 * <LI>"-r {ops-per-second}" or "--ratePerSecond {ops-per-second}" -- 164 * specifies the target number of operations to perform per second. Each 165 * search and modify operation will be counted separately for this 166 * purpose, so if a value of 1 is specified and a search returns two 167 * entries, then a total of three seconds will be required (one for the 168 * search and one for the modify for each entry). It is still necessary 169 * to specify a sufficient number of threads for achieving this rate. If 170 * this option is not provided, then the tool will run at the maximum rate 171 * for the specified number of threads.</LI> 172 * <LI>"--variableRateData {path}" -- specifies the path to a file containing 173 * information needed to allow the tool to vary the target rate over time. 174 * If this option is not provided, then the tool will either use a fixed 175 * target rate as specified by the "--ratePerSecond" argument, or it will 176 * run at the maximum rate.</LI> 177 * <LI>"--generateSampleRateFile {path}" -- specifies the path to a file to 178 * which sample data will be written illustrating and describing the 179 * format of the file expected to be used in conjunction with the 180 * "--variableRateData" argument.</LI> 181 * <LI>"--warmUpIntervals {num}" -- specifies the number of intervals to 182 * complete before beginning overall statistics collection.</LI> 183 * <LI>"--timestampFormat {format}" -- specifies the format to use for 184 * timestamps included before each output line. The format may be one of 185 * "none" (for no timestamps), "with-date" (to include both the date and 186 * the time), or "without-date" (to include only time time).</LI> 187 * <LI>"-Y {authzID}" or "--proxyAs {authzID}" -- Use the proxied 188 * authorization v2 control to request that the operations be processed 189 * using an alternate authorization identity. In this case, the bind DN 190 * should be that of a user that has permission to use this control. The 191 * authorization identity may be a value pattern.</LI> 192 * <LI>"--suppressErrorResultCodes" -- Indicates that information about the 193 * result codes for failed operations should not be displayed.</LI> 194 * <LI>"-c" or "--csv" -- Generate output in CSV format rather than a 195 * display-friendly format.</LI> 196 * </UL> 197 */ 198@ThreadSafety(level=ThreadSafetyLevel.NOT_THREADSAFE) 199public final class SearchAndModRate 200 extends LDAPCommandLineTool 201 implements Serializable 202{ 203 /** 204 * The serial version UID for this serializable class. 205 */ 206 private static final long serialVersionUID = 3242469381380526294L; 207 208 209 210 // Indicates whether a request has been made to stop running. 211 @NotNull private final AtomicBoolean stopRequested; 212 213 // The number of search-and-mod-rate threads that are currently running. 214 @NotNull private final AtomicInteger runningThreads; 215 216 // The argument used to indicate whether to generate output in CSV format. 217 @Nullable private BooleanArgument csvFormat; 218 219 // Indicates that modify requests should include the permissive modify request 220 // control. 221 @Nullable private BooleanArgument permissiveModify; 222 223 // The argument used to indicate whether to suppress information about error 224 // result codes. 225 @Nullable private BooleanArgument suppressErrors; 226 227 // The argument used to specify a set of generic controls to include in modify 228 // requests. 229 @Nullable private ControlArgument modifyControl; 230 231 // The argument used to specify a set of generic controls to include in search 232 // requests. 233 @Nullable private ControlArgument searchControl; 234 235 // The argument used to specify a variable rate file. 236 @Nullable private FileArgument sampleRateFile; 237 238 // The argument used to specify a variable rate file. 239 @Nullable private FileArgument variableRateData; 240 241 // The argument used to specify an LDAP assertion filter for modify requests. 242 @Nullable private FilterArgument modifyAssertionFilter; 243 244 // The argument used to specify an LDAP assertion filter for search requests. 245 @Nullable private FilterArgument searchAssertionFilter; 246 247 // The argument used to specify the collection interval. 248 @Nullable private IntegerArgument collectionInterval; 249 250 // The argument used to specify the number of search and modify iterations on 251 // a connection before it is closed and re-established. 252 @Nullable private IntegerArgument iterationsBeforeReconnect; 253 254 // The argument used to specify the number of intervals. 255 @Nullable private IntegerArgument numIntervals; 256 257 // The argument used to specify the number of threads. 258 @Nullable private IntegerArgument numThreads; 259 260 // The argument used to specify the seed to use for the random number 261 // generator. 262 @Nullable private IntegerArgument randomSeed; 263 264 // The target rate of operations per second. 265 @Nullable private IntegerArgument ratePerSecond; 266 267 // The argument used to indicate that the search should use the simple paged 268 // results control with the specified page size. 269 @Nullable private IntegerArgument simplePageSize; 270 271 // The argument used to specify the length of the values to generate. 272 @Nullable private IntegerArgument valueLength; 273 274 // The number of warm-up intervals to perform. 275 @Nullable private IntegerArgument warmUpIntervals; 276 277 // The argument used to specify the scope for the searches. 278 @Nullable private ScopeArgument scopeArg; 279 280 // The argument used to specify the base DNs for the searches. 281 @Nullable private StringArgument baseDN; 282 283 // The argument used to specify the set of characters to use when generating 284 // values. 285 @Nullable private StringArgument characterSet; 286 287 // The argument used to specify the filters for the searches. 288 @Nullable private StringArgument filter; 289 290 // The argument used to specify the attributes to modify. 291 @Nullable private StringArgument modifyAttributes; 292 293 // Indicates that modify requests should include the post-read request control 294 // to request the specified attribute. 295 @Nullable private StringArgument postReadAttribute; 296 297 // Indicates that modify requests should include the pre-read request control 298 // to request the specified attribute. 299 @Nullable private StringArgument preReadAttribute; 300 301 // The argument used to specify the proxied authorization identity. 302 @Nullable private StringArgument proxyAs; 303 304 // The argument used to specify the attributes to return. 305 @Nullable private StringArgument returnAttributes; 306 307 // The argument used to specify the timestamp format. 308 @Nullable private StringArgument timestampFormat; 309 310 // A wakeable sleeper that will be used to sleep between reporting intervals. 311 @NotNull private final WakeableSleeper sleeper; 312 313 314 315 /** 316 * Parse the provided command line arguments and make the appropriate set of 317 * changes. 318 * 319 * @param args The command line arguments provided to this program. 320 */ 321 public static void main(@NotNull final String[] args) 322 { 323 final ResultCode resultCode = main(args, System.out, System.err); 324 if (resultCode != ResultCode.SUCCESS) 325 { 326 System.exit(resultCode.intValue()); 327 } 328 } 329 330 331 332 /** 333 * Parse the provided command line arguments and make the appropriate set of 334 * changes. 335 * 336 * @param args The command line arguments provided to this program. 337 * @param outStream The output stream to which standard out should be 338 * written. It may be {@code null} if output should be 339 * suppressed. 340 * @param errStream The output stream to which standard error should be 341 * written. It may be {@code null} if error messages 342 * should be suppressed. 343 * 344 * @return A result code indicating whether the processing was successful. 345 */ 346 @NotNull() 347 public static ResultCode main(@NotNull final String[] args, 348 @Nullable final OutputStream outStream, 349 @Nullable final OutputStream errStream) 350 { 351 final SearchAndModRate searchAndModRate = 352 new SearchAndModRate(outStream, errStream); 353 return searchAndModRate.runTool(args); 354 } 355 356 357 358 /** 359 * Creates a new instance of this tool. 360 * 361 * @param outStream The output stream to which standard out should be 362 * written. It may be {@code null} if output should be 363 * suppressed. 364 * @param errStream The output stream to which standard error should be 365 * written. It may be {@code null} if error messages 366 * should be suppressed. 367 */ 368 public SearchAndModRate(@Nullable final OutputStream outStream, 369 @Nullable final OutputStream errStream) 370 { 371 super(outStream, errStream); 372 373 stopRequested = new AtomicBoolean(false); 374 runningThreads = new AtomicInteger(0); 375 sleeper = new WakeableSleeper(); 376 } 377 378 379 380 /** 381 * Retrieves the name for this tool. 382 * 383 * @return The name for this tool. 384 */ 385 @Override() 386 @NotNull() 387 public String getToolName() 388 { 389 return "search-and-mod-rate"; 390 } 391 392 393 394 /** 395 * Retrieves the description for this tool. 396 * 397 * @return The description for this tool. 398 */ 399 @Override() 400 @NotNull() 401 public String getToolDescription() 402 { 403 return "Perform repeated searches against an " + 404 "LDAP directory server and modify each entry returned."; 405 } 406 407 408 409 /** 410 * Retrieves the version string for this tool. 411 * 412 * @return The version string for this tool. 413 */ 414 @Override() 415 @NotNull() 416 public String getToolVersion() 417 { 418 return Version.NUMERIC_VERSION_STRING; 419 } 420 421 422 423 /** 424 * Indicates whether this tool should provide support for an interactive mode, 425 * in which the tool offers a mode in which the arguments can be provided in 426 * a text-driven menu rather than requiring them to be given on the command 427 * line. If interactive mode is supported, it may be invoked using the 428 * "--interactive" argument. Alternately, if interactive mode is supported 429 * and {@link #defaultsToInteractiveMode()} returns {@code true}, then 430 * interactive mode may be invoked by simply launching the tool without any 431 * arguments. 432 * 433 * @return {@code true} if this tool supports interactive mode, or 434 * {@code false} if not. 435 */ 436 @Override() 437 public boolean supportsInteractiveMode() 438 { 439 return true; 440 } 441 442 443 444 /** 445 * Indicates whether this tool defaults to launching in interactive mode if 446 * the tool is invoked without any command-line arguments. This will only be 447 * used if {@link #supportsInteractiveMode()} returns {@code true}. 448 * 449 * @return {@code true} if this tool defaults to using interactive mode if 450 * launched without any command-line arguments, or {@code false} if 451 * not. 452 */ 453 @Override() 454 public boolean defaultsToInteractiveMode() 455 { 456 return true; 457 } 458 459 460 461 /** 462 * Indicates whether this tool should provide arguments for redirecting output 463 * to a file. If this method returns {@code true}, then the tool will offer 464 * an "--outputFile" argument that will specify the path to a file to which 465 * all standard output and standard error content will be written, and it will 466 * also offer a "--teeToStandardOut" argument that can only be used if the 467 * "--outputFile" argument is present and will cause all output to be written 468 * to both the specified output file and to standard output. 469 * 470 * @return {@code true} if this tool should provide arguments for redirecting 471 * output to a file, or {@code false} if not. 472 */ 473 @Override() 474 protected boolean supportsOutputFile() 475 { 476 return true; 477 } 478 479 480 481 /** 482 * Indicates whether this tool should default to interactively prompting for 483 * the bind password if a password is required but no argument was provided 484 * to indicate how to get the password. 485 * 486 * @return {@code true} if this tool should default to interactively 487 * prompting for the bind password, or {@code false} if not. 488 */ 489 @Override() 490 protected boolean defaultToPromptForBindPassword() 491 { 492 return true; 493 } 494 495 496 497 /** 498 * Indicates whether this tool supports the use of a properties file for 499 * specifying default values for arguments that aren't specified on the 500 * command line. 501 * 502 * @return {@code true} if this tool supports the use of a properties file 503 * for specifying default values for arguments that aren't specified 504 * on the command line, or {@code false} if not. 505 */ 506 @Override() 507 public boolean supportsPropertiesFile() 508 { 509 return true; 510 } 511 512 513 514 /** 515 * Indicates whether the LDAP-specific arguments should include alternate 516 * versions of all long identifiers that consist of multiple words so that 517 * they are available in both camelCase and dash-separated versions. 518 * 519 * @return {@code true} if this tool should provide multiple versions of 520 * long identifiers for LDAP-specific arguments, or {@code false} if 521 * not. 522 */ 523 @Override() 524 protected boolean includeAlternateLongIdentifiers() 525 { 526 return true; 527 } 528 529 530 531 /** 532 * {@inheritDoc} 533 */ 534 @Override() 535 protected boolean logToolInvocationByDefault() 536 { 537 return true; 538 } 539 540 541 542 /** 543 * Adds the arguments used by this program that aren't already provided by the 544 * generic {@code LDAPCommandLineTool} framework. 545 * 546 * @param parser The argument parser to which the arguments should be added. 547 * 548 * @throws ArgumentException If a problem occurs while adding the arguments. 549 */ 550 @Override() 551 public void addNonLDAPArguments(@NotNull final ArgumentParser parser) 552 throws ArgumentException 553 { 554 String description = "The base DN to use for the searches. It may be a " + 555 "simple DN or a value pattern to specify a range of DNs (e.g., " + 556 "\"uid=user.[1-1000],ou=People,dc=example,dc=com\"). See " + 557 ValuePattern.PUBLIC_JAVADOC_URL + " for complete details about the " + 558 "value pattern syntax. This must be provided."; 559 baseDN = new StringArgument('b', "baseDN", true, 1, "{dn}", description); 560 baseDN.setArgumentGroupName("Search And Modification Arguments"); 561 baseDN.addLongIdentifier("base-dn", true); 562 parser.addArgument(baseDN); 563 564 565 description = "The scope to use for the searches. It should be 'base', " + 566 "'one', 'sub', or 'subord'. If this is not provided, then " + 567 "a default scope of 'sub' will be used."; 568 scopeArg = new ScopeArgument('s', "scope", false, "{scope}", description, 569 SearchScope.SUB); 570 scopeArg.setArgumentGroupName("Search And Modification Arguments"); 571 parser.addArgument(scopeArg); 572 573 574 description = "The filter to use for the searches. It may be a simple " + 575 "filter or a value pattern to specify a range of filters " + 576 "(e.g., \"(uid=user.[1-1000])\"). See " + 577 ValuePattern.PUBLIC_JAVADOC_URL + " for complete details " + 578 "about the value pattern syntax. This must be provided."; 579 filter = new StringArgument('f', "filter", true, 1, "{filter}", 580 description); 581 filter.setArgumentGroupName("Search And Modification Arguments"); 582 parser.addArgument(filter); 583 584 585 description = "The name of an attribute to include in entries returned " + 586 "from the searches. Multiple attributes may be requested " + 587 "by providing this argument multiple times. If no request " + 588 "attributes are provided, then the entries returned will " + 589 "include all user attributes."; 590 returnAttributes = new StringArgument('A', "attribute", false, 0, "{name}", 591 description); 592 returnAttributes.setArgumentGroupName("Search And Modification Arguments"); 593 parser.addArgument(returnAttributes); 594 595 596 description = "The name of the attribute to modify. Multiple attributes " + 597 "may be specified by providing this argument multiple " + 598 "times. At least one attribute must be specified."; 599 modifyAttributes = new StringArgument('m', "modifyAttribute", true, 0, 600 "{name}", description); 601 modifyAttributes.setArgumentGroupName("Search And Modification Arguments"); 602 modifyAttributes.addLongIdentifier("modify-attribute", true); 603 parser.addArgument(modifyAttributes); 604 605 606 description = "The length in bytes to use when generating values for the " + 607 "modifications. If this is not provided, then a default " + 608 "length of ten bytes will be used."; 609 valueLength = new IntegerArgument('l', "valueLength", true, 1, "{num}", 610 description, 1, Integer.MAX_VALUE, 10); 611 valueLength.setArgumentGroupName("Search And Modification Arguments"); 612 valueLength.addLongIdentifier("value-length", true); 613 parser.addArgument(valueLength); 614 615 616 description = "The set of characters to use to generate the values for " + 617 "the modifications. It should only include ASCII " + 618 "characters. If this is not provided, then a default set " + 619 "of lowercase alphabetic characters will be used."; 620 characterSet = new StringArgument('C', "characterSet", true, 1, "{chars}", 621 description, 622 "abcdefghijklmnopqrstuvwxyz"); 623 characterSet.setArgumentGroupName("Search And Modification Arguments"); 624 characterSet.addLongIdentifier("character-set", true); 625 parser.addArgument(characterSet); 626 627 628 description = "Indicates that search requests should include the " + 629 "assertion request control with the specified filter."; 630 searchAssertionFilter = new FilterArgument(null, "searchAssertionFilter", 631 false, 1, "{filter}", 632 description); 633 searchAssertionFilter.setArgumentGroupName("Request Control Arguments"); 634 searchAssertionFilter.addLongIdentifier("search-assertion-filter", true); 635 parser.addArgument(searchAssertionFilter); 636 637 638 description = "Indicates that modify requests should include the " + 639 "assertion request control with the specified filter."; 640 modifyAssertionFilter = new FilterArgument(null, "modifyAssertionFilter", 641 false, 1, "{filter}", 642 description); 643 modifyAssertionFilter.setArgumentGroupName("Request Control Arguments"); 644 modifyAssertionFilter.addLongIdentifier("modify-assertion-filter", true); 645 parser.addArgument(modifyAssertionFilter); 646 647 648 description = "Indicates that search requests should include the simple " + 649 "paged results control with the specified page size."; 650 simplePageSize = new IntegerArgument(null, "simplePageSize", false, 1, 651 "{size}", description, 1, 652 Integer.MAX_VALUE); 653 simplePageSize.setArgumentGroupName("Request Control Arguments"); 654 simplePageSize.addLongIdentifier("simple-page-size", true); 655 parser.addArgument(simplePageSize); 656 657 658 description = "Indicates that modify requests should include the " + 659 "permissive modify request control."; 660 permissiveModify = new BooleanArgument(null, "permissiveModify", 1, 661 description); 662 permissiveModify.setArgumentGroupName("Request Control Arguments"); 663 permissiveModify.addLongIdentifier("permissive-modify", true); 664 parser.addArgument(permissiveModify); 665 666 667 description = "Indicates that modify requests should include the " + 668 "pre-read request control with the specified requested " + 669 "attribute. This argument may be provided multiple times " + 670 "to indicate that multiple requested attributes should be " + 671 "included in the pre-read request control."; 672 preReadAttribute = new StringArgument(null, "preReadAttribute", false, 0, 673 "{attribute}", description); 674 preReadAttribute.setArgumentGroupName("Request Control Arguments"); 675 preReadAttribute.addLongIdentifier("pre-read-attribute", true); 676 parser.addArgument(preReadAttribute); 677 678 679 description = "Indicates that modify requests should include the " + 680 "post-read request control with the specified requested " + 681 "attribute. This argument may be provided multiple times " + 682 "to indicate that multiple requested attributes should be " + 683 "included in the post-read request control."; 684 postReadAttribute = new StringArgument(null, "postReadAttribute", false, 0, 685 "{attribute}", description); 686 postReadAttribute.setArgumentGroupName("Request Control Arguments"); 687 postReadAttribute.addLongIdentifier("post-read-attribute", true); 688 parser.addArgument(postReadAttribute); 689 690 691 description = "Indicates that the proxied authorization control (as " + 692 "defined in RFC 4370) should be used to request that " + 693 "operations be processed using an alternate authorization " + 694 "identity. This may be a simple authorization ID or it " + 695 "may be a value pattern to specify a range of " + 696 "identities. See " + ValuePattern.PUBLIC_JAVADOC_URL + 697 " for complete details about the value pattern syntax."; 698 proxyAs = new StringArgument('Y', "proxyAs", false, 1, "{authzID}", 699 description); 700 proxyAs.setArgumentGroupName("Request Control Arguments"); 701 proxyAs.addLongIdentifier("proxy-as", true); 702 parser.addArgument(proxyAs); 703 704 705 description = "Indicates that search requests should include the " + 706 "specified request control. This may be provided multiple " + 707 "times to include multiple search request controls."; 708 searchControl = new ControlArgument(null, "searchControl", false, 0, null, 709 description); 710 searchControl.setArgumentGroupName("Request Control Arguments"); 711 searchControl.addLongIdentifier("search-control", true); 712 parser.addArgument(searchControl); 713 714 715 description = "Indicates that modify requests should include the " + 716 "specified request control. This may be provided multiple " + 717 "times to include multiple modify request controls."; 718 modifyControl = new ControlArgument(null, "modifyControl", false, 0, null, 719 description); 720 modifyControl.setArgumentGroupName("Request Control Arguments"); 721 modifyControl.addLongIdentifier("modify-control", true); 722 parser.addArgument(modifyControl); 723 724 725 description = "The number of threads to use to perform the searches. If " + 726 "this is not provided, then a default of one thread will " + 727 "be used."; 728 numThreads = new IntegerArgument('t', "numThreads", true, 1, "{num}", 729 description, 1, Integer.MAX_VALUE, 1); 730 numThreads.setArgumentGroupName("Rate Management Arguments"); 731 numThreads.addLongIdentifier("num-threads", true); 732 parser.addArgument(numThreads); 733 734 735 description = "The length of time in seconds between output lines. If " + 736 "this is not provided, then a default interval of five " + 737 "seconds will be used."; 738 collectionInterval = new IntegerArgument('i', "intervalDuration", true, 1, 739 "{num}", description, 1, 740 Integer.MAX_VALUE, 5); 741 collectionInterval.setArgumentGroupName("Rate Management Arguments"); 742 collectionInterval.addLongIdentifier("interval-duration", true); 743 parser.addArgument(collectionInterval); 744 745 746 description = "The maximum number of intervals for which to run. If " + 747 "this is not provided, then the tool will run until it is " + 748 "interrupted."; 749 numIntervals = new IntegerArgument('I', "numIntervals", true, 1, "{num}", 750 description, 1, Integer.MAX_VALUE, 751 Integer.MAX_VALUE); 752 numIntervals.setArgumentGroupName("Rate Management Arguments"); 753 numIntervals.addLongIdentifier("num-intervals", true); 754 parser.addArgument(numIntervals); 755 756 description = "The number of search and modify iterations that should be " + 757 "processed on a connection before that connection is " + 758 "closed and replaced with a newly-established (and " + 759 "authenticated, if appropriate) connection. If this is " + 760 "not provided, then connections will not be periodically " + 761 "closed and re-established."; 762 iterationsBeforeReconnect = new IntegerArgument(null, 763 "iterationsBeforeReconnect", false, 1, "{num}", description, 0); 764 iterationsBeforeReconnect.setArgumentGroupName("Rate Management Arguments"); 765 iterationsBeforeReconnect.addLongIdentifier("iterations-before-reconnect", 766 true); 767 parser.addArgument(iterationsBeforeReconnect); 768 769 description = "The target number of searches to perform per second. It " + 770 "is still necessary to specify a sufficient number of " + 771 "threads for achieving this rate. If neither this option " + 772 "nor --variableRateData is provided, then the tool will " + 773 "run at the maximum rate for the specified number of " + 774 "threads."; 775 ratePerSecond = new IntegerArgument('r', "ratePerSecond", false, 1, 776 "{searches-per-second}", description, 777 1, Integer.MAX_VALUE); 778 ratePerSecond.setArgumentGroupName("Rate Management Arguments"); 779 ratePerSecond.addLongIdentifier("rate-per-second", true); 780 parser.addArgument(ratePerSecond); 781 782 final String variableRateDataArgName = "variableRateData"; 783 final String generateSampleRateFileArgName = "generateSampleRateFile"; 784 description = RateAdjustor.getVariableRateDataArgumentDescription( 785 generateSampleRateFileArgName); 786 variableRateData = new FileArgument(null, variableRateDataArgName, false, 1, 787 "{path}", description, true, true, true, 788 false); 789 variableRateData.setArgumentGroupName("Rate Management Arguments"); 790 variableRateData.addLongIdentifier("variable-rate-data", true); 791 parser.addArgument(variableRateData); 792 793 description = RateAdjustor.getGenerateSampleVariableRateFileDescription( 794 variableRateDataArgName); 795 sampleRateFile = new FileArgument(null, generateSampleRateFileArgName, 796 false, 1, "{path}", description, false, 797 true, true, false); 798 sampleRateFile.setArgumentGroupName("Rate Management Arguments"); 799 sampleRateFile.addLongIdentifier("generate-sample-rate-file", true); 800 sampleRateFile.setUsageArgument(true); 801 parser.addArgument(sampleRateFile); 802 parser.addExclusiveArgumentSet(variableRateData, sampleRateFile); 803 804 description = "The number of intervals to complete before beginning " + 805 "overall statistics collection. Specifying a nonzero " + 806 "number of warm-up intervals gives the client and server " + 807 "a chance to warm up without skewing performance results."; 808 warmUpIntervals = new IntegerArgument(null, "warmUpIntervals", true, 1, 809 "{num}", description, 0, Integer.MAX_VALUE, 0); 810 warmUpIntervals.setArgumentGroupName("Rate Management Arguments"); 811 warmUpIntervals.addLongIdentifier("warm-up-intervals", true); 812 parser.addArgument(warmUpIntervals); 813 814 description = "Indicates the format to use for timestamps included in " + 815 "the output. A value of 'none' indicates that no " + 816 "timestamps should be included. A value of 'with-date' " + 817 "indicates that both the date and the time should be " + 818 "included. A value of 'without-date' indicates that only " + 819 "the time should be included."; 820 final Set<String> allowedFormats = 821 StaticUtils.setOf("none", "with-date", "without-date"); 822 timestampFormat = new StringArgument(null, "timestampFormat", true, 1, 823 "{format}", description, allowedFormats, "none"); 824 timestampFormat.addLongIdentifier("timestamp-format", true); 825 parser.addArgument(timestampFormat); 826 827 description = "Indicates that information about the result codes for " + 828 "failed operations should not be displayed."; 829 suppressErrors = new BooleanArgument(null, 830 "suppressErrorResultCodes", 1, description); 831 suppressErrors.addLongIdentifier("suppress-error-result-codes", true); 832 parser.addArgument(suppressErrors); 833 834 description = "Generate output in CSV format rather than a " + 835 "display-friendly format"; 836 csvFormat = new BooleanArgument('c', "csv", 1, description); 837 parser.addArgument(csvFormat); 838 839 description = "Specifies the seed to use for the random number generator."; 840 randomSeed = new IntegerArgument('R', "randomSeed", false, 1, "{value}", 841 description); 842 randomSeed.addLongIdentifier("random-seed", true); 843 parser.addArgument(randomSeed); 844 } 845 846 847 848 /** 849 * Indicates whether this tool supports creating connections to multiple 850 * servers. If it is to support multiple servers, then the "--hostname" and 851 * "--port" arguments will be allowed to be provided multiple times, and 852 * will be required to be provided the same number of times. The same type of 853 * communication security and bind credentials will be used for all servers. 854 * 855 * @return {@code true} if this tool supports creating connections to 856 * multiple servers, or {@code false} if not. 857 */ 858 @Override() 859 protected boolean supportsMultipleServers() 860 { 861 return true; 862 } 863 864 865 866 /** 867 * Retrieves the connection options that should be used for connections 868 * created for use with this tool. 869 * 870 * @return The connection options that should be used for connections created 871 * for use with this tool. 872 */ 873 @Override() 874 @NotNull() 875 public LDAPConnectionOptions getConnectionOptions() 876 { 877 final LDAPConnectionOptions options = new LDAPConnectionOptions(); 878 options.setUseSynchronousMode(true); 879 return options; 880 } 881 882 883 884 /** 885 * Performs the actual processing for this tool. In this case, it gets a 886 * connection to the directory server and uses it to perform the requested 887 * searches. 888 * 889 * @return The result code for the processing that was performed. 890 */ 891 @Override() 892 @NotNull() 893 public ResultCode doToolProcessing() 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(), 1189 runningThreads, barrier, searchCounter, modCounter, searchDurations, 1190 modDurations, 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 while (true) 1427 { 1428 final int stillRunning = runningThreads.get(); 1429 if (stillRunning <= 0) 1430 { 1431 break; 1432 } 1433 else 1434 { 1435 try 1436 { 1437 Thread.sleep(1L); 1438 } catch (final Exception e) {} 1439 } 1440 } 1441 } 1442 1443 1444 1445 /** 1446 * {@inheritDoc} 1447 */ 1448 @Override() 1449 @NotNull() 1450 public LinkedHashMap<String[],String> getExampleUsages() 1451 { 1452 final LinkedHashMap<String[],String> examples = 1453 new LinkedHashMap<>(StaticUtils.computeMapCapacity(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}