001/* 002 * Copyright 2010-2024 Ping Identity Corporation 003 * All Rights Reserved. 004 */ 005/* 006 * Copyright 2010-2024 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-2024 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 this tool supports the ability to generate a debug log 516 * file. If this method returns {@code true}, then the tool will expose 517 * additional arguments that can control debug logging. 518 * 519 * @return {@code true} if this tool supports the ability to generate a debug 520 * log file, or {@code false} if not. 521 */ 522 @Override() 523 protected boolean supportsDebugLogging() 524 { 525 return true; 526 } 527 528 529 530 /** 531 * Indicates whether the LDAP-specific arguments should include alternate 532 * versions of all long identifiers that consist of multiple words so that 533 * they are available in both camelCase and dash-separated versions. 534 * 535 * @return {@code true} if this tool should provide multiple versions of 536 * long identifiers for LDAP-specific arguments, or {@code false} if 537 * not. 538 */ 539 @Override() 540 protected boolean includeAlternateLongIdentifiers() 541 { 542 return true; 543 } 544 545 546 547 /** 548 * {@inheritDoc} 549 */ 550 @Override() 551 protected boolean logToolInvocationByDefault() 552 { 553 return true; 554 } 555 556 557 558 /** 559 * Adds the arguments used by this program that aren't already provided by the 560 * generic {@code LDAPCommandLineTool} framework. 561 * 562 * @param parser The argument parser to which the arguments should be added. 563 * 564 * @throws ArgumentException If a problem occurs while adding the arguments. 565 */ 566 @Override() 567 public void addNonLDAPArguments(@NotNull final ArgumentParser parser) 568 throws ArgumentException 569 { 570 String description = "The base DN to use for the searches. It may be a " + 571 "simple DN or a value pattern to specify a range of DNs (e.g., " + 572 "\"uid=user.[1-1000],ou=People,dc=example,dc=com\"). See " + 573 ValuePattern.PUBLIC_JAVADOC_URL + " for complete details about the " + 574 "value pattern syntax. This must be provided."; 575 baseDN = new StringArgument('b', "baseDN", true, 1, "{dn}", description); 576 baseDN.setArgumentGroupName("Search And Modification Arguments"); 577 baseDN.addLongIdentifier("base-dn", true); 578 parser.addArgument(baseDN); 579 580 581 description = "The scope to use for the searches. It should be 'base', " + 582 "'one', 'sub', or 'subord'. If this is not provided, then " + 583 "a default scope of 'sub' will be used."; 584 scopeArg = new ScopeArgument('s', "scope", false, "{scope}", description, 585 SearchScope.SUB); 586 scopeArg.setArgumentGroupName("Search And Modification Arguments"); 587 parser.addArgument(scopeArg); 588 589 590 description = "The filter to use for the searches. It may be a simple " + 591 "filter or a value pattern to specify a range of filters " + 592 "(e.g., \"(uid=user.[1-1000])\"). See " + 593 ValuePattern.PUBLIC_JAVADOC_URL + " for complete details " + 594 "about the value pattern syntax. This must be provided."; 595 filter = new StringArgument('f', "filter", true, 1, "{filter}", 596 description); 597 filter.setArgumentGroupName("Search And Modification Arguments"); 598 parser.addArgument(filter); 599 600 601 description = "The name of an attribute to include in entries returned " + 602 "from the searches. Multiple attributes may be requested " + 603 "by providing this argument multiple times. If no request " + 604 "attributes are provided, then the entries returned will " + 605 "include all user attributes."; 606 returnAttributes = new StringArgument('A', "attribute", false, 0, "{name}", 607 description); 608 returnAttributes.setArgumentGroupName("Search And Modification Arguments"); 609 parser.addArgument(returnAttributes); 610 611 612 description = "The name of the attribute to modify. Multiple attributes " + 613 "may be specified by providing this argument multiple " + 614 "times. At least one attribute must be specified."; 615 modifyAttributes = new StringArgument('m', "modifyAttribute", true, 0, 616 "{name}", description); 617 modifyAttributes.setArgumentGroupName("Search And Modification Arguments"); 618 modifyAttributes.addLongIdentifier("modify-attribute", true); 619 parser.addArgument(modifyAttributes); 620 621 622 description = "The length in bytes to use when generating values for the " + 623 "modifications. If this is not provided, then a default " + 624 "length of ten bytes will be used."; 625 valueLength = new IntegerArgument('l', "valueLength", true, 1, "{num}", 626 description, 1, Integer.MAX_VALUE, 10); 627 valueLength.setArgumentGroupName("Search And Modification Arguments"); 628 valueLength.addLongIdentifier("value-length", true); 629 parser.addArgument(valueLength); 630 631 632 description = "The set of characters to use to generate the values for " + 633 "the modifications. It should only include ASCII " + 634 "characters. If this is not provided, then a default set " + 635 "of lowercase alphabetic characters will be used."; 636 characterSet = new StringArgument('C', "characterSet", true, 1, "{chars}", 637 description, 638 "abcdefghijklmnopqrstuvwxyz"); 639 characterSet.setArgumentGroupName("Search And Modification Arguments"); 640 characterSet.addLongIdentifier("character-set", true); 641 parser.addArgument(characterSet); 642 643 644 description = "Indicates that search requests should include the " + 645 "assertion request control with the specified filter."; 646 searchAssertionFilter = new FilterArgument(null, "searchAssertionFilter", 647 false, 1, "{filter}", 648 description); 649 searchAssertionFilter.setArgumentGroupName("Request Control Arguments"); 650 searchAssertionFilter.addLongIdentifier("search-assertion-filter", true); 651 parser.addArgument(searchAssertionFilter); 652 653 654 description = "Indicates that modify requests should include the " + 655 "assertion request control with the specified filter."; 656 modifyAssertionFilter = new FilterArgument(null, "modifyAssertionFilter", 657 false, 1, "{filter}", 658 description); 659 modifyAssertionFilter.setArgumentGroupName("Request Control Arguments"); 660 modifyAssertionFilter.addLongIdentifier("modify-assertion-filter", true); 661 parser.addArgument(modifyAssertionFilter); 662 663 664 description = "Indicates that search requests should include the simple " + 665 "paged results control with the specified page size."; 666 simplePageSize = new IntegerArgument(null, "simplePageSize", false, 1, 667 "{size}", description, 1, 668 Integer.MAX_VALUE); 669 simplePageSize.setArgumentGroupName("Request Control Arguments"); 670 simplePageSize.addLongIdentifier("simple-page-size", true); 671 parser.addArgument(simplePageSize); 672 673 674 description = "Indicates that modify requests should include the " + 675 "permissive modify request control."; 676 permissiveModify = new BooleanArgument(null, "permissiveModify", 1, 677 description); 678 permissiveModify.setArgumentGroupName("Request Control Arguments"); 679 permissiveModify.addLongIdentifier("permissive-modify", true); 680 parser.addArgument(permissiveModify); 681 682 683 description = "Indicates that modify requests should include the " + 684 "pre-read request control with the specified requested " + 685 "attribute. This argument may be provided multiple times " + 686 "to indicate that multiple requested attributes should be " + 687 "included in the pre-read request control."; 688 preReadAttribute = new StringArgument(null, "preReadAttribute", false, 0, 689 "{attribute}", description); 690 preReadAttribute.setArgumentGroupName("Request Control Arguments"); 691 preReadAttribute.addLongIdentifier("pre-read-attribute", true); 692 parser.addArgument(preReadAttribute); 693 694 695 description = "Indicates that modify requests should include the " + 696 "post-read request control with the specified requested " + 697 "attribute. This argument may be provided multiple times " + 698 "to indicate that multiple requested attributes should be " + 699 "included in the post-read request control."; 700 postReadAttribute = new StringArgument(null, "postReadAttribute", false, 0, 701 "{attribute}", description); 702 postReadAttribute.setArgumentGroupName("Request Control Arguments"); 703 postReadAttribute.addLongIdentifier("post-read-attribute", true); 704 parser.addArgument(postReadAttribute); 705 706 707 description = "Indicates that the proxied authorization control (as " + 708 "defined in RFC 4370) should be used to request that " + 709 "operations be processed using an alternate authorization " + 710 "identity. This may be a simple authorization ID or it " + 711 "may be a value pattern to specify a range of " + 712 "identities. See " + ValuePattern.PUBLIC_JAVADOC_URL + 713 " for complete details about the value pattern syntax."; 714 proxyAs = new StringArgument('Y', "proxyAs", false, 1, "{authzID}", 715 description); 716 proxyAs.setArgumentGroupName("Request Control Arguments"); 717 proxyAs.addLongIdentifier("proxy-as", true); 718 parser.addArgument(proxyAs); 719 720 721 description = "Indicates that search requests should include the " + 722 "specified request control. This may be provided multiple " + 723 "times to include multiple search request controls."; 724 searchControl = new ControlArgument(null, "searchControl", false, 0, null, 725 description); 726 searchControl.setArgumentGroupName("Request Control Arguments"); 727 searchControl.addLongIdentifier("search-control", true); 728 parser.addArgument(searchControl); 729 730 731 description = "Indicates that modify requests should include the " + 732 "specified request control. This may be provided multiple " + 733 "times to include multiple modify request controls."; 734 modifyControl = new ControlArgument(null, "modifyControl", false, 0, null, 735 description); 736 modifyControl.setArgumentGroupName("Request Control Arguments"); 737 modifyControl.addLongIdentifier("modify-control", true); 738 parser.addArgument(modifyControl); 739 740 741 description = "The number of threads to use to perform the searches. If " + 742 "this is not provided, then a default of one thread will " + 743 "be used."; 744 numThreads = new IntegerArgument('t', "numThreads", true, 1, "{num}", 745 description, 1, Integer.MAX_VALUE, 1); 746 numThreads.setArgumentGroupName("Rate Management Arguments"); 747 numThreads.addLongIdentifier("num-threads", true); 748 parser.addArgument(numThreads); 749 750 751 description = "The length of time in seconds between output lines. If " + 752 "this is not provided, then a default interval of five " + 753 "seconds will be used."; 754 collectionInterval = new IntegerArgument('i', "intervalDuration", true, 1, 755 "{num}", description, 1, 756 Integer.MAX_VALUE, 5); 757 collectionInterval.setArgumentGroupName("Rate Management Arguments"); 758 collectionInterval.addLongIdentifier("interval-duration", true); 759 parser.addArgument(collectionInterval); 760 761 762 description = "The maximum number of intervals for which to run. If " + 763 "this is not provided, then the tool will run until it is " + 764 "interrupted."; 765 numIntervals = new IntegerArgument('I', "numIntervals", true, 1, "{num}", 766 description, 1, Integer.MAX_VALUE, 767 Integer.MAX_VALUE); 768 numIntervals.setArgumentGroupName("Rate Management Arguments"); 769 numIntervals.addLongIdentifier("num-intervals", true); 770 parser.addArgument(numIntervals); 771 772 description = "The number of search and modify iterations that should be " + 773 "processed on a connection before that connection is " + 774 "closed and replaced with a newly-established (and " + 775 "authenticated, if appropriate) connection. If this is " + 776 "not provided, then connections will not be periodically " + 777 "closed and re-established."; 778 iterationsBeforeReconnect = new IntegerArgument(null, 779 "iterationsBeforeReconnect", false, 1, "{num}", description, 0); 780 iterationsBeforeReconnect.setArgumentGroupName("Rate Management Arguments"); 781 iterationsBeforeReconnect.addLongIdentifier("iterations-before-reconnect", 782 true); 783 parser.addArgument(iterationsBeforeReconnect); 784 785 description = "The target number of searches to perform per second. It " + 786 "is still necessary to specify a sufficient number of " + 787 "threads for achieving this rate. If neither this option " + 788 "nor --variableRateData is provided, then the tool will " + 789 "run at the maximum rate for the specified number of " + 790 "threads."; 791 ratePerSecond = new IntegerArgument('r', "ratePerSecond", false, 1, 792 "{searches-per-second}", description, 793 1, Integer.MAX_VALUE); 794 ratePerSecond.setArgumentGroupName("Rate Management Arguments"); 795 ratePerSecond.addLongIdentifier("rate-per-second", true); 796 parser.addArgument(ratePerSecond); 797 798 final String variableRateDataArgName = "variableRateData"; 799 final String generateSampleRateFileArgName = "generateSampleRateFile"; 800 description = RateAdjustor.getVariableRateDataArgumentDescription( 801 generateSampleRateFileArgName); 802 variableRateData = new FileArgument(null, variableRateDataArgName, false, 1, 803 "{path}", description, true, true, true, 804 false); 805 variableRateData.setArgumentGroupName("Rate Management Arguments"); 806 variableRateData.addLongIdentifier("variable-rate-data", true); 807 parser.addArgument(variableRateData); 808 809 description = RateAdjustor.getGenerateSampleVariableRateFileDescription( 810 variableRateDataArgName); 811 sampleRateFile = new FileArgument(null, generateSampleRateFileArgName, 812 false, 1, "{path}", description, false, 813 true, true, false); 814 sampleRateFile.setArgumentGroupName("Rate Management Arguments"); 815 sampleRateFile.addLongIdentifier("generate-sample-rate-file", true); 816 sampleRateFile.setUsageArgument(true); 817 parser.addArgument(sampleRateFile); 818 parser.addExclusiveArgumentSet(variableRateData, sampleRateFile); 819 820 description = "The number of intervals to complete before beginning " + 821 "overall statistics collection. Specifying a nonzero " + 822 "number of warm-up intervals gives the client and server " + 823 "a chance to warm up without skewing performance results."; 824 warmUpIntervals = new IntegerArgument(null, "warmUpIntervals", true, 1, 825 "{num}", description, 0, Integer.MAX_VALUE, 0); 826 warmUpIntervals.setArgumentGroupName("Rate Management Arguments"); 827 warmUpIntervals.addLongIdentifier("warm-up-intervals", true); 828 parser.addArgument(warmUpIntervals); 829 830 description = "Indicates the format to use for timestamps included in " + 831 "the output. A value of 'none' indicates that no " + 832 "timestamps should be included. A value of 'with-date' " + 833 "indicates that both the date and the time should be " + 834 "included. A value of 'without-date' indicates that only " + 835 "the time should be included."; 836 final Set<String> allowedFormats = 837 StaticUtils.setOf("none", "with-date", "without-date"); 838 timestampFormat = new StringArgument(null, "timestampFormat", true, 1, 839 "{format}", description, allowedFormats, "none"); 840 timestampFormat.addLongIdentifier("timestamp-format", true); 841 parser.addArgument(timestampFormat); 842 843 description = "Indicates that information about the result codes for " + 844 "failed operations should not be displayed."; 845 suppressErrors = new BooleanArgument(null, 846 "suppressErrorResultCodes", 1, description); 847 suppressErrors.addLongIdentifier("suppress-error-result-codes", true); 848 parser.addArgument(suppressErrors); 849 850 description = "Generate output in CSV format rather than a " + 851 "display-friendly format"; 852 csvFormat = new BooleanArgument('c', "csv", 1, description); 853 parser.addArgument(csvFormat); 854 855 description = "Specifies the seed to use for the random number generator."; 856 randomSeed = new IntegerArgument('R', "randomSeed", false, 1, "{value}", 857 description); 858 randomSeed.addLongIdentifier("random-seed", true); 859 parser.addArgument(randomSeed); 860 } 861 862 863 864 /** 865 * Indicates whether this tool supports creating connections to multiple 866 * servers. If it is to support multiple servers, then the "--hostname" and 867 * "--port" arguments will be allowed to be provided multiple times, and 868 * will be required to be provided the same number of times. The same type of 869 * communication security and bind credentials will be used for all servers. 870 * 871 * @return {@code true} if this tool supports creating connections to 872 * multiple servers, or {@code false} if not. 873 */ 874 @Override() 875 protected boolean supportsMultipleServers() 876 { 877 return true; 878 } 879 880 881 882 /** 883 * Retrieves the connection options that should be used for connections 884 * created for use with this tool. 885 * 886 * @return The connection options that should be used for connections created 887 * for use with this tool. 888 */ 889 @Override() 890 @NotNull() 891 public LDAPConnectionOptions getConnectionOptions() 892 { 893 final LDAPConnectionOptions options = new LDAPConnectionOptions(); 894 options.setUseSynchronousMode(true); 895 return options; 896 } 897 898 899 900 /** 901 * Performs the actual processing for this tool. In this case, it gets a 902 * connection to the directory server and uses it to perform the requested 903 * searches. 904 * 905 * @return The result code for the processing that was performed. 906 */ 907 @Override() 908 @NotNull() 909 public ResultCode doToolProcessing() 910 { 911 // If the sample rate file argument was specified, then generate the sample 912 // variable rate data file and return. 913 if (sampleRateFile.isPresent()) 914 { 915 try 916 { 917 RateAdjustor.writeSampleVariableRateFile(sampleRateFile.getValue()); 918 return ResultCode.SUCCESS; 919 } 920 catch (final Exception e) 921 { 922 Debug.debugException(e); 923 err("An error occurred while trying to write sample variable data " + 924 "rate file '", sampleRateFile.getValue().getAbsolutePath(), 925 "': ", StaticUtils.getExceptionMessage(e)); 926 return ResultCode.LOCAL_ERROR; 927 } 928 } 929 930 931 // Determine the random seed to use. 932 final Long seed; 933 if (randomSeed.isPresent()) 934 { 935 seed = Long.valueOf(randomSeed.getValue()); 936 } 937 else 938 { 939 seed = null; 940 } 941 942 // Create value patterns for the base DN, filter, and proxied authorization 943 // DN. 944 final ValuePattern dnPattern; 945 try 946 { 947 dnPattern = new ValuePattern(baseDN.getValue(), seed); 948 } 949 catch (final ParseException pe) 950 { 951 Debug.debugException(pe); 952 err("Unable to parse the base DN value pattern: ", pe.getMessage()); 953 return ResultCode.PARAM_ERROR; 954 } 955 956 final ValuePattern filterPattern; 957 try 958 { 959 filterPattern = new ValuePattern(filter.getValue(), seed); 960 } 961 catch (final ParseException pe) 962 { 963 Debug.debugException(pe); 964 err("Unable to parse the filter pattern: ", pe.getMessage()); 965 return ResultCode.PARAM_ERROR; 966 } 967 968 final ValuePattern authzIDPattern; 969 if (proxyAs.isPresent()) 970 { 971 try 972 { 973 authzIDPattern = new ValuePattern(proxyAs.getValue(), seed); 974 } 975 catch (final ParseException pe) 976 { 977 Debug.debugException(pe); 978 err("Unable to parse the proxied authorization pattern: ", 979 pe.getMessage()); 980 return ResultCode.PARAM_ERROR; 981 } 982 } 983 else 984 { 985 authzIDPattern = null; 986 } 987 988 989 // Get the set of controls to include in search requests. 990 final ArrayList<Control> searchControls = new ArrayList<>(5); 991 if (searchAssertionFilter.isPresent()) 992 { 993 searchControls.add(new AssertionRequestControl( 994 searchAssertionFilter.getValue())); 995 } 996 997 if (searchControl.isPresent()) 998 { 999 searchControls.addAll(searchControl.getValues()); 1000 } 1001 1002 1003 // Get the set of controls to include in modify requests. 1004 final ArrayList<Control> modifyControls = new ArrayList<>(5); 1005 if (modifyAssertionFilter.isPresent()) 1006 { 1007 modifyControls.add(new AssertionRequestControl( 1008 modifyAssertionFilter.getValue())); 1009 } 1010 1011 if (permissiveModify.isPresent()) 1012 { 1013 modifyControls.add(new PermissiveModifyRequestControl()); 1014 } 1015 1016 if (preReadAttribute.isPresent()) 1017 { 1018 final List<String> attrList = preReadAttribute.getValues(); 1019 final String[] attrArray = new String[attrList.size()]; 1020 attrList.toArray(attrArray); 1021 modifyControls.add(new PreReadRequestControl(attrArray)); 1022 } 1023 1024 if (postReadAttribute.isPresent()) 1025 { 1026 final List<String> attrList = postReadAttribute.getValues(); 1027 final String[] attrArray = new String[attrList.size()]; 1028 attrList.toArray(attrArray); 1029 modifyControls.add(new PostReadRequestControl(attrArray)); 1030 } 1031 1032 if (modifyControl.isPresent()) 1033 { 1034 modifyControls.addAll(modifyControl.getValues()); 1035 } 1036 1037 1038 // Get the attributes to return. 1039 final String[] returnAttrs; 1040 if (returnAttributes.isPresent()) 1041 { 1042 final List<String> attrList = returnAttributes.getValues(); 1043 returnAttrs = new String[attrList.size()]; 1044 attrList.toArray(returnAttrs); 1045 } 1046 else 1047 { 1048 returnAttrs = StaticUtils.NO_STRINGS; 1049 } 1050 1051 1052 // Get the names of the attributes to modify. 1053 final String[] modAttrs = new String[modifyAttributes.getValues().size()]; 1054 modifyAttributes.getValues().toArray(modAttrs); 1055 1056 1057 // Get the character set as a byte array. 1058 final byte[] charSet = StaticUtils.getBytes(characterSet.getValue()); 1059 1060 1061 // If the --ratePerSecond option was specified, then limit the rate 1062 // accordingly. 1063 FixedRateBarrier fixedRateBarrier = null; 1064 if (ratePerSecond.isPresent() || variableRateData.isPresent()) 1065 { 1066 // We might not have a rate per second if --variableRateData is specified. 1067 // The rate typically doesn't matter except when we have warm-up 1068 // intervals. In this case, we'll run at the max rate. 1069 final int intervalSeconds = collectionInterval.getValue(); 1070 final int ratePerInterval = 1071 (ratePerSecond.getValue() == null) 1072 ? Integer.MAX_VALUE 1073 : ratePerSecond.getValue() * intervalSeconds; 1074 fixedRateBarrier = 1075 new FixedRateBarrier(1000L * intervalSeconds, ratePerInterval); 1076 } 1077 1078 1079 // If --variableRateData was specified, then initialize a RateAdjustor. 1080 RateAdjustor rateAdjustor = null; 1081 if (variableRateData.isPresent()) 1082 { 1083 try 1084 { 1085 rateAdjustor = RateAdjustor.newInstance(fixedRateBarrier, 1086 ratePerSecond.getValue(), variableRateData.getValue()); 1087 } 1088 catch (final IOException | IllegalArgumentException e) 1089 { 1090 Debug.debugException(e); 1091 err("Initializing the variable rates failed: " + e.getMessage()); 1092 return ResultCode.PARAM_ERROR; 1093 } 1094 } 1095 1096 1097 // Determine whether to include timestamps in the output and if so what 1098 // format should be used for them. 1099 final boolean includeTimestamp; 1100 final String timeFormat; 1101 if (timestampFormat.getValue().equalsIgnoreCase("with-date")) 1102 { 1103 includeTimestamp = true; 1104 timeFormat = "dd/MM/yyyy HH:mm:ss"; 1105 } 1106 else if (timestampFormat.getValue().equalsIgnoreCase("without-date")) 1107 { 1108 includeTimestamp = true; 1109 timeFormat = "HH:mm:ss"; 1110 } 1111 else 1112 { 1113 includeTimestamp = false; 1114 timeFormat = null; 1115 } 1116 1117 1118 // Determine whether any warm-up intervals should be run. 1119 final long totalIntervals; 1120 final boolean warmUp; 1121 int remainingWarmUpIntervals = warmUpIntervals.getValue(); 1122 if (remainingWarmUpIntervals > 0) 1123 { 1124 warmUp = true; 1125 totalIntervals = 0L + numIntervals.getValue() + remainingWarmUpIntervals; 1126 } 1127 else 1128 { 1129 warmUp = true; 1130 totalIntervals = 0L + numIntervals.getValue(); 1131 } 1132 1133 1134 // Create the table that will be used to format the output. 1135 final OutputFormat outputFormat; 1136 if (csvFormat.isPresent()) 1137 { 1138 outputFormat = OutputFormat.CSV; 1139 } 1140 else 1141 { 1142 outputFormat = OutputFormat.COLUMNS; 1143 } 1144 1145 final ColumnFormatter formatter = new ColumnFormatter(includeTimestamp, 1146 timeFormat, outputFormat, " ", 1147 new FormattableColumn(12, HorizontalAlignment.RIGHT, "Recent", 1148 "Searches/Sec"), 1149 new FormattableColumn(12, HorizontalAlignment.RIGHT, "Recent", 1150 "Srch Dur ms"), 1151 new FormattableColumn(12, HorizontalAlignment.RIGHT, "Recent", 1152 "Mods/Sec"), 1153 new FormattableColumn(12, HorizontalAlignment.RIGHT, "Recent", 1154 "Mod Dur ms"), 1155 new FormattableColumn(12, HorizontalAlignment.RIGHT, "Recent", 1156 "Errors/Sec"), 1157 new FormattableColumn(12, HorizontalAlignment.RIGHT, "Overall", 1158 "Searches/Sec"), 1159 new FormattableColumn(12, HorizontalAlignment.RIGHT, "Overall", 1160 "Srch Dur ms"), 1161 new FormattableColumn(12, HorizontalAlignment.RIGHT, "Overall", 1162 "Mods/Sec"), 1163 new FormattableColumn(12, HorizontalAlignment.RIGHT, "Overall", 1164 "Mod Dur ms")); 1165 1166 1167 // Create values to use for statistics collection. 1168 final AtomicLong searchCounter = new AtomicLong(0L); 1169 final AtomicLong errorCounter = new AtomicLong(0L); 1170 final AtomicLong modCounter = new AtomicLong(0L); 1171 final AtomicLong modDurations = new AtomicLong(0L); 1172 final AtomicLong searchDurations = new AtomicLong(0L); 1173 final ResultCodeCounter rcCounter = new ResultCodeCounter(); 1174 1175 1176 // Determine the length of each interval in milliseconds. 1177 final long intervalMillis = 1000L * collectionInterval.getValue(); 1178 1179 1180 // Create the threads to use for the searches. 1181 final Random random = new Random(); 1182 final CyclicBarrier barrier = new CyclicBarrier(numThreads.getValue() + 1); 1183 final SearchAndModRateThread[] threads = 1184 new SearchAndModRateThread[numThreads.getValue()]; 1185 for (int i=0; i < threads.length; i++) 1186 { 1187 final LDAPConnection connection; 1188 try 1189 { 1190 connection = getConnection(); 1191 } 1192 catch (final LDAPException le) 1193 { 1194 Debug.debugException(le); 1195 err("Unable to connect to the directory server: ", 1196 StaticUtils.getExceptionMessage(le)); 1197 return le.getResultCode(); 1198 } 1199 1200 threads[i] = new SearchAndModRateThread(this, i, connection, dnPattern, 1201 scopeArg.getValue(), filterPattern, returnAttrs, modAttrs, 1202 valueLength.getValue(), charSet, authzIDPattern, 1203 simplePageSize.getValue(), searchControls, modifyControls, 1204 iterationsBeforeReconnect.getValue(), random.nextLong(), 1205 runningThreads, barrier, searchCounter, modCounter, searchDurations, 1206 modDurations, errorCounter, rcCounter, fixedRateBarrier); 1207 threads[i].start(); 1208 } 1209 1210 1211 // Display the table header. 1212 for (final String headerLine : formatter.getHeaderLines(true)) 1213 { 1214 out(headerLine); 1215 } 1216 1217 1218 // Start the RateAdjustor before the threads so that the initial value is 1219 // in place before any load is generated unless we're doing a warm-up in 1220 // which case, we'll start it after the warm-up is complete. 1221 if ((rateAdjustor != null) && (remainingWarmUpIntervals <= 0)) 1222 { 1223 rateAdjustor.start(); 1224 } 1225 1226 1227 // Indicate that the threads can start running. 1228 try 1229 { 1230 barrier.await(); 1231 } 1232 catch (final Exception e) 1233 { 1234 Debug.debugException(e); 1235 } 1236 1237 long overallStartTime = System.nanoTime(); 1238 long nextIntervalStartTime = System.currentTimeMillis() + intervalMillis; 1239 1240 1241 boolean setOverallStartTime = false; 1242 long lastSearchDuration = 0L; 1243 long lastModDuration = 0L; 1244 long lastNumErrors = 0L; 1245 long lastNumSearches = 0L; 1246 long lastNumMods = 0L; 1247 long lastEndTime = System.nanoTime(); 1248 for (long i=0; i < totalIntervals; i++) 1249 { 1250 if (rateAdjustor != null) 1251 { 1252 if (! rateAdjustor.isAlive()) 1253 { 1254 out("All of the rates in " + variableRateData.getValue().getName() + 1255 " have been completed."); 1256 break; 1257 } 1258 } 1259 1260 final long startTimeMillis = System.currentTimeMillis(); 1261 final long sleepTimeMillis = nextIntervalStartTime - startTimeMillis; 1262 nextIntervalStartTime += intervalMillis; 1263 if (sleepTimeMillis > 0) 1264 { 1265 sleeper.sleep(sleepTimeMillis); 1266 } 1267 1268 if (stopRequested.get()) 1269 { 1270 break; 1271 } 1272 1273 final long endTime = System.nanoTime(); 1274 final long intervalDuration = endTime - lastEndTime; 1275 1276 final long numSearches; 1277 final long numMods; 1278 final long numErrors; 1279 final long totalSearchDuration; 1280 final long totalModDuration; 1281 if (warmUp && (remainingWarmUpIntervals > 0)) 1282 { 1283 numSearches = searchCounter.getAndSet(0L); 1284 numMods = modCounter.getAndSet(0L); 1285 numErrors = errorCounter.getAndSet(0L); 1286 totalSearchDuration = searchDurations.getAndSet(0L); 1287 totalModDuration = modDurations.getAndSet(0L); 1288 } 1289 else 1290 { 1291 numSearches = searchCounter.get(); 1292 numMods = modCounter.get(); 1293 numErrors = errorCounter.get(); 1294 totalSearchDuration = searchDurations.get(); 1295 totalModDuration = modDurations.get(); 1296 } 1297 1298 final long recentNumSearches = numSearches - lastNumSearches; 1299 final long recentNumMods = numMods - lastNumMods; 1300 final long recentNumErrors = numErrors - lastNumErrors; 1301 final long recentSearchDuration = 1302 totalSearchDuration - lastSearchDuration; 1303 final long recentModDuration = totalModDuration - lastModDuration; 1304 1305 final double numSeconds = intervalDuration / 1_000_000_000.0d; 1306 final double recentSearchRate = recentNumSearches / numSeconds; 1307 final double recentModRate = recentNumMods / numSeconds; 1308 final double recentErrorRate = recentNumErrors / numSeconds; 1309 1310 final double recentAvgSearchDuration; 1311 if (recentNumSearches > 0L) 1312 { 1313 recentAvgSearchDuration = 1314 1.0d * recentSearchDuration / recentNumSearches / 1_000_000; 1315 } 1316 else 1317 { 1318 recentAvgSearchDuration = 0.0d; 1319 } 1320 1321 final double recentAvgModDuration; 1322 if (recentNumMods > 0L) 1323 { 1324 recentAvgModDuration = 1325 1.0d * recentModDuration / recentNumMods / 1_000_000; 1326 } 1327 else 1328 { 1329 recentAvgModDuration = 0.0d; 1330 } 1331 1332 if (warmUp && (remainingWarmUpIntervals > 0)) 1333 { 1334 out(formatter.formatRow(recentSearchRate, recentAvgSearchDuration, 1335 recentModRate, recentAvgModDuration, recentErrorRate, "warming up", 1336 "warming up", "warming up", "warming up")); 1337 1338 remainingWarmUpIntervals--; 1339 if (remainingWarmUpIntervals == 0) 1340 { 1341 out("Warm-up completed. Beginning overall statistics collection."); 1342 setOverallStartTime = true; 1343 if (rateAdjustor != null) 1344 { 1345 rateAdjustor.start(); 1346 } 1347 } 1348 } 1349 else 1350 { 1351 if (setOverallStartTime) 1352 { 1353 overallStartTime = lastEndTime; 1354 setOverallStartTime = false; 1355 } 1356 1357 final double numOverallSeconds = 1358 (endTime - overallStartTime) / 1_000_000_000.0d; 1359 final double overallSearchRate = numSearches / numOverallSeconds; 1360 final double overallModRate = numMods / numOverallSeconds; 1361 1362 final double overallAvgSearchDuration; 1363 if (numSearches > 0L) 1364 { 1365 overallAvgSearchDuration = 1366 1.0d * totalSearchDuration / numSearches / 1_000_000; 1367 } 1368 else 1369 { 1370 overallAvgSearchDuration = 0.0d; 1371 } 1372 1373 final double overallAvgModDuration; 1374 if (numMods > 0L) 1375 { 1376 overallAvgModDuration = 1377 1.0d * totalModDuration / numMods / 1_000_000; 1378 } 1379 else 1380 { 1381 overallAvgModDuration = 0.0d; 1382 } 1383 1384 out(formatter.formatRow(recentSearchRate, recentAvgSearchDuration, 1385 recentModRate, recentAvgModDuration, recentErrorRate, 1386 overallSearchRate, overallAvgSearchDuration, overallModRate, 1387 overallAvgModDuration)); 1388 1389 lastNumSearches = numSearches; 1390 lastNumMods = numMods; 1391 lastNumErrors = numErrors; 1392 lastSearchDuration = totalSearchDuration; 1393 lastModDuration = totalModDuration; 1394 } 1395 1396 final List<ObjectPair<ResultCode,Long>> rcCounts = 1397 rcCounter.getCounts(true); 1398 if ((! suppressErrors.isPresent()) && (! rcCounts.isEmpty())) 1399 { 1400 err("\tError Results:"); 1401 for (final ObjectPair<ResultCode,Long> p : rcCounts) 1402 { 1403 err("\t", p.getFirst().getName(), ": ", p.getSecond()); 1404 } 1405 } 1406 1407 lastEndTime = endTime; 1408 } 1409 1410 1411 // Shut down the RateAdjustor if we have one. 1412 if (rateAdjustor != null) 1413 { 1414 rateAdjustor.shutDown(); 1415 } 1416 1417 // Stop all of the threads. 1418 ResultCode resultCode = ResultCode.SUCCESS; 1419 for (final SearchAndModRateThread t : threads) 1420 { 1421 final ResultCode r = t.stopRunning(); 1422 if (resultCode == ResultCode.SUCCESS) 1423 { 1424 resultCode = r; 1425 } 1426 } 1427 1428 return resultCode; 1429 } 1430 1431 1432 1433 /** 1434 * Requests that this tool stop running. This method will attempt to wait 1435 * for all threads to complete before returning control to the caller. 1436 */ 1437 public void stopRunning() 1438 { 1439 stopRequested.set(true); 1440 sleeper.wakeup(); 1441 1442 while (true) 1443 { 1444 final int stillRunning = runningThreads.get(); 1445 if (stillRunning <= 0) 1446 { 1447 break; 1448 } 1449 else 1450 { 1451 try 1452 { 1453 Thread.sleep(1L); 1454 } catch (final Exception e) {} 1455 } 1456 } 1457 } 1458 1459 1460 1461 /** 1462 * {@inheritDoc} 1463 */ 1464 @Override() 1465 @NotNull() 1466 public LinkedHashMap<String[],String> getExampleUsages() 1467 { 1468 final LinkedHashMap<String[],String> examples = 1469 new LinkedHashMap<>(StaticUtils.computeMapCapacity(2)); 1470 1471 String[] args = 1472 { 1473 "--hostname", "server.example.com", 1474 "--port", "389", 1475 "--bindDN", "uid=admin,dc=example,dc=com", 1476 "--bindPassword", "password", 1477 "--baseDN", "dc=example,dc=com", 1478 "--scope", "sub", 1479 "--filter", "(uid=user.[1-1000000])", 1480 "--attribute", "givenName", 1481 "--attribute", "sn", 1482 "--attribute", "mail", 1483 "--modifyAttribute", "description", 1484 "--valueLength", "10", 1485 "--characterSet", "abcdefghijklmnopqrstuvwxyz0123456789", 1486 "--numThreads", "10" 1487 }; 1488 String description = 1489 "Test search and modify performance by searching randomly across a " + 1490 "set of one million users located below 'dc=example,dc=com' with " + 1491 "ten concurrent threads. The entries returned to the client will " + 1492 "include the givenName, sn, and mail attributes, and the " + 1493 "description attribute of each entry returned will be replaced " + 1494 "with a string of ten randomly-selected alphanumeric characters."; 1495 examples.put(args, description); 1496 1497 args = new String[] 1498 { 1499 "--generateSampleRateFile", "variable-rate-data.txt" 1500 }; 1501 description = 1502 "Generate a sample variable rate definition file that may be used " + 1503 "in conjunction with the --variableRateData argument. The sample " + 1504 "file will include comments that describe the format for data to be " + 1505 "included in this file."; 1506 examples.put(args, description); 1507 1508 return examples; 1509 } 1510}