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