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