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 */
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 * 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 }