001 /*
002 * Copyright 2008-2016 UnboundID Corp.
003 * All Rights Reserved.
004 */
005 /*
006 * Copyright (C) 2008-2016 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 */
021 package com.unboundid.ldap.sdk.examples;
022
023
024
025 import java.io.IOException;
026 import java.io.OutputStream;
027 import java.io.Serializable;
028 import java.text.ParseException;
029 import java.util.LinkedHashMap;
030 import java.util.LinkedHashSet;
031 import java.util.List;
032 import java.util.Random;
033 import java.util.concurrent.CyclicBarrier;
034 import java.util.concurrent.atomic.AtomicBoolean;
035 import java.util.concurrent.atomic.AtomicLong;
036
037 import com.unboundid.ldap.sdk.LDAPConnection;
038 import com.unboundid.ldap.sdk.LDAPConnectionOptions;
039 import com.unboundid.ldap.sdk.LDAPException;
040 import com.unboundid.ldap.sdk.ResultCode;
041 import com.unboundid.ldap.sdk.Version;
042 import com.unboundid.util.ColumnFormatter;
043 import com.unboundid.util.FixedRateBarrier;
044 import com.unboundid.util.FormattableColumn;
045 import com.unboundid.util.HorizontalAlignment;
046 import com.unboundid.util.LDAPCommandLineTool;
047 import com.unboundid.util.ObjectPair;
048 import com.unboundid.util.OutputFormat;
049 import com.unboundid.util.RateAdjustor;
050 import com.unboundid.util.ResultCodeCounter;
051 import com.unboundid.util.ThreadSafety;
052 import com.unboundid.util.ThreadSafetyLevel;
053 import com.unboundid.util.ValuePattern;
054 import com.unboundid.util.WakeableSleeper;
055 import com.unboundid.util.args.ArgumentException;
056 import com.unboundid.util.args.ArgumentParser;
057 import com.unboundid.util.args.BooleanArgument;
058 import com.unboundid.util.args.FileArgument;
059 import com.unboundid.util.args.IntegerArgument;
060 import com.unboundid.util.args.StringArgument;
061
062 import static com.unboundid.util.Debug.*;
063 import 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)
151 public 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 * Indicates whether this tool should provide support for an interactive mode,
331 * in which the tool offers a mode in which the arguments can be provided in
332 * a text-driven menu rather than requiring them to be given on the command
333 * line. If interactive mode is supported, it may be invoked using the
334 * "--interactive" argument. Alternately, if interactive mode is supported
335 * and {@link #defaultsToInteractiveMode()} returns {@code true}, then
336 * interactive mode may be invoked by simply launching the tool without any
337 * arguments.
338 *
339 * @return {@code true} if this tool supports interactive mode, or
340 * {@code false} if not.
341 */
342 @Override()
343 public boolean supportsInteractiveMode()
344 {
345 return true;
346 }
347
348
349
350 /**
351 * Indicates whether this tool defaults to launching in interactive mode if
352 * the tool is invoked without any command-line arguments. This will only be
353 * used if {@link #supportsInteractiveMode()} returns {@code true}.
354 *
355 * @return {@code true} if this tool defaults to using interactive mode if
356 * launched without any command-line arguments, or {@code false} if
357 * not.
358 */
359 @Override()
360 public boolean defaultsToInteractiveMode()
361 {
362 return true;
363 }
364
365
366
367 /**
368 * Indicates whether this tool supports the use of a properties file for
369 * specifying default values for arguments that aren't specified on the
370 * command line.
371 *
372 * @return {@code true} if this tool supports the use of a properties file
373 * for specifying default values for arguments that aren't specified
374 * on the command line, or {@code false} if not.
375 */
376 @Override()
377 public boolean supportsPropertiesFile()
378 {
379 return true;
380 }
381
382
383
384 /**
385 * Indicates whether the LDAP-specific arguments should include alternate
386 * versions of all long identifiers that consist of multiple words so that
387 * they are available in both camelCase and dash-separated versions.
388 *
389 * @return {@code true} if this tool should provide multiple versions of
390 * long identifiers for LDAP-specific arguments, or {@code false} if
391 * not.
392 */
393 @Override()
394 protected boolean includeAlternateLongIdentifiers()
395 {
396 return true;
397 }
398
399
400
401 /**
402 * Adds the arguments used by this program that aren't already provided by the
403 * generic {@code LDAPCommandLineTool} framework.
404 *
405 * @param parser The argument parser to which the arguments should be added.
406 *
407 * @throws ArgumentException If a problem occurs while adding the arguments.
408 */
409 @Override()
410 public void addNonLDAPArguments(final ArgumentParser parser)
411 throws ArgumentException
412 {
413 String description = "The DN of the entry to modify. It may be a simple " +
414 "DN or a value pattern to specify a range of DN (e.g., " +
415 "\"uid=user.[1-1000],ou=People,dc=example,dc=com\"). See " +
416 ValuePattern.PUBLIC_JAVADOC_URL + " for complete details about the " +
417 "value pattern syntax. This must be provided.";
418 entryDN = new StringArgument('b', "entryDN", true, 1, "{dn}", description);
419 entryDN.setArgumentGroupName("Modification Arguments");
420 entryDN.addLongIdentifier("entry-dn");
421 parser.addArgument(entryDN);
422
423
424 description = "The name of the attribute to modify. Multiple attributes " +
425 "may be specified by providing this argument multiple " +
426 "times. At least one attribute must be specified.";
427 attribute = new StringArgument('A', "attribute", true, 0, "{name}",
428 description);
429 attribute.setArgumentGroupName("Modification Arguments");
430 parser.addArgument(attribute);
431
432
433 description = "The length in bytes to use when generating values for the " +
434 "modifications. If this is not provided, then a default " +
435 "length of ten bytes will be used.";
436 valueLength = new IntegerArgument('l', "valueLength", true, 1, "{num}",
437 description, 1, Integer.MAX_VALUE, 10);
438 valueLength.setArgumentGroupName("Modification Arguments");
439 valueLength.addLongIdentifier("value-length");
440 parser.addArgument(valueLength);
441
442
443 description = "The set of characters to use to generate the values for " +
444 "the modifications. It should only include ASCII " +
445 "characters. If this is not provided, then a default set " +
446 "of lowercase alphabetic characters will be used.";
447 characterSet = new StringArgument('C', "characterSet", true, 1, "{chars}",
448 description,
449 "abcdefghijklmnopqrstuvwxyz");
450 characterSet.setArgumentGroupName("Modification Arguments");
451 characterSet.addLongIdentifier("character-set");
452 parser.addArgument(characterSet);
453
454
455 description = "The number of threads to use to perform the " +
456 "modifications. If this is not provided, a single thread " +
457 "will be used.";
458 numThreads = new IntegerArgument('t', "numThreads", true, 1, "{num}",
459 description, 1, Integer.MAX_VALUE, 1);
460 numThreads.setArgumentGroupName("Rate Management Arguments");
461 numThreads.addLongIdentifier("num-threads");
462 parser.addArgument(numThreads);
463
464
465 description = "The length of time in seconds between output lines. If " +
466 "this is not provided, then a default interval of five " +
467 "seconds will be used.";
468 collectionInterval = new IntegerArgument('i', "intervalDuration", true, 1,
469 "{num}", description, 1,
470 Integer.MAX_VALUE, 5);
471 collectionInterval.setArgumentGroupName("Rate Management Arguments");
472 collectionInterval.addLongIdentifier("interval-duration");
473 parser.addArgument(collectionInterval);
474
475
476 description = "The maximum number of intervals for which to run. If " +
477 "this is not provided, then the tool will run until it is " +
478 "interrupted.";
479 numIntervals = new IntegerArgument('I', "numIntervals", true, 1, "{num}",
480 description, 1, Integer.MAX_VALUE,
481 Integer.MAX_VALUE);
482 numIntervals.setArgumentGroupName("Rate Management Arguments");
483 numIntervals.addLongIdentifier("num-intervals");
484 parser.addArgument(numIntervals);
485
486 description = "The number of modify iterations that should be processed " +
487 "on a connection before that connection is closed and " +
488 "replaced with a newly-established (and authenticated, if " +
489 "appropriate) connection. If this is not provided, then " +
490 "connections will not be periodically closed and " +
491 "re-established.";
492 iterationsBeforeReconnect = new IntegerArgument(null,
493 "iterationsBeforeReconnect", false, 1, "{num}", description, 0);
494 iterationsBeforeReconnect.setArgumentGroupName("Rate Management Arguments");
495 iterationsBeforeReconnect.addLongIdentifier("iterations-before-reconnect");
496 parser.addArgument(iterationsBeforeReconnect);
497
498 description = "The target number of modifies to perform per second. It " +
499 "is still necessary to specify a sufficient number of " +
500 "threads for achieving this rate. If neither this option " +
501 "nor --variableRateData is provided, then the tool will " +
502 "run at the maximum rate for the specified number of " +
503 "threads.";
504 ratePerSecond = new IntegerArgument('r', "ratePerSecond", false, 1,
505 "{modifies-per-second}", description,
506 1, Integer.MAX_VALUE);
507 ratePerSecond.setArgumentGroupName("Rate Management Arguments");
508 ratePerSecond.addLongIdentifier("rate-per-second");
509 parser.addArgument(ratePerSecond);
510
511 final String variableRateDataArgName = "variableRateData";
512 final String generateSampleRateFileArgName = "generateSampleRateFile";
513 description = RateAdjustor.getVariableRateDataArgumentDescription(
514 generateSampleRateFileArgName);
515 variableRateData = new FileArgument(null, variableRateDataArgName, false, 1,
516 "{path}", description, true, true, true,
517 false);
518 variableRateData.setArgumentGroupName("Rate Management Arguments");
519 variableRateData.addLongIdentifier("variable-rate-data");
520 parser.addArgument(variableRateData);
521
522 description = RateAdjustor.getGenerateSampleVariableRateFileDescription(
523 variableRateDataArgName);
524 sampleRateFile = new FileArgument(null, generateSampleRateFileArgName,
525 false, 1, "{path}", description, false,
526 true, true, false);
527 sampleRateFile.setArgumentGroupName("Rate Management Arguments");
528 sampleRateFile.addLongIdentifier("generate-sample-rate-file");
529 sampleRateFile.setUsageArgument(true);
530 parser.addArgument(sampleRateFile);
531 parser.addExclusiveArgumentSet(variableRateData, sampleRateFile);
532
533 description = "The number of intervals to complete before beginning " +
534 "overall statistics collection. Specifying a nonzero " +
535 "number of warm-up intervals gives the client and server " +
536 "a chance to warm up without skewing performance results.";
537 warmUpIntervals = new IntegerArgument(null, "warmUpIntervals", true, 1,
538 "{num}", description, 0, Integer.MAX_VALUE, 0);
539 warmUpIntervals.setArgumentGroupName("Rate Management Arguments");
540 warmUpIntervals.addLongIdentifier("warm-up-intervals");
541 parser.addArgument(warmUpIntervals);
542
543 description = "Indicates the format to use for timestamps included in " +
544 "the output. A value of 'none' indicates that no " +
545 "timestamps should be included. A value of 'with-date' " +
546 "indicates that both the date and the time should be " +
547 "included. A value of 'without-date' indicates that only " +
548 "the time should be included.";
549 final LinkedHashSet<String> allowedFormats = new LinkedHashSet<String>(3);
550 allowedFormats.add("none");
551 allowedFormats.add("with-date");
552 allowedFormats.add("without-date");
553 timestampFormat = new StringArgument(null, "timestampFormat", true, 1,
554 "{format}", description, allowedFormats, "none");
555 timestampFormat.addLongIdentifier("timestamp-format");
556 parser.addArgument(timestampFormat);
557
558 description = "Indicates that the proxied authorization control (as " +
559 "defined in RFC 4370) should be used to request that " +
560 "operations be processed using an alternate authorization " +
561 "identity. This may be a simple authorization ID or it " +
562 "may be a value pattern to specify a range of " +
563 "identities. See " + ValuePattern.PUBLIC_JAVADOC_URL +
564 " for complete details about the value pattern syntax.";
565 proxyAs = new StringArgument('Y', "proxyAs", false, 1, "{authzID}",
566 description);
567 proxyAs.addLongIdentifier("proxy-as");
568 parser.addArgument(proxyAs);
569
570 description = "Indicates that information about the result codes for " +
571 "failed operations should not be displayed.";
572 suppressErrorsArgument = new BooleanArgument(null,
573 "suppressErrorResultCodes", 1, description);
574 suppressErrorsArgument.addLongIdentifier("suppress-error-result-codes");
575 parser.addArgument(suppressErrorsArgument);
576
577 description = "Generate output in CSV format rather than a " +
578 "display-friendly format";
579 csvFormat = new BooleanArgument('c', "csv", 1, description);
580 parser.addArgument(csvFormat);
581
582 description = "Specifies the seed to use for the random number generator.";
583 randomSeed = new IntegerArgument('R', "randomSeed", false, 1, "{value}",
584 description);
585 randomSeed.addLongIdentifier("random-seed");
586 parser.addArgument(randomSeed);
587 }
588
589
590
591 /**
592 * Indicates whether this tool supports creating connections to multiple
593 * servers. If it is to support multiple servers, then the "--hostname" and
594 * "--port" arguments will be allowed to be provided multiple times, and
595 * will be required to be provided the same number of times. The same type of
596 * communication security and bind credentials will be used for all servers.
597 *
598 * @return {@code true} if this tool supports creating connections to
599 * multiple servers, or {@code false} if not.
600 */
601 @Override()
602 protected boolean supportsMultipleServers()
603 {
604 return true;
605 }
606
607
608
609 /**
610 * Retrieves the connection options that should be used for connections
611 * created for use with this tool.
612 *
613 * @return The connection options that should be used for connections created
614 * for use with this tool.
615 */
616 @Override()
617 public LDAPConnectionOptions getConnectionOptions()
618 {
619 final LDAPConnectionOptions options = new LDAPConnectionOptions();
620 options.setUseSynchronousMode(true);
621 return options;
622 }
623
624
625
626 /**
627 * Performs the actual processing for this tool. In this case, it gets a
628 * connection to the directory server and uses it to perform the requested
629 * modifications.
630 *
631 * @return The result code for the processing that was performed.
632 */
633 @Override()
634 public ResultCode doToolProcessing()
635 {
636 runningThread = Thread.currentThread();
637
638 try
639 {
640 return doToolProcessingInternal();
641 }
642 finally
643 {
644 runningThread = null;
645 }
646
647 }
648
649
650 /**
651 * Performs the actual processing for this tool. In this case, it gets a
652 * connection to the directory server and uses it to perform the requested
653 * modifications.
654 *
655 * @return The result code for the processing that was performed.
656 */
657 private ResultCode doToolProcessingInternal()
658 {
659 // If the sample rate file argument was specified, then generate the sample
660 // variable rate data file and return.
661 if (sampleRateFile.isPresent())
662 {
663 try
664 {
665 RateAdjustor.writeSampleVariableRateFile(sampleRateFile.getValue());
666 return ResultCode.SUCCESS;
667 }
668 catch (final Exception e)
669 {
670 debugException(e);
671 err("An error occurred while trying to write sample variable data " +
672 "rate file '", sampleRateFile.getValue().getAbsolutePath(),
673 "': ", getExceptionMessage(e));
674 return ResultCode.LOCAL_ERROR;
675 }
676 }
677
678
679 // Determine the random seed to use.
680 final Long seed;
681 if (randomSeed.isPresent())
682 {
683 seed = Long.valueOf(randomSeed.getValue());
684 }
685 else
686 {
687 seed = null;
688 }
689
690 // Create the value patterns for the target entry DN and proxied
691 // authorization identities.
692 final ValuePattern dnPattern;
693 try
694 {
695 dnPattern = new ValuePattern(entryDN.getValue(), seed);
696 }
697 catch (final ParseException pe)
698 {
699 debugException(pe);
700 err("Unable to parse the entry DN value pattern: ", pe.getMessage());
701 return ResultCode.PARAM_ERROR;
702 }
703
704 final ValuePattern authzIDPattern;
705 if (proxyAs.isPresent())
706 {
707 try
708 {
709 authzIDPattern = new ValuePattern(proxyAs.getValue(), seed);
710 }
711 catch (final ParseException pe)
712 {
713 debugException(pe);
714 err("Unable to parse the proxied authorization pattern: ",
715 pe.getMessage());
716 return ResultCode.PARAM_ERROR;
717 }
718 }
719 else
720 {
721 authzIDPattern = null;
722 }
723
724
725 // Get the names of the attributes to modify.
726 final String[] attrs = new String[attribute.getValues().size()];
727 attribute.getValues().toArray(attrs);
728
729
730 // Get the character set as a byte array.
731 final byte[] charSet = getBytes(characterSet.getValue());
732
733
734 // If the --ratePerSecond option was specified, then limit the rate
735 // accordingly.
736 FixedRateBarrier fixedRateBarrier = null;
737 if (ratePerSecond.isPresent() || variableRateData.isPresent())
738 {
739 // We might not have a rate per second if --variableRateData is specified.
740 // The rate typically doesn't matter except when we have warm-up
741 // intervals. In this case, we'll run at the max rate.
742 final int intervalSeconds = collectionInterval.getValue();
743 final int ratePerInterval =
744 (ratePerSecond.getValue() == null)
745 ? Integer.MAX_VALUE
746 : ratePerSecond.getValue() * intervalSeconds;
747 fixedRateBarrier =
748 new FixedRateBarrier(1000L * intervalSeconds, ratePerInterval);
749 }
750
751
752 // If --variableRateData was specified, then initialize a RateAdjustor.
753 RateAdjustor rateAdjustor = null;
754 if (variableRateData.isPresent())
755 {
756 try
757 {
758 rateAdjustor = RateAdjustor.newInstance(fixedRateBarrier,
759 ratePerSecond.getValue(), variableRateData.getValue());
760 }
761 catch (final IOException e)
762 {
763 debugException(e);
764 err("Initializing the variable rates failed: " + e.getMessage());
765 return ResultCode.PARAM_ERROR;
766 }
767 catch (final IllegalArgumentException e)
768 {
769 debugException(e);
770 err("Initializing the variable rates failed: " + e.getMessage());
771 return ResultCode.PARAM_ERROR;
772 }
773 }
774
775
776 // Determine whether to include timestamps in the output and if so what
777 // format should be used for them.
778 final boolean includeTimestamp;
779 final String timeFormat;
780 if (timestampFormat.getValue().equalsIgnoreCase("with-date"))
781 {
782 includeTimestamp = true;
783 timeFormat = "dd/MM/yyyy HH:mm:ss";
784 }
785 else if (timestampFormat.getValue().equalsIgnoreCase("without-date"))
786 {
787 includeTimestamp = true;
788 timeFormat = "HH:mm:ss";
789 }
790 else
791 {
792 includeTimestamp = false;
793 timeFormat = null;
794 }
795
796
797 // Determine whether any warm-up intervals should be run.
798 final long totalIntervals;
799 final boolean warmUp;
800 int remainingWarmUpIntervals = warmUpIntervals.getValue();
801 if (remainingWarmUpIntervals > 0)
802 {
803 warmUp = true;
804 totalIntervals = 0L + numIntervals.getValue() + remainingWarmUpIntervals;
805 }
806 else
807 {
808 warmUp = true;
809 totalIntervals = 0L + numIntervals.getValue();
810 }
811
812
813 // Create the table that will be used to format the output.
814 final OutputFormat outputFormat;
815 if (csvFormat.isPresent())
816 {
817 outputFormat = OutputFormat.CSV;
818 }
819 else
820 {
821 outputFormat = OutputFormat.COLUMNS;
822 }
823
824 final ColumnFormatter formatter = new ColumnFormatter(includeTimestamp,
825 timeFormat, outputFormat, " ",
826 new FormattableColumn(12, HorizontalAlignment.RIGHT, "Recent",
827 "Mods/Sec"),
828 new FormattableColumn(12, HorizontalAlignment.RIGHT, "Recent",
829 "Avg Dur ms"),
830 new FormattableColumn(12, HorizontalAlignment.RIGHT, "Recent",
831 "Errors/Sec"),
832 new FormattableColumn(12, HorizontalAlignment.RIGHT, "Overall",
833 "Mods/Sec"),
834 new FormattableColumn(12, HorizontalAlignment.RIGHT, "Overall",
835 "Avg Dur ms"));
836
837
838 // Create values to use for statistics collection.
839 final AtomicLong modCounter = new AtomicLong(0L);
840 final AtomicLong errorCounter = new AtomicLong(0L);
841 final AtomicLong modDurations = new AtomicLong(0L);
842 final ResultCodeCounter rcCounter = new ResultCodeCounter();
843
844
845 // Determine the length of each interval in milliseconds.
846 final long intervalMillis = 1000L * collectionInterval.getValue();
847
848
849 // Create a random number generator to use for seeding the per-thread
850 // generators.
851 final Random random = new Random();
852
853
854 // Create the threads to use for the modifications.
855 final CyclicBarrier barrier = new CyclicBarrier(numThreads.getValue() + 1);
856 final ModRateThread[] threads = new ModRateThread[numThreads.getValue()];
857 for (int i=0; i < threads.length; i++)
858 {
859 final LDAPConnection connection;
860 try
861 {
862 connection = getConnection();
863 }
864 catch (final LDAPException le)
865 {
866 debugException(le);
867 err("Unable to connect to the directory server: ",
868 getExceptionMessage(le));
869 return le.getResultCode();
870 }
871
872 threads[i] = new ModRateThread(this, i, connection, dnPattern, attrs,
873 charSet, valueLength.getValue(), authzIDPattern, random.nextLong(),
874 iterationsBeforeReconnect.getValue(), barrier, modCounter,
875 modDurations, errorCounter, rcCounter, fixedRateBarrier);
876 threads[i].start();
877 }
878
879
880 // Display the table header.
881 for (final String headerLine : formatter.getHeaderLines(true))
882 {
883 out(headerLine);
884 }
885
886
887 // Start the RateAdjustor before the threads so that the initial value is
888 // in place before any load is generated unless we're doing a warm-up in
889 // which case, we'll start it after the warm-up is complete.
890 if ((rateAdjustor != null) && (remainingWarmUpIntervals <= 0))
891 {
892 rateAdjustor.start();
893 }
894
895
896 // Indicate that the threads can start running.
897 try
898 {
899 barrier.await();
900 }
901 catch (final Exception e)
902 {
903 debugException(e);
904 }
905
906 long overallStartTime = System.nanoTime();
907 long nextIntervalStartTime = System.currentTimeMillis() + intervalMillis;
908
909
910 boolean setOverallStartTime = false;
911 long lastDuration = 0L;
912 long lastNumErrors = 0L;
913 long lastNumMods = 0L;
914 long lastEndTime = System.nanoTime();
915 for (long i=0; i < totalIntervals; i++)
916 {
917 if (rateAdjustor != null)
918 {
919 if (! rateAdjustor.isAlive())
920 {
921 out("All of the rates in " + variableRateData.getValue().getName() +
922 " have been completed.");
923 break;
924 }
925 }
926
927 final long startTimeMillis = System.currentTimeMillis();
928 final long sleepTimeMillis = nextIntervalStartTime - startTimeMillis;
929 nextIntervalStartTime += intervalMillis;
930 if (sleepTimeMillis > 0)
931 {
932 sleeper.sleep(sleepTimeMillis);
933 }
934
935 if (stopRequested.get())
936 {
937 break;
938 }
939
940 final long endTime = System.nanoTime();
941 final long intervalDuration = endTime - lastEndTime;
942
943 final long numMods;
944 final long numErrors;
945 final long totalDuration;
946 if (warmUp && (remainingWarmUpIntervals > 0))
947 {
948 numMods = modCounter.getAndSet(0L);
949 numErrors = errorCounter.getAndSet(0L);
950 totalDuration = modDurations.getAndSet(0L);
951 }
952 else
953 {
954 numMods = modCounter.get();
955 numErrors = errorCounter.get();
956 totalDuration = modDurations.get();
957 }
958
959 final long recentNumMods = numMods - lastNumMods;
960 final long recentNumErrors = numErrors - lastNumErrors;
961 final long recentDuration = totalDuration - lastDuration;
962
963 final double numSeconds = intervalDuration / 1000000000.0d;
964 final double recentModRate = recentNumMods / numSeconds;
965 final double recentErrorRate = recentNumErrors / numSeconds;
966
967 final double recentAvgDuration;
968 if (recentNumMods > 0L)
969 {
970 recentAvgDuration = 1.0d * recentDuration / recentNumMods / 1000000;
971 }
972 else
973 {
974 recentAvgDuration = 0.0d;
975 }
976
977 if (warmUp && (remainingWarmUpIntervals > 0))
978 {
979 out(formatter.formatRow(recentModRate, recentAvgDuration,
980 recentErrorRate, "warming up", "warming up"));
981
982 remainingWarmUpIntervals--;
983 if (remainingWarmUpIntervals == 0)
984 {
985 out("Warm-up completed. Beginning overall statistics collection.");
986 setOverallStartTime = true;
987 if (rateAdjustor != null)
988 {
989 rateAdjustor.start();
990 }
991 }
992 }
993 else
994 {
995 if (setOverallStartTime)
996 {
997 overallStartTime = lastEndTime;
998 setOverallStartTime = false;
999 }
1000
1001 final double numOverallSeconds =
1002 (endTime - overallStartTime) / 1000000000.0d;
1003 final double overallAuthRate = numMods / numOverallSeconds;
1004
1005 final double overallAvgDuration;
1006 if (numMods > 0L)
1007 {
1008 overallAvgDuration = 1.0d * totalDuration / numMods / 1000000;
1009 }
1010 else
1011 {
1012 overallAvgDuration = 0.0d;
1013 }
1014
1015 out(formatter.formatRow(recentModRate, recentAvgDuration,
1016 recentErrorRate, overallAuthRate, overallAvgDuration));
1017
1018 lastNumMods = numMods;
1019 lastNumErrors = numErrors;
1020 lastDuration = totalDuration;
1021 }
1022
1023 final List<ObjectPair<ResultCode,Long>> rcCounts =
1024 rcCounter.getCounts(true);
1025 if ((! suppressErrorsArgument.isPresent()) && (! rcCounts.isEmpty()))
1026 {
1027 err("\tError Results:");
1028 for (final ObjectPair<ResultCode,Long> p : rcCounts)
1029 {
1030 err("\t", p.getFirst().getName(), ": ", p.getSecond());
1031 }
1032 }
1033
1034 lastEndTime = endTime;
1035 }
1036
1037 // Shut down the RateAdjustor if we have one.
1038 if (rateAdjustor != null)
1039 {
1040 rateAdjustor.shutDown();
1041 }
1042
1043 // Stop all of the threads.
1044 ResultCode resultCode = ResultCode.SUCCESS;
1045 for (final ModRateThread t : threads)
1046 {
1047 final ResultCode r = t.stopRunning();
1048 if (resultCode == ResultCode.SUCCESS)
1049 {
1050 resultCode = r;
1051 }
1052 }
1053
1054 return resultCode;
1055 }
1056
1057
1058
1059 /**
1060 * Requests that this tool stop running. This method will attempt to wait
1061 * for all threads to complete before returning control to the caller.
1062 */
1063 public void stopRunning()
1064 {
1065 stopRequested.set(true);
1066 sleeper.wakeup();
1067
1068 final Thread t = runningThread;
1069 if (t != null)
1070 {
1071 try
1072 {
1073 t.join();
1074 }
1075 catch (final Exception e)
1076 {
1077 debugException(e);
1078 }
1079 }
1080 }
1081
1082
1083
1084 /**
1085 * {@inheritDoc}
1086 */
1087 @Override()
1088 public LinkedHashMap<String[],String> getExampleUsages()
1089 {
1090 final LinkedHashMap<String[],String> examples =
1091 new LinkedHashMap<String[],String>(2);
1092
1093 String[] args =
1094 {
1095 "--hostname", "server.example.com",
1096 "--port", "389",
1097 "--bindDN", "uid=admin,dc=example,dc=com",
1098 "--bindPassword", "password",
1099 "--entryDN", "uid=user.[1-1000000],ou=People,dc=example,dc=com",
1100 "--attribute", "description",
1101 "--valueLength", "12",
1102 "--numThreads", "10"
1103 };
1104 String description =
1105 "Test modify performance by randomly selecting entries across a set " +
1106 "of one million users located below 'ou=People,dc=example,dc=com' " +
1107 "with ten concurrent threads and replacing the values for the " +
1108 "description attribute with a string of 12 randomly-selected " +
1109 "lowercase alphabetic characters.";
1110 examples.put(args, description);
1111
1112 args = new String[]
1113 {
1114 "--generateSampleRateFile", "variable-rate-data.txt"
1115 };
1116 description =
1117 "Generate a sample variable rate definition file that may be used " +
1118 "in conjunction with the --variableRateData argument. The sample " +
1119 "file will include comments that describe the format for data to be " +
1120 "included in this file.";
1121 examples.put(args, description);
1122
1123 return examples;
1124 }
1125 }