001/* 002 * Copyright 2008-2014 UnboundID Corp. 003 * All Rights Reserved. 004 */ 005/* 006 * Copyright (C) 2008-2014 UnboundID Corp. 007 * 008 * This program is free software; you can redistribute it and/or modify 009 * it under the terms of the GNU General Public License (GPLv2 only) 010 * or the terms of the GNU Lesser General Public License (LGPLv2.1 only) 011 * as published by the Free Software Foundation. 012 * 013 * This program is distributed in the hope that it will be useful, 014 * but WITHOUT ANY WARRANTY; without even the implied warranty of 015 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 016 * GNU General Public License for more details. 017 * 018 * You should have received a copy of the GNU General Public License 019 * along with this program; if not, see <http://www.gnu.org/licenses>. 020 */ 021package com.unboundid.ldap.sdk.examples; 022 023 024 025import java.io.IOException; 026import java.io.OutputStream; 027import java.io.Serializable; 028import java.text.ParseException; 029import java.util.LinkedHashMap; 030import java.util.LinkedHashSet; 031import java.util.List; 032import java.util.Random; 033import java.util.concurrent.CyclicBarrier; 034import java.util.concurrent.atomic.AtomicBoolean; 035import java.util.concurrent.atomic.AtomicLong; 036 037import com.unboundid.ldap.sdk.LDAPConnection; 038import com.unboundid.ldap.sdk.LDAPConnectionOptions; 039import com.unboundid.ldap.sdk.LDAPException; 040import com.unboundid.ldap.sdk.ResultCode; 041import com.unboundid.ldap.sdk.Version; 042import com.unboundid.util.ColumnFormatter; 043import com.unboundid.util.FixedRateBarrier; 044import com.unboundid.util.FormattableColumn; 045import com.unboundid.util.HorizontalAlignment; 046import com.unboundid.util.LDAPCommandLineTool; 047import com.unboundid.util.ObjectPair; 048import com.unboundid.util.OutputFormat; 049import com.unboundid.util.RateAdjustor; 050import com.unboundid.util.ResultCodeCounter; 051import com.unboundid.util.ThreadSafety; 052import com.unboundid.util.ThreadSafetyLevel; 053import com.unboundid.util.ValuePattern; 054import com.unboundid.util.WakeableSleeper; 055import com.unboundid.util.args.ArgumentException; 056import com.unboundid.util.args.ArgumentParser; 057import com.unboundid.util.args.BooleanArgument; 058import com.unboundid.util.args.FileArgument; 059import com.unboundid.util.args.IntegerArgument; 060import com.unboundid.util.args.StringArgument; 061 062import static com.unboundid.util.Debug.*; 063import static com.unboundid.util.StaticUtils.*; 064 065 066 067/** 068 * This class provides a tool that can be used to perform repeated modifications 069 * in an LDAP directory server using multiple threads. It can help provide an 070 * estimate of the modify performance that a directory server is able to 071 * achieve. The target entry DN may be a value pattern as described in the 072 * {@link ValuePattern} class. This makes it possible to modify a range of 073 * entries rather than repeatedly updating the same entry. 074 * <BR><BR> 075 * Some of the APIs demonstrated by this example include: 076 * <UL> 077 * <LI>Argument Parsing (from the {@code com.unboundid.util.args} 078 * package)</LI> 079 * <LI>LDAP Command-Line Tool (from the {@code com.unboundid.util} 080 * package)</LI> 081 * <LI>LDAP Communication (from the {@code com.unboundid.ldap.sdk} 082 * package)</LI> 083 * <LI>Value Patterns (from the {@code com.unboundid.util} package)</LI> 084 * </UL> 085 * <BR><BR> 086 * All of the necessary information is provided using command line arguments. 087 * Supported arguments include those allowed by the {@link LDAPCommandLineTool} 088 * class, as well as the following additional arguments: 089 * <UL> 090 * <LI>"-b {entryDN}" or "--targetDN {baseDN}" -- specifies the DN of the 091 * entry to be modified. This must be provided. It may be a simple DN, 092 * or it may be a value pattern to express a range of entry DNs.</LI> 093 * <LI>"-A {name}" or "--attribute {name}" -- specifies the name of the 094 * attribute to modify. Multiple attributes may be modified by providing 095 * multiple instances of this argument. At least one attribute must be 096 * provided.</LI> 097 * <LI>"-l {num}" or "--valueLength {num}" -- specifies the length in bytes to 098 * use for the values of the target attributes. If this is not provided, 099 * then a default length of 10 bytes will be used.</LI> 100 * <LI>"-C {chars}" or "--characterSet {chars}" -- specifies the set of 101 * characters that will be used to generate the values to use for the 102 * target attributes. It should only include ASCII characters. Values 103 * will be generated from randomly-selected characters from this set. If 104 * this is not provided, then a default set of lowercase alphabetic 105 * characters will be used.</LI> 106 * <LI>"-t {num}" or "--numThreads {num}" -- specifies the number of 107 * concurrent threads to use when performing the modifications. If this 108 * is not provided, then a default of one thread will be used.</LI> 109 * <LI>"-i {sec}" or "--intervalDuration {sec}" -- specifies the length of 110 * time in seconds between lines out output. If this is not provided, 111 * then a default interval duration of five seconds will be used.</LI> 112 * <LI>"-I {num}" or "--numIntervals {num}" -- specifies the maximum number of 113 * intervals for which to run. If this is not provided, then it will 114 * run forever.</LI> 115 * <LI>"--iterationsBeforeReconnect {num}" -- specifies the number of modify 116 * iterations that should be performed on a connection before that 117 * connection is closed and replaced with a newly-established (and 118 * authenticated, if appropriate) connection.</LI> 119 * <LI>"-r {modifies-per-second}" or "--ratePerSecond {modifies-per-second}" 120 * -- specifies the target number of modifies to perform per second. It 121 * is still necessary to specify a sufficient number of threads for 122 * achieving this rate. If this option is not provided, then the tool 123 * will run at the maximum rate for the specified number of threads.</LI> 124 * <LI>"--variableRateData {path}" -- specifies the path to a file containing 125 * information needed to allow the tool to vary the target rate over time. 126 * If this option is not provided, then the tool will either use a fixed 127 * target rate as specified by the "--ratePerSecond" argument, or it will 128 * run at the maximum rate.</LI> 129 * <LI>"--generateSampleRateFile {path}" -- specifies the path to a file to 130 * which sample data will be written illustrating and describing the 131 * format of the file expected to be used in conjunction with the 132 * "--variableRateData" argument.</LI> 133 * <LI>"--warmUpIntervals {num}" -- specifies the number of intervals to 134 * complete before beginning overall statistics collection.</LI> 135 * <LI>"--timestampFormat {format}" -- specifies the format to use for 136 * timestamps included before each output line. The format may be one of 137 * "none" (for no timestamps), "with-date" (to include both the date and 138 * the time), or "without-date" (to include only time time).</LI> 139 * <LI>"-Y {authzID}" or "--proxyAs {authzID}" -- Use the proxied 140 * authorization v2 control to request that the operation be processed 141 * using an alternate authorization identity. In this case, the bind DN 142 * should be that of a user that has permission to use this control. The 143 * authorization identity may be a value pattern.</LI> 144 * <LI>"--suppressErrorResultCodes" -- Indicates that information about the 145 * result codes for failed operations should not be displayed.</LI> 146 * <LI>"-c" or "--csv" -- Generate output in CSV format rather than a 147 * display-friendly format.</LI> 148 * </UL> 149 */ 150@ThreadSafety(level=ThreadSafetyLevel.NOT_THREADSAFE) 151public final class ModRate 152 extends LDAPCommandLineTool 153 implements Serializable 154{ 155 /** 156 * The serial version UID for this serializable class. 157 */ 158 private static final long serialVersionUID = 2709717414202815822L; 159 160 161 162 // Indicates whether a request has been made to stop running. 163 private final AtomicBoolean stopRequested; 164 165 // The argument used to indicate whether to generate output in CSV format. 166 private BooleanArgument csvFormat; 167 168 // The argument used to indicate whether to suppress information about error 169 // result codes. 170 private BooleanArgument suppressErrorsArgument; 171 172 // The argument used to specify the collection interval. 173 private IntegerArgument collectionInterval; 174 175 // The argument used to specify the number of modify iterations on a 176 // connection before it is closed and re-established. 177 private IntegerArgument iterationsBeforeReconnect; 178 179 // The argument used to specify the number of intervals. 180 private IntegerArgument numIntervals; 181 182 // The argument used to specify the number of threads. 183 private IntegerArgument numThreads; 184 185 // The argument used to specify the seed to use for the random number 186 // generator. 187 private IntegerArgument randomSeed; 188 189 // The target rate of modifies per second. 190 private IntegerArgument ratePerSecond; 191 192 // The argument used to specify a variable rate file. 193 private FileArgument sampleRateFile; 194 195 // The argument used to specify a variable rate file. 196 private FileArgument variableRateData; 197 198 // The argument used to specify the length of the values to generate. 199 private IntegerArgument valueLength; 200 201 // The number of warm-up intervals to perform. 202 private IntegerArgument warmUpIntervals; 203 204 // The argument used to specify the name of the attribute to modify. 205 private StringArgument attribute; 206 207 // The argument used to specify the set of characters to use when generating 208 // values. 209 private StringArgument characterSet; 210 211 // The argument used to specify the DNs of the entries to modify. 212 private StringArgument entryDN; 213 214 // The argument used to specify the proxied authorization identity. 215 private StringArgument proxyAs; 216 217 // The argument used to specify the timestamp format. 218 private StringArgument timestampFormat; 219 220 // The thread currently being used to run the searchrate tool. 221 private volatile Thread runningThread; 222 223 // A wakeable sleeper that will be used to sleep between reporting intervals. 224 private final WakeableSleeper sleeper; 225 226 227 228 /** 229 * Parse the provided command line arguments and make the appropriate set of 230 * changes. 231 * 232 * @param args The command line arguments provided to this program. 233 */ 234 public static void main(final String[] args) 235 { 236 final ResultCode resultCode = main(args, System.out, System.err); 237 if (resultCode != ResultCode.SUCCESS) 238 { 239 System.exit(resultCode.intValue()); 240 } 241 } 242 243 244 245 /** 246 * Parse the provided command line arguments and make the appropriate set of 247 * changes. 248 * 249 * @param args The command line arguments provided to this program. 250 * @param outStream The output stream to which standard out should be 251 * written. It may be {@code null} if output should be 252 * suppressed. 253 * @param errStream The output stream to which standard error should be 254 * written. It may be {@code null} if error messages 255 * should be suppressed. 256 * 257 * @return A result code indicating whether the processing was successful. 258 */ 259 public static ResultCode main(final String[] args, 260 final OutputStream outStream, 261 final OutputStream errStream) 262 { 263 final ModRate modRate = new ModRate(outStream, errStream); 264 return modRate.runTool(args); 265 } 266 267 268 269 /** 270 * Creates a new instance of this tool. 271 * 272 * @param outStream The output stream to which standard out should be 273 * written. It may be {@code null} if output should be 274 * suppressed. 275 * @param errStream The output stream to which standard error should be 276 * written. It may be {@code null} if error messages 277 * should be suppressed. 278 */ 279 public ModRate(final OutputStream outStream, final OutputStream errStream) 280 { 281 super(outStream, errStream); 282 283 stopRequested = new AtomicBoolean(false); 284 sleeper = new WakeableSleeper(); 285 } 286 287 288 289 /** 290 * Retrieves the name for this tool. 291 * 292 * @return The name for this tool. 293 */ 294 @Override() 295 public String getToolName() 296 { 297 return "modrate"; 298 } 299 300 301 302 /** 303 * Retrieves the description for this tool. 304 * 305 * @return The description for this tool. 306 */ 307 @Override() 308 public String getToolDescription() 309 { 310 return "Perform repeated modifications against " + 311 "an LDAP directory server."; 312 } 313 314 315 316 /** 317 * Retrieves the version string for this tool. 318 * 319 * @return The version string for this tool. 320 */ 321 @Override() 322 public String getToolVersion() 323 { 324 return Version.NUMERIC_VERSION_STRING; 325 } 326 327 328 329 /** 330 * Adds the arguments used by this program that aren't already provided by the 331 * generic {@code LDAPCommandLineTool} framework. 332 * 333 * @param parser The argument parser to which the arguments should be added. 334 * 335 * @throws ArgumentException If a problem occurs while adding the arguments. 336 */ 337 @Override() 338 public void addNonLDAPArguments(final ArgumentParser parser) 339 throws ArgumentException 340 { 341 String description = "The DN of the entry to modify. It may be a simple " + 342 "DN or a value pattern to specify a range of DN (e.g., " + 343 "\"uid=user.[1-1000],ou=People,dc=example,dc=com\"). This must be " + 344 "provided."; 345 entryDN = new StringArgument('b', "entryDN", true, 1, "{dn}", description); 346 parser.addArgument(entryDN); 347 348 349 description = "The name of the attribute to modify. Multiple attributes " + 350 "may be specified by providing this argument multiple " + 351 "times. At least one attribute must be specified."; 352 attribute = new StringArgument('A', "attribute", true, 0, "{name}", 353 description); 354 parser.addArgument(attribute); 355 356 357 description = "The length in bytes to use when generating values for the " + 358 "modifications. If this is not provided, then a default " + 359 "length of ten bytes will be used."; 360 valueLength = new IntegerArgument('l', "valueLength", true, 1, "{num}", 361 description, 1, Integer.MAX_VALUE, 10); 362 parser.addArgument(valueLength); 363 364 365 description = "The set of characters to use to generate the values for " + 366 "the modifications. It should only include ASCII " + 367 "characters. If this is not provided, then a default set " + 368 "of lowercase alphabetic characters will be used."; 369 characterSet = new StringArgument('C', "characterSet", true, 1, "{chars}", 370 description, 371 "abcdefghijklmnopqrstuvwxyz"); 372 parser.addArgument(characterSet); 373 374 375 description = "The number of threads to use to perform the " + 376 "modifications. If this is not provided, a single thread " + 377 "will be used."; 378 numThreads = new IntegerArgument('t', "numThreads", true, 1, "{num}", 379 description, 1, Integer.MAX_VALUE, 1); 380 parser.addArgument(numThreads); 381 382 383 description = "The length of time in seconds between output lines. If " + 384 "this is not provided, then a default interval of five " + 385 "seconds will be used."; 386 collectionInterval = new IntegerArgument('i', "intervalDuration", true, 1, 387 "{num}", description, 1, 388 Integer.MAX_VALUE, 5); 389 parser.addArgument(collectionInterval); 390 391 392 description = "The maximum number of intervals for which to run. If " + 393 "this is not provided, then the tool will run until it is " + 394 "interrupted."; 395 numIntervals = new IntegerArgument('I', "numIntervals", true, 1, "{num}", 396 description, 1, Integer.MAX_VALUE, 397 Integer.MAX_VALUE); 398 parser.addArgument(numIntervals); 399 400 description = "The number of modify iterations that should be processed " + 401 "on a connection before that connection is closed and " + 402 "replaced with a newly-established (and authenticated, if " + 403 "appropriate) connection. If this is not provided, then " + 404 "connections will not be periodically closed and " + 405 "re-established."; 406 iterationsBeforeReconnect = new IntegerArgument(null, 407 "iterationsBeforeReconnect", false, 1, "{num}", description, 0); 408 parser.addArgument(iterationsBeforeReconnect); 409 410 description = "The target number of modifies to perform per second. It " + 411 "is still necessary to specify a sufficient number of " + 412 "threads for achieving this rate. If neither this option " + 413 "nor --variableRateData is provided, then the tool will " + 414 "run at the maximum rate for the specified number of " + 415 "threads."; 416 ratePerSecond = new IntegerArgument('r', "ratePerSecond", false, 1, 417 "{modifies-per-second}", description, 418 1, Integer.MAX_VALUE); 419 parser.addArgument(ratePerSecond); 420 421 final String variableRateDataArgName = "variableRateData"; 422 final String generateSampleRateFileArgName = "generateSampleRateFile"; 423 description = RateAdjustor.getVariableRateDataArgumentDescription( 424 generateSampleRateFileArgName); 425 variableRateData = new FileArgument(null, variableRateDataArgName, false, 1, 426 "{path}", description, true, true, true, 427 false); 428 parser.addArgument(variableRateData); 429 430 description = RateAdjustor.getGenerateSampleVariableRateFileDescription( 431 variableRateDataArgName); 432 sampleRateFile = new FileArgument(null, generateSampleRateFileArgName, 433 false, 1, "{path}", description, false, 434 true, true, false); 435 sampleRateFile.setUsageArgument(true); 436 parser.addArgument(sampleRateFile); 437 parser.addExclusiveArgumentSet(variableRateData, sampleRateFile); 438 439 description = "The number of intervals to complete before beginning " + 440 "overall statistics collection. Specifying a nonzero " + 441 "number of warm-up intervals gives the client and server " + 442 "a chance to warm up without skewing performance results."; 443 warmUpIntervals = new IntegerArgument(null, "warmUpIntervals", true, 1, 444 "{num}", description, 0, Integer.MAX_VALUE, 0); 445 parser.addArgument(warmUpIntervals); 446 447 description = "Indicates the format to use for timestamps included in " + 448 "the output. A value of 'none' indicates that no " + 449 "timestamps should be included. A value of 'with-date' " + 450 "indicates that both the date and the time should be " + 451 "included. A value of 'without-date' indicates that only " + 452 "the time should be included."; 453 final LinkedHashSet<String> allowedFormats = new LinkedHashSet<String>(3); 454 allowedFormats.add("none"); 455 allowedFormats.add("with-date"); 456 allowedFormats.add("without-date"); 457 timestampFormat = new StringArgument(null, "timestampFormat", true, 1, 458 "{format}", description, allowedFormats, "none"); 459 parser.addArgument(timestampFormat); 460 461 description = "Indicates that the proxied authorization control (as " + 462 "defined in RFC 4370) should be used to request that " + 463 "operations be processed using an alternate authorization " + 464 "identity."; 465 proxyAs = new StringArgument('Y', "proxyAs", false, 1, "{authzID}", 466 description); 467 parser.addArgument(proxyAs); 468 469 description = "Indicates that information about the result codes for " + 470 "failed operations should not be displayed."; 471 suppressErrorsArgument = new BooleanArgument(null, 472 "suppressErrorResultCodes", 1, description); 473 parser.addArgument(suppressErrorsArgument); 474 475 description = "Generate output in CSV format rather than a " + 476 "display-friendly format"; 477 csvFormat = new BooleanArgument('c', "csv", 1, description); 478 parser.addArgument(csvFormat); 479 480 description = "Specifies the seed to use for the random number generator."; 481 randomSeed = new IntegerArgument('R', "randomSeed", false, 1, "{value}", 482 description); 483 parser.addArgument(randomSeed); 484 } 485 486 487 488 /** 489 * Indicates whether this tool supports creating connections to multiple 490 * servers. If it is to support multiple servers, then the "--hostname" and 491 * "--port" arguments will be allowed to be provided multiple times, and 492 * will be required to be provided the same number of times. The same type of 493 * communication security and bind credentials will be used for all servers. 494 * 495 * @return {@code true} if this tool supports creating connections to 496 * multiple servers, or {@code false} if not. 497 */ 498 @Override() 499 protected boolean supportsMultipleServers() 500 { 501 return true; 502 } 503 504 505 506 /** 507 * Retrieves the connection options that should be used for connections 508 * created for use with this tool. 509 * 510 * @return The connection options that should be used for connections created 511 * for use with this tool. 512 */ 513 @Override() 514 public LDAPConnectionOptions getConnectionOptions() 515 { 516 final LDAPConnectionOptions options = new LDAPConnectionOptions(); 517 options.setAutoReconnect(true); 518 options.setUseSynchronousMode(true); 519 return options; 520 } 521 522 523 524 /** 525 * Performs the actual processing for this tool. In this case, it gets a 526 * connection to the directory server and uses it to perform the requested 527 * modifications. 528 * 529 * @return The result code for the processing that was performed. 530 */ 531 @Override() 532 public ResultCode doToolProcessing() 533 { 534 runningThread = Thread.currentThread(); 535 536 try 537 { 538 return doToolProcessingInternal(); 539 } 540 finally 541 { 542 runningThread = null; 543 } 544 545 } 546 547 548 /** 549 * Performs the actual processing for this tool. In this case, it gets a 550 * connection to the directory server and uses it to perform the requested 551 * modifications. 552 * 553 * @return The result code for the processing that was performed. 554 */ 555 private ResultCode doToolProcessingInternal() 556 { 557 // If the sample rate file argument was specified, then generate the sample 558 // variable rate data file and return. 559 if (sampleRateFile.isPresent()) 560 { 561 try 562 { 563 RateAdjustor.writeSampleVariableRateFile(sampleRateFile.getValue()); 564 return ResultCode.SUCCESS; 565 } 566 catch (final Exception e) 567 { 568 debugException(e); 569 err("An error occurred while trying to write sample variable data " + 570 "rate file '", sampleRateFile.getValue().getAbsolutePath(), 571 "': ", getExceptionMessage(e)); 572 return ResultCode.LOCAL_ERROR; 573 } 574 } 575 576 577 // Determine the random seed to use. 578 final Long seed; 579 if (randomSeed.isPresent()) 580 { 581 seed = Long.valueOf(randomSeed.getValue()); 582 } 583 else 584 { 585 seed = null; 586 } 587 588 // Create the value patterns for the target entry DN and proxied 589 // authorization identities. 590 final ValuePattern dnPattern; 591 try 592 { 593 dnPattern = new ValuePattern(entryDN.getValue(), seed); 594 } 595 catch (final ParseException pe) 596 { 597 debugException(pe); 598 err("Unable to parse the entry DN value pattern: ", pe.getMessage()); 599 return ResultCode.PARAM_ERROR; 600 } 601 602 final ValuePattern authzIDPattern; 603 if (proxyAs.isPresent()) 604 { 605 try 606 { 607 authzIDPattern = new ValuePattern(proxyAs.getValue(), seed); 608 } 609 catch (final ParseException pe) 610 { 611 debugException(pe); 612 err("Unable to parse the proxied authorization pattern: ", 613 pe.getMessage()); 614 return ResultCode.PARAM_ERROR; 615 } 616 } 617 else 618 { 619 authzIDPattern = null; 620 } 621 622 623 // Get the names of the attributes to modify. 624 final String[] attrs = new String[attribute.getValues().size()]; 625 attribute.getValues().toArray(attrs); 626 627 628 // Get the character set as a byte array. 629 final byte[] charSet = getBytes(characterSet.getValue()); 630 631 632 // If the --ratePerSecond option was specified, then limit the rate 633 // accordingly. 634 FixedRateBarrier fixedRateBarrier = null; 635 if (ratePerSecond.isPresent() || variableRateData.isPresent()) 636 { 637 // We might not have a rate per second if --variableRateData is specified. 638 // The rate typically doesn't matter except when we have warm-up 639 // intervals. In this case, we'll run at the max rate. 640 final int intervalSeconds = collectionInterval.getValue(); 641 final int ratePerInterval = 642 (ratePerSecond.getValue() == null) 643 ? Integer.MAX_VALUE 644 : ratePerSecond.getValue() * intervalSeconds; 645 fixedRateBarrier = 646 new FixedRateBarrier(1000L * intervalSeconds, ratePerInterval); 647 } 648 649 650 // If --variableRateData was specified, then initialize a RateAdjustor. 651 RateAdjustor rateAdjustor = null; 652 if (variableRateData.isPresent()) 653 { 654 try 655 { 656 rateAdjustor = RateAdjustor.newInstance(fixedRateBarrier, 657 ratePerSecond.getValue(), variableRateData.getValue()); 658 } 659 catch (final IOException e) 660 { 661 debugException(e); 662 err("Initializing the variable rates failed: " + e.getMessage()); 663 return ResultCode.PARAM_ERROR; 664 } 665 catch (final IllegalArgumentException e) 666 { 667 debugException(e); 668 err("Initializing the variable rates failed: " + e.getMessage()); 669 return ResultCode.PARAM_ERROR; 670 } 671 } 672 673 674 // Determine whether to include timestamps in the output and if so what 675 // format should be used for them. 676 final boolean includeTimestamp; 677 final String timeFormat; 678 if (timestampFormat.getValue().equalsIgnoreCase("with-date")) 679 { 680 includeTimestamp = true; 681 timeFormat = "dd/MM/yyyy HH:mm:ss"; 682 } 683 else if (timestampFormat.getValue().equalsIgnoreCase("without-date")) 684 { 685 includeTimestamp = true; 686 timeFormat = "HH:mm:ss"; 687 } 688 else 689 { 690 includeTimestamp = false; 691 timeFormat = null; 692 } 693 694 695 // Determine whether any warm-up intervals should be run. 696 final long totalIntervals; 697 final boolean warmUp; 698 int remainingWarmUpIntervals = warmUpIntervals.getValue(); 699 if (remainingWarmUpIntervals > 0) 700 { 701 warmUp = true; 702 totalIntervals = 0L + numIntervals.getValue() + remainingWarmUpIntervals; 703 } 704 else 705 { 706 warmUp = true; 707 totalIntervals = 0L + numIntervals.getValue(); 708 } 709 710 711 // Create the table that will be used to format the output. 712 final OutputFormat outputFormat; 713 if (csvFormat.isPresent()) 714 { 715 outputFormat = OutputFormat.CSV; 716 } 717 else 718 { 719 outputFormat = OutputFormat.COLUMNS; 720 } 721 722 final ColumnFormatter formatter = new ColumnFormatter(includeTimestamp, 723 timeFormat, outputFormat, " ", 724 new FormattableColumn(12, HorizontalAlignment.RIGHT, "Recent", 725 "Mods/Sec"), 726 new FormattableColumn(12, HorizontalAlignment.RIGHT, "Recent", 727 "Avg Dur ms"), 728 new FormattableColumn(12, HorizontalAlignment.RIGHT, "Recent", 729 "Errors/Sec"), 730 new FormattableColumn(12, HorizontalAlignment.RIGHT, "Overall", 731 "Mods/Sec"), 732 new FormattableColumn(12, HorizontalAlignment.RIGHT, "Overall", 733 "Avg Dur ms")); 734 735 736 // Create values to use for statistics collection. 737 final AtomicLong modCounter = new AtomicLong(0L); 738 final AtomicLong errorCounter = new AtomicLong(0L); 739 final AtomicLong modDurations = new AtomicLong(0L); 740 final ResultCodeCounter rcCounter = new ResultCodeCounter(); 741 742 743 // Determine the length of each interval in milliseconds. 744 final long intervalMillis = 1000L * collectionInterval.getValue(); 745 746 747 // Create a random number generator to use for seeding the per-thread 748 // generators. 749 final Random random = new Random(); 750 751 752 // Create the threads to use for the modifications. 753 final CyclicBarrier barrier = new CyclicBarrier(numThreads.getValue() + 1); 754 final ModRateThread[] threads = new ModRateThread[numThreads.getValue()]; 755 for (int i=0; i < threads.length; i++) 756 { 757 final LDAPConnection connection; 758 try 759 { 760 connection = getConnection(); 761 } 762 catch (final LDAPException le) 763 { 764 debugException(le); 765 err("Unable to connect to the directory server: ", 766 getExceptionMessage(le)); 767 return le.getResultCode(); 768 } 769 770 threads[i] = new ModRateThread(this, i, connection, dnPattern, attrs, 771 charSet, valueLength.getValue(), authzIDPattern, random.nextLong(), 772 iterationsBeforeReconnect.getValue(), barrier, modCounter, 773 modDurations, errorCounter, rcCounter, fixedRateBarrier); 774 threads[i].start(); 775 } 776 777 778 // Display the table header. 779 for (final String headerLine : formatter.getHeaderLines(true)) 780 { 781 out(headerLine); 782 } 783 784 785 // Start the RateAdjustor before the threads so that the initial value is 786 // in place before any load is generated unless we're doing a warm-up in 787 // which case, we'll start it after the warm-up is complete. 788 if ((rateAdjustor != null) && (remainingWarmUpIntervals <= 0)) 789 { 790 rateAdjustor.start(); 791 } 792 793 794 // Indicate that the threads can start running. 795 try 796 { 797 barrier.await(); 798 } 799 catch (final Exception e) 800 { 801 debugException(e); 802 } 803 804 long overallStartTime = System.nanoTime(); 805 long nextIntervalStartTime = System.currentTimeMillis() + intervalMillis; 806 807 808 boolean setOverallStartTime = false; 809 long lastDuration = 0L; 810 long lastNumErrors = 0L; 811 long lastNumMods = 0L; 812 long lastEndTime = System.nanoTime(); 813 for (long i=0; i < totalIntervals; i++) 814 { 815 if (rateAdjustor != null) 816 { 817 if (! rateAdjustor.isAlive()) 818 { 819 out("All of the rates in " + variableRateData.getValue().getName() + 820 " have been completed."); 821 break; 822 } 823 } 824 825 final long startTimeMillis = System.currentTimeMillis(); 826 final long sleepTimeMillis = nextIntervalStartTime - startTimeMillis; 827 nextIntervalStartTime += intervalMillis; 828 if (sleepTimeMillis > 0) 829 { 830 sleeper.sleep(sleepTimeMillis); 831 } 832 833 if (stopRequested.get()) 834 { 835 break; 836 } 837 838 final long endTime = System.nanoTime(); 839 final long intervalDuration = endTime - lastEndTime; 840 841 final long numMods; 842 final long numErrors; 843 final long totalDuration; 844 if (warmUp && (remainingWarmUpIntervals > 0)) 845 { 846 numMods = modCounter.getAndSet(0L); 847 numErrors = errorCounter.getAndSet(0L); 848 totalDuration = modDurations.getAndSet(0L); 849 } 850 else 851 { 852 numMods = modCounter.get(); 853 numErrors = errorCounter.get(); 854 totalDuration = modDurations.get(); 855 } 856 857 final long recentNumMods = numMods - lastNumMods; 858 final long recentNumErrors = numErrors - lastNumErrors; 859 final long recentDuration = totalDuration - lastDuration; 860 861 final double numSeconds = intervalDuration / 1000000000.0d; 862 final double recentModRate = recentNumMods / numSeconds; 863 final double recentErrorRate = recentNumErrors / numSeconds; 864 865 final double recentAvgDuration; 866 if (recentNumMods > 0L) 867 { 868 recentAvgDuration = 1.0d * recentDuration / recentNumMods / 1000000; 869 } 870 else 871 { 872 recentAvgDuration = 0.0d; 873 } 874 875 if (warmUp && (remainingWarmUpIntervals > 0)) 876 { 877 out(formatter.formatRow(recentModRate, recentAvgDuration, 878 recentErrorRate, "warming up", "warming up")); 879 880 remainingWarmUpIntervals--; 881 if (remainingWarmUpIntervals == 0) 882 { 883 out("Warm-up completed. Beginning overall statistics collection."); 884 setOverallStartTime = true; 885 if (rateAdjustor != null) 886 { 887 rateAdjustor.start(); 888 } 889 } 890 } 891 else 892 { 893 if (setOverallStartTime) 894 { 895 overallStartTime = lastEndTime; 896 setOverallStartTime = false; 897 } 898 899 final double numOverallSeconds = 900 (endTime - overallStartTime) / 1000000000.0d; 901 final double overallAuthRate = numMods / numOverallSeconds; 902 903 final double overallAvgDuration; 904 if (numMods > 0L) 905 { 906 overallAvgDuration = 1.0d * totalDuration / numMods / 1000000; 907 } 908 else 909 { 910 overallAvgDuration = 0.0d; 911 } 912 913 out(formatter.formatRow(recentModRate, recentAvgDuration, 914 recentErrorRate, overallAuthRate, overallAvgDuration)); 915 916 lastNumMods = numMods; 917 lastNumErrors = numErrors; 918 lastDuration = totalDuration; 919 } 920 921 final List<ObjectPair<ResultCode,Long>> rcCounts = 922 rcCounter.getCounts(true); 923 if ((! suppressErrorsArgument.isPresent()) && (! rcCounts.isEmpty())) 924 { 925 err("\tError Results:"); 926 for (final ObjectPair<ResultCode,Long> p : rcCounts) 927 { 928 err("\t", p.getFirst().getName(), ": ", p.getSecond()); 929 } 930 } 931 932 lastEndTime = endTime; 933 } 934 935 // Shut down the RateAdjustor if we have one. 936 if (rateAdjustor != null) 937 { 938 rateAdjustor.shutDown(); 939 } 940 941 // Stop all of the threads. 942 ResultCode resultCode = ResultCode.SUCCESS; 943 for (final ModRateThread t : threads) 944 { 945 final ResultCode r = t.stopRunning(); 946 if (resultCode == ResultCode.SUCCESS) 947 { 948 resultCode = r; 949 } 950 } 951 952 return resultCode; 953 } 954 955 956 957 /** 958 * Requests that this tool stop running. This method will attempt to wait 959 * for all threads to complete before returning control to the caller. 960 */ 961 public void stopRunning() 962 { 963 stopRequested.set(true); 964 sleeper.wakeup(); 965 966 final Thread t = runningThread; 967 if (t != null) 968 { 969 try 970 { 971 t.join(); 972 } 973 catch (final Exception e) 974 { 975 debugException(e); 976 } 977 } 978 } 979 980 981 982 /** 983 * {@inheritDoc} 984 */ 985 @Override() 986 public LinkedHashMap<String[],String> getExampleUsages() 987 { 988 final LinkedHashMap<String[],String> examples = 989 new LinkedHashMap<String[],String>(2); 990 991 String[] args = 992 { 993 "--hostname", "server.example.com", 994 "--port", "389", 995 "--bindDN", "uid=admin,dc=example,dc=com", 996 "--bindPassword", "password", 997 "--entryDN", "uid=user.[1-1000000],ou=People,dc=example,dc=com", 998 "--attribute", "description", 999 "--valueLength", "12", 1000 "--numThreads", "10" 1001 }; 1002 String description = 1003 "Test modify performance by randomly selecting entries across a set " + 1004 "of one million users located below 'ou=People,dc=example,dc=com' " + 1005 "with ten concurrent threads and replacing the values for the " + 1006 "description attribute with a string of 12 randomly-selected " + 1007 "lowercase alphabetic characters."; 1008 examples.put(args, description); 1009 1010 args = new String[] 1011 { 1012 "--generateSampleRateFile", "variable-rate-data.txt" 1013 }; 1014 description = 1015 "Generate a sample variable rate definition file that may be used " + 1016 "in conjunction with the --variableRateData argument. The sample " + 1017 "file will include comments that describe the format for data to be " + 1018 "included in this file."; 1019 examples.put(args, description); 1020 1021 return examples; 1022 } 1023}