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 */
021package com.unboundid.ldap.sdk.examples;
022
023
024
025import java.io.IOException;
026import java.io.OutputStream;
027import java.io.Serializable;
028import java.text.ParseException;
029import java.util.LinkedHashMap;
030import java.util.LinkedHashSet;
031import java.util.List;
032import java.util.Random;
033import java.util.concurrent.CyclicBarrier;
034import java.util.concurrent.atomic.AtomicBoolean;
035import java.util.concurrent.atomic.AtomicLong;
036
037import com.unboundid.ldap.sdk.LDAPConnection;
038import com.unboundid.ldap.sdk.LDAPConnectionOptions;
039import com.unboundid.ldap.sdk.LDAPException;
040import com.unboundid.ldap.sdk.ResultCode;
041import com.unboundid.ldap.sdk.Version;
042import com.unboundid.util.ColumnFormatter;
043import com.unboundid.util.FixedRateBarrier;
044import com.unboundid.util.FormattableColumn;
045import com.unboundid.util.HorizontalAlignment;
046import com.unboundid.util.LDAPCommandLineTool;
047import com.unboundid.util.ObjectPair;
048import com.unboundid.util.OutputFormat;
049import com.unboundid.util.RateAdjustor;
050import com.unboundid.util.ResultCodeCounter;
051import com.unboundid.util.ThreadSafety;
052import com.unboundid.util.ThreadSafetyLevel;
053import com.unboundid.util.ValuePattern;
054import com.unboundid.util.WakeableSleeper;
055import com.unboundid.util.args.ArgumentException;
056import com.unboundid.util.args.ArgumentParser;
057import com.unboundid.util.args.BooleanArgument;
058import com.unboundid.util.args.FileArgument;
059import com.unboundid.util.args.IntegerArgument;
060import com.unboundid.util.args.StringArgument;
061
062import static com.unboundid.util.Debug.*;
063import static com.unboundid.util.StaticUtils.*;
064
065
066
067/**
068 * This class provides a tool that can be used to perform repeated modifications
069 * in an LDAP directory server using multiple threads.  It can help provide an
070 * estimate of the modify performance that a directory server is able to
071 * achieve.  The target entry DN may be a value pattern as described in the
072 * {@link ValuePattern} class.  This makes it possible to modify a range of
073 * entries rather than repeatedly updating the same entry.
074 * <BR><BR>
075 * Some of the APIs demonstrated by this example include:
076 * <UL>
077 *   <LI>Argument Parsing (from the {@code com.unboundid.util.args}
078 *       package)</LI>
079 *   <LI>LDAP Command-Line Tool (from the {@code com.unboundid.util}
080 *       package)</LI>
081 *   <LI>LDAP Communication (from the {@code com.unboundid.ldap.sdk}
082 *       package)</LI>
083 *   <LI>Value Patterns (from the {@code com.unboundid.util} package)</LI>
084 * </UL>
085 * <BR><BR>
086 * All of the necessary information is provided using command line arguments.
087 * Supported arguments include those allowed by the {@link LDAPCommandLineTool}
088 * class, as well as the following additional arguments:
089 * <UL>
090 *   <LI>"-b {entryDN}" or "--targetDN {baseDN}" -- specifies the DN of the
091 *       entry to be modified.  This must be provided.  It may be a simple DN,
092 *       or it may be a value pattern to express a range of entry DNs.</LI>
093 *   <LI>"-A {name}" or "--attribute {name}" -- specifies the name of the
094 *       attribute to modify.  Multiple attributes may be modified by providing
095 *       multiple instances of this argument.  At least one attribute must be
096 *       provided.</LI>
097 *   <LI>"-l {num}" or "--valueLength {num}" -- specifies the length in bytes to
098 *       use for the values of the target attributes.  If this is not provided,
099 *       then a default length of 10 bytes will be used.</LI>
100 *   <LI>"-C {chars}" or "--characterSet {chars}" -- specifies the set of
101 *       characters that will be used to generate the values to use for the
102 *       target attributes.  It should only include ASCII characters.  Values
103 *       will be generated from randomly-selected characters from this set.  If
104 *       this is not provided, then a default set of lowercase alphabetic
105 *       characters will be used.</LI>
106 *   <LI>"-t {num}" or "--numThreads {num}" -- specifies the number of
107 *       concurrent threads to use when performing the modifications.  If this
108 *       is not provided, then a default of one thread will be used.</LI>
109 *   <LI>"-i {sec}" or "--intervalDuration {sec}" -- specifies the length of
110 *       time in seconds between lines out output.  If this is not provided,
111 *       then a default interval duration of five seconds will be used.</LI>
112 *   <LI>"-I {num}" or "--numIntervals {num}" -- specifies the maximum number of
113 *       intervals for which to run.  If this is not provided, then it will
114 *       run forever.</LI>
115 *   <LI>"--iterationsBeforeReconnect {num}" -- specifies the number of modify
116 *       iterations that should be performed on a connection before that
117 *       connection is closed and replaced with a newly-established (and
118 *       authenticated, if appropriate) connection.</LI>
119 *   <LI>"-r {modifies-per-second}" or "--ratePerSecond {modifies-per-second}"
120 *       -- specifies the target number of modifies to perform per second.  It
121 *       is still necessary to specify a sufficient number of threads for
122 *       achieving this rate.  If this option is not provided, then the tool
123 *       will run at the maximum rate for the specified number of threads.</LI>
124 *   <LI>"--variableRateData {path}" -- specifies the path to a file containing
125 *       information needed to allow the tool to vary the target rate over time.
126 *       If this option is not provided, then the tool will either use a fixed
127 *       target rate as specified by the "--ratePerSecond" argument, or it will
128 *       run at the maximum rate.</LI>
129 *   <LI>"--generateSampleRateFile {path}" -- specifies the path to a file to
130 *       which sample data will be written illustrating and describing the
131 *       format of the file expected to be used in conjunction with the
132 *       "--variableRateData" argument.</LI>
133 *   <LI>"--warmUpIntervals {num}" -- specifies the number of intervals to
134 *       complete before beginning overall statistics collection.</LI>
135 *   <LI>"--timestampFormat {format}" -- specifies the format to use for
136 *       timestamps included before each output line.  The format may be one of
137 *       "none" (for no timestamps), "with-date" (to include both the date and
138 *       the time), or "without-date" (to include only time time).</LI>
139 *   <LI>"-Y {authzID}" or "--proxyAs {authzID}" -- Use the proxied
140 *       authorization v2 control to request that the operation be processed
141 *       using an alternate authorization identity.  In this case, the bind DN
142 *       should be that of a user that has permission to use this control.  The
143 *       authorization identity may be a value pattern.</LI>
144 *   <LI>"--suppressErrorResultCodes" -- Indicates that information about the
145 *       result codes for failed operations should not be displayed.</LI>
146 *   <LI>"-c" or "--csv" -- Generate output in CSV format rather than a
147 *       display-friendly format.</LI>
148 * </UL>
149 */
150@ThreadSafety(level=ThreadSafetyLevel.NOT_THREADSAFE)
151public final class ModRate
152       extends LDAPCommandLineTool
153       implements Serializable
154{
155  /**
156   * The serial version UID for this serializable class.
157   */
158  private static final long serialVersionUID = 2709717414202815822L;
159
160
161
162  // Indicates whether a request has been made to stop running.
163  private final AtomicBoolean stopRequested;
164
165  // The argument used to indicate whether to generate output in CSV format.
166  private BooleanArgument csvFormat;
167
168  // The argument used to indicate whether to suppress information about error
169  // result codes.
170  private BooleanArgument suppressErrorsArgument;
171
172  // The argument used to specify the collection interval.
173  private IntegerArgument collectionInterval;
174
175  // The argument used to specify the number of modify iterations on a
176  // connection before it is closed and re-established.
177  private IntegerArgument iterationsBeforeReconnect;
178
179  // The argument used to specify the number of intervals.
180  private IntegerArgument numIntervals;
181
182  // The argument used to specify the number of threads.
183  private IntegerArgument numThreads;
184
185  // The argument used to specify the seed to use for the random number
186  // generator.
187  private IntegerArgument randomSeed;
188
189  // The target rate of modifies per second.
190  private IntegerArgument ratePerSecond;
191
192  // The argument used to specify a variable rate file.
193  private FileArgument sampleRateFile;
194
195  // The argument used to specify a variable rate file.
196  private FileArgument variableRateData;
197
198  // The argument used to specify the length of the values to generate.
199  private IntegerArgument valueLength;
200
201  // The number of warm-up intervals to perform.
202  private IntegerArgument warmUpIntervals;
203
204  // The argument used to specify the name of the attribute to modify.
205  private StringArgument attribute;
206
207  // The argument used to specify the set of characters to use when generating
208  // values.
209  private StringArgument characterSet;
210
211  // The argument used to specify the DNs of the entries to modify.
212  private StringArgument entryDN;
213
214  // The argument used to specify the proxied authorization identity.
215  private StringArgument proxyAs;
216
217  // The argument used to specify the timestamp format.
218  private StringArgument timestampFormat;
219
220  // The thread currently being used to run the searchrate tool.
221  private volatile Thread runningThread;
222
223  // A wakeable sleeper that will be used to sleep between reporting intervals.
224  private final WakeableSleeper sleeper;
225
226
227
228  /**
229   * Parse the provided command line arguments and make the appropriate set of
230   * changes.
231   *
232   * @param  args  The command line arguments provided to this program.
233   */
234  public static void main(final String[] args)
235  {
236    final ResultCode resultCode = main(args, System.out, System.err);
237    if (resultCode != ResultCode.SUCCESS)
238    {
239      System.exit(resultCode.intValue());
240    }
241  }
242
243
244
245  /**
246   * Parse the provided command line arguments and make the appropriate set of
247   * changes.
248   *
249   * @param  args       The command line arguments provided to this program.
250   * @param  outStream  The output stream to which standard out should be
251   *                    written.  It may be {@code null} if output should be
252   *                    suppressed.
253   * @param  errStream  The output stream to which standard error should be
254   *                    written.  It may be {@code null} if error messages
255   *                    should be suppressed.
256   *
257   * @return  A result code indicating whether the processing was successful.
258   */
259  public static ResultCode main(final String[] args,
260                                final OutputStream outStream,
261                                final OutputStream errStream)
262  {
263    final ModRate modRate = new ModRate(outStream, errStream);
264    return modRate.runTool(args);
265  }
266
267
268
269  /**
270   * Creates a new instance of this tool.
271   *
272   * @param  outStream  The output stream to which standard out should be
273   *                    written.  It may be {@code null} if output should be
274   *                    suppressed.
275   * @param  errStream  The output stream to which standard error should be
276   *                    written.  It may be {@code null} if error messages
277   *                    should be suppressed.
278   */
279  public ModRate(final OutputStream outStream, final OutputStream errStream)
280  {
281    super(outStream, errStream);
282
283    stopRequested = new AtomicBoolean(false);
284    sleeper = new WakeableSleeper();
285  }
286
287
288
289  /**
290   * Retrieves the name for this tool.
291   *
292   * @return  The name for this tool.
293   */
294  @Override()
295  public String getToolName()
296  {
297    return "modrate";
298  }
299
300
301
302  /**
303   * Retrieves the description for this tool.
304   *
305   * @return  The description for this tool.
306   */
307  @Override()
308  public String getToolDescription()
309  {
310    return "Perform repeated modifications against " +
311           "an LDAP directory server.";
312  }
313
314
315
316  /**
317   * Retrieves the version string for this tool.
318   *
319   * @return  The version string for this tool.
320   */
321  @Override()
322  public String getToolVersion()
323  {
324    return Version.NUMERIC_VERSION_STRING;
325  }
326
327
328
329  /**
330   * 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}