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