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