001    /*
002     * Copyright 2008-2015 UnboundID Corp.
003     * All Rights Reserved.
004     */
005    /*
006     * Copyright (C) 2008-2015 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    }