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