From 26bd244ad8628b240892d4c59f17a133f5f85f5b Mon Sep 17 00:00:00 2001 From: Maxim Muzafarov Date: Sat, 15 Jun 2024 21:04:26 +0200 Subject: [PATCH 01/22] CASSANDRA-17445 use picocli with nodetoolV2 instead of ariline --- .build/cassandra-deps-template.xml | 8 + .build/parent-pom-template.xml | 10 + bin/nodetool | 10 +- .../cassandra/management/BaseCommand.java | 38 ++ .../management/CassandraHelpCommand.java | 100 +++ .../management/CassandraHelpLayout.java | 444 +++++++++++++ .../cassandra/management/CommandUtils.java | 39 ++ .../management/ManagementContext.java | 27 + .../management/api/AbortBootstrap.java | 47 ++ .../cassandra/management/api/Assassinate.java | 38 ++ .../management/api/ForceCompact.java | 72 +++ .../cassandra/management/api/JmxConnect.java | 92 +++ .../management/api/TopLevelCommand.java | 29 + .../org/apache/cassandra/tools/NodeProbe.java | 64 +- .../org/apache/cassandra/tools/NodeTool.java | 6 +- .../apache/cassandra/tools/NodeToolV2.java | 585 ++++++++++++++++++ .../cassandra/tools/NodeToolSynopsisTest.java | 137 ++++ .../apache/cassandra/tools/ToolRunner.java | 5 + 18 files changed, 1722 insertions(+), 29 deletions(-) create mode 100644 src/java/org/apache/cassandra/management/BaseCommand.java create mode 100644 src/java/org/apache/cassandra/management/CassandraHelpCommand.java create mode 100644 src/java/org/apache/cassandra/management/CassandraHelpLayout.java create mode 100644 src/java/org/apache/cassandra/management/CommandUtils.java create mode 100644 src/java/org/apache/cassandra/management/ManagementContext.java create mode 100644 src/java/org/apache/cassandra/management/api/AbortBootstrap.java create mode 100644 src/java/org/apache/cassandra/management/api/Assassinate.java create mode 100644 src/java/org/apache/cassandra/management/api/ForceCompact.java create mode 100644 src/java/org/apache/cassandra/management/api/JmxConnect.java create mode 100644 src/java/org/apache/cassandra/management/api/TopLevelCommand.java create mode 100644 src/java/org/apache/cassandra/tools/NodeToolV2.java create mode 100644 test/unit/org/apache/cassandra/tools/NodeToolSynopsisTest.java diff --git a/.build/cassandra-deps-template.xml b/.build/cassandra-deps-template.xml index 437c0c31b5a9..32c7e4a2c0af 100644 --- a/.build/cassandra-deps-template.xml +++ b/.build/cassandra-deps-template.xml @@ -120,6 +120,14 @@ io.airlift airline + + info.picocli + picocli + + + io.github.java-diff-utils + java-diff-utils + io.dropwizard.metrics metrics-core diff --git a/.build/parent-pom-template.xml b/.build/parent-pom-template.xml index 0c0e5d4ccc9e..23caee6fd61c 100644 --- a/.build/parent-pom-template.xml +++ b/.build/parent-pom-template.xml @@ -714,6 +714,16 @@ jbcrypt 0.4 + + info.picocli + picocli + 4.7.5 + + + io.github.java-diff-utils + java-diff-utils + 4.12 + io.airlift airline diff --git a/bin/nodetool b/bin/nodetool index f78b02e34418..b1fb4f5a091d 100755 --- a/bin/nodetool +++ b/bin/nodetool @@ -98,13 +98,19 @@ if [ "x$MAX_HEAP_SIZE" = "x" ]; then MAX_HEAP_SIZE="128m" fi +if [ "x$NODETOOL_RUNNER" = "x" ]; then + NODETOOL_RUNNER="org.apache.cassandra.tools.NodeTool" +fi + +# shellcheck disable=SC2116 CMD=$(echo "$JAVA" $JAVA_AGENT -ea -cp "$CLASSPATH" $JVM_OPTS -Xmx$MAX_HEAP_SIZE \ -XX:ParallelGCThreads=1 \ -Dcassandra.storagedir="$cassandra_storagedir" \ -Dcassandra.logdir="$CASSANDRA_LOG_DIR" \ -Dlogback.configurationFile=logback-tools.xml \ - $JVM_ARGS \ - org.apache.cassandra.tools.NodeTool -p $JMX_PORT $ARGS) + "$JVM_ARGS" \ + $NODETOOL_RUNNER \ + -p "$JMX_PORT" "$ARGS") if [ "x$ARCHIVE_COMMAND" != "x" ] then diff --git a/src/java/org/apache/cassandra/management/BaseCommand.java b/src/java/org/apache/cassandra/management/BaseCommand.java new file mode 100644 index 000000000000..aa22619854af --- /dev/null +++ b/src/java/org/apache/cassandra/management/BaseCommand.java @@ -0,0 +1,38 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.management; + +import javax.inject.Inject; + +/** + * Base class for all nodetool commands. + */ +public abstract class BaseCommand implements Runnable +{ + @Inject + protected ManagementContext probe; + + @Override + public void run() + { + execute(probe); + } + + protected abstract void execute(ManagementContext probe); +} diff --git a/src/java/org/apache/cassandra/management/CassandraHelpCommand.java b/src/java/org/apache/cassandra/management/CassandraHelpCommand.java new file mode 100644 index 000000000000..5efd76b8500c --- /dev/null +++ b/src/java/org/apache/cassandra/management/CassandraHelpCommand.java @@ -0,0 +1,100 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.management; + +import java.io.PrintWriter; +import java.util.Map; + +import com.google.common.base.Preconditions; + +import picocli.CommandLine; + +@CommandLine.Command(name = "help", + header = "Display help information about the specified command.", + synopsisHeading = "%nUsage: ", + helpCommand = true, + description = { "%nWhen no COMMAND is given, the usage help for the main command is displayed.", + "If a COMMAND is specified, the help for that command is shown.%n" }) +public class CassandraHelpCommand implements CommandLine.IHelpCommandInitializable2, Runnable +{ + @CommandLine.Option(names = { "--help" }, usageHelp = true, descriptionKey = "helpCommand.help", + description = "Show usage help for the help command and exit.") + private boolean helpRequested; + + @CommandLine.Parameters(paramLabel = "command", arity = "0..1", descriptionKey = "helpCommand.command", + description = "The COMMAND to display the usage help message for.") + private String commands; + + private CommandLine self; + private PrintWriter out; + private CommandLine.Help.ColorScheme colorScheme; + + /** + * Invokes {@code #usage(PrintStream, CommandLine.Help.ColorScheme) usage} for the specified command, + * or for the parent command. + */ + public void run() + { + CommandLine parent = self == null ? null : self.getParent(); + if (parent == null) + return; + + CommandLine.Help.ColorScheme colors = colorScheme == null ? + CommandLine.Help.defaultColorScheme(CommandLine.Help.Ansi.AUTO) : + colorScheme; + // If no input command argument is specified, print help for the top-level command. + if (commands == null) + { + printUsage(parent, colors, out); + return; + } + + Map parentSubcommands = parent.getCommandSpec().subcommands(); + CommandLine subcommand = parentSubcommands.get(commands); + + if (parent.isAbbreviatedSubcommandsAllowed()) + throw new CommandLine.ParameterException(parent, "Abbreviated subcommands are not allowed.", null, commands); + + if (subcommand == null) + throw new CommandLine.ParameterException(parent, "Unknown subcommand '" + commands + "'.", null, commands); + + printUsage(subcommand, colors, out); + } + + private static void printUsage(CommandLine command, CommandLine.Help.ColorScheme colors, PrintWriter out) + { + if (command == null) + return; + command.usage(out, colors); + } + + /** + * The printHelpIfRequested method calls the init method on commands marked + * as helpCommand before the help command's run or call method is called. + */ + public void init(CommandLine helpCommandLine, + CommandLine.Help.ColorScheme colorScheme, + PrintWriter out, + PrintWriter err) + { + this.self = Preconditions.checkNotNull(helpCommandLine, "helpCommandLine"); + this.colorScheme = Preconditions.checkNotNull(colorScheme, "colorScheme"); + this.out = Preconditions.checkNotNull(out, "outWriter"); + } +} diff --git a/src/java/org/apache/cassandra/management/CassandraHelpLayout.java b/src/java/org/apache/cassandra/management/CassandraHelpLayout.java new file mode 100644 index 000000000000..76346bf28a4e --- /dev/null +++ b/src/java/org/apache/cassandra/management/CassandraHelpLayout.java @@ -0,0 +1,444 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.management; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Comparator; +import java.util.HashSet; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.Set; + +import com.google.common.base.Preconditions; + +import org.apache.cassandra.utils.AbstractGuavaIterator; +import picocli.CommandLine; + +import static com.google.common.collect.ObjectArrays.concat; +import static org.apache.cassandra.management.CommandUtils.leadingSpaces; +import static picocli.CommandLine.Model.UsageMessageSpec.SECTION_KEY_COMMAND_LIST; +import static picocli.CommandLine.Model.UsageMessageSpec.SECTION_KEY_COMMAND_LIST_HEADING; +import static picocli.CommandLine.Model.UsageMessageSpec.SECTION_KEY_DESCRIPTION; +import static picocli.CommandLine.Model.UsageMessageSpec.SECTION_KEY_DESCRIPTION_HEADING; +import static picocli.CommandLine.Model.UsageMessageSpec.SECTION_KEY_END_OF_OPTIONS; +import static picocli.CommandLine.Model.UsageMessageSpec.SECTION_KEY_EXIT_CODE_LIST; +import static picocli.CommandLine.Model.UsageMessageSpec.SECTION_KEY_EXIT_CODE_LIST_HEADING; +import static picocli.CommandLine.Model.UsageMessageSpec.SECTION_KEY_FOOTER; +import static picocli.CommandLine.Model.UsageMessageSpec.SECTION_KEY_FOOTER_HEADING; +import static picocli.CommandLine.Model.UsageMessageSpec.SECTION_KEY_HEADER; +import static picocli.CommandLine.Model.UsageMessageSpec.SECTION_KEY_HEADER_HEADING; +import static picocli.CommandLine.Model.UsageMessageSpec.SECTION_KEY_OPTION_LIST; +import static picocli.CommandLine.Model.UsageMessageSpec.SECTION_KEY_OPTION_LIST_HEADING; +import static picocli.CommandLine.Model.UsageMessageSpec.SECTION_KEY_PARAMETER_LIST; +import static picocli.CommandLine.Model.UsageMessageSpec.SECTION_KEY_PARAMETER_LIST_HEADING; +import static picocli.CommandLine.Model.UsageMessageSpec.SECTION_KEY_SYNOPSIS; +import static picocli.CommandLine.Model.UsageMessageSpec.SECTION_KEY_SYNOPSIS_HEADING; + +/** + * Help facotry for the Cassandra nodetool to generate the help output. This class is used to match + * the command output with the previously available nodetool help output format. + */ +public class CassandraHelpLayout extends CommandLine.Help +{ + public static final int DEFAULT_USAGE_HELP_WIDTH = 90; + private static final String DESCRIPTION_HEADING = "NAME%n"; + private static final String SYNOPSIS_HEADING = "SYNOPSIS%n"; + private static final String OPTIONS_HEADING = "OPTIONS%n"; + private static final int COLUMN_INDENT = 8; + private static final int DESCRIPTION_INDENT = 4; + private static final CommandLine.Model.OptionSpec CASSANDRA_END_OF_OPTIONS_OPTION = + CommandLine.Model.OptionSpec.builder("--") + .description("This option can be used to separate command-line options from the " + + "list of argument, (useful when arguments might be mistaken for " + + "command-line options") + .arity("0") + .build(); + + public CassandraHelpLayout(CommandLine.Model.CommandSpec spec, ColorScheme scheme) + { + super(spec, scheme); + } + + @Override + public String descriptionHeading(Object... params) + { + return createHeading(DESCRIPTION_HEADING, params); + } + + /** + * @param params Arguments referenced by the format specifiers in the header strings + * @return the header string. + */ + @Override + public String description(Object... params) { + CommandLine.Model.CommandSpec spec = commandSpec(); + String fullName = spec.qualifiedName(); + + TextTable table = TextTable.forColumns(colorScheme(), + new Column(spec.usageMessage().width() - COLUMN_INDENT, COLUMN_INDENT, + Column.Overflow.WRAP)); + table.setAdjustLineBreaksForWideCJKCharacters(spec.usageMessage().adjustLineBreaksForWideCJKCharacters()); + table.indentWrappedLines = 0; + + table.addRowValues(colorScheme().commandText(fullName) + .concat(" - ") + .concat(colorScheme().text(String.join(" ", spec.usageMessage().description())))); + table.addRowValues(Ansi.OFF.new Text("", colorScheme())); + return table.toString(new StringBuilder()).toString(); + } + + @Override + public String synopsisHeading(Object... params) + { + return createHeading(SYNOPSIS_HEADING, params); + } + + @Override + public String detailedSynopsis(int synopsisHeadingLength, Comparator optionSort, boolean clusterBooleanOptions) + { + Preconditions.checkState(synopsisHeadingLength >= 0, + "synopsisHeadingLength must be a positive number but was " + synopsisHeadingLength); + + // Cassandra uses end of options delimiter in usage help. + commandSpec().usageMessage().showEndOfOptionsDelimiterInUsageHelp(true); + + Set argsInGroups = new HashSet<>(); + Ansi.Text groupsText = createDetailedSynopsisGroupsText(argsInGroups); + + List optionsList = createCassandraSynopsisOptionsText(argsInGroups); + Ansi.Text endOfOptionsText = createDetailedSynopsisEndOfOptionsText(); + Ansi.Text positionalParamText = createCassandraSynopsisPositionalsText(argsInGroups); + Ansi.Text commandText = createDetailedSynopsisCommandText(); + + CommandLine.Model.CommandSpec commandSpec = commandSpec(); + String parentCommandName = commandSpec.parent().qualifiedName(); + ColorScheme colorScheme = colorScheme(); + + int leadingColumnWidth = parentCommandName.length() + COLUMN_INDENT; + int followingColumnWidth = commandSpec.usageMessage().width() - leadingColumnWidth; + TextTable textTable = TextTable.forColumns(colorScheme, + new Column(leadingColumnWidth, 0, Column.Overflow.TRUNCATE), + new Column(followingColumnWidth, 0, Column.Overflow.WRAP)); + textTable.setAdjustLineBreaksForWideCJKCharacters(commandSpec.usageMessage().adjustLineBreaksForWideCJKCharacters()); + textTable.indentWrappedLines = 0; + + Ansi.Text emptyCell = Ansi.OFF.new Text(leadingSpaces(leadingColumnWidth), colorScheme); + Ansi.Text cmdPadding = Ansi.OFF.new Text(leadingSpaces(COLUMN_INDENT), colorScheme); + Ansi.Text parentCommandText = cmdPadding.concat(colorScheme.commandText(parentCommandName)).concat(" "); + // All other fields added to the synopsis are left-adjusted, so we don't need to align them. + Ansi.Text text = groupsText.concat(" ") + .concat(commandSpec.name()) + .concat(endOfOptionsText) + .concat(" ") + .concat(positionalParamText) + .concat(commandText); + + LineBreakingOptionsIterator iter = new LineBreakingOptionsIterator(optionsList.iterator(), followingColumnWidth); + boolean commandTextNotAdded = true; + while (iter.hasNext()) + { + Ansi.Text row = iter.next(); + Ansi.Text leadingCell = emptyCell; + + if (commandTextNotAdded) + { + leadingCell = parentCommandText; + row = colorScheme.text(" ").concat(row); + commandTextNotAdded = false; + } + + if (iter.hasNext()) + textTable.addRowValues(leadingCell, row); + else + textTable.addRowValues(leadingCell, row.concat(text)); + } + + textTable.addRowValues(Ansi.OFF.new Text("", colorScheme)); + return textTable.toString(); + } + + private Ansi.Text createCassandraSynopsisPositionalsText(Collection done) + { + List positionals = cassandraPositionals(commandSpec()); + positionals.removeAll(done); + + IParamLabelRenderer parameterLabelRenderer = createMinimalSpacedParamLabelRenderer(); + Ansi.Text text = colorScheme().text(""); + for (CommandLine.Model.PositionalParamSpec positionalParam : positionals) + { + Ansi.Text label = parameterLabelRenderer.renderParameterLabel(positionalParam, colorScheme().ansi(), colorScheme().parameterStyles()); + text = text.plainString().isEmpty() ? label : text.concat(" ").concat(label); + } + return text; + } + + private List createCassandraSynopsisOptionsText(Collection done) + { + // Cassandra uses alphabetical order for options, ordered by short name. + List optionList = new ArrayList<>(commandSpec().options()); + optionList.sort(createShortOptionNameComparator()); + List result = new ArrayList<>(); + optionList.removeAll(done); + + ColorScheme colorScheme = colorScheme(); + IParamLabelRenderer parameterLabelRenderer = createMinimalSpacedParamLabelRenderer(); + + for (CommandLine.Model.OptionSpec option : optionList) + { + if (option.hidden()) + continue; + + Ansi.Text text = ansi().new Text(0); + String nameString = option.shortestName(); + Ansi.Text name = colorScheme.optionText(nameString); + Ansi.Text nameFull = colorScheme.optionText(option.longestName()); + text = text.concat("[(") + .concat(name) + .concat(spacedParamLabel(option, parameterLabelRenderer, colorScheme)) + .concat(" | ") + .concat(nameFull) + .concat(spacedParamLabel(option, parameterLabelRenderer, colorScheme)) + .concat(")]"); + + result.add(text); + } + return result; + } + + public static IParamLabelRenderer createMinimalSpacedParamLabelRenderer() + { + return new IParamLabelRenderer() + { + public Ansi.Text renderParameterLabel(CommandLine.Model.ArgSpec argSpec, Ansi ansi, List styles) + { + ColorScheme colorScheme = CommandLine.Help.defaultColorScheme(ansi); + if (argSpec.equals(CASSANDRA_END_OF_OPTIONS_OPTION)) + return colorScheme.text(""); + if (argSpec instanceof CommandLine.Model.OptionSpec && argSpec.typeInfo().isBoolean()) + return colorScheme.text(""); + return argSpec.isOption() ? colorScheme.optionText(argSpec.paramLabel()) : + colorScheme.parameterText(argSpec.paramLabel()); + } + + public String separator() + { + return ""; + } + }; + } + + @Override + public String optionListHeading(Object... params) + { + return createHeading(OPTIONS_HEADING, params); + } + + @Override + public String optionList() + { + Comparator comparator = createShortOptionNameComparator(); + List optionList = commandSpec().options(); + + List options = new ArrayList<>(optionList); + options.sort(comparator); + + Layout layout = cassandraSingleColumnOptionsParametersLayout(); + layout.addAllOptions(options, createMinimalSpacedParamLabelRenderer()); + return layout.toString(); + } + + @Override + public String endOfOptionsList() { + Layout layout = cassandraSingleColumnOptionsParametersLayout(); + layout.addOption(CASSANDRA_END_OF_OPTIONS_OPTION, createMinimalSpacedParamLabelRenderer()); + return layout.toString(); + } + + private Layout cassandraSingleColumnOptionsParametersLayout() + { + TextTable table = TextTable.forColumns(colorScheme(), new Column(commandSpec().usageMessage().width() - COLUMN_INDENT, + COLUMN_INDENT, Column.Overflow.WRAP)); + table.setAdjustLineBreaksForWideCJKCharacters(commandSpec().usageMessage().adjustLineBreaksForWideCJKCharacters()); + table.indentWrappedLines = DESCRIPTION_INDENT; + return new Layout(colorScheme(), table, new CassandraStyleOptionRenderer(), new CassandraStyleParameterRenderer()); + } + + @Override + public String parameterList() + { + List positionalParams = cassandraPositionals(commandSpec()); + Layout layout = cassandraSingleColumnOptionsParametersLayout(); + layout.addAllPositionalParameters(positionalParams, createMinimalSpacedParamLabelRenderer()); + return layout.toString(); + } + + @Override + public String footerHeading(Object... params) + { + return createHeading("%n", params); + } + + private static List cassandraPositionals(CommandLine.Model.CommandSpec commandSpec) + { + List positionals = new ArrayList<>(commandSpec.positionalParameters()); + for (CommandLine.Model.PositionalParamSpec param : positionals) + { + if (param.hidden()) + { + if (param.description()[0].equals(CommandUtils.CASSANDRA_BACKWARD_COMPATIBLE_MARKER)) + { + positionals.clear(); + positionals.add(param); + break; + } + else + positionals.remove(param); + } + } + return positionals; + } + + /** + * Layout for cassandra help CLI output. + * @return List of keys for the help sections. + */ + public static List cassandraHelpSectionKeys() + { + List result = new LinkedList<>(); + result.add(SECTION_KEY_HEADER_HEADING); + result.add(SECTION_KEY_HEADER); + result.add(SECTION_KEY_DESCRIPTION_HEADING); + result.add(SECTION_KEY_DESCRIPTION); + result.add(SECTION_KEY_SYNOPSIS_HEADING); + result.add(SECTION_KEY_SYNOPSIS); + result.add(SECTION_KEY_OPTION_LIST_HEADING); + result.add(SECTION_KEY_OPTION_LIST); + result.add(SECTION_KEY_END_OF_OPTIONS); + result.add(SECTION_KEY_PARAMETER_LIST_HEADING); + result.add(SECTION_KEY_PARAMETER_LIST); + result.add(SECTION_KEY_COMMAND_LIST_HEADING); + result.add(SECTION_KEY_COMMAND_LIST); + result.add(SECTION_KEY_EXIT_CODE_LIST_HEADING); + result.add(SECTION_KEY_EXIT_CODE_LIST); + result.add(SECTION_KEY_FOOTER_HEADING); + result.add(SECTION_KEY_FOOTER); + return result; + } + + private static class LineBreakingOptionsIterator extends AbstractGuavaIterator + { + private final Iterator optionsIterator; + private final int width; + private Ansi.Text prev; + + LineBreakingOptionsIterator(Iterator optionsIterator, int width) + { + this.optionsIterator = optionsIterator; + this.width = width; + } + + @Override + protected Ansi.Text computeNext() + { + while (optionsIterator.hasNext()) + { + Ansi.Text next = optionsIterator.next(); + if (prev == null) + prev = next; + + Ansi.Text curr; + if (prev == next) + curr = next; + else + curr = prev.concat(" ").concat(next); + + if (curr.plainString().length() > width) + { + Ansi.Text result = prev; + prev = next; + return result; + } + else + { + prev = curr; + } + } + + if (prev == null) + return endOfData(); + else + { + Ansi.Text result = prev; + prev = null; + return result; + } + } + } + + private static Ansi.Text spacedParamLabel(CommandLine.Model.OptionSpec optionSpec, + IParamLabelRenderer parameterLabelRenderer, + ColorScheme scheme) + { + return optionSpec.typeInfo().isBoolean() ? scheme.text("") : + scheme.text(" ").concat(parameterLabelRenderer.renderParameterLabel(optionSpec, scheme.ansi(), scheme.optionParamStyles())); + } + + private static class CassandraStyleOptionRenderer implements IOptionRenderer + { + public Ansi.Text[][] render(CommandLine.Model.OptionSpec option, IParamLabelRenderer parameterLabelRenderer, ColorScheme scheme) + { + Ansi.Text optionText = scheme.optionText(""); + for (int i = 0; i < option.names().length; i++) + { + String name = option.names()[i]; + optionText = optionText.concat(scheme.optionText(name)) + .concat(spacedParamLabel(option, parameterLabelRenderer, scheme)) + .concat(i == option.names().length - 1 ? "" : ", "); + } + + Ansi.Text descPadding = Ansi.OFF.new Text(leadingSpaces(DESCRIPTION_INDENT), scheme); + Ansi.Text desc = scheme.optionText(option.description().length == 0 ? "" : option.description()[0]); + + Ansi.Text[][] result = new Ansi.Text[3][]; + result[0] = new Ansi.Text[]{optionText}; + result[1] = new Ansi.Text[]{descPadding.concat(desc)}; + result[2] = new Ansi.Text[]{Ansi.OFF.new Text("", scheme)}; + return result; + } + } + + private static class CassandraStyleParameterRenderer implements IParameterRenderer + { + @Override + public Ansi.Text[][] render(CommandLine.Model.PositionalParamSpec param, IParamLabelRenderer parameterLabelRenderer, ColorScheme scheme) + { + String descriptionString = param.description()[0].equals(CommandUtils.CASSANDRA_BACKWARD_COMPATIBLE_MARKER) ? + param.description()[1] : param.description()[0]; + + Ansi.Text descPadding = Ansi.OFF.new Text(leadingSpaces(DESCRIPTION_INDENT), scheme); + Ansi.Text[][] result = new Ansi.Text[3][]; + result[0] = new Ansi.Text[]{ parameterLabelRenderer.renderParameterLabel(param, scheme.ansi(), scheme.parameterStyles()) }; + result[1] = new Ansi.Text[]{ descPadding.concat(scheme.text(descriptionString)) }; + result[2] = new Ansi.Text[]{ Ansi.OFF.new Text("", scheme) }; + return result; + } + } +} diff --git a/src/java/org/apache/cassandra/management/CommandUtils.java b/src/java/org/apache/cassandra/management/CommandUtils.java new file mode 100644 index 000000000000..86fc0d8c3cc0 --- /dev/null +++ b/src/java/org/apache/cassandra/management/CommandUtils.java @@ -0,0 +1,39 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.management; + +import java.util.Arrays; + +public final class CommandUtils +{ + public static final String CASSANDRA_BACKWARD_COMPATIBLE_MARKER = "cassandra-backward-compatible"; + + /** + * Returns a string with the given number of leading spaces. + * + * @param num the number of leading spaces + * @return the string with the given number of leading spaces + */ + public static String leadingSpaces(int num) + { + char[] buff = new char[num]; + Arrays.fill(buff, ' '); + return new String(buff); + } +} diff --git a/src/java/org/apache/cassandra/management/ManagementContext.java b/src/java/org/apache/cassandra/management/ManagementContext.java new file mode 100644 index 000000000000..cddfcb81abef --- /dev/null +++ b/src/java/org/apache/cassandra/management/ManagementContext.java @@ -0,0 +1,27 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.management; + +/** + * Management context for nodetool commands to access management services like StorageServiceMBean etc. + */ +public interface ManagementContext +{ + T getManagementService(Class serviceClass); +} diff --git a/src/java/org/apache/cassandra/management/api/AbortBootstrap.java b/src/java/org/apache/cassandra/management/api/AbortBootstrap.java new file mode 100644 index 000000000000..d741f9dce195 --- /dev/null +++ b/src/java/org/apache/cassandra/management/api/AbortBootstrap.java @@ -0,0 +1,47 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.cassandra.management.api; + +import org.apache.cassandra.management.BaseCommand; +import org.apache.cassandra.management.ManagementContext; +import org.apache.cassandra.service.StorageServiceMBean; +import picocli.CommandLine.Command; +import picocli.CommandLine.Option; + +import static org.apache.commons.lang3.StringUtils.EMPTY; +import static org.apache.commons.lang3.StringUtils.isEmpty; + +@Command(name = "abortbootstrap", description = "Abort a failed bootstrap") +public class AbortBootstrap extends BaseCommand +{ + @Option(names = "--node", description = "Node ID of the node that failed bootstrap") + private String nodeId = EMPTY; + + @Option(names = "--ip", description = "IP of the node that failed bootstrap") + private String endpoint = EMPTY; + + @Override + public void execute(ManagementContext probe) + { + if (isEmpty(nodeId) && isEmpty(endpoint)) + throw new IllegalArgumentException("Either --node or --ip needs to be set"); + if (!isEmpty(nodeId) && !isEmpty(endpoint)) + throw new IllegalArgumentException("Only one of --node or --ip need to be set"); + probe.getManagementService(StorageServiceMBean.class).abortBootstrap(nodeId, endpoint); + } +} diff --git a/src/java/org/apache/cassandra/management/api/Assassinate.java b/src/java/org/apache/cassandra/management/api/Assassinate.java new file mode 100644 index 000000000000..0ecb7696436a --- /dev/null +++ b/src/java/org/apache/cassandra/management/api/Assassinate.java @@ -0,0 +1,38 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.cassandra.management.api; + +import org.apache.cassandra.management.BaseCommand; +import org.apache.cassandra.management.ManagementContext; +import org.apache.cassandra.service.StorageServiceMBean; +import picocli.CommandLine; + +import static org.apache.commons.lang3.StringUtils.EMPTY; + +@CommandLine.Command(name = "assassinate", description = "Forcefully remove a dead node without re-replicating any data. Use as a last resort if you cannot removenode") +public class Assassinate extends BaseCommand +{ + @CommandLine.Parameters(description = "IP address of the endpoint to assassinate", arity = "1") + private String ip_address = EMPTY; + + @Override + public void execute(ManagementContext probe) + { + probe.getManagementService(StorageServiceMBean.class).assassinateEndpoint(ip_address); + } +} diff --git a/src/java/org/apache/cassandra/management/api/ForceCompact.java b/src/java/org/apache/cassandra/management/api/ForceCompact.java new file mode 100644 index 000000000000..aad5e7bed1c0 --- /dev/null +++ b/src/java/org/apache/cassandra/management/api/ForceCompact.java @@ -0,0 +1,72 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.management.api; + +import java.util.List; + +import com.google.common.collect.Lists; + +import org.apache.cassandra.management.BaseCommand; +import org.apache.cassandra.management.CommandUtils; +import org.apache.cassandra.management.ManagementContext; +import org.apache.cassandra.service.StorageServiceMBean; +import picocli.CommandLine; + +import static com.google.common.base.Preconditions.checkArgument; +import static org.apache.cassandra.tools.NodeToolV2.NodeToolCmd.parsePartitionKeys; + +@CommandLine.Command(name = "forcecompact", description = "Force a (major) compaction on a table") +public class ForceCompact extends BaseCommand +{ + @CommandLine.Parameters(hidden = true, arity = "0", paramLabel = "[ ]", + description = { CommandUtils.CASSANDRA_BACKWARD_COMPATIBLE_MARKER, + "The keyspace, table, and a list of partition keys ignoring the gc_grace_seconds" }) + private List args; + + @CommandLine.Parameters(index = "0", arity = "1", description = "The keyspace name to compact") + private String keyspace; + + @CommandLine.Parameters(index = "1", arity = "1", description = "The table name to compact") + private String table; + + @CommandLine.Parameters(index = "2..*", arity = "1", description = "The partition keys to compact") + private String[] keys; + + @Override + public void execute(ManagementContext probe) + { + args = Lists.asList(keyspace, table, keys); + // Check if the input has valid size + checkArgument(args.size() >= 3, "forcecompact requires keyspace, table and keys args"); + + // We rely on lower-level APIs to check and throw exceptions if the input keyspace or table name are invalid + String keyspaceName = args.get(0); + String tableName = args.get(1); + String[] partitionKeysIgnoreGcGrace = parsePartitionKeys(args); + + try + { + probe.getManagementService(StorageServiceMBean.class).forceCompactionKeysIgnoringGcGrace(keyspaceName, tableName, partitionKeysIgnoreGcGrace); + } + catch (Exception e) + { + throw new RuntimeException("Error occurred during compaction keys", e); + } + } +} diff --git a/src/java/org/apache/cassandra/management/api/JmxConnect.java b/src/java/org/apache/cassandra/management/api/JmxConnect.java new file mode 100644 index 000000000000..f169b523be80 --- /dev/null +++ b/src/java/org/apache/cassandra/management/api/JmxConnect.java @@ -0,0 +1,92 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.management.api; + +import java.io.IOException; +import javax.inject.Inject; + +import com.google.common.base.Throwables; + +import org.apache.cassandra.management.ManagementContext; +import org.apache.cassandra.tools.INodeProbeFactory; +import org.apache.cassandra.tools.Output; +import picocli.CommandLine; + +import static java.lang.Integer.parseInt; +import static org.apache.commons.lang3.StringUtils.EMPTY; + +/** + * Command options for NodeTool commands that are executed via JMX. + */ +@CommandLine.Command(name = "connect", description = "Connect NodeTool to a Cassandra node using JMX") +public class JmxConnect implements Runnable +{ + public static final String MIXIN_KEY = "jmx"; + + @CommandLine.Option(names = { "-h", "--host"}, description = "Node hostname or ip address") + private String host = "127.0.0.1"; + + @CommandLine.Option(names = {"-p", "--port"}, description = "Remote jmx agent port number") + private String port = "7199"; + + @CommandLine.Option(names = {"-u", "--username"}, description = "Remote jmx agent username") + private String username = EMPTY; + + @CommandLine.Option(names = {"-pw", "--password"}, description = "Remote jmx agent password") + private String password = EMPTY; + + @CommandLine.Option(names = {"-pwf", "--password-file"}, description = "Path to the JMX password file") + private String passwordFilePath = EMPTY; + + @CommandLine.Option(names = { "-pp", "--print-port"}, description = "Operate in 4.0 mode with hosts disambiguated by port number") + private boolean printPort = false; + + @Inject + private INodeProbeFactory nodeProbeFactory; + @Inject + private Output output; + public ManagementContext nodeClient; + + public ManagementContext init() + { + try + { + if (username.isEmpty()) + nodeClient = nodeProbeFactory.create(host, parseInt(port)); + else + nodeClient = nodeProbeFactory.create(host, parseInt(port), username, password); + + return nodeClient; + } + catch (IOException | SecurityException e) + { + Throwable rootCause = Throwables.getRootCause(e); + output.err.printf("nodetool: Failed to connect to '%s:%s' - %s: '%s'.%n", host, port, + rootCause.getClass().getSimpleName(), rootCause.getMessage()); + } + + return null; + } + + @Override + public void run() + { + // no-op + } +} diff --git a/src/java/org/apache/cassandra/management/api/TopLevelCommand.java b/src/java/org/apache/cassandra/management/api/TopLevelCommand.java new file mode 100644 index 000000000000..ff5c7658c665 --- /dev/null +++ b/src/java/org/apache/cassandra/management/api/TopLevelCommand.java @@ -0,0 +1,29 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.management.api; + +import org.apache.cassandra.management.CassandraHelpCommand; +import picocli.CommandLine; + +@CommandLine.Command(name = "nodetool", + subcommands = { CassandraHelpCommand.class }, + description = "Manage your Cassandra cluster") +public class TopLevelCommand +{ +} diff --git a/src/java/org/apache/cassandra/tools/NodeProbe.java b/src/java/org/apache/cassandra/tools/NodeProbe.java index b121cb310091..1151fc685c02 100644 --- a/src/java/org/apache/cassandra/tools/NodeProbe.java +++ b/src/java/org/apache/cassandra/tools/NodeProbe.java @@ -37,11 +37,11 @@ import java.util.List; import java.util.Map; import java.util.Map.Entry; +import java.util.Optional; import java.util.Set; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; - import javax.annotation.Nullable; import javax.management.JMX; import javax.management.MBeanServerConnection; @@ -55,12 +55,20 @@ import javax.management.remote.JMXServiceURL; import javax.rmi.ssl.SslRMIClientSocketFactory; +import com.google.common.base.Function; +import com.google.common.base.Strings; +import com.google.common.collect.HashMultimap; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Iterables; +import com.google.common.collect.Maps; +import com.google.common.collect.Multimap; +import com.google.common.collect.Sets; +import com.google.common.util.concurrent.Uninterruptibles; + import org.apache.cassandra.audit.AuditLogManager; import org.apache.cassandra.audit.AuditLogManagerMBean; import org.apache.cassandra.audit.AuditLogOptions; import org.apache.cassandra.audit.AuditLogOptionsCompositeData; - -import com.google.common.collect.ImmutableMap; import org.apache.cassandra.auth.AuthCache; import org.apache.cassandra.auth.AuthCacheMBean; import org.apache.cassandra.auth.CIDRGroupsMappingManager; @@ -97,12 +105,12 @@ import org.apache.cassandra.metrics.StorageMetrics; import org.apache.cassandra.metrics.TableMetrics; import org.apache.cassandra.metrics.ThreadPoolMetrics; +import org.apache.cassandra.management.ManagementContext; import org.apache.cassandra.net.MessagingService; import org.apache.cassandra.net.MessagingServiceMBean; import org.apache.cassandra.service.ActiveRepairServiceMBean; import org.apache.cassandra.service.CacheService; import org.apache.cassandra.service.CacheServiceMBean; -import org.apache.cassandra.tcm.CMSOperationsMBean; import org.apache.cassandra.service.GCInspector; import org.apache.cassandra.service.GCInspectorMXBean; import org.apache.cassandra.service.StorageProxy; @@ -111,19 +119,10 @@ import org.apache.cassandra.streaming.StreamManagerMBean; import org.apache.cassandra.streaming.StreamState; import org.apache.cassandra.streaming.management.StreamStateCompositeData; -import org.apache.cassandra.tools.nodetool.formatter.TableBuilder; - -import com.google.common.base.Function; -import com.google.common.base.Strings; -import com.google.common.collect.HashMultimap; -import com.google.common.collect.Iterables; -import com.google.common.collect.Maps; -import com.google.common.collect.Multimap; -import com.google.common.collect.Sets; -import com.google.common.util.concurrent.Uninterruptibles; - import org.apache.cassandra.tcm.CMSOperations; +import org.apache.cassandra.tcm.CMSOperationsMBean; import org.apache.cassandra.tools.nodetool.GetTimeout; +import org.apache.cassandra.tools.nodetool.formatter.TableBuilder; import org.apache.cassandra.utils.NativeLibrary; import static org.apache.cassandra.config.CassandraRelevantProperties.NODETOOL_JMX_NOTIFICATION_POLL_INTERVAL_SECONDS; @@ -132,7 +131,7 @@ /** * JMX client operations for Cassandra. */ -public class NodeProbe implements AutoCloseable +public class NodeProbe implements AutoCloseable, ManagementContext { private static final String fmtUrl = "service:jmx:rmi:///jndi/rmi://%s:%d/jmxrmi"; private static final String ssObjName = "org.apache.cassandra.db:type=StorageService"; @@ -175,6 +174,7 @@ public class NodeProbe implements AutoCloseable private boolean failed; protected CIDRFilteringMetricsTableMBean cfmProxy; + private final Map cachedMBeans = new HashMap<>(); /** * Creates a NodeProbe using the specified JMX host, port, username, and password. @@ -233,6 +233,20 @@ protected NodeProbe() this.output = Output.CONSOLE; } + private T cacheProxy(T proxy) + { + cachedMBeans.put(proxy.getClass().getName(), proxy); + return proxy; + } + + @Override + public T getManagementService(Class serviceClass) + { + + return serviceClass.cast(Optional.ofNullable(cachedMBeans.get(serviceClass.getName())) + .orElseThrow(() -> new IllegalArgumentException("No MBean found for " + serviceClass.getName()))); + } + /** * Create a connection to the JMX agent and setup the M[X]Bean proxies. * @@ -262,23 +276,23 @@ protected void connect() throws IOException try { ObjectName name = new ObjectName(ssObjName); - ssProxy = JMX.newMBeanProxy(mbeanServerConn, name, StorageServiceMBean.class); + ssProxy = cacheProxy(JMX.newMBeanProxy(mbeanServerConn, name, StorageServiceMBean.class)); name = new ObjectName(CMSOperations.MBEAN_OBJECT_NAME); - cmsProxy = JMX.newMBeanProxy(mbeanServerConn, name, CMSOperationsMBean.class); + cmsProxy = cacheProxy(JMX.newMBeanProxy(mbeanServerConn, name, CMSOperationsMBean.class)); name = new ObjectName(MessagingService.MBEAN_NAME); - msProxy = JMX.newMBeanProxy(mbeanServerConn, name, MessagingServiceMBean.class); + msProxy = cacheProxy(JMX.newMBeanProxy(mbeanServerConn, name, MessagingServiceMBean.class)); name = new ObjectName(StreamManagerMBean.OBJECT_NAME); - streamProxy = JMX.newMBeanProxy(mbeanServerConn, name, StreamManagerMBean.class); + streamProxy = cacheProxy(JMX.newMBeanProxy(mbeanServerConn, name, StreamManagerMBean.class)); name = new ObjectName(CompactionManager.MBEAN_OBJECT_NAME); - compactionProxy = JMX.newMBeanProxy(mbeanServerConn, name, CompactionManagerMBean.class); + compactionProxy = cacheProxy(JMX.newMBeanProxy(mbeanServerConn, name, CompactionManagerMBean.class)); name = new ObjectName(FailureDetector.MBEAN_NAME); - fdProxy = JMX.newMBeanProxy(mbeanServerConn, name, FailureDetectorMBean.class); + fdProxy = cacheProxy(JMX.newMBeanProxy(mbeanServerConn, name, FailureDetectorMBean.class)); name = new ObjectName(CacheService.MBEAN_NAME); - cacheService = JMX.newMBeanProxy(mbeanServerConn, name, CacheServiceMBean.class); + cacheService = cacheProxy(JMX.newMBeanProxy(mbeanServerConn, name, CacheServiceMBean.class)); name = new ObjectName(StorageProxy.MBEAN_NAME); - spProxy = JMX.newMBeanProxy(mbeanServerConn, name, StorageProxyMBean.class); + spProxy = cacheProxy(JMX.newMBeanProxy(mbeanServerConn, name, StorageProxyMBean.class)); name = new ObjectName(HintsService.MBEAN_NAME); - hsProxy = JMX.newMBeanProxy(mbeanServerConn, name, HintsServiceMBean.class); + hsProxy = cacheProxy(JMX.newMBeanProxy(mbeanServerConn, name, HintsServiceMBean.class)); name = new ObjectName(GCInspector.MBEAN_NAME); gcProxy = JMX.newMBeanProxy(mbeanServerConn, name, GCInspectorMXBean.class); name = new ObjectName(Gossiper.MBEAN_NAME); diff --git a/src/java/org/apache/cassandra/tools/NodeTool.java b/src/java/org/apache/cassandra/tools/NodeTool.java index bd1e302ba066..94a784bfc06d 100644 --- a/src/java/org/apache/cassandra/tools/NodeTool.java +++ b/src/java/org/apache/cassandra/tools/NodeTool.java @@ -337,11 +337,13 @@ public static class CassHelp extends Help implements NodeToolCmdRunnable { public void run(INodeProbeFactory nodeProbeFactory, Output output) { - run(); + StringBuilder sb = new StringBuilder(); + help(global, command, sb); + output.out.println(sb); } } - interface NodeToolCmdRunnable + public interface NodeToolCmdRunnable { void run(INodeProbeFactory nodeProbeFactory, Output output); } diff --git a/src/java/org/apache/cassandra/tools/NodeToolV2.java b/src/java/org/apache/cassandra/tools/NodeToolV2.java new file mode 100644 index 000000000000..bcfb776873c5 --- /dev/null +++ b/src/java/org/apache/cassandra/tools/NodeToolV2.java @@ -0,0 +1,585 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.cassandra.tools; + +import java.io.Console; +import java.io.FileNotFoundException; +import java.io.IOError; +import java.io.IOException; +import java.io.PrintWriter; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Date; +import java.util.List; +import java.util.Scanner; +import javax.management.InstanceNotFoundException; + +import com.google.common.base.Joiner; +import com.google.common.base.Throwables; + +import io.airlift.airline.Cli; +import io.airlift.airline.Help; +import io.airlift.airline.Option; +import io.airlift.airline.OptionType; +import io.airlift.airline.ParseArgumentsMissingException; +import io.airlift.airline.ParseArgumentsUnexpectedException; +import io.airlift.airline.ParseCommandMissingException; +import io.airlift.airline.ParseCommandUnrecognizedException; +import io.airlift.airline.ParseOptionConversionException; +import io.airlift.airline.ParseOptionMissingException; +import io.airlift.airline.ParseOptionMissingValueException; +import org.apache.cassandra.io.util.File; +import org.apache.cassandra.io.util.FileWriter; +import org.apache.cassandra.management.BaseCommand; +import org.apache.cassandra.management.CassandraHelpLayout; +import org.apache.cassandra.management.api.AbortBootstrap; +import org.apache.cassandra.management.api.Assassinate; +import org.apache.cassandra.management.api.ForceCompact; +import org.apache.cassandra.management.api.JmxConnect; +import org.apache.cassandra.management.api.TopLevelCommand; +import org.apache.cassandra.utils.FBUtilities; +import picocli.CommandLine; + +import static com.google.common.base.Throwables.getStackTraceAsString; +import static com.google.common.collect.Iterables.toArray; +import static com.google.common.collect.Lists.newArrayList; +import static java.lang.Integer.parseInt; +import static java.lang.String.format; +import static org.apache.cassandra.io.util.File.WriteMode.APPEND; +import static org.apache.commons.lang3.ArrayUtils.EMPTY_STRING_ARRAY; +import static org.apache.commons.lang3.StringUtils.EMPTY; +import static org.apache.commons.lang3.StringUtils.isEmpty; +import static org.apache.commons.lang3.StringUtils.isNotEmpty; + +public class NodeToolV2 +{ + static + { + FBUtilities.preventIllegalAccessWarnings(); + } + + private static final String HISTORYFILE = "nodetool.history"; + + private final INodeProbeFactory nodeProbeFactory; + private final Output output; + + public static void main(String... args) + { + System.exit(new NodeToolV2(new NodeProbeFactory(), Output.CONSOLE).execute(args)); + } + + public NodeToolV2(INodeProbeFactory nodeProbeFactory, Output output) + { + this.nodeProbeFactory = nodeProbeFactory; + this.output = output; + } + + public int execute(String... args) + { + List> commands = newArrayList( + CassHelp.class +// CIDRFilteringStats.class, +// Cleanup.class, +// ClearSnapshot.class, +// ClientStats.class, +// Compact.class, +// CompactionHistory.class, +// CompactionStats.class, +// DataPaths.class, +// Decommission.class, +// DescribeCluster.class, +// DescribeRing.class, +// DisableAuditLog.class, +// DisableAutoCompaction.class, +// DisableBackup.class, +// DisableBinary.class, +// DisableFullQueryLog.class, +// DisableGossip.class, +// DisableHandoff.class, +// DisableHintsForDC.class, +// DisableOldProtocolVersions.class, +// Drain.class, +// DropCIDRGroup.class, +// EnableAuditLog.class, +// EnableAutoCompaction.class, +// EnableBackup.class, +// EnableBinary.class, +// EnableFullQueryLog.class, +// EnableGossip.class, +// EnableHandoff.class, +// EnableHintsForDC.class, +// EnableOldProtocolVersions.class, +// FailureDetectorInfo.class, +// Flush.class, +// GarbageCollect.class, +// GcStats.class, +// GetAuditLog.class, +// GetAuthCacheConfig.class, +// GetBatchlogReplayTrottle.class, +// GetCIDRGroupsOfIP.class, +// GetColumnIndexSize.class, +// GetCompactionThreshold.class, +// GetCompactionThroughput.class, +// GetConcurrency.class, +// GetConcurrentCompactors.class, +// GetConcurrentViewBuilders.class, +// GetDefaultKeyspaceRF.class, +// GetEndpoints.class, +// GetFullQueryLog.class, +// GetInterDCStreamThroughput.class, +// GetLoggingLevels.class, +// GetMaxHintWindow.class, +// GetSSTables.class, +// GetSeeds.class, +// GetSnapshotThrottle.class, +// GetStreamThroughput.class, +// GetTimeout.class, +// GetTraceProbability.class, +// GossipInfo.class, +// Import.class, +// Info.class, +// InvalidateCIDRPermissionsCache.class, +// InvalidateCounterCache.class, +// InvalidateCredentialsCache.class, +// InvalidateJmxPermissionsCache.class, +// ReloadCIDRGroupsCache.class, +// InvalidateKeyCache.class, +// InvalidateNetworkPermissionsCache.class, +// InvalidatePermissionsCache.class, +// InvalidateRolesCache.class, +// InvalidateRowCache.class, +// Join.class, +// ListCIDRGroups.class, +// ListPendingHints.class, +// ListSnapshots.class, +// Move.class, +// NetStats.class, +// PauseHandoff.class, +// ProfileLoad.class, +// ProxyHistograms.class, +// RangeKeySample.class, +// Rebuild.class, +// RebuildIndex.class, +// RecompressSSTables.class, +// Refresh.class, +// RefreshSizeEstimates.class, +// ReloadLocalSchema.class, +// ReloadSeeds.class, +// ReloadSslCertificates.class, +// ReloadTriggers.class, +// RelocateSSTables.class, +// RemoveNode.class, +// Repair.class, +// ReplayBatchlog.class, +// ResetFullQueryLog.class, +// ResetLocalSchema.class, +// ResumeHandoff.class, +// Ring.class, +// Scrub.class, +// SetAuthCacheConfig.class, +// SetBatchlogReplayThrottle.class, +// SetCacheCapacity.class, +// SetCacheKeysToSave.class, +// SetColumnIndexSize.class, +// SetCompactionThreshold.class, +// SetCompactionThroughput.class, +// SetConcurrency.class, +// SetConcurrentCompactors.class, +// SetConcurrentViewBuilders.class, +// SetDefaultKeyspaceRF.class, +// SetHintedHandoffThrottleInKB.class, +// SetInterDCStreamThroughput.class, +// SetLoggingLevel.class, +// SetMaxHintWindow.class, +// SetSnapshotThrottle.class, +// SetStreamThroughput.class, +// SetTimeout.class, +// SetTraceProbability.class, +// Sjk.class, +// Snapshot.class, +// Status.class, +// StatusAutoCompaction.class, +// StatusBackup.class, +// StatusBinary.class, +// StatusGossip.class, +// StatusHandoff.class, +// Stop.class, +// StopDaemon.class, +// TableHistograms.class, +// TableStats.class, +// TopPartitions.class, +// TpStats.class, +// TruncateHints.class, +// UpdateCIDRGroup.class, +// UpgradeSSTable.class, +// Verify.class, +// Version.class, +// ViewBuildStatus.class + ); + + List> cliCommands = newArrayList( + AbortBootstrap.class, + Assassinate.class, + ForceCompact.class); + + CommandLine commandLine = new CommandLine(new TopLevelCommand()); +// commandLine.setExecutionStrategy(new CommandLine.RunAll()); + commandLine.addMixin(JmxConnect.MIXIN_KEY, new JmxConnect()); + commandLine.setOut(new PrintWriter(output.out)) + .setErr(new PrintWriter(output.err)) + .setExecutionExceptionHandler((ex, cmdLine, parseResult) -> { + if (ex instanceof CommandLine.ParameterException) + { + output.err.println(ex.getMessage()); + commandLine.usage(output.err); + } + return 1; + }); + + try + { + for (Class commandClass : cliCommands) + commandLine.addSubcommand(commandLine.getFactory().create(commandClass)); + + // This must be after all the subcommands are added to the commandLine. + commandLine.setHelpFactory(CassandraHelpLayout::new); + commandLine.getSubcommands().values().forEach(sub -> sub.addMixin(JmxConnect.MIXIN_KEY, new JmxConnect())); + commandLine.setUsageHelpWidth(CassandraHelpLayout.DEFAULT_USAGE_HELP_WIDTH); + commandLine.setHelpSectionKeys(CassandraHelpLayout.cassandraHelpSectionKeys()); + } + catch (Exception e) + { + err(Throwables.getRootCause(e)); + return 2; + } + +// return commandLine.execute(args); + + Cli.CliBuilder builder = Cli.builder("nodetool"); + + builder.withDescription("Manage your Cassandra cluster") + .withDefaultCommand(CassHelp.class) + .withCommands(commands); + + // bootstrap commands +// builder.withGroup("bootstrap") +// .withDescription("Monitor/manage node's bootstrap process") +// .withDefaultCommand(CassHelp.class) +// .withCommand(BootstrapResume.class); +// +// builder.withGroup("repair_admin") +// .withDescription("list and fail incremental repair sessions") +// .withDefaultCommand(RepairAdmin.ListCmd.class) +// .withCommand(RepairAdmin.ListCmd.class) +// .withCommand(RepairAdmin.CancelCmd.class) +// .withCommand(RepairAdmin.CleanupDataCmd.class) +// .withCommand(RepairAdmin.SummarizePendingCmd.class) +// .withCommand(RepairAdmin.SummarizeRepairedCmd.class); +// +// builder.withGroup("cms") +// .withDescription("Manage cluster metadata") +// .withDefaultCommand(CMSAdmin.DescribeCMS.class) +// .withCommand(CMSAdmin.DescribeCMS.class) +// .withCommand(CMSAdmin.InitializeCMS.class) +// .withCommand(CMSAdmin.ReconfigureCMS.class) +// .withCommand(CMSAdmin.Snapshot.class) +// .withCommand(CMSAdmin.Unregister.class); + + Cli parser = builder.build(); + + int status = 0; + try + { +// NodeToolCmdRunnable parse = parser.parse(args); + printHistory(args); + commandLine.execute(args); +// parse.run(nodeProbeFactory, output); + } catch (IllegalArgumentException | + IllegalStateException | + ParseArgumentsMissingException | + ParseArgumentsUnexpectedException | + ParseOptionConversionException | + ParseOptionMissingException | + ParseOptionMissingValueException | + ParseCommandMissingException | + ParseCommandUnrecognizedException e) + { + badUse(e); + status = 1; + } catch (Throwable throwable) + { + err(Throwables.getRootCause(throwable)); + status = 2; + } + + return status; + } + + private static Object instantiateCommand(Class commandClass) + { + try + { + return commandClass.getDeclaredConstructor().newInstance(); + } + catch (Exception e) + { + throw new RuntimeException("Failed to instantiate command " + commandClass.getName(), e); + } + } + + private static void printHistory(String... args) + { + //don't bother to print if no args passed (meaning, nodetool is just printing out the sub-commands list) + if (args.length == 0) + return; + + String cmdLine = Joiner.on(" ").skipNulls().join(args); + cmdLine = cmdLine.replaceFirst("(?<=(-pw|--password))\\s+\\S+", " "); + + try (FileWriter writer = new File(FBUtilities.getToolsOutputDirectory(), HISTORYFILE).newWriter(APPEND)) + { + SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss,SSS"); + writer.append(sdf.format(new Date())).append(": ").append(cmdLine).append(System.lineSeparator()); + } + catch (IOException | IOError ioe) + { + //quietly ignore any errors about not being able to write out history + } + } + + protected void badUse(Exception e) + { + output.out.println("nodetool: " + e.getMessage()); + output.out.println("See 'nodetool help' or 'nodetool help '."); + } + + protected void err(Throwable e) + { + // CASSANDRA-11537: friendly error message when server is not ready + if (e instanceof InstanceNotFoundException) + throw new IllegalArgumentException("Server is not initialized yet, cannot run nodetool."); + + output.err.println("error: " + e.getMessage()); + output.err.println("-- StackTrace --"); + output.err.println(getStackTraceAsString(e)); + } + + public static class CassHelp extends Help implements NodeToolCmdRunnable + { + public void run(INodeProbeFactory nodeProbeFactory, Output output) + { + run(); + } + } + + interface NodeToolCmdRunnable + { + void run(INodeProbeFactory nodeProbeFactory, Output output); + } + + public static abstract class NodeToolCmd implements NodeToolCmdRunnable + { + + @Option(type = OptionType.GLOBAL, name = {"-h", "--host"}, description = "Node hostname or ip address") + private String host = "127.0.0.1"; + + @Option(type = OptionType.GLOBAL, name = {"-p", "--port"}, description = "Remote jmx agent port number") + private String port = "7199"; + + @Option(type = OptionType.GLOBAL, name = {"-u", "--username"}, description = "Remote jmx agent username") + private String username = EMPTY; + + @Option(type = OptionType.GLOBAL, name = {"-pw", "--password"}, description = "Remote jmx agent password") + private String password = EMPTY; + + @Option(type = OptionType.GLOBAL, name = {"-pwf", "--password-file"}, description = "Path to the JMX password file") + private String passwordFilePath = EMPTY; + + @Option(type = OptionType.GLOBAL, name = { "-pp", "--print-port"}, description = "Operate in 4.0 mode with hosts disambiguated by port number", arity = 0) + protected boolean printPort = false; + + private INodeProbeFactory nodeProbeFactory; + protected Output output; + + @Override + public void run(INodeProbeFactory nodeProbeFactory, Output output) + { + this.nodeProbeFactory = nodeProbeFactory; + this.output = output; + runInternal(); + } + + public void runInternal() + { + if (isNotEmpty(username)) { + if (isNotEmpty(passwordFilePath)) + password = readUserPasswordFromFile(username, passwordFilePath); + + if (isEmpty(password)) + password = promptAndReadPassword(); + } + + try (NodeProbe probe = connect()) + { + execute(probe); + if (probe.isFailed()) + throw new RuntimeException("nodetool failed, check server logs"); + } + catch (IOException e) + { + throw new RuntimeException("Error while closing JMX connection", e); + } + + } + + private String readUserPasswordFromFile(String username, String passwordFilePath) { + String password = EMPTY; + + File passwordFile = new File(passwordFilePath); + try (Scanner scanner = new Scanner(passwordFile.toJavaIOFile()).useDelimiter("\\s+")) + { + while (scanner.hasNextLine()) + { + if (scanner.hasNext()) + { + String jmxRole = scanner.next(); + if (jmxRole.equals(username) && scanner.hasNext()) + { + password = scanner.next(); + break; + } + } + scanner.nextLine(); + } + } + catch (FileNotFoundException e) + { + throw new RuntimeException(e); + } + + return password; + } + + private String promptAndReadPassword() + { + String password = EMPTY; + + Console console = System.console(); + if (console != null) + password = String.valueOf(console.readPassword("Password:")); + + return password; + } + + protected abstract void execute(NodeProbe probe); + + private NodeProbe connect() + { + NodeProbe nodeClient = null; + + try + { + if (username.isEmpty()) + nodeClient = nodeProbeFactory.create(host, parseInt(port)); + else + nodeClient = nodeProbeFactory.create(host, parseInt(port), username, password); + + nodeClient.setOutput(output); + } catch (IOException | SecurityException e) + { + Throwable rootCause = Throwables.getRootCause(e); + output.err.println(format("nodetool: Failed to connect to '%s:%s' - %s: '%s'.", host, port, rootCause.getClass().getSimpleName(), rootCause.getMessage())); + System.exit(1); + } + + return nodeClient; + } + + protected enum KeyspaceSet + { + ALL, NON_SYSTEM, NON_LOCAL_STRATEGY + } + + protected List parseOptionalKeyspace(List cmdArgs, NodeProbe nodeProbe) + { + return parseOptionalKeyspace(cmdArgs, nodeProbe, KeyspaceSet.ALL); + } + + protected List parseOptionalKeyspace(List cmdArgs, NodeProbe nodeProbe, KeyspaceSet defaultKeyspaceSet) + { + List keyspaces = new ArrayList<>(); + + + if (cmdArgs == null || cmdArgs.isEmpty()) + { + if (defaultKeyspaceSet == KeyspaceSet.NON_LOCAL_STRATEGY) + keyspaces.addAll(keyspaces = nodeProbe.getNonLocalStrategyKeyspaces()); + else if (defaultKeyspaceSet == KeyspaceSet.NON_SYSTEM) + keyspaces.addAll(keyspaces = nodeProbe.getNonSystemKeyspaces()); + else + keyspaces.addAll(nodeProbe.getKeyspaces()); + } + else + { + keyspaces.add(cmdArgs.get(0)); + } + + for (String keyspace : keyspaces) + { + if (!nodeProbe.getKeyspaces().contains(keyspace)) + throw new IllegalArgumentException("Keyspace [" + keyspace + "] does not exist."); + } + + return Collections.unmodifiableList(keyspaces); + } + + protected String[] parseOptionalTables(List cmdArgs) + { + return cmdArgs.size() <= 1 ? EMPTY_STRING_ARRAY : toArray(cmdArgs.subList(1, cmdArgs.size()), String.class); + } + + public static String[] parsePartitionKeys(List cmdArgs) + { + return cmdArgs.size() <= 2 ? EMPTY_STRING_ARRAY : toArray(cmdArgs.subList(2, cmdArgs.size()), String.class); + } + } + +// public static SortedMap getOwnershipByDcWithPort(NodeProbe probe, boolean resolveIp, +// Map tokenToEndpoint, +// Map ownerships) +// { +// SortedMap ownershipByDc = Maps.newTreeMap(); +// EndpointSnitchInfoMBean epSnitchInfo = probe.getEndpointSnitchInfoProxy(); +// try +// { +// for (Entry tokenAndEndPoint : tokenToEndpoint.entrySet()) +// { +// String dc = epSnitchInfo.getDatacenter(tokenAndEndPoint.getValue()); +// if (!ownershipByDc.containsKey(dc)) +// ownershipByDc.put(dc, new SetHostStatWithPort(resolveIp)); +// ownershipByDc.get(dc).add(tokenAndEndPoint.getKey(), tokenAndEndPoint.getValue(), ownerships); +// } +// } +// catch (UnknownHostException e) +// { +// throw new RuntimeException(e); +// } +// return ownershipByDc; +// } +} diff --git a/test/unit/org/apache/cassandra/tools/NodeToolSynopsisTest.java b/test/unit/org/apache/cassandra/tools/NodeToolSynopsisTest.java new file mode 100644 index 000000000000..67d7d2bbd4c4 --- /dev/null +++ b/test/unit/org/apache/cassandra/tools/NodeToolSynopsisTest.java @@ -0,0 +1,137 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.tools; + +import java.io.IOException; +import java.io.OutputStream; +import java.io.PrintStream; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import com.google.common.collect.ImmutableMap; +import org.apache.commons.lang3.StringUtils; +import org.junit.Test; + +import com.github.difflib.DiffUtils; +import com.github.difflib.patch.AbstractDelta; +import com.github.difflib.patch.Patch; +import org.apache.cassandra.cql3.CQLTester; +import org.apache.cassandra.tools.ToolRunner.ToolResult; + +import static org.apache.cassandra.tools.ToolRunner.invokeNodetool; +import static org.junit.Assert.assertTrue; + +public class NodeToolSynopsisTest +{ + private static final Map NODETOOLV2_ENV = ImmutableMap.of("NODETOOL_RUNNER", "org.apache.cassandra.tools.NodeToolV2"); + + @Test + public void cliHelpForcecompact() + { + ToolResult toolHistory = invokeNodetool(List.of("help", "forcecompact")); + toolHistory.assertOnCleanExit(); + String outputV1 = toolHistory.getStdout(); + + toolHistory = invokeNodetool(NODETOOLV2_ENV, List.of("help", "forcecompact")); + String outputV2 = toolHistory.getStdout(); + } + + @Test + public void cliHelp() + { + ToolResult toolHistory = invokeNodetool("help"); + toolHistory.assertOnCleanExit(); + String outputV1 = toolHistory.getStdout(); + + toolHistory = invokeNodetool(NODETOOLV2_ENV, List.of("help")); + String outputV2 = toolHistory.getStdout(); + } + + @Test + public void cliDryRun() throws Exception + { + List args = CQLTester.buildNodetoolArgs(List.of("help", "assassinate")); + args.remove("bin/nodetool"); + ListOutputStream outputV1 = new ListOutputStream(); + ListOutputStream outputV2 = new ListOutputStream(); + + new NodeTool(new NodeProbeFactory(), new Output(new PrintStream(outputV1), new PrintStream(outputV1))) + .execute(args.toArray(new String[0])); + new NodeToolV2(new NodeProbeFactory(), new Output(new PrintStream(outputV2), new PrintStream(outputV2))) + .execute(args.toArray(new String[0])); + + String diff = computeDiff(outputV1.getOutputLines(), outputV2.getOutputLines()); + assertTrue(String.join("\n", outputV1.getOutputLines()) + + '\n' + String.join("\n", outputV2.getOutputLines()) + + '\n' + " difference: " + diff, + StringUtils.isBlank(diff)); + } + + public static String computeDiff(List original, List revised) { + Patch patch = DiffUtils.diff(original, revised); + List diffLines = new ArrayList<>(); + + for (AbstractDelta delta : patch.getDeltas()) { + for (String line : delta.getSource().getLines()) { + diffLines.add(delta.getType().toString().toLowerCase() + " source: " + line); + } + for (String line : delta.getTarget().getLines()) { + diffLines.add(delta.getType().toString().toLowerCase() + " target: " + line); + } + } + + return '\n' + String.join("\n", diffLines); + } + + private static class ListOutputStream extends OutputStream + { + private final List outputLines = new ArrayList<>(); + private final StringBuilder buffer = new StringBuilder(); + + @Override + public void write(int b) + { + char c = (char) b; + if (c == '\n') + { + // Add the buffer to the list if it's a new line + outputLines.add(buffer.toString()); + buffer.setLength(0); // Clear the buffer + } + else + buffer.append(c); + } + + public void flush() + { + if (buffer.length() > 0) + { + outputLines.add(buffer.toString()); + buffer.setLength(0); + } + } + + public List getOutputLines() + { + flush(); + return new ArrayList<>(outputLines); + } + } +} diff --git a/test/unit/org/apache/cassandra/tools/ToolRunner.java b/test/unit/org/apache/cassandra/tools/ToolRunner.java index ad1f80e8127d..13eeadd64c97 100644 --- a/test/unit/org/apache/cassandra/tools/ToolRunner.java +++ b/test/unit/org/apache/cassandra/tools/ToolRunner.java @@ -191,6 +191,11 @@ public static ToolResult invokeNodetool(List args) return invoke(CQLTester.buildNodetoolArgs(args)); } + public static ToolResult invokeNodetool(Map env, List args) + { + return invoke(env, CQLTester.buildNodetoolArgs(args)); + } + public static ToolResult invoke(List args) { return invoke(args.toArray(new String[args.size()])); From 7a0a282a71e65736ecbc901bf2d845aa5ab4fb33 Mon Sep 17 00:00:00 2001 From: Maxim Muzafarov Date: Mon, 17 Jun 2024 21:42:10 +0200 Subject: [PATCH 02/22] top level command help message --- .../management/CassandraHelpCommand.java | 30 ++- .../management/CassandraHelpLayout.java | 199 ++++++++++-------- .../cassandra/management/CommandUtils.java | 19 ++ .../apache/cassandra/tools/NodeToolV2.java | 7 +- .../cassandra/tools/NodeToolSynopsisTest.java | 87 +++++--- 5 files changed, 214 insertions(+), 128 deletions(-) diff --git a/src/java/org/apache/cassandra/management/CassandraHelpCommand.java b/src/java/org/apache/cassandra/management/CassandraHelpCommand.java index 5efd76b8500c..905b46091d15 100644 --- a/src/java/org/apache/cassandra/management/CassandraHelpCommand.java +++ b/src/java/org/apache/cassandra/management/CassandraHelpCommand.java @@ -58,10 +58,11 @@ public void run() CommandLine.Help.ColorScheme colors = colorScheme == null ? CommandLine.Help.defaultColorScheme(CommandLine.Help.Ansi.AUTO) : colorScheme; - // If no input command argument is specified, print help for the top-level command. + if (commands == null) { - printUsage(parent, colors, out); + // If the parent command is the top-level command, print help for the top-level command. + printTopCommandUsage(parent, colors, out); return; } @@ -74,14 +75,33 @@ public void run() if (subcommand == null) throw new CommandLine.ParameterException(parent, "Unknown subcommand '" + commands + "'.", null, commands); - printUsage(subcommand, colors, out); + subcommand.usage(out, colors); } - private static void printUsage(CommandLine command, CommandLine.Help.ColorScheme colors, PrintWriter out) + private static void printTopCommandUsage(CommandLine command, CommandLine.Help.ColorScheme colors, PrintWriter writer) { if (command == null) return; - command.usage(out, colors); + + StringBuilder sb = new StringBuilder(); + CommandLine.Help help = command.getHelpFactory().create(command.getCommandSpec(), colors); + if (!(help instanceof CassandraHelpLayout)) + { + command.usage(writer, colors); + return; + } + + Map helpSectionMap = CassandraHelpLayout.cassandraTopLevelHelpSectionKeys((CassandraHelpLayout) help); + for (String key : command.getHelpSectionKeys()) + { + CommandLine.IHelpSectionRenderer renderer = helpSectionMap.get(key); + if (renderer == null) + continue; + sb.append(renderer.render(help)); + } + + writer.println(sb); + writer.flush(); } /** diff --git a/src/java/org/apache/cassandra/management/CassandraHelpLayout.java b/src/java/org/apache/cassandra/management/CassandraHelpLayout.java index 76346bf28a4e..c8ce1a7e6732 100644 --- a/src/java/org/apache/cassandra/management/CassandraHelpLayout.java +++ b/src/java/org/apache/cassandra/management/CassandraHelpLayout.java @@ -23,8 +23,10 @@ import java.util.Comparator; import java.util.HashSet; import java.util.Iterator; +import java.util.LinkedHashMap; import java.util.LinkedList; import java.util.List; +import java.util.Map; import java.util.Set; import com.google.common.base.Preconditions; @@ -32,8 +34,8 @@ import org.apache.cassandra.utils.AbstractGuavaIterator; import picocli.CommandLine; -import static com.google.common.collect.ObjectArrays.concat; import static org.apache.cassandra.management.CommandUtils.leadingSpaces; +import static org.apache.commons.lang3.ArrayUtils.isEmpty; import static picocli.CommandLine.Model.UsageMessageSpec.SECTION_KEY_COMMAND_LIST; import static picocli.CommandLine.Model.UsageMessageSpec.SECTION_KEY_COMMAND_LIST_HEADING; import static picocli.CommandLine.Model.UsageMessageSpec.SECTION_KEY_DESCRIPTION; @@ -64,6 +66,7 @@ public class CassandraHelpLayout extends CommandLine.Help private static final String OPTIONS_HEADING = "OPTIONS%n"; private static final int COLUMN_INDENT = 8; private static final int DESCRIPTION_INDENT = 4; + private static final int SUBCOMMANDS_INDENT = 4; private static final CommandLine.Model.OptionSpec CASSANDRA_END_OF_OPTIONS_OPTION = CommandLine.Model.OptionSpec.builder("--") .description("This option can be used to separate command-line options from the " + @@ -71,6 +74,9 @@ public class CassandraHelpLayout extends CommandLine.Help "command-line options") .arity("0") .build(); + private static final String TOP_LEVEL_SYNOPSIS_LIST_HEADING = "usage:"; + private static final String TOP_LEVEL_COMMAND_HEADING = "The most commonly used nodetool commands are:%n"; + private static final String TOP_LEVEL_SYNOPSIS_SUBCOMMANDS_LABEL = " []"; public CassandraHelpLayout(CommandLine.Model.CommandSpec spec, ColorScheme scheme) { @@ -120,61 +126,86 @@ public String detailedSynopsis(int synopsisHeadingLength, Comparator argsInGroups = new HashSet<>(); Ansi.Text groupsText = createDetailedSynopsisGroupsText(argsInGroups); - List optionsList = createCassandraSynopsisOptionsText(argsInGroups); Ansi.Text endOfOptionsText = createDetailedSynopsisEndOfOptionsText(); Ansi.Text positionalParamText = createCassandraSynopsisPositionalsText(argsInGroups); Ansi.Text commandText = createDetailedSynopsisCommandText(); - CommandLine.Model.CommandSpec commandSpec = commandSpec(); - String parentCommandName = commandSpec.parent().qualifiedName(); - ColorScheme colorScheme = colorScheme(); + boolean isTopLevelCommand = commandSpec.parent() == null; + String parentCommandName = isTopLevelCommand ? "" : commandSpec.parent().qualifiedName(); + int width = commandSpec.usageMessage().width(); - int leadingColumnWidth = parentCommandName.length() + COLUMN_INDENT; - int followingColumnWidth = commandSpec.usageMessage().width() - leadingColumnWidth; - TextTable textTable = TextTable.forColumns(colorScheme, - new Column(leadingColumnWidth, 0, Column.Overflow.TRUNCATE), - new Column(followingColumnWidth, 0, Column.Overflow.WRAP)); + Ansi.Text parentCommandText = isTopLevelCommand ? colorScheme.commandText(commandSpec.name()) + : colorScheme.commandText(parentCommandName); + TextTable textTable = TextTable.forColumns(colorScheme, new Column(width, COLUMN_INDENT, Column.Overflow.WRAP)); + textTable.indentWrappedLines = parentCommandText.plainString().length(); textTable.setAdjustLineBreaksForWideCJKCharacters(commandSpec.usageMessage().adjustLineBreaksForWideCJKCharacters()); - textTable.indentWrappedLines = 0; - Ansi.Text emptyCell = Ansi.OFF.new Text(leadingSpaces(leadingColumnWidth), colorScheme); - Ansi.Text cmdPadding = Ansi.OFF.new Text(leadingSpaces(COLUMN_INDENT), colorScheme); - Ansi.Text parentCommandText = cmdPadding.concat(colorScheme.commandText(parentCommandName)).concat(" "); // All other fields added to the synopsis are left-adjusted, so we don't need to align them. - Ansi.Text text = groupsText.concat(" ") - .concat(commandSpec.name()) - .concat(endOfOptionsText) - .concat(" ") - .concat(positionalParamText) - .concat(commandText); - - LineBreakingOptionsIterator iter = new LineBreakingOptionsIterator(optionsList.iterator(), followingColumnWidth); - boolean commandTextNotAdded = true; - while (iter.hasNext()) + Ansi.Text text; + if (isTopLevelCommand) + { + text = groupsText.concat(" ") + .concat(TOP_LEVEL_SYNOPSIS_LIST_HEADING) + .concat(" ") + .concat(positionalParamText) + .concat(commandText); + + textTable.addRowValues(text); + textTable.addEmptyRow(); + } + else { - Ansi.Text row = iter.next(); - Ansi.Text leadingCell = emptyCell; + text = groupsText.concat(" ").concat(commandSpec.name()).concat(endOfOptionsText).concat(" ") + .concat(positionalParamText).concat(commandText); + } - if (commandTextNotAdded) - { - leadingCell = parentCommandText; - row = colorScheme.text(" ").concat(row); - commandTextNotAdded = false; - } + Ansi.Text padding = Ansi.OFF.new Text(leadingSpaces(parentCommandText.plainString().length()), colorScheme); + List alignedOptions = alignByWidth(optionsList, + width - COLUMN_INDENT - textTable.indentWrappedLines, + colorScheme); + // Align options by width + for (int i = 0; i < alignedOptions.size(); i++) + { + Ansi.Text option = alignedOptions.get(i); + if (i == 0) + option = parentCommandText.concat(" ").concat(option); + else + option = padding.concat(option); - if (iter.hasNext()) - textTable.addRowValues(leadingCell, row); + if (i == alignedOptions.size() - 1) + textTable.addRowValues(option.concat(text)); else - textTable.addRowValues(leadingCell, row.concat(text)); + textTable.addRowValues(option); } - textTable.addRowValues(Ansi.OFF.new Text("", colorScheme)); + textTable.addEmptyRow(); return textTable.toString(); } + private static List alignByWidth(List optionsList, int width, ColorScheme colorScheme) + { + List result = new ArrayList<>(); + Ansi.Text current = Ansi.OFF.new Text("", colorScheme); + for (Ansi.Text option : optionsList) + { + if (current.plainString().length() + option.plainString().length() >= width) + { + result.add(current); + current = Ansi.OFF.new Text("", colorScheme); + } + current = current.plainString().isEmpty() ? option : current.concat(" ").concat(option); + } + if (!current.plainString().isEmpty()) + result.add(current); + return result; + } + private Ansi.Text createCassandraSynopsisPositionalsText(Collection done) { List positionals = cassandraPositionals(commandSpec()); @@ -290,12 +321,43 @@ public String parameterList() return layout.toString(); } + @Override + public String commandList(Map subcommands) + { + if (subcommands.isEmpty()) + return ""; + int width = commandSpec().usageMessage().width(); + int commandLength = Math.min(CommandUtils.maxLength(subcommands.keySet()), width / 2); + int leadinColumnWidth = commandLength + SUBCOMMANDS_INDENT; + CommandLine.Help.TextTable table = TextTable.forColumns(colorScheme(), + new CommandLine.Help.Column(leadinColumnWidth, SUBCOMMANDS_INDENT, + CommandLine.Help.Column.Overflow.SPAN), + new CommandLine.Help.Column(width - leadinColumnWidth, SUBCOMMANDS_INDENT, + CommandLine.Help.Column.Overflow.WRAP)); + table.setAdjustLineBreaksForWideCJKCharacters(commandSpec().usageMessage().adjustLineBreaksForWideCJKCharacters()); + + for (Map.Entry entry : subcommands.entrySet()) + { + CommandLine.Help help = entry.getValue(); + CommandLine.Model.UsageMessageSpec usage = help.commandSpec().usageMessage(); + String header = isEmpty(usage.header()) ? (isEmpty(usage.description()) ? "" : usage.description()[0]) : usage.header()[0]; + Ansi.Text[] lines = colorScheme().text(header).splitLines(); + for (int i = 0; i < lines.length; i++) + table.addRowValues(i == 0 ? help.commandNamesText(", ") : Ansi.OFF.new Text(0), lines[i]); + } + return table.toString(); + } + @Override public String footerHeading(Object... params) { return createHeading("%n", params); } + public String topLevelCommandListHeading(Object... params) { + return createHeading(TOP_LEVEL_COMMAND_HEADING, params); + } + private static List cassandraPositionals(CommandLine.Model.CommandSpec commandSpec) { List positionals = new ArrayList<>(commandSpec.positionalParameters()); @@ -343,54 +405,25 @@ public static List cassandraHelpSectionKeys() return result; } - private static class LineBreakingOptionsIterator extends AbstractGuavaIterator + /** + * Top-level help command (includes all the available nodetool commands) has a different layout, so we need to + * provide a different set of keys for the help sections. + * @param layout The help class layout. + * @return Map of supported keys for the help sections. + */ + public static Map cassandraTopLevelHelpSectionKeys(CassandraHelpLayout layout) { - private final Iterator optionsIterator; - private final int width; - private Ansi.Text prev; - - LineBreakingOptionsIterator(Iterator optionsIterator, int width) - { - this.optionsIterator = optionsIterator; - this.width = width; - } - - @Override - protected Ansi.Text computeNext() - { - while (optionsIterator.hasNext()) - { - Ansi.Text next = optionsIterator.next(); - if (prev == null) - prev = next; - - Ansi.Text curr; - if (prev == next) - curr = next; - else - curr = prev.concat(" ").concat(next); - - if (curr.plainString().length() > width) - { - Ansi.Text result = prev; - prev = next; - return result; - } - else - { - prev = curr; - } - } - - if (prev == null) - return endOfData(); - else - { - Ansi.Text result = prev; - prev = null; - return result; - } - } + Map sectionMap = new LinkedHashMap<>(); + sectionMap.put(SECTION_KEY_HEADER_HEADING, CommandLine.Help::headerHeading); + sectionMap.put(SECTION_KEY_HEADER, CommandLine.Help::header); + sectionMap.put(SECTION_KEY_SYNOPSIS, help -> help.synopsis(help.synopsisHeadingLength())); + sectionMap.put(SECTION_KEY_COMMAND_LIST_HEADING, layout::topLevelCommandListHeading); + sectionMap.put(SECTION_KEY_COMMAND_LIST, CommandLine.Help::commandList); + sectionMap.put(SECTION_KEY_EXIT_CODE_LIST_HEADING, CommandLine.Help::exitCodeListHeading); + sectionMap.put(SECTION_KEY_EXIT_CODE_LIST, CommandLine.Help::exitCodeList); + sectionMap.put(SECTION_KEY_FOOTER_HEADING, CommandLine.Help::footerHeading); + sectionMap.put(SECTION_KEY_FOOTER, CommandLine.Help::footer); + return sectionMap; } private static Ansi.Text spacedParamLabel(CommandLine.Model.OptionSpec optionSpec, diff --git a/src/java/org/apache/cassandra/management/CommandUtils.java b/src/java/org/apache/cassandra/management/CommandUtils.java index 86fc0d8c3cc0..27efcd9f2bc9 100644 --- a/src/java/org/apache/cassandra/management/CommandUtils.java +++ b/src/java/org/apache/cassandra/management/CommandUtils.java @@ -19,6 +19,7 @@ package org.apache.cassandra.management; import java.util.Arrays; +import java.util.Collection; public final class CommandUtils { @@ -36,4 +37,22 @@ public static String leadingSpaces(int num) Arrays.fill(buff, ' '); return new String(buff); } + + public static int maxLength(Collection any) + { + int result = 0; + for (Object value : any) + result = Math.max(result, String.valueOf(value).length()); + return result; + } + + public static boolean empty(String str) + { + return str == null || str.trim().isEmpty(); + } + + private static boolean empty(Object[] array) + { + return array == null || array.length == 0; + } } diff --git a/src/java/org/apache/cassandra/tools/NodeToolV2.java b/src/java/org/apache/cassandra/tools/NodeToolV2.java index bcfb776873c5..83135a29bb18 100644 --- a/src/java/org/apache/cassandra/tools/NodeToolV2.java +++ b/src/java/org/apache/cassandra/tools/NodeToolV2.java @@ -244,11 +244,8 @@ public int execute(String... args) commandLine.setOut(new PrintWriter(output.out)) .setErr(new PrintWriter(output.err)) .setExecutionExceptionHandler((ex, cmdLine, parseResult) -> { - if (ex instanceof CommandLine.ParameterException) - { - output.err.println(ex.getMessage()); - commandLine.usage(output.err); - } + output.err.println(ex.getMessage()); + commandLine.usage(output.err); return 1; }); diff --git a/test/unit/org/apache/cassandra/tools/NodeToolSynopsisTest.java b/test/unit/org/apache/cassandra/tools/NodeToolSynopsisTest.java index 67d7d2bbd4c4..c7ad71164066 100644 --- a/test/unit/org/apache/cassandra/tools/NodeToolSynopsisTest.java +++ b/test/unit/org/apache/cassandra/tools/NodeToolSynopsisTest.java @@ -18,14 +18,12 @@ package org.apache.cassandra.tools; -import java.io.IOException; import java.io.OutputStream; import java.io.PrintStream; import java.util.ArrayList; import java.util.List; -import java.util.Map; +import java.util.function.BiFunction; -import com.google.common.collect.ImmutableMap; import org.apache.commons.lang3.StringUtils; import org.junit.Test; @@ -33,55 +31,74 @@ import com.github.difflib.patch.AbstractDelta; import com.github.difflib.patch.Patch; import org.apache.cassandra.cql3.CQLTester; -import org.apache.cassandra.tools.ToolRunner.ToolResult; -import static org.apache.cassandra.tools.ToolRunner.invokeNodetool; import static org.junit.Assert.assertTrue; public class NodeToolSynopsisTest { - private static final Map NODETOOLV2_ENV = ImmutableMap.of("NODETOOL_RUNNER", "org.apache.cassandra.tools.NodeToolV2"); - @Test - public void cliHelpForcecompact() + public void cliHelp() { - ToolResult toolHistory = invokeNodetool(List.of("help", "forcecompact")); - toolHistory.assertOnCleanExit(); - String outputV1 = toolHistory.getStdout(); + List outNodeTool = invokeNodetool(NodeTool::new, "help"); + List outNodeToolV2 = invokeNodetool(NodeToolV2::new, "help"); + + String diff = computeDiff(outNodeTool, outNodeToolV2); + assertTrue(concatNodetoolOutput(outNodeTool) + + '\n' + "-----------------------------------------------------" + + '\n' + concatNodetoolOutput(outNodeToolV2) + + '\n' + "Difference for \"" + "help" + "\":" + diff, + StringUtils.isBlank(diff)); + } - toolHistory = invokeNodetool(NODETOOLV2_ENV, List.of("help", "forcecompact")); - String outputV2 = toolHistory.getStdout(); + @Test + public void dummy() + { + List outNodeToolV2 = invokeNodetool(NodeToolV2::new, "help"); + System.out.println(concatNodetoolOutput(outNodeToolV2)); } @Test - public void cliHelp() + public void compareNodeToolHelpOutput() throws Exception { - ToolResult toolHistory = invokeNodetool("help"); - toolHistory.assertOnCleanExit(); - String outputV1 = toolHistory.getStdout(); +// runCommandHelpOutputComparison("abortbootstrap"); + runCommandHelpOutputComparison("assassinate"); + runCommandHelpOutputComparison("forcecompact"); + } - toolHistory = invokeNodetool(NODETOOLV2_ENV, List.of("help")); - String outputV2 = toolHistory.getStdout(); + public void runCommandHelpOutputComparison(String commandName) + { + List outNodeTool = invokeNodetool(NodeTool::new, "help", commandName); + List outNodeToolV2 = invokeNodetool(NodeToolV2::new, "help", commandName); + String diff = computeDiff(outNodeTool, outNodeToolV2); + assertTrue(concatNodetoolOutput(outNodeTool) + + '\n' + "-----------------------------------------------------" + + '\n' + concatNodetoolOutput(outNodeToolV2) + + '\n' + " difference for \"" + commandName + "\":" + diff, + StringUtils.isBlank(diff)); } - @Test - public void cliDryRun() throws Exception + private static String concatNodetoolOutput(List output) { - List args = CQLTester.buildNodetoolArgs(List.of("help", "assassinate")); + return '\n' + String.join("\n", output); + } + private static List invokeNodetool(BiFunction factory, String... commands) + { + ListOutputStream output = new ListOutputStream(); + List args = CQLTester.buildNodetoolArgs(List.of(commands)); args.remove("bin/nodetool"); - ListOutputStream outputV1 = new ListOutputStream(); - ListOutputStream outputV2 = new ListOutputStream(); - - new NodeTool(new NodeProbeFactory(), new Output(new PrintStream(outputV1), new PrintStream(outputV1))) - .execute(args.toArray(new String[0])); - new NodeToolV2(new NodeProbeFactory(), new Output(new PrintStream(outputV2), new PrintStream(outputV2))) - .execute(args.toArray(new String[0])); - - String diff = computeDiff(outputV1.getOutputLines(), outputV2.getOutputLines()); - assertTrue(String.join("\n", outputV1.getOutputLines()) + - '\n' + String.join("\n", outputV2.getOutputLines()) + - '\n' + " difference: " + diff, - StringUtils.isBlank(diff)); + try + { + Object runner = factory.apply(new NodeProbeFactory(), new Output(new PrintStream(output), new PrintStream(output))); + Object result = runner.getClass().getMethod("execute", String[].class) + .invoke(runner, new Object[] { args.toArray(new String[0]) }); + if (result instanceof Integer && (Integer) result != 0) + throw new RuntimeException("Command failed with exit code " + result); + return output.getOutputLines(); + } + catch (Exception e) + { + throw new RuntimeException(e); + } } public static String computeDiff(List original, List revised) { From f641bb94531c37c257f441e9ae38f05f1c09c87a Mon Sep 17 00:00:00 2001 From: Maxim Muzafarov Date: Tue, 18 Jun 2024 15:41:27 +0200 Subject: [PATCH 03/22] align help and command nodetool outputs --- .../management/CassandraHelpLayout.java | 77 +++++++++---------- 1 file changed, 37 insertions(+), 40 deletions(-) diff --git a/src/java/org/apache/cassandra/management/CassandraHelpLayout.java b/src/java/org/apache/cassandra/management/CassandraHelpLayout.java index c8ce1a7e6732..85d1cccf406b 100644 --- a/src/java/org/apache/cassandra/management/CassandraHelpLayout.java +++ b/src/java/org/apache/cassandra/management/CassandraHelpLayout.java @@ -22,16 +22,12 @@ import java.util.Collection; import java.util.Comparator; import java.util.HashSet; -import java.util.Iterator; import java.util.LinkedHashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Set; -import com.google.common.base.Preconditions; - -import org.apache.cassandra.utils.AbstractGuavaIterator; import picocli.CommandLine; import static org.apache.cassandra.management.CommandUtils.leadingSpaces; @@ -74,9 +70,9 @@ public class CassandraHelpLayout extends CommandLine.Help "command-line options") .arity("0") .build(); - private static final String TOP_LEVEL_SYNOPSIS_LIST_HEADING = "usage:"; + private static final String TOP_LEVEL_SYNOPSIS_LIST_PREFIX = "usage:"; private static final String TOP_LEVEL_COMMAND_HEADING = "The most commonly used nodetool commands are:%n"; - private static final String TOP_LEVEL_SYNOPSIS_SUBCOMMANDS_LABEL = " []"; + private static final String SYNOPSIS_SUBCOMMANDS_LABEL = " []"; public CassandraHelpLayout(CommandLine.Model.CommandSpec spec, ColorScheme scheme) { @@ -118,13 +114,23 @@ public String synopsisHeading(Object... params) } @Override - public String detailedSynopsis(int synopsisHeadingLength, Comparator optionSort, boolean clusterBooleanOptions) + public String synopsis(int synopsisHeadingLength) + { + return printDetailedSynopsis("", COLUMN_INDENT, true); + } + + private Ansi.Text createCassandraSynopsisCommandText() { - Preconditions.checkState(synopsisHeadingLength >= 0, - "synopsisHeadingLength must be a positive number but was " + synopsisHeadingLength); + Ansi.Text commandText = ansi().new Text(0); + if (!commandSpec().subcommands().isEmpty()) + return commandText.concat(SYNOPSIS_SUBCOMMANDS_LABEL); + return commandText; + } + private String printDetailedSynopsis(String synopsisPrefix, int columnIndent, boolean showEndOfOptionsDelimiter) + { // Cassandra uses end of options delimiter in usage help. - commandSpec().usageMessage().showEndOfOptionsDelimiterInUsageHelp(true); + commandSpec().usageMessage().showEndOfOptionsDelimiterInUsageHelp(showEndOfOptionsDelimiter); CommandLine.Model.CommandSpec commandSpec = commandSpec(); ColorScheme colorScheme = colorScheme(); @@ -134,47 +140,33 @@ public String detailedSynopsis(int synopsisHeadingLength, Comparator optionsList = createCassandraSynopsisOptionsText(argsInGroups); Ansi.Text endOfOptionsText = createDetailedSynopsisEndOfOptionsText(); Ansi.Text positionalParamText = createCassandraSynopsisPositionalsText(argsInGroups); - Ansi.Text commandText = createDetailedSynopsisCommandText(); + Ansi.Text commandText = createCassandraSynopsisCommandText(); - boolean isTopLevelCommand = commandSpec.parent() == null; - String parentCommandName = isTopLevelCommand ? "" : commandSpec.parent().qualifiedName(); int width = commandSpec.usageMessage().width(); - - Ansi.Text parentCommandText = isTopLevelCommand ? colorScheme.commandText(commandSpec.name()) - : colorScheme.commandText(parentCommandName); - TextTable textTable = TextTable.forColumns(colorScheme, new Column(width, COLUMN_INDENT, Column.Overflow.WRAP)); - textTable.indentWrappedLines = parentCommandText.plainString().length(); + boolean isEmptyParent = commandSpec.parent() == null; + Ansi.Text mainCommandText = isEmptyParent ? colorScheme.commandText(commandSpec.name()) : + colorScheme.commandText(commandSpec.parent().qualifiedName()); + TextTable textTable = TextTable.forColumns(colorScheme, new Column(width, columnIndent, Column.Overflow.WRAP)); + textTable.indentWrappedLines = COLUMN_INDENT; textTable.setAdjustLineBreaksForWideCJKCharacters(commandSpec.usageMessage().adjustLineBreaksForWideCJKCharacters()); // All other fields added to the synopsis are left-adjusted, so we don't need to align them. - Ansi.Text text; - if (isTopLevelCommand) - { - text = groupsText.concat(" ") - .concat(TOP_LEVEL_SYNOPSIS_LIST_HEADING) - .concat(" ") - .concat(positionalParamText) - .concat(commandText); - - textTable.addRowValues(text); - textTable.addEmptyRow(); - } - else - { - text = groupsText.concat(" ").concat(commandSpec.name()).concat(endOfOptionsText).concat(" ") - .concat(positionalParamText).concat(commandText); - } - - Ansi.Text padding = Ansi.OFF.new Text(leadingSpaces(parentCommandText.plainString().length()), colorScheme); + Ansi.Text text = groupsText.concat(isEmptyParent ? Ansi.OFF.new Text(0) : + colorScheme.text(" ").concat(commandSpec.name())) + .concat(endOfOptionsText).concat(" ") + .concat(positionalParamText).concat(commandText); + Ansi.Text padding = Ansi.OFF.new Text(leadingSpaces(mainCommandText.plainString().length()), colorScheme); List alignedOptions = alignByWidth(optionsList, - width - COLUMN_INDENT - textTable.indentWrappedLines, + width - columnIndent - textTable.indentWrappedLines - synopsisPrefix.length(), colorScheme); // Align options by width for (int i = 0; i < alignedOptions.size(); i++) { Ansi.Text option = alignedOptions.get(i); if (i == 0) - option = parentCommandText.concat(" ").concat(option); + option = colorScheme.text(synopsisPrefix).concat(synopsisPrefix.isEmpty() ? "" : " ") + .concat(mainCommandText).concat(" ") + .concat(option); else option = padding.concat(option); @@ -358,6 +350,11 @@ public String topLevelCommandListHeading(Object... params) { return createHeading(TOP_LEVEL_COMMAND_HEADING, params); } + public String topLevelSynopsis(Object... params) + { + return printDetailedSynopsis(TOP_LEVEL_SYNOPSIS_LIST_PREFIX, 0, false); + } + private static List cassandraPositionals(CommandLine.Model.CommandSpec commandSpec) { List positionals = new ArrayList<>(commandSpec.positionalParameters()); @@ -416,7 +413,7 @@ public static Map cassandraTopLevelHel Map sectionMap = new LinkedHashMap<>(); sectionMap.put(SECTION_KEY_HEADER_HEADING, CommandLine.Help::headerHeading); sectionMap.put(SECTION_KEY_HEADER, CommandLine.Help::header); - sectionMap.put(SECTION_KEY_SYNOPSIS, help -> help.synopsis(help.synopsisHeadingLength())); + sectionMap.put(SECTION_KEY_SYNOPSIS, layout::topLevelSynopsis); sectionMap.put(SECTION_KEY_COMMAND_LIST_HEADING, layout::topLevelCommandListHeading); sectionMap.put(SECTION_KEY_COMMAND_LIST, CommandLine.Help::commandList); sectionMap.put(SECTION_KEY_EXIT_CODE_LIST_HEADING, CommandLine.Help::exitCodeListHeading); From 80dfba026533d6630d72d9cbe5ad33b00d7cca3f Mon Sep 17 00:00:00 2001 From: Maxim Muzafarov Date: Wed, 19 Jun 2024 23:27:47 +0200 Subject: [PATCH 04/22] init context for nodetool2 --- .../cassandra/management/BaseCommand.java | 23 +++- .../cassandra/management/CommandUtils.java | 21 ++- ...agementContext.java => ServiceBridge.java} | 4 +- .../management/api/AbortBootstrap.java | 8 +- .../cassandra/management/api/Assassinate.java | 8 +- .../cassandra/management/api/Compact.java | 108 +++++++++++++++ .../management/api/ForceCompact.java | 8 +- .../cassandra/management/api/JmxConnect.java | 92 ------------- .../management/api/JmxConnectionMixin.java | 129 ++++++++++++++++++ .../org/apache/cassandra/tools/NodeProbe.java | 11 +- .../org/apache/cassandra/tools/NodeTool.java | 57 ++++---- .../apache/cassandra/tools/NodeToolV2.java | 62 +++++++-- .../cassandra/tools/NodeToolSynopsisTest.java | 6 +- .../apache/cassandra/tools/ToolRunner.java | 30 ++++ .../cassandra/tools/nodetool/CompactTest.java | 10 +- 15 files changed, 412 insertions(+), 165 deletions(-) rename src/java/org/apache/cassandra/management/{ManagementContext.java => ServiceBridge.java} (91%) create mode 100644 src/java/org/apache/cassandra/management/api/Compact.java delete mode 100644 src/java/org/apache/cassandra/management/api/JmxConnect.java create mode 100644 src/java/org/apache/cassandra/management/api/JmxConnectionMixin.java diff --git a/src/java/org/apache/cassandra/management/BaseCommand.java b/src/java/org/apache/cassandra/management/BaseCommand.java index aa22619854af..3de461bb12f5 100644 --- a/src/java/org/apache/cassandra/management/BaseCommand.java +++ b/src/java/org/apache/cassandra/management/BaseCommand.java @@ -18,21 +18,34 @@ package org.apache.cassandra.management; -import javax.inject.Inject; +import picocli.CommandLine; /** * Base class for all nodetool commands. */ public abstract class BaseCommand implements Runnable { - @Inject - protected ManagementContext probe; + /** The command specification, used to access command-specific properties. */ + @CommandLine.Spec + protected CommandLine.Model.CommandSpec spec; // injected by picocli + /** The ServiceBridge instance to interact with the Cassandra node. */ + protected ServiceBridge bridge; + + /** + * The ServiceBridge instance is injected by the picocli framework during command execution and is used to + * interact with the Cassandra node. This method is called by picocli and used depending on the execution strategy. + * @param bridge The ServiceBridge instance to inject. + */ + public void setBridge(ServiceBridge bridge) + { + this.bridge = bridge; + } @Override public void run() { - execute(probe); + execute(bridge); } - protected abstract void execute(ManagementContext probe); + protected abstract void execute(ServiceBridge probe); } diff --git a/src/java/org/apache/cassandra/management/CommandUtils.java b/src/java/org/apache/cassandra/management/CommandUtils.java index 27efcd9f2bc9..05ece3c86151 100644 --- a/src/java/org/apache/cassandra/management/CommandUtils.java +++ b/src/java/org/apache/cassandra/management/CommandUtils.java @@ -18,8 +18,21 @@ package org.apache.cassandra.management; +import java.io.IOException; +import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.ExecutionException; + +import org.apache.cassandra.db.compaction.CompactionManagerMBean; +import org.apache.cassandra.service.StorageServiceMBean; +import org.apache.cassandra.tools.NodeProbe; +import org.apache.cassandra.tools.NodeTool; + +import static com.google.common.collect.Iterables.toArray; +import static org.apache.commons.lang3.ArrayUtils.EMPTY_STRING_ARRAY; public final class CommandUtils { @@ -46,13 +59,13 @@ public static int maxLength(Collection any) return result; } - public static boolean empty(String str) + public static StorageServiceMBean ssProxy(ServiceBridge bridge) { - return str == null || str.trim().isEmpty(); + return bridge.mBean(StorageServiceMBean.class); } - private static boolean empty(Object[] array) + public static CompactionManagerMBean cmProxy(ServiceBridge bridge) { - return array == null || array.length == 0; + return bridge.mBean(CompactionManagerMBean.class); } } diff --git a/src/java/org/apache/cassandra/management/ManagementContext.java b/src/java/org/apache/cassandra/management/ServiceBridge.java similarity index 91% rename from src/java/org/apache/cassandra/management/ManagementContext.java rename to src/java/org/apache/cassandra/management/ServiceBridge.java index cddfcb81abef..80edce06824a 100644 --- a/src/java/org/apache/cassandra/management/ManagementContext.java +++ b/src/java/org/apache/cassandra/management/ServiceBridge.java @@ -21,7 +21,7 @@ /** * Management context for nodetool commands to access management services like StorageServiceMBean etc. */ -public interface ManagementContext +public interface ServiceBridge { - T getManagementService(Class serviceClass); + T mBean(Class clazz); } diff --git a/src/java/org/apache/cassandra/management/api/AbortBootstrap.java b/src/java/org/apache/cassandra/management/api/AbortBootstrap.java index d741f9dce195..e5ce36506a4b 100644 --- a/src/java/org/apache/cassandra/management/api/AbortBootstrap.java +++ b/src/java/org/apache/cassandra/management/api/AbortBootstrap.java @@ -18,11 +18,11 @@ package org.apache.cassandra.management.api; import org.apache.cassandra.management.BaseCommand; -import org.apache.cassandra.management.ManagementContext; -import org.apache.cassandra.service.StorageServiceMBean; +import org.apache.cassandra.management.ServiceBridge; import picocli.CommandLine.Command; import picocli.CommandLine.Option; +import static org.apache.cassandra.management.CommandUtils.ssProxy; import static org.apache.commons.lang3.StringUtils.EMPTY; import static org.apache.commons.lang3.StringUtils.isEmpty; @@ -36,12 +36,12 @@ public class AbortBootstrap extends BaseCommand private String endpoint = EMPTY; @Override - public void execute(ManagementContext probe) + public void execute(ServiceBridge probe) { if (isEmpty(nodeId) && isEmpty(endpoint)) throw new IllegalArgumentException("Either --node or --ip needs to be set"); if (!isEmpty(nodeId) && !isEmpty(endpoint)) throw new IllegalArgumentException("Only one of --node or --ip need to be set"); - probe.getManagementService(StorageServiceMBean.class).abortBootstrap(nodeId, endpoint); + ssProxy(probe).abortBootstrap(nodeId, endpoint); } } diff --git a/src/java/org/apache/cassandra/management/api/Assassinate.java b/src/java/org/apache/cassandra/management/api/Assassinate.java index 0ecb7696436a..93bdb58b0de7 100644 --- a/src/java/org/apache/cassandra/management/api/Assassinate.java +++ b/src/java/org/apache/cassandra/management/api/Assassinate.java @@ -18,10 +18,10 @@ package org.apache.cassandra.management.api; import org.apache.cassandra.management.BaseCommand; -import org.apache.cassandra.management.ManagementContext; -import org.apache.cassandra.service.StorageServiceMBean; +import org.apache.cassandra.management.ServiceBridge; import picocli.CommandLine; +import static org.apache.cassandra.management.CommandUtils.ssProxy; import static org.apache.commons.lang3.StringUtils.EMPTY; @CommandLine.Command(name = "assassinate", description = "Forcefully remove a dead node without re-replicating any data. Use as a last resort if you cannot removenode") @@ -31,8 +31,8 @@ public class Assassinate extends BaseCommand private String ip_address = EMPTY; @Override - public void execute(ManagementContext probe) + public void execute(ServiceBridge probe) { - probe.getManagementService(StorageServiceMBean.class).assassinateEndpoint(ip_address); + ssProxy(probe).assassinateEndpoint(ip_address); } } diff --git a/src/java/org/apache/cassandra/management/api/Compact.java b/src/java/org/apache/cassandra/management/api/Compact.java new file mode 100644 index 000000000000..a5596b8c357a --- /dev/null +++ b/src/java/org/apache/cassandra/management/api/Compact.java @@ -0,0 +1,108 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.cassandra.management.api; + +import java.util.ArrayList; +import java.util.List; + +import org.apache.cassandra.db.compaction.CompactionManagerMBean; +import org.apache.cassandra.management.BaseCommand; +import org.apache.cassandra.management.ServiceBridge; +import picocli.CommandLine; + +import static org.apache.cassandra.management.CommandUtils.cmProxy; +import static org.apache.cassandra.management.CommandUtils.ssProxy; +import static org.apache.cassandra.tools.NodeTool.NodeToolCmd.parseOptionalKeyspace; +import static org.apache.cassandra.tools.NodeTool.NodeToolCmd.parseOptionalTables; +import static org.apache.commons.lang3.StringUtils.EMPTY; + +@CommandLine.Command(name = "compact", description = "Force a (major) compaction on one or more tables or user-defined compaction on given SSTables") +public class Compact extends BaseCommand +{ + @CommandLine.Parameters(paramLabel = "[ ...] or ...", arity = "0..*", + description = "The keyspace followed by one or many tables or list of SSTable data files when using --user-defined") + private List args = new ArrayList<>(); + + @CommandLine.Option(names = { "-s", "--split-output"}, description = "Use -s to not create a single big file") + private boolean splitOutput = false; + + @CommandLine.Option(names = { "--user-defined"}, description = "Use --user-defined to submit listed files for user-defined compaction") + private boolean userDefined = false; + + @CommandLine.Option(names = { "-st", "--start-token"}, description = "Use -st to specify a token at which the compaction range starts (inclusive)") + private String startToken = EMPTY; + + @CommandLine.Option(names = { "-et", "--end-token"}, description = "Use -et to specify a token at which compaction range ends (inclusive)") + private String endToken = EMPTY; + + @CommandLine.Option(names = { "--partition"}, description = "String representation of the partition key") + private String partitionKey = EMPTY; + + @Override + public void execute(ServiceBridge probe) + { + final boolean startEndTokenProvided = !(startToken.isEmpty() && endToken.isEmpty()); + final boolean partitionKeyProvided = !partitionKey.isEmpty(); + final boolean tokenProvided = startEndTokenProvided || partitionKeyProvided; + if (splitOutput && (userDefined || tokenProvided)) + { + throw new RuntimeException("Invalid option combination: Can not use split-output here"); + } + if (userDefined && tokenProvided) + { + throw new RuntimeException("Invalid option combination: Can not provide tokens when using user-defined"); + } + + if (userDefined) + { + try + { + String userDefinedFiles = String.join(",", args); + cmProxy(probe).forceUserDefinedCompaction(userDefinedFiles); + } catch (Exception e) { + throw new RuntimeException("Error occurred during user defined compaction", e); + } + return; + } + + List keyspaces = parseOptionalKeyspace(args, probe); + String[] tableNames = parseOptionalTables(args); + + for (String keyspace : keyspaces) + { + try + { + if (startEndTokenProvided) + { + ssProxy(probe).forceKeyspaceCompactionForTokenRange(keyspace, startToken, endToken, tableNames); + } + else if (partitionKeyProvided) + { + ssProxy(probe).forceKeyspaceCompactionForPartitionKey(keyspace, partitionKey, tableNames); + } + else + { + ssProxy(probe).forceKeyspaceCompaction(splitOutput, keyspace, tableNames); + } + } catch (Exception e) + { + throw new RuntimeException("Error occurred during compaction", e); + } + } + } +} diff --git a/src/java/org/apache/cassandra/management/api/ForceCompact.java b/src/java/org/apache/cassandra/management/api/ForceCompact.java index aad5e7bed1c0..aa758b8e4eb8 100644 --- a/src/java/org/apache/cassandra/management/api/ForceCompact.java +++ b/src/java/org/apache/cassandra/management/api/ForceCompact.java @@ -24,11 +24,11 @@ import org.apache.cassandra.management.BaseCommand; import org.apache.cassandra.management.CommandUtils; -import org.apache.cassandra.management.ManagementContext; -import org.apache.cassandra.service.StorageServiceMBean; +import org.apache.cassandra.management.ServiceBridge; import picocli.CommandLine; import static com.google.common.base.Preconditions.checkArgument; +import static org.apache.cassandra.management.CommandUtils.ssProxy; import static org.apache.cassandra.tools.NodeToolV2.NodeToolCmd.parsePartitionKeys; @CommandLine.Command(name = "forcecompact", description = "Force a (major) compaction on a table") @@ -49,7 +49,7 @@ public class ForceCompact extends BaseCommand private String[] keys; @Override - public void execute(ManagementContext probe) + public void execute(ServiceBridge probe) { args = Lists.asList(keyspace, table, keys); // Check if the input has valid size @@ -62,7 +62,7 @@ public void execute(ManagementContext probe) try { - probe.getManagementService(StorageServiceMBean.class).forceCompactionKeysIgnoringGcGrace(keyspaceName, tableName, partitionKeysIgnoreGcGrace); + ssProxy(probe).forceCompactionKeysIgnoringGcGrace(keyspaceName, tableName, partitionKeysIgnoreGcGrace); } catch (Exception e) { diff --git a/src/java/org/apache/cassandra/management/api/JmxConnect.java b/src/java/org/apache/cassandra/management/api/JmxConnect.java deleted file mode 100644 index f169b523be80..000000000000 --- a/src/java/org/apache/cassandra/management/api/JmxConnect.java +++ /dev/null @@ -1,92 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.apache.cassandra.management.api; - -import java.io.IOException; -import javax.inject.Inject; - -import com.google.common.base.Throwables; - -import org.apache.cassandra.management.ManagementContext; -import org.apache.cassandra.tools.INodeProbeFactory; -import org.apache.cassandra.tools.Output; -import picocli.CommandLine; - -import static java.lang.Integer.parseInt; -import static org.apache.commons.lang3.StringUtils.EMPTY; - -/** - * Command options for NodeTool commands that are executed via JMX. - */ -@CommandLine.Command(name = "connect", description = "Connect NodeTool to a Cassandra node using JMX") -public class JmxConnect implements Runnable -{ - public static final String MIXIN_KEY = "jmx"; - - @CommandLine.Option(names = { "-h", "--host"}, description = "Node hostname or ip address") - private String host = "127.0.0.1"; - - @CommandLine.Option(names = {"-p", "--port"}, description = "Remote jmx agent port number") - private String port = "7199"; - - @CommandLine.Option(names = {"-u", "--username"}, description = "Remote jmx agent username") - private String username = EMPTY; - - @CommandLine.Option(names = {"-pw", "--password"}, description = "Remote jmx agent password") - private String password = EMPTY; - - @CommandLine.Option(names = {"-pwf", "--password-file"}, description = "Path to the JMX password file") - private String passwordFilePath = EMPTY; - - @CommandLine.Option(names = { "-pp", "--print-port"}, description = "Operate in 4.0 mode with hosts disambiguated by port number") - private boolean printPort = false; - - @Inject - private INodeProbeFactory nodeProbeFactory; - @Inject - private Output output; - public ManagementContext nodeClient; - - public ManagementContext init() - { - try - { - if (username.isEmpty()) - nodeClient = nodeProbeFactory.create(host, parseInt(port)); - else - nodeClient = nodeProbeFactory.create(host, parseInt(port), username, password); - - return nodeClient; - } - catch (IOException | SecurityException e) - { - Throwable rootCause = Throwables.getRootCause(e); - output.err.printf("nodetool: Failed to connect to '%s:%s' - %s: '%s'.%n", host, port, - rootCause.getClass().getSimpleName(), rootCause.getMessage()); - } - - return null; - } - - @Override - public void run() - { - // no-op - } -} diff --git a/src/java/org/apache/cassandra/management/api/JmxConnectionMixin.java b/src/java/org/apache/cassandra/management/api/JmxConnectionMixin.java new file mode 100644 index 000000000000..4439f9afaf98 --- /dev/null +++ b/src/java/org/apache/cassandra/management/api/JmxConnectionMixin.java @@ -0,0 +1,129 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.management.api; + +import java.io.IOException; +import java.util.List; +import javax.inject.Inject; + +import com.google.common.base.Preconditions; +import com.google.common.base.Throwables; + +import org.apache.cassandra.management.BaseCommand; +import org.apache.cassandra.management.ServiceBridge; +import org.apache.cassandra.tools.INodeProbeFactory; +import org.apache.cassandra.tools.Output; +import picocli.CommandLine; + +import static java.lang.Integer.parseInt; +import static org.apache.cassandra.tools.NodeTool.NodeToolCmd.promptAndReadPassword; +import static org.apache.cassandra.tools.NodeTool.NodeToolCmd.readUserPasswordFromFile; +import static org.apache.commons.lang3.StringUtils.EMPTY; +import static org.apache.commons.lang3.StringUtils.isEmpty; +import static org.apache.commons.lang3.StringUtils.isNotEmpty; + +/** + * Command options for NodeTool commands that are executed via JMX. + */ +@CommandLine.Command(name = "connect", description = "Connect NodeTool to a Cassandra node using JMX") +public class JmxConnectionMixin +{ + public static final String MIXIN_KEY = "jmx"; + + @CommandLine.Option(names = { "-h", "--host" }, description = "Node hostname or ip address") + public String host = "127.0.0.1"; + + @CommandLine.Option(names = { "-p", "--port" }, description = "Remote jmx agent port number") + public String port = "7199"; + + @CommandLine.Option(names = { "-u", "--username" }, description = "Remote jmx agent username") + public String username = EMPTY; + + @CommandLine.Option(names = { "-pw", "--password" }, description = "Remote jmx agent password") + public String password = EMPTY; + + @CommandLine.Option(names = { "-pwf", "--password-file" }, description = "Path to the JMX password file") + public String passwordFilePath = EMPTY; + + @CommandLine.Option(names = { "-pp", "--print-port" }, description = "Operate in 4.0 mode with hosts disambiguated by port number") + public boolean printPort = false; + + @Inject + private INodeProbeFactory nodeProbeFactory; + @Inject + private Output output; + + /** + * This method is called by picocli and used depending on the execution strategy. + * @param parseResult The parsed command line. + * @return The exit code. + */ + public static int executionStrategy(CommandLine.ParseResult parseResult) + { + List parsedCommands = parseResult.asCommandLineList(); + int start = indexOfLastSubcommandWithSameParent(parsedCommands); + CommandLine.Model.CommandSpec lastParent = parsedCommands.get(start).getCommandSpec(); + CommandLine.Model.CommandSpec jmx = lastParent.mixins().get(MIXIN_KEY); + Preconditions.checkNotNull(jmx, "No JmxConnect mixin found in the command hierarchy"); + + ((BaseCommand) lastParent.userObject()).setBridge(((JmxConnectionMixin) jmx.userObject()).init(lastParent)); + return new CommandLine.RunLast().execute(parseResult); + } + + private static int indexOfLastSubcommandWithSameParent(List parsedCommands) + { + int start = parsedCommands.size() - 1; + for (int i = parsedCommands.size() - 2; i >= 0; i--) + { + if (parsedCommands.get(i).getParent() != parsedCommands.get(i + 1).getParent()) + break; + start = i; + } + return start; + } + + /** + * Initialize the JMX connection to the Cassandra node. + * @param spec The command specification to be executed after the initialization. + * @return The ServiceBridge instance to interact with the Cassandra node. + */ + private ServiceBridge init(CommandLine.Model.CommandSpec spec) + { + try + { + if (isNotEmpty(username)) { + if (isNotEmpty(passwordFilePath)) + password = readUserPasswordFromFile(username, passwordFilePath); + + if (isEmpty(password)) + password = promptAndReadPassword(); + } + + return username.isEmpty() ? nodeProbeFactory.create(host, parseInt(port)) + : nodeProbeFactory.create(host, parseInt(port), username, password); + } + catch (IOException | SecurityException e) + { + Throwable rootCause = Throwables.getRootCause(e); + output.err.printf("nodetool: Failed to connect to '%s:%s' - %s: '%s'.%n", host, port, + rootCause.getClass().getSimpleName(), rootCause.getMessage()); + throw new CommandLine.ExecutionException(spec.commandLine(), "Failed to connect to JMX", e); + } + } +} diff --git a/src/java/org/apache/cassandra/tools/NodeProbe.java b/src/java/org/apache/cassandra/tools/NodeProbe.java index 1151fc685c02..f5268fca37e8 100644 --- a/src/java/org/apache/cassandra/tools/NodeProbe.java +++ b/src/java/org/apache/cassandra/tools/NodeProbe.java @@ -105,7 +105,7 @@ import org.apache.cassandra.metrics.StorageMetrics; import org.apache.cassandra.metrics.TableMetrics; import org.apache.cassandra.metrics.ThreadPoolMetrics; -import org.apache.cassandra.management.ManagementContext; +import org.apache.cassandra.management.ServiceBridge; import org.apache.cassandra.net.MessagingService; import org.apache.cassandra.net.MessagingServiceMBean; import org.apache.cassandra.service.ActiveRepairServiceMBean; @@ -131,7 +131,7 @@ /** * JMX client operations for Cassandra. */ -public class NodeProbe implements AutoCloseable, ManagementContext +public class NodeProbe implements AutoCloseable, ServiceBridge { private static final String fmtUrl = "service:jmx:rmi:///jndi/rmi://%s:%d/jmxrmi"; private static final String ssObjName = "org.apache.cassandra.db:type=StorageService"; @@ -240,11 +240,10 @@ private T cacheProxy(T proxy) } @Override - public T getManagementService(Class serviceClass) + public T mBean(Class clazz) { - - return serviceClass.cast(Optional.ofNullable(cachedMBeans.get(serviceClass.getName())) - .orElseThrow(() -> new IllegalArgumentException("No MBean found for " + serviceClass.getName()))); + return clazz.cast(Optional.ofNullable(cachedMBeans.get(clazz.getName())) + .orElseThrow(() -> new IllegalArgumentException("No MBean found for " + clazz.getName()))); } /** diff --git a/src/java/org/apache/cassandra/tools/NodeTool.java b/src/java/org/apache/cassandra/tools/NodeTool.java index 94a784bfc06d..d4bc7e130b7d 100644 --- a/src/java/org/apache/cassandra/tools/NodeTool.java +++ b/src/java/org/apache/cassandra/tools/NodeTool.java @@ -17,20 +17,7 @@ */ package org.apache.cassandra.tools; -import static com.google.common.base.Throwables.getStackTraceAsString; -import static com.google.common.collect.Iterables.toArray; -import static com.google.common.collect.Lists.newArrayList; -import static java.lang.Integer.parseInt; -import static java.lang.String.format; -import static org.apache.cassandra.io.util.File.WriteMode.APPEND; -import static org.apache.commons.lang3.ArrayUtils.EMPTY_STRING_ARRAY; -import static org.apache.commons.lang3.StringUtils.EMPTY; -import static org.apache.commons.lang3.StringUtils.isEmpty; -import static org.apache.commons.lang3.StringUtils.isNotEmpty; - import java.io.Console; -import org.apache.cassandra.io.util.File; -import org.apache.cassandra.io.util.FileWriter; import java.io.FileNotFoundException; import java.io.IOError; import java.io.IOException; @@ -44,16 +31,10 @@ import java.util.Map.Entry; import java.util.Scanner; import java.util.SortedMap; - import javax.management.InstanceNotFoundException; import com.google.common.base.Joiner; import com.google.common.base.Throwables; - -import org.apache.cassandra.locator.EndpointSnitchInfoMBean; -import org.apache.cassandra.tools.nodetool.*; -import org.apache.cassandra.utils.FBUtilities; - import com.google.common.collect.Maps; import io.airlift.airline.Cli; @@ -67,6 +48,24 @@ import io.airlift.airline.ParseOptionConversionException; import io.airlift.airline.ParseOptionMissingException; import io.airlift.airline.ParseOptionMissingValueException; +import org.apache.cassandra.io.util.File; +import org.apache.cassandra.io.util.FileWriter; +import org.apache.cassandra.locator.EndpointSnitchInfoMBean; +import org.apache.cassandra.management.ServiceBridge; +import org.apache.cassandra.tools.nodetool.*; +import org.apache.cassandra.utils.FBUtilities; + +import static com.google.common.base.Throwables.getStackTraceAsString; +import static com.google.common.collect.Iterables.toArray; +import static com.google.common.collect.Lists.newArrayList; +import static java.lang.Integer.parseInt; +import static java.lang.String.format; +import static org.apache.cassandra.io.util.File.WriteMode.APPEND; +import static org.apache.cassandra.management.CommandUtils.ssProxy; +import static org.apache.commons.lang3.ArrayUtils.EMPTY_STRING_ARRAY; +import static org.apache.commons.lang3.StringUtils.EMPTY; +import static org.apache.commons.lang3.StringUtils.isEmpty; +import static org.apache.commons.lang3.StringUtils.isNotEmpty; public class NodeTool { @@ -403,7 +402,7 @@ public void runInternal() } - private String readUserPasswordFromFile(String username, String passwordFilePath) { + public static String readUserPasswordFromFile(String username, String passwordFilePath) { String password = EMPTY; File passwordFile = new File(passwordFilePath); @@ -431,7 +430,7 @@ private String readUserPasswordFromFile(String username, String passwordFilePath return password; } - private String promptAndReadPassword() + public static String promptAndReadPassword() { String password = EMPTY; @@ -471,12 +470,12 @@ protected enum KeyspaceSet ALL, NON_SYSTEM, NON_LOCAL_STRATEGY } - protected List parseOptionalKeyspace(List cmdArgs, NodeProbe nodeProbe) + public static List parseOptionalKeyspace(List cmdArgs, ServiceBridge nodeProbe) { return parseOptionalKeyspace(cmdArgs, nodeProbe, KeyspaceSet.ALL); } - protected List parseOptionalKeyspace(List cmdArgs, NodeProbe nodeProbe, KeyspaceSet defaultKeyspaceSet) + public static List parseOptionalKeyspace(List cmdArgs, ServiceBridge nodeProbe, KeyspaceSet defaultKeyspaceSet) { List keyspaces = new ArrayList<>(); @@ -484,11 +483,11 @@ protected List parseOptionalKeyspace(List cmdArgs, NodeProbe nod if (cmdArgs == null || cmdArgs.isEmpty()) { if (defaultKeyspaceSet == KeyspaceSet.NON_LOCAL_STRATEGY) - keyspaces.addAll(keyspaces = nodeProbe.getNonLocalStrategyKeyspaces()); + keyspaces.addAll(keyspaces = ssProxy(nodeProbe).getNonLocalStrategyKeyspaces()); else if (defaultKeyspaceSet == KeyspaceSet.NON_SYSTEM) - keyspaces.addAll(keyspaces = nodeProbe.getNonSystemKeyspaces()); + keyspaces.addAll(keyspaces = ssProxy(nodeProbe).getNonSystemKeyspaces()); else - keyspaces.addAll(nodeProbe.getKeyspaces()); + keyspaces.addAll(ssProxy(nodeProbe).getKeyspaces()); } else { @@ -497,19 +496,19 @@ else if (defaultKeyspaceSet == KeyspaceSet.NON_SYSTEM) for (String keyspace : keyspaces) { - if (!nodeProbe.getKeyspaces().contains(keyspace)) + if (!ssProxy(nodeProbe).getKeyspaces().contains(keyspace)) throw new IllegalArgumentException("Keyspace [" + keyspace + "] does not exist."); } return Collections.unmodifiableList(keyspaces); } - protected String[] parseOptionalTables(List cmdArgs) + public static String[] parseOptionalTables(List cmdArgs) { return cmdArgs.size() <= 1 ? EMPTY_STRING_ARRAY : toArray(cmdArgs.subList(1, cmdArgs.size()), String.class); } - protected String[] parsePartitionKeys(List cmdArgs) + public static String[] parsePartitionKeys(List cmdArgs) { return cmdArgs.size() <= 2 ? EMPTY_STRING_ARRAY : toArray(cmdArgs.subList(2, cmdArgs.size()), String.class); } diff --git a/src/java/org/apache/cassandra/tools/NodeToolV2.java b/src/java/org/apache/cassandra/tools/NodeToolV2.java index 83135a29bb18..a8ed8fdea8a9 100644 --- a/src/java/org/apache/cassandra/tools/NodeToolV2.java +++ b/src/java/org/apache/cassandra/tools/NodeToolV2.java @@ -22,12 +22,14 @@ import java.io.IOError; import java.io.IOException; import java.io.PrintWriter; +import java.lang.reflect.Field; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Collections; import java.util.Date; import java.util.List; import java.util.Scanner; +import javax.inject.Inject; import javax.management.InstanceNotFoundException; import com.google.common.base.Joiner; @@ -50,8 +52,9 @@ import org.apache.cassandra.management.CassandraHelpLayout; import org.apache.cassandra.management.api.AbortBootstrap; import org.apache.cassandra.management.api.Assassinate; +import org.apache.cassandra.management.api.Compact; import org.apache.cassandra.management.api.ForceCompact; -import org.apache.cassandra.management.api.JmxConnect; +import org.apache.cassandra.management.api.JmxConnectionMixin; import org.apache.cassandra.management.api.TopLevelCommand; import org.apache.cassandra.utils.FBUtilities; import picocli.CommandLine; @@ -236,16 +239,19 @@ public int execute(String... args) List> cliCommands = newArrayList( AbortBootstrap.class, Assassinate.class, - ForceCompact.class); + ForceCompact.class, + Compact.class); - CommandLine commandLine = new CommandLine(new TopLevelCommand()); -// commandLine.setExecutionStrategy(new CommandLine.RunAll()); - commandLine.addMixin(JmxConnect.MIXIN_KEY, new JmxConnect()); + CommandLine.IFactory factory; + CommandLine commandLine = new CommandLine(new TopLevelCommand(), factory = new CassandraCliFactory(nodeProbeFactory, output)); + commandLine.setExecutionStrategy(JmxConnectionMixin::executionStrategy); + + JmxConnectionMixin mixin = create(factory, JmxConnectionMixin.class); + commandLine.addMixin(JmxConnectionMixin.MIXIN_KEY, mixin); commandLine.setOut(new PrintWriter(output.out)) .setErr(new PrintWriter(output.err)) .setExecutionExceptionHandler((ex, cmdLine, parseResult) -> { - output.err.println(ex.getMessage()); - commandLine.usage(output.err); + ex.printStackTrace(cmdLine.getErr()); return 1; }); @@ -256,7 +262,7 @@ public int execute(String... args) // This must be after all the subcommands are added to the commandLine. commandLine.setHelpFactory(CassandraHelpLayout::new); - commandLine.getSubcommands().values().forEach(sub -> sub.addMixin(JmxConnect.MIXIN_KEY, new JmxConnect())); + commandLine.getSubcommands().values().forEach(sub -> sub.addMixin(JmxConnectionMixin.MIXIN_KEY, mixin)); commandLine.setUsageHelpWidth(CassandraHelpLayout.DEFAULT_USAGE_HELP_WIDTH); commandLine.setHelpSectionKeys(CassandraHelpLayout.cassandraHelpSectionKeys()); } @@ -305,7 +311,7 @@ public int execute(String... args) { // NodeToolCmdRunnable parse = parser.parse(args); printHistory(args); - commandLine.execute(args); + status = commandLine.execute(args); // parse.run(nodeProbeFactory, output); } catch (IllegalArgumentException | IllegalStateException | @@ -328,15 +334,47 @@ public int execute(String... args) return status; } - private static Object instantiateCommand(Class commandClass) + private static T create(CommandLine.IFactory factory, Class clazz) { try { - return commandClass.getDeclaredConstructor().newInstance(); + return factory.create(clazz); } catch (Exception e) { - throw new RuntimeException("Failed to instantiate command " + commandClass.getName(), e); + throw new RuntimeException(e); + } + } + + private static class CassandraCliFactory implements CommandLine.IFactory + { + private final CommandLine.IFactory fallback; + private final INodeProbeFactory nodeProbeFactory; + private final Output output; + + + public CassandraCliFactory(INodeProbeFactory nodeProbeFactory, Output output) + { + this.fallback = CommandLine.defaultFactory(); + this.nodeProbeFactory = nodeProbeFactory; + this.output = output; + } + + public K create(Class cls) throws Exception + { + Object bean = this.fallback.create(cls); + Field[] fields = bean.getClass().getDeclaredFields(); + for (Field field : fields) + { + if (!field.isAnnotationPresent(Inject.class)) + continue; + field.setAccessible(true); + if (field.getType().equals(INodeProbeFactory.class)) + field.set(bean, nodeProbeFactory); + else if (field.getType().equals(Output.class)) + field.set(bean, Output.CONSOLE); + } + return (K) bean; } } diff --git a/test/unit/org/apache/cassandra/tools/NodeToolSynopsisTest.java b/test/unit/org/apache/cassandra/tools/NodeToolSynopsisTest.java index c7ad71164066..a29570a06225 100644 --- a/test/unit/org/apache/cassandra/tools/NodeToolSynopsisTest.java +++ b/test/unit/org/apache/cassandra/tools/NodeToolSynopsisTest.java @@ -63,6 +63,7 @@ public void compareNodeToolHelpOutput() throws Exception // runCommandHelpOutputComparison("abortbootstrap"); runCommandHelpOutputComparison("assassinate"); runCommandHelpOutputComparison("forcecompact"); + runCommandHelpOutputComparison("compact"); } public void runCommandHelpOutputComparison(String commandName) @@ -81,7 +82,8 @@ private static String concatNodetoolOutput(List output) { return '\n' + String.join("\n", output); } - private static List invokeNodetool(BiFunction factory, String... commands) + + public static List invokeNodetool(BiFunction factory, String... commands) { ListOutputStream output = new ListOutputStream(); List args = CQLTester.buildNodetoolArgs(List.of(commands)); @@ -117,7 +119,7 @@ public static String computeDiff(List original, List revised) { return '\n' + String.join("\n", diffLines); } - private static class ListOutputStream extends OutputStream + public static class ListOutputStream extends OutputStream { private final List outputLines = new ArrayList<>(); private final StringBuilder buffer = new StringBuilder(); diff --git a/test/unit/org/apache/cassandra/tools/ToolRunner.java b/test/unit/org/apache/cassandra/tools/ToolRunner.java index 13eeadd64c97..dd1995909384 100644 --- a/test/unit/org/apache/cassandra/tools/ToolRunner.java +++ b/test/unit/org/apache/cassandra/tools/ToolRunner.java @@ -32,6 +32,7 @@ import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.function.BiFunction; import java.util.function.Supplier; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -45,6 +46,7 @@ import org.apache.cassandra.cql3.CQLTester; import org.apache.cassandra.distributed.api.IInstance; import org.apache.cassandra.distributed.api.NodeToolResult; +import org.apache.cassandra.tools.nodetool.CompactTest; import org.apache.cassandra.utils.Pair; import org.assertj.core.util.Lists; @@ -191,6 +193,11 @@ public static ToolResult invokeNodetool(List args) return invoke(CQLTester.buildNodetoolArgs(args)); } + public static ToolResult invokeNodetool(Map env, String... args) + { + return invokeNodetool(env, Arrays.asList(args)); + } + public static ToolResult invokeNodetool(Map env, List args) { return invoke(env, CQLTester.buildNodetoolArgs(args)); @@ -310,6 +317,29 @@ public NodeToolResult get() res.right.getException()); } + public static ToolRunner.ToolResult invokeNodetoolInJvm(BiFunction factory, String... commands) + { + NodeToolSynopsisTest.ListOutputStream out = new NodeToolSynopsisTest.ListOutputStream(); + NodeToolSynopsisTest.ListOutputStream err = new NodeToolSynopsisTest.ListOutputStream(); + List args = CQLTester.buildNodetoolArgs(List.of(commands)); + args.remove("bin/nodetool"); + try + { + Object runner = factory.apply(new NodeProbeFactory(), new Output(new PrintStream(out), new PrintStream(err))); + Object result = runner.getClass().getMethod("execute", String[].class) + .invoke(runner, new Object[] { args.toArray(new String[0]) }); + assertTrue(result instanceof Integer); + + return new ToolResult(args, (Integer) result, String.join("\n", out.getOutputLines()), + String.join("\n", err.getOutputLines()), null); + } + catch (Exception e) + { + return new ToolResult(args, -1, String.join("\n", out.getOutputLines()), + String.join("\n", err.getOutputLines()), e); + } + } + public static Pair invokeSupplier(Supplier runMe) { return invokeSupplier(runMe, null); diff --git a/test/unit/org/apache/cassandra/tools/nodetool/CompactTest.java b/test/unit/org/apache/cassandra/tools/nodetool/CompactTest.java index 928f8851e8a9..91c4ba54af21 100644 --- a/test/unit/org/apache/cassandra/tools/nodetool/CompactTest.java +++ b/test/unit/org/apache/cassandra/tools/nodetool/CompactTest.java @@ -26,6 +26,8 @@ import org.apache.cassandra.db.ColumnFamilyStore; import org.apache.cassandra.db.Keyspace; import org.apache.cassandra.dht.Murmur3Partitioner; +import org.apache.cassandra.tools.NodeToolV2; +import org.apache.cassandra.tools.ToolRunner; import org.assertj.core.api.Assertions; import static org.apache.cassandra.tools.ToolRunner.invokeNodetool; @@ -54,7 +56,8 @@ public void keyPresent() throws Throwable flush(keyspace()); } Assertions.assertThat(cfs.getTracker().getView().liveSSTables()).hasSize(10); - invokeNodetool("compact", "--partition", Long.toString(key), keyspace(), currentTable()).assertOnCleanExit(); +// createMBeanServerConnection(); + invokeNodeToolV2("compact", "--partition", Long.toString(key), keyspace(), currentTable()).assertOnCleanExit(); // only 1 SSTable should exist Assertions.assertThat(cfs.getTracker().getView().liveSSTables()).hasSize(1); @@ -104,4 +107,9 @@ public void keyWrongType() .failure() .errorContains(String.format("Unable to parse partition key 'this_will_not_work' for table %s.%s; Unable to make long from 'this_will_not_work'", keyspace(), currentTable())); } + + public static ToolRunner.ToolResult invokeNodeToolV2(String... commands) + { + return ToolRunner.invokeNodetoolInJvm(NodeToolV2::new, commands); + } } From c9ef5d5214e1c0d107ffeeb3a590f01ecfcbcdf2 Mon Sep 17 00:00:00 2001 From: Maxim Muzafarov Date: Thu, 20 Jun 2024 15:46:30 +0200 Subject: [PATCH 05/22] cleanup code nodetool2 --- .../config/CassandraRelevantProperties.java | 1 + .../management/api/ForceCompact.java | 2 +- .../management/api/TopLevelCommand.java | 8 +- .../org/apache/cassandra/tools/NodeProbe.java | 60 +- .../org/apache/cassandra/tools/NodeTool.java | 25 +- .../apache/cassandra/tools/NodeToolV2.java | 530 ++---------------- .../cassandra/tools/NodeToolSynopsisTest.java | 6 + .../apache/cassandra/tools/ToolRunner.java | 19 +- .../tools/nodetool/CQLToolRunnerTester.java | 58 ++ .../cassandra/tools/nodetool/CompactTest.java | 15 +- 10 files changed, 170 insertions(+), 554 deletions(-) create mode 100644 test/unit/org/apache/cassandra/tools/nodetool/CQLToolRunnerTester.java diff --git a/src/java/org/apache/cassandra/config/CassandraRelevantProperties.java b/src/java/org/apache/cassandra/config/CassandraRelevantProperties.java index e1f6d28d86a9..565d21e6ff39 100644 --- a/src/java/org/apache/cassandra/config/CassandraRelevantProperties.java +++ b/src/java/org/apache/cassandra/config/CassandraRelevantProperties.java @@ -77,6 +77,7 @@ public enum CassandraRelevantProperties CACHEABLE_MUTATION_SIZE_LIMIT("cassandra.cacheable_mutation_size_limit_bytes", convertToString(1_000_000)), CASSANDRA_ALLOW_SIMPLE_STRATEGY("cassandra.allow_simplestrategy"), CASSANDRA_AVAILABLE_PROCESSORS("cassandra.available_processors"), + CASSANDRA_CLI_DEFAULT_LAYOUT("cassandra.cli.picocli.layout", "false"), /** The classpath storage configuration file. */ CASSANDRA_CONFIG("cassandra.config", "cassandra.yaml"), /** diff --git a/src/java/org/apache/cassandra/management/api/ForceCompact.java b/src/java/org/apache/cassandra/management/api/ForceCompact.java index aa758b8e4eb8..146aa345e80f 100644 --- a/src/java/org/apache/cassandra/management/api/ForceCompact.java +++ b/src/java/org/apache/cassandra/management/api/ForceCompact.java @@ -29,7 +29,7 @@ import static com.google.common.base.Preconditions.checkArgument; import static org.apache.cassandra.management.CommandUtils.ssProxy; -import static org.apache.cassandra.tools.NodeToolV2.NodeToolCmd.parsePartitionKeys; +import static org.apache.cassandra.tools.NodeTool.NodeToolCmd.parsePartitionKeys; @CommandLine.Command(name = "forcecompact", description = "Force a (major) compaction on a table") public class ForceCompact extends BaseCommand diff --git a/src/java/org/apache/cassandra/management/api/TopLevelCommand.java b/src/java/org/apache/cassandra/management/api/TopLevelCommand.java index ff5c7658c665..4716b6e7bc59 100644 --- a/src/java/org/apache/cassandra/management/api/TopLevelCommand.java +++ b/src/java/org/apache/cassandra/management/api/TopLevelCommand.java @@ -22,8 +22,12 @@ import picocli.CommandLine; @CommandLine.Command(name = "nodetool", - subcommands = { CassandraHelpCommand.class }, - description = "Manage your Cassandra cluster") + description = "Manage your Cassandra cluster", + subcommands = { CassandraHelpCommand.class, + AbortBootstrap.class, + Assassinate.class, + ForceCompact.class, + Compact.class }) public class TopLevelCommand { } diff --git a/src/java/org/apache/cassandra/tools/NodeProbe.java b/src/java/org/apache/cassandra/tools/NodeProbe.java index f5268fca37e8..ee7a41640260 100644 --- a/src/java/org/apache/cassandra/tools/NodeProbe.java +++ b/src/java/org/apache/cassandra/tools/NodeProbe.java @@ -42,6 +42,7 @@ import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; +import java.util.function.BiConsumer; import javax.annotation.Nullable; import javax.management.JMX; import javax.management.MBeanServerConnection; @@ -233,9 +234,14 @@ protected NodeProbe() this.output = Output.CONSOLE; } - private T cacheProxy(T proxy) + private static T cachedNewMBeanProxy(BiConsumer cache, + MBeanServerConnection connection, + ObjectName objectName, + Class clazz) { - cachedMBeans.put(proxy.getClass().getName(), proxy); + + T proxy = JMX.newMBeanProxy(connection, objectName, clazz); + cache.accept(clazz.getName(), proxy); return proxy; } @@ -275,55 +281,49 @@ protected void connect() throws IOException try { ObjectName name = new ObjectName(ssObjName); - ssProxy = cacheProxy(JMX.newMBeanProxy(mbeanServerConn, name, StorageServiceMBean.class)); + ssProxy = cachedNewMBeanProxy(cachedMBeans::put, mbeanServerConn, name, StorageServiceMBean.class); name = new ObjectName(CMSOperations.MBEAN_OBJECT_NAME); - cmsProxy = cacheProxy(JMX.newMBeanProxy(mbeanServerConn, name, CMSOperationsMBean.class)); + cmsProxy = cachedNewMBeanProxy(cachedMBeans::put, mbeanServerConn, name, CMSOperationsMBean.class); name = new ObjectName(MessagingService.MBEAN_NAME); - msProxy = cacheProxy(JMX.newMBeanProxy(mbeanServerConn, name, MessagingServiceMBean.class)); + msProxy = cachedNewMBeanProxy(cachedMBeans::put, mbeanServerConn, name, MessagingServiceMBean.class); name = new ObjectName(StreamManagerMBean.OBJECT_NAME); - streamProxy = cacheProxy(JMX.newMBeanProxy(mbeanServerConn, name, StreamManagerMBean.class)); + streamProxy = cachedNewMBeanProxy(cachedMBeans::put, mbeanServerConn, name, StreamManagerMBean.class); name = new ObjectName(CompactionManager.MBEAN_OBJECT_NAME); - compactionProxy = cacheProxy(JMX.newMBeanProxy(mbeanServerConn, name, CompactionManagerMBean.class)); + compactionProxy = cachedNewMBeanProxy(cachedMBeans::put, mbeanServerConn, name, CompactionManagerMBean.class); name = new ObjectName(FailureDetector.MBEAN_NAME); - fdProxy = cacheProxy(JMX.newMBeanProxy(mbeanServerConn, name, FailureDetectorMBean.class)); + fdProxy = cachedNewMBeanProxy(cachedMBeans::put, mbeanServerConn, name, FailureDetectorMBean.class); name = new ObjectName(CacheService.MBEAN_NAME); - cacheService = cacheProxy(JMX.newMBeanProxy(mbeanServerConn, name, CacheServiceMBean.class)); + cacheService = cachedNewMBeanProxy(cachedMBeans::put, mbeanServerConn, name, CacheServiceMBean.class); name = new ObjectName(StorageProxy.MBEAN_NAME); - spProxy = cacheProxy(JMX.newMBeanProxy(mbeanServerConn, name, StorageProxyMBean.class)); + spProxy = cachedNewMBeanProxy(cachedMBeans::put, mbeanServerConn, name, StorageProxyMBean.class); name = new ObjectName(HintsService.MBEAN_NAME); - hsProxy = cacheProxy(JMX.newMBeanProxy(mbeanServerConn, name, HintsServiceMBean.class)); + hsProxy = cachedNewMBeanProxy(cachedMBeans::put, mbeanServerConn, name, HintsServiceMBean.class); name = new ObjectName(GCInspector.MBEAN_NAME); - gcProxy = JMX.newMBeanProxy(mbeanServerConn, name, GCInspectorMXBean.class); + gcProxy = cachedNewMBeanProxy(cachedMBeans::put, mbeanServerConn, name, GCInspectorMXBean.class); name = new ObjectName(Gossiper.MBEAN_NAME); - gossProxy = JMX.newMBeanProxy(mbeanServerConn, name, GossiperMBean.class); + gossProxy = cachedNewMBeanProxy(cachedMBeans::put, mbeanServerConn, name, GossiperMBean.class); name = new ObjectName(BatchlogManager.MBEAN_NAME); - bmProxy = JMX.newMBeanProxy(mbeanServerConn, name, BatchlogManagerMBean.class); + bmProxy = cachedNewMBeanProxy(cachedMBeans::put, mbeanServerConn, name, BatchlogManagerMBean.class); name = new ObjectName(ActiveRepairServiceMBean.MBEAN_NAME); - arsProxy = JMX.newMBeanProxy(mbeanServerConn, name, ActiveRepairServiceMBean.class); + arsProxy = cachedNewMBeanProxy(cachedMBeans::put, mbeanServerConn, name, ActiveRepairServiceMBean.class); name = new ObjectName(AuditLogManager.MBEAN_NAME); - almProxy = JMX.newMBeanProxy(mbeanServerConn, name, AuditLogManagerMBean.class); + almProxy = cachedNewMBeanProxy(cachedMBeans::put, mbeanServerConn, name, AuditLogManagerMBean.class); name = new ObjectName(AuthCache.MBEAN_NAME_BASE + PasswordAuthenticator.CredentialsCacheMBean.CACHE_NAME); - ccProxy = JMX.newMBeanProxy(mbeanServerConn, name, PasswordAuthenticator.CredentialsCacheMBean.class); + ccProxy = cachedNewMBeanProxy(cachedMBeans::put, mbeanServerConn, name, PasswordAuthenticator.CredentialsCacheMBean.class); name = new ObjectName(AuthCache.MBEAN_NAME_BASE + AuthorizationProxy.JmxPermissionsCacheMBean.CACHE_NAME); - jpcProxy = JMX.newMBeanProxy(mbeanServerConn, name, AuthorizationProxy.JmxPermissionsCacheMBean.class); - + jpcProxy = cachedNewMBeanProxy(cachedMBeans::put, mbeanServerConn, name, AuthorizationProxy.JmxPermissionsCacheMBean.class); name = new ObjectName(AuthCache.MBEAN_NAME_BASE + NetworkPermissionsCache.CACHE_NAME); - npcProxy = JMX.newMBeanProxy(mbeanServerConn, name, NetworkPermissionsCacheMBean.class); - + npcProxy = cachedNewMBeanProxy(cachedMBeans::put, mbeanServerConn, name, NetworkPermissionsCacheMBean.class); name = new ObjectName(AuthCache.MBEAN_NAME_BASE + PermissionsCache.CACHE_NAME); - pcProxy = JMX.newMBeanProxy(mbeanServerConn, name, PermissionsCacheMBean.class); - + pcProxy = cachedNewMBeanProxy(cachedMBeans::put, mbeanServerConn, name, PermissionsCacheMBean.class); name = new ObjectName(AuthCache.MBEAN_NAME_BASE + RolesCache.CACHE_NAME); - rcProxy = JMX.newMBeanProxy(mbeanServerConn, name, RolesCacheMBean.class); - + rcProxy = cachedNewMBeanProxy(cachedMBeans::put, mbeanServerConn, name, RolesCacheMBean.class); name = new ObjectName(CIDRPermissionsManager.MBEAN_NAME); - cpbProxy = JMX.newMBeanProxy(mbeanServerConn, name, CIDRPermissionsManagerMBean.class); - + cpbProxy = cachedNewMBeanProxy(cachedMBeans::put, mbeanServerConn, name, CIDRPermissionsManagerMBean.class); name = new ObjectName(CIDRGroupsMappingManager.MBEAN_NAME); - cmbProxy = JMX.newMBeanProxy(mbeanServerConn, name, CIDRGroupsMappingManagerMBean.class); - + cmbProxy = cachedNewMBeanProxy(cachedMBeans::put, mbeanServerConn, name, CIDRGroupsMappingManagerMBean.class); name = new ObjectName(CIDRFilteringMetricsTable.MBEAN_NAME); - cfmProxy = JMX.newMBeanProxy(mbeanServerConn, name, CIDRFilteringMetricsTableMBean.class); + cfmProxy = cachedNewMBeanProxy(cachedMBeans::put, mbeanServerConn, name, CIDRFilteringMetricsTableMBean.class); } catch (MalformedObjectNameException e) { diff --git a/src/java/org/apache/cassandra/tools/NodeTool.java b/src/java/org/apache/cassandra/tools/NodeTool.java index d4bc7e130b7d..fe72e2ed0bb7 100644 --- a/src/java/org/apache/cassandra/tools/NodeTool.java +++ b/src/java/org/apache/cassandra/tools/NodeTool.java @@ -31,6 +31,7 @@ import java.util.Map.Entry; import java.util.Scanner; import java.util.SortedMap; +import java.util.function.Consumer; import javax.management.InstanceNotFoundException; import com.google.common.base.Joiner; @@ -295,7 +296,7 @@ public int execute(String... args) return status; } - private static void printHistory(String... args) + public static void printHistory(String... args) { //don't bother to print if no args passed (meaning, nodetool is just printing out the sub-commands list) if (args.length == 0) @@ -315,21 +316,31 @@ private static void printHistory(String... args) } } + public static void badUse(Consumer out, Throwable e) + { + out.accept("nodetool: " + e.getMessage()); + out.accept("See 'nodetool help' or 'nodetool help '."); + } + protected void badUse(Exception e) { - output.out.println("nodetool: " + e.getMessage()); - output.out.println("See 'nodetool help' or 'nodetool help '."); + badUse(output.out::println, e); } - protected void err(Throwable e) + public static void err(Consumer out, Throwable e) { // CASSANDRA-11537: friendly error message when server is not ready if (e instanceof InstanceNotFoundException) throw new IllegalArgumentException("Server is not initialized yet, cannot run nodetool."); - output.err.println("error: " + e.getMessage()); - output.err.println("-- StackTrace --"); - output.err.println(getStackTraceAsString(e)); + out.accept("error: " + e.getMessage()); + out.accept("-- StackTrace --"); + out.accept(getStackTraceAsString(e)); + } + + protected void err(Throwable e) + { + err(output.err::println, e); } public static class CassHelp extends Help implements NodeToolCmdRunnable diff --git a/src/java/org/apache/cassandra/tools/NodeToolV2.java b/src/java/org/apache/cassandra/tools/NodeToolV2.java index a8ed8fdea8a9..7f12758e7f73 100644 --- a/src/java/org/apache/cassandra/tools/NodeToolV2.java +++ b/src/java/org/apache/cassandra/tools/NodeToolV2.java @@ -17,58 +17,22 @@ */ package org.apache.cassandra.tools; -import java.io.Console; -import java.io.FileNotFoundException; -import java.io.IOError; -import java.io.IOException; import java.io.PrintWriter; import java.lang.reflect.Field; -import java.text.SimpleDateFormat; -import java.util.ArrayList; -import java.util.Collections; -import java.util.Date; -import java.util.List; -import java.util.Scanner; import javax.inject.Inject; -import javax.management.InstanceNotFoundException; -import com.google.common.base.Joiner; import com.google.common.base.Throwables; -import io.airlift.airline.Cli; -import io.airlift.airline.Help; -import io.airlift.airline.Option; -import io.airlift.airline.OptionType; -import io.airlift.airline.ParseArgumentsMissingException; -import io.airlift.airline.ParseArgumentsUnexpectedException; -import io.airlift.airline.ParseCommandMissingException; -import io.airlift.airline.ParseCommandUnrecognizedException; -import io.airlift.airline.ParseOptionConversionException; -import io.airlift.airline.ParseOptionMissingException; -import io.airlift.airline.ParseOptionMissingValueException; -import org.apache.cassandra.io.util.File; -import org.apache.cassandra.io.util.FileWriter; -import org.apache.cassandra.management.BaseCommand; +import org.apache.cassandra.config.CassandraRelevantProperties; import org.apache.cassandra.management.CassandraHelpLayout; -import org.apache.cassandra.management.api.AbortBootstrap; -import org.apache.cassandra.management.api.Assassinate; -import org.apache.cassandra.management.api.Compact; -import org.apache.cassandra.management.api.ForceCompact; import org.apache.cassandra.management.api.JmxConnectionMixin; import org.apache.cassandra.management.api.TopLevelCommand; import org.apache.cassandra.utils.FBUtilities; import picocli.CommandLine; -import static com.google.common.base.Throwables.getStackTraceAsString; -import static com.google.common.collect.Iterables.toArray; -import static com.google.common.collect.Lists.newArrayList; -import static java.lang.Integer.parseInt; -import static java.lang.String.format; -import static org.apache.cassandra.io.util.File.WriteMode.APPEND; -import static org.apache.commons.lang3.ArrayUtils.EMPTY_STRING_ARRAY; -import static org.apache.commons.lang3.StringUtils.EMPTY; -import static org.apache.commons.lang3.StringUtils.isEmpty; -import static org.apache.commons.lang3.StringUtils.isNotEmpty; +import static org.apache.cassandra.tools.NodeTool.badUse; +import static org.apache.cassandra.tools.NodeTool.err; +import static org.apache.cassandra.tools.NodeTool.printHistory; public class NodeToolV2 { @@ -77,8 +41,6 @@ public class NodeToolV2 FBUtilities.preventIllegalAccessWarnings(); } - private static final String HISTORYFILE = "nodetool.history"; - private final INodeProbeFactory nodeProbeFactory; private final Output output; @@ -93,257 +55,53 @@ public NodeToolV2(INodeProbeFactory nodeProbeFactory, Output output) this.output = output; } + /** + * Execute the command line utility with the given arguments via the JMX connection. + * @param args command line arguments + * @return 0 on success, 1 on bad use, 2 on execution error + */ public int execute(String... args) { - List> commands = newArrayList( - CassHelp.class -// CIDRFilteringStats.class, -// Cleanup.class, -// ClearSnapshot.class, -// ClientStats.class, -// Compact.class, -// CompactionHistory.class, -// CompactionStats.class, -// DataPaths.class, -// Decommission.class, -// DescribeCluster.class, -// DescribeRing.class, -// DisableAuditLog.class, -// DisableAutoCompaction.class, -// DisableBackup.class, -// DisableBinary.class, -// DisableFullQueryLog.class, -// DisableGossip.class, -// DisableHandoff.class, -// DisableHintsForDC.class, -// DisableOldProtocolVersions.class, -// Drain.class, -// DropCIDRGroup.class, -// EnableAuditLog.class, -// EnableAutoCompaction.class, -// EnableBackup.class, -// EnableBinary.class, -// EnableFullQueryLog.class, -// EnableGossip.class, -// EnableHandoff.class, -// EnableHintsForDC.class, -// EnableOldProtocolVersions.class, -// FailureDetectorInfo.class, -// Flush.class, -// GarbageCollect.class, -// GcStats.class, -// GetAuditLog.class, -// GetAuthCacheConfig.class, -// GetBatchlogReplayTrottle.class, -// GetCIDRGroupsOfIP.class, -// GetColumnIndexSize.class, -// GetCompactionThreshold.class, -// GetCompactionThroughput.class, -// GetConcurrency.class, -// GetConcurrentCompactors.class, -// GetConcurrentViewBuilders.class, -// GetDefaultKeyspaceRF.class, -// GetEndpoints.class, -// GetFullQueryLog.class, -// GetInterDCStreamThroughput.class, -// GetLoggingLevels.class, -// GetMaxHintWindow.class, -// GetSSTables.class, -// GetSeeds.class, -// GetSnapshotThrottle.class, -// GetStreamThroughput.class, -// GetTimeout.class, -// GetTraceProbability.class, -// GossipInfo.class, -// Import.class, -// Info.class, -// InvalidateCIDRPermissionsCache.class, -// InvalidateCounterCache.class, -// InvalidateCredentialsCache.class, -// InvalidateJmxPermissionsCache.class, -// ReloadCIDRGroupsCache.class, -// InvalidateKeyCache.class, -// InvalidateNetworkPermissionsCache.class, -// InvalidatePermissionsCache.class, -// InvalidateRolesCache.class, -// InvalidateRowCache.class, -// Join.class, -// ListCIDRGroups.class, -// ListPendingHints.class, -// ListSnapshots.class, -// Move.class, -// NetStats.class, -// PauseHandoff.class, -// ProfileLoad.class, -// ProxyHistograms.class, -// RangeKeySample.class, -// Rebuild.class, -// RebuildIndex.class, -// RecompressSSTables.class, -// Refresh.class, -// RefreshSizeEstimates.class, -// ReloadLocalSchema.class, -// ReloadSeeds.class, -// ReloadSslCertificates.class, -// ReloadTriggers.class, -// RelocateSSTables.class, -// RemoveNode.class, -// Repair.class, -// ReplayBatchlog.class, -// ResetFullQueryLog.class, -// ResetLocalSchema.class, -// ResumeHandoff.class, -// Ring.class, -// Scrub.class, -// SetAuthCacheConfig.class, -// SetBatchlogReplayThrottle.class, -// SetCacheCapacity.class, -// SetCacheKeysToSave.class, -// SetColumnIndexSize.class, -// SetCompactionThreshold.class, -// SetCompactionThroughput.class, -// SetConcurrency.class, -// SetConcurrentCompactors.class, -// SetConcurrentViewBuilders.class, -// SetDefaultKeyspaceRF.class, -// SetHintedHandoffThrottleInKB.class, -// SetInterDCStreamThroughput.class, -// SetLoggingLevel.class, -// SetMaxHintWindow.class, -// SetSnapshotThrottle.class, -// SetStreamThroughput.class, -// SetTimeout.class, -// SetTraceProbability.class, -// Sjk.class, -// Snapshot.class, -// Status.class, -// StatusAutoCompaction.class, -// StatusBackup.class, -// StatusBinary.class, -// StatusGossip.class, -// StatusHandoff.class, -// Stop.class, -// StopDaemon.class, -// TableHistograms.class, -// TableStats.class, -// TopPartitions.class, -// TpStats.class, -// TruncateHints.class, -// UpdateCIDRGroup.class, -// UpgradeSSTable.class, -// Verify.class, -// Version.class, -// ViewBuildStatus.class - ); - - List> cliCommands = newArrayList( - AbortBootstrap.class, - Assassinate.class, - ForceCompact.class, - Compact.class); - CommandLine.IFactory factory; CommandLine commandLine = new CommandLine(new TopLevelCommand(), factory = new CassandraCliFactory(nodeProbeFactory, output)); - commandLine.setExecutionStrategy(JmxConnectionMixin::executionStrategy); - JmxConnectionMixin mixin = create(factory, JmxConnectionMixin.class); - commandLine.addMixin(JmxConnectionMixin.MIXIN_KEY, mixin); - commandLine.setOut(new PrintWriter(output.out)) - .setErr(new PrintWriter(output.err)) - .setExecutionExceptionHandler((ex, cmdLine, parseResult) -> { - ex.printStackTrace(cmdLine.getErr()); - return 1; - }); + configureCliLayout(commandLine); + commandLine.setOut(new PrintWriter(output.out, true)) + .setErr(new PrintWriter(output.err, true)) + .setExecutionExceptionHandler((ex, cmdLine, parseResult) -> { + err(cmdLine.getErr()::println, Throwables.getRootCause(ex)); + return 2; + }) + .setParameterExceptionHandler((ex, arg) -> { + badUse(commandLine.getOut()::println, Throwables.getRootCause(ex)); + return 1; + }); try { - for (Class commandClass : cliCommands) - commandLine.addSubcommand(commandLine.getFactory().create(commandClass)); - - // This must be after all the subcommands are added to the commandLine. - commandLine.setHelpFactory(CassandraHelpLayout::new); + JmxConnectionMixin mixin = factory.create(JmxConnectionMixin.class); + commandLine.setExecutionStrategy(JmxConnectionMixin::executionStrategy); + commandLine.addMixin(JmxConnectionMixin.MIXIN_KEY, mixin); commandLine.getSubcommands().values().forEach(sub -> sub.addMixin(JmxConnectionMixin.MIXIN_KEY, mixin)); - commandLine.setUsageHelpWidth(CassandraHelpLayout.DEFAULT_USAGE_HELP_WIDTH); - commandLine.setHelpSectionKeys(CassandraHelpLayout.cassandraHelpSectionKeys()); + + printHistory(args); + return commandLine.execute(args); } catch (Exception e) { - err(Throwables.getRootCause(e)); + err(commandLine.getErr()::println, e); return 2; } - -// return commandLine.execute(args); - - Cli.CliBuilder builder = Cli.builder("nodetool"); - - builder.withDescription("Manage your Cassandra cluster") - .withDefaultCommand(CassHelp.class) - .withCommands(commands); - - // bootstrap commands -// builder.withGroup("bootstrap") -// .withDescription("Monitor/manage node's bootstrap process") -// .withDefaultCommand(CassHelp.class) -// .withCommand(BootstrapResume.class); -// -// builder.withGroup("repair_admin") -// .withDescription("list and fail incremental repair sessions") -// .withDefaultCommand(RepairAdmin.ListCmd.class) -// .withCommand(RepairAdmin.ListCmd.class) -// .withCommand(RepairAdmin.CancelCmd.class) -// .withCommand(RepairAdmin.CleanupDataCmd.class) -// .withCommand(RepairAdmin.SummarizePendingCmd.class) -// .withCommand(RepairAdmin.SummarizeRepairedCmd.class); -// -// builder.withGroup("cms") -// .withDescription("Manage cluster metadata") -// .withDefaultCommand(CMSAdmin.DescribeCMS.class) -// .withCommand(CMSAdmin.DescribeCMS.class) -// .withCommand(CMSAdmin.InitializeCMS.class) -// .withCommand(CMSAdmin.ReconfigureCMS.class) -// .withCommand(CMSAdmin.Snapshot.class) -// .withCommand(CMSAdmin.Unregister.class); - - Cli parser = builder.build(); - - int status = 0; - try - { -// NodeToolCmdRunnable parse = parser.parse(args); - printHistory(args); - status = commandLine.execute(args); -// parse.run(nodeProbeFactory, output); - } catch (IllegalArgumentException | - IllegalStateException | - ParseArgumentsMissingException | - ParseArgumentsUnexpectedException | - ParseOptionConversionException | - ParseOptionMissingException | - ParseOptionMissingValueException | - ParseCommandMissingException | - ParseCommandUnrecognizedException e) - { - badUse(e); - status = 1; - } catch (Throwable throwable) - { - err(Throwables.getRootCause(throwable)); - status = 2; - } - - return status; } - private static T create(CommandLine.IFactory factory, Class clazz) + private static void configureCliLayout(CommandLine commandLine) { - try - { - return factory.create(clazz); - } - catch (Exception e) - { - throw new RuntimeException(e); - } + if (CassandraRelevantProperties.CASSANDRA_CLI_DEFAULT_LAYOUT.getBoolean()) + return; + + commandLine.setHelpFactory(CassandraHelpLayout::new) + .setUsageHelpWidth(CassandraHelpLayout.DEFAULT_USAGE_HELP_WIDTH) + .setHelpSectionKeys(CassandraHelpLayout.cassandraHelpSectionKeys()); } private static class CassandraCliFactory implements CommandLine.IFactory @@ -352,7 +110,6 @@ private static class CassandraCliFactory implements CommandLine.IFactory private final INodeProbeFactory nodeProbeFactory; private final Output output; - public CassandraCliFactory(INodeProbeFactory nodeProbeFactory, Output output) { this.fallback = CommandLine.defaultFactory(); @@ -372,229 +129,12 @@ public K create(Class cls) throws Exception if (field.getType().equals(INodeProbeFactory.class)) field.set(bean, nodeProbeFactory); else if (field.getType().equals(Output.class)) - field.set(bean, Output.CONSOLE); + field.set(bean, output); } return (K) bean; } } - private static void printHistory(String... args) - { - //don't bother to print if no args passed (meaning, nodetool is just printing out the sub-commands list) - if (args.length == 0) - return; - - String cmdLine = Joiner.on(" ").skipNulls().join(args); - cmdLine = cmdLine.replaceFirst("(?<=(-pw|--password))\\s+\\S+", " "); - - try (FileWriter writer = new File(FBUtilities.getToolsOutputDirectory(), HISTORYFILE).newWriter(APPEND)) - { - SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss,SSS"); - writer.append(sdf.format(new Date())).append(": ").append(cmdLine).append(System.lineSeparator()); - } - catch (IOException | IOError ioe) - { - //quietly ignore any errors about not being able to write out history - } - } - - protected void badUse(Exception e) - { - output.out.println("nodetool: " + e.getMessage()); - output.out.println("See 'nodetool help' or 'nodetool help '."); - } - - protected void err(Throwable e) - { - // CASSANDRA-11537: friendly error message when server is not ready - if (e instanceof InstanceNotFoundException) - throw new IllegalArgumentException("Server is not initialized yet, cannot run nodetool."); - - output.err.println("error: " + e.getMessage()); - output.err.println("-- StackTrace --"); - output.err.println(getStackTraceAsString(e)); - } - - public static class CassHelp extends Help implements NodeToolCmdRunnable - { - public void run(INodeProbeFactory nodeProbeFactory, Output output) - { - run(); - } - } - - interface NodeToolCmdRunnable - { - void run(INodeProbeFactory nodeProbeFactory, Output output); - } - - public static abstract class NodeToolCmd implements NodeToolCmdRunnable - { - - @Option(type = OptionType.GLOBAL, name = {"-h", "--host"}, description = "Node hostname or ip address") - private String host = "127.0.0.1"; - - @Option(type = OptionType.GLOBAL, name = {"-p", "--port"}, description = "Remote jmx agent port number") - private String port = "7199"; - - @Option(type = OptionType.GLOBAL, name = {"-u", "--username"}, description = "Remote jmx agent username") - private String username = EMPTY; - - @Option(type = OptionType.GLOBAL, name = {"-pw", "--password"}, description = "Remote jmx agent password") - private String password = EMPTY; - - @Option(type = OptionType.GLOBAL, name = {"-pwf", "--password-file"}, description = "Path to the JMX password file") - private String passwordFilePath = EMPTY; - - @Option(type = OptionType.GLOBAL, name = { "-pp", "--print-port"}, description = "Operate in 4.0 mode with hosts disambiguated by port number", arity = 0) - protected boolean printPort = false; - - private INodeProbeFactory nodeProbeFactory; - protected Output output; - - @Override - public void run(INodeProbeFactory nodeProbeFactory, Output output) - { - this.nodeProbeFactory = nodeProbeFactory; - this.output = output; - runInternal(); - } - - public void runInternal() - { - if (isNotEmpty(username)) { - if (isNotEmpty(passwordFilePath)) - password = readUserPasswordFromFile(username, passwordFilePath); - - if (isEmpty(password)) - password = promptAndReadPassword(); - } - - try (NodeProbe probe = connect()) - { - execute(probe); - if (probe.isFailed()) - throw new RuntimeException("nodetool failed, check server logs"); - } - catch (IOException e) - { - throw new RuntimeException("Error while closing JMX connection", e); - } - - } - - private String readUserPasswordFromFile(String username, String passwordFilePath) { - String password = EMPTY; - - File passwordFile = new File(passwordFilePath); - try (Scanner scanner = new Scanner(passwordFile.toJavaIOFile()).useDelimiter("\\s+")) - { - while (scanner.hasNextLine()) - { - if (scanner.hasNext()) - { - String jmxRole = scanner.next(); - if (jmxRole.equals(username) && scanner.hasNext()) - { - password = scanner.next(); - break; - } - } - scanner.nextLine(); - } - } - catch (FileNotFoundException e) - { - throw new RuntimeException(e); - } - - return password; - } - - private String promptAndReadPassword() - { - String password = EMPTY; - - Console console = System.console(); - if (console != null) - password = String.valueOf(console.readPassword("Password:")); - - return password; - } - - protected abstract void execute(NodeProbe probe); - - private NodeProbe connect() - { - NodeProbe nodeClient = null; - - try - { - if (username.isEmpty()) - nodeClient = nodeProbeFactory.create(host, parseInt(port)); - else - nodeClient = nodeProbeFactory.create(host, parseInt(port), username, password); - - nodeClient.setOutput(output); - } catch (IOException | SecurityException e) - { - Throwable rootCause = Throwables.getRootCause(e); - output.err.println(format("nodetool: Failed to connect to '%s:%s' - %s: '%s'.", host, port, rootCause.getClass().getSimpleName(), rootCause.getMessage())); - System.exit(1); - } - - return nodeClient; - } - - protected enum KeyspaceSet - { - ALL, NON_SYSTEM, NON_LOCAL_STRATEGY - } - - protected List parseOptionalKeyspace(List cmdArgs, NodeProbe nodeProbe) - { - return parseOptionalKeyspace(cmdArgs, nodeProbe, KeyspaceSet.ALL); - } - - protected List parseOptionalKeyspace(List cmdArgs, NodeProbe nodeProbe, KeyspaceSet defaultKeyspaceSet) - { - List keyspaces = new ArrayList<>(); - - - if (cmdArgs == null || cmdArgs.isEmpty()) - { - if (defaultKeyspaceSet == KeyspaceSet.NON_LOCAL_STRATEGY) - keyspaces.addAll(keyspaces = nodeProbe.getNonLocalStrategyKeyspaces()); - else if (defaultKeyspaceSet == KeyspaceSet.NON_SYSTEM) - keyspaces.addAll(keyspaces = nodeProbe.getNonSystemKeyspaces()); - else - keyspaces.addAll(nodeProbe.getKeyspaces()); - } - else - { - keyspaces.add(cmdArgs.get(0)); - } - - for (String keyspace : keyspaces) - { - if (!nodeProbe.getKeyspaces().contains(keyspace)) - throw new IllegalArgumentException("Keyspace [" + keyspace + "] does not exist."); - } - - return Collections.unmodifiableList(keyspaces); - } - - protected String[] parseOptionalTables(List cmdArgs) - { - return cmdArgs.size() <= 1 ? EMPTY_STRING_ARRAY : toArray(cmdArgs.subList(1, cmdArgs.size()), String.class); - } - - public static String[] parsePartitionKeys(List cmdArgs) - { - return cmdArgs.size() <= 2 ? EMPTY_STRING_ARRAY : toArray(cmdArgs.subList(2, cmdArgs.size()), String.class); - } - } - // public static SortedMap getOwnershipByDcWithPort(NodeProbe probe, boolean resolveIp, // Map tokenToEndpoint, // Map ownerships) diff --git a/test/unit/org/apache/cassandra/tools/NodeToolSynopsisTest.java b/test/unit/org/apache/cassandra/tools/NodeToolSynopsisTest.java index a29570a06225..884a77cdc966 100644 --- a/test/unit/org/apache/cassandra/tools/NodeToolSynopsisTest.java +++ b/test/unit/org/apache/cassandra/tools/NodeToolSynopsisTest.java @@ -152,5 +152,11 @@ public List getOutputLines() flush(); return new ArrayList<>(outputLines); } + + public String getOutput() + { + flush(); + return String.join(System.lineSeparator(), outputLines); + } } } diff --git a/test/unit/org/apache/cassandra/tools/ToolRunner.java b/test/unit/org/apache/cassandra/tools/ToolRunner.java index dd1995909384..c8dac8fe6ca1 100644 --- a/test/unit/org/apache/cassandra/tools/ToolRunner.java +++ b/test/unit/org/apache/cassandra/tools/ToolRunner.java @@ -46,7 +46,6 @@ import org.apache.cassandra.cql3.CQLTester; import org.apache.cassandra.distributed.api.IInstance; import org.apache.cassandra.distributed.api.NodeToolResult; -import org.apache.cassandra.tools.nodetool.CompactTest; import org.apache.cassandra.utils.Pair; import org.assertj.core.util.Lists; @@ -317,6 +316,16 @@ public NodeToolResult get() res.right.getException()); } + public static ToolResult invokeNodetoolInJvmV2(String... commands) + { + return ToolRunner.invokeNodetoolInJvm(NodeToolV2::new, commands); + } + + public static ToolResult invokeNodetoolInJvmV1(String... commands) + { + return ToolRunner.invokeNodetoolInJvm(NodeTool::new, commands); + } + public static ToolRunner.ToolResult invokeNodetoolInJvm(BiFunction factory, String... commands) { NodeToolSynopsisTest.ListOutputStream out = new NodeToolSynopsisTest.ListOutputStream(); @@ -329,14 +338,12 @@ public static ToolRunner.ToolResult invokeNodetoolInJvm(BiFunction runnersMap = Map.of( + "invokeNodetool", ToolRunner::invokeNodetool, + "invokeNodetoolInJvmV1", ToolRunner::invokeNodetoolInJvmV1, + "invokeNodetoolInJvmV2", ToolRunner::invokeNodetoolInJvmV2); + + @Parameterized.Parameter + public String runner; + + @Parameterized.Parameters(name = "{0}") + public static Set runners() + { + return runnersMap.keySet(); + } + + protected ToolRunner.ToolResult invokeNodetool(String... args) + { + return runnersMap.get(runner).execute(args); + } + + public interface ToolHandler + { + ToolRunner.ToolResult execute(String... args); + default ToolRunner.ToolResult execute(List args) { return execute(args.toArray(new String[0])); } + } +} diff --git a/test/unit/org/apache/cassandra/tools/nodetool/CompactTest.java b/test/unit/org/apache/cassandra/tools/nodetool/CompactTest.java index 91c4ba54af21..60b55bf226f8 100644 --- a/test/unit/org/apache/cassandra/tools/nodetool/CompactTest.java +++ b/test/unit/org/apache/cassandra/tools/nodetool/CompactTest.java @@ -22,17 +22,12 @@ import org.junit.BeforeClass; import org.junit.Test; -import org.apache.cassandra.cql3.CQLTester; import org.apache.cassandra.db.ColumnFamilyStore; import org.apache.cassandra.db.Keyspace; import org.apache.cassandra.dht.Murmur3Partitioner; -import org.apache.cassandra.tools.NodeToolV2; -import org.apache.cassandra.tools.ToolRunner; import org.assertj.core.api.Assertions; -import static org.apache.cassandra.tools.ToolRunner.invokeNodetool; - -public class CompactTest extends CQLTester +public class CompactTest extends CQLToolRunnerTester { @BeforeClass public static void setup() throws Throwable @@ -56,8 +51,7 @@ public void keyPresent() throws Throwable flush(keyspace()); } Assertions.assertThat(cfs.getTracker().getView().liveSSTables()).hasSize(10); -// createMBeanServerConnection(); - invokeNodeToolV2("compact", "--partition", Long.toString(key), keyspace(), currentTable()).assertOnCleanExit(); + invokeNodetool("compact", "--partition", Long.toString(key), keyspace(), currentTable()).assertOnCleanExit(); // only 1 SSTable should exist Assertions.assertThat(cfs.getTracker().getView().liveSSTables()).hasSize(1); @@ -107,9 +101,4 @@ public void keyWrongType() .failure() .errorContains(String.format("Unable to parse partition key 'this_will_not_work' for table %s.%s; Unable to make long from 'this_will_not_work'", keyspace(), currentTable())); } - - public static ToolRunner.ToolResult invokeNodeToolV2(String... commands) - { - return ToolRunner.invokeNodetoolInJvm(NodeToolV2::new, commands); - } } From 4a0dc9fd2ca6ef159bff24b18ca7f3070ed5acf5 Mon Sep 17 00:00:00 2001 From: Maxim Muzafarov Date: Thu, 20 Jun 2024 22:52:03 +0200 Subject: [PATCH 06/22] command initialization fix --- .../management/CassandraHelpCommand.java | 2 +- .../management/CassandraHelpLayout.java | 23 +++++++++++++++---- .../cassandra/management/CommandUtils.java | 10 -------- .../management/api/AbortBootstrap.java | 4 ++-- .../cassandra/management/api/Assassinate.java | 2 +- .../cassandra/management/api/Compact.java | 15 ++++++------ .../management/api/ForceCompact.java | 8 +++---- .../management/api/JmxConnectionMixin.java | 4 ++-- .../management/api/TopLevelCommand.java | 13 ++++++++++- .../apache/cassandra/tools/NodeToolV2.java | 23 ------------------- .../tools/nodetool/CQLToolRunnerTester.java | 8 +++++++ .../cassandra/tools/nodetool/CompactTest.java | 8 ------- .../tools/nodetool/ForceCompactionTest.java | 15 ++++++------ 13 files changed, 64 insertions(+), 71 deletions(-) diff --git a/src/java/org/apache/cassandra/management/CassandraHelpCommand.java b/src/java/org/apache/cassandra/management/CassandraHelpCommand.java index 905b46091d15..6d8d6c912b87 100644 --- a/src/java/org/apache/cassandra/management/CassandraHelpCommand.java +++ b/src/java/org/apache/cassandra/management/CassandraHelpCommand.java @@ -78,7 +78,7 @@ public void run() subcommand.usage(out, colors); } - private static void printTopCommandUsage(CommandLine command, CommandLine.Help.ColorScheme colors, PrintWriter writer) + public static void printTopCommandUsage(CommandLine command, CommandLine.Help.ColorScheme colors, PrintWriter writer) { if (command == null) return; diff --git a/src/java/org/apache/cassandra/management/CassandraHelpLayout.java b/src/java/org/apache/cassandra/management/CassandraHelpLayout.java index 85d1cccf406b..c965f7826d57 100644 --- a/src/java/org/apache/cassandra/management/CassandraHelpLayout.java +++ b/src/java/org/apache/cassandra/management/CassandraHelpLayout.java @@ -346,6 +346,21 @@ public String footerHeading(Object... params) return createHeading("%n", params); } + @Override + public String footer(Object... params) + { + + String[] footer = isEmpty( + commandSpec().usageMessage().footer()) ? new String[]{ "See 'nodetool help ' for more information on a specific command." } : + commandSpec().usageMessage().footer(); + return join(ansi(), + commandSpec().usageMessage().width(), + commandSpec().usageMessage().adjustLineBreaksForWideCJKCharacters(), + footer, + new StringBuilder(), + params).toString(); + } + public String topLevelCommandListHeading(Object... params) { return createHeading(TOP_LEVEL_COMMAND_HEADING, params); } @@ -448,9 +463,9 @@ public Ansi.Text[][] render(CommandLine.Model.OptionSpec option, IParamLabelRend Ansi.Text desc = scheme.optionText(option.description().length == 0 ? "" : option.description()[0]); Ansi.Text[][] result = new Ansi.Text[3][]; - result[0] = new Ansi.Text[]{optionText}; - result[1] = new Ansi.Text[]{descPadding.concat(desc)}; - result[2] = new Ansi.Text[]{Ansi.OFF.new Text("", scheme)}; + result[0] = new Ansi.Text[]{ optionText }; + result[1] = new Ansi.Text[]{ descPadding.concat(desc) }; + result[2] = new Ansi.Text[]{ Ansi.OFF.new Text("", scheme) }; return result; } } @@ -466,7 +481,7 @@ public Ansi.Text[][] render(CommandLine.Model.PositionalParamSpec param, IParamL Ansi.Text descPadding = Ansi.OFF.new Text(leadingSpaces(DESCRIPTION_INDENT), scheme); Ansi.Text[][] result = new Ansi.Text[3][]; result[0] = new Ansi.Text[]{ parameterLabelRenderer.renderParameterLabel(param, scheme.ansi(), scheme.parameterStyles()) }; - result[1] = new Ansi.Text[]{ descPadding.concat(scheme.text(descriptionString)) }; + result[1] = new Ansi.Text[]{ descPadding.concat(scheme.parameterText(descriptionString)) }; result[2] = new Ansi.Text[]{ Ansi.OFF.new Text("", scheme) }; return result; } diff --git a/src/java/org/apache/cassandra/management/CommandUtils.java b/src/java/org/apache/cassandra/management/CommandUtils.java index 05ece3c86151..58706f21e162 100644 --- a/src/java/org/apache/cassandra/management/CommandUtils.java +++ b/src/java/org/apache/cassandra/management/CommandUtils.java @@ -18,21 +18,11 @@ package org.apache.cassandra.management; -import java.io.IOException; -import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; -import java.util.Collections; -import java.util.List; -import java.util.concurrent.ExecutionException; import org.apache.cassandra.db.compaction.CompactionManagerMBean; import org.apache.cassandra.service.StorageServiceMBean; -import org.apache.cassandra.tools.NodeProbe; -import org.apache.cassandra.tools.NodeTool; - -import static com.google.common.collect.Iterables.toArray; -import static org.apache.commons.lang3.ArrayUtils.EMPTY_STRING_ARRAY; public final class CommandUtils { diff --git a/src/java/org/apache/cassandra/management/api/AbortBootstrap.java b/src/java/org/apache/cassandra/management/api/AbortBootstrap.java index e5ce36506a4b..cbea96efedc5 100644 --- a/src/java/org/apache/cassandra/management/api/AbortBootstrap.java +++ b/src/java/org/apache/cassandra/management/api/AbortBootstrap.java @@ -30,10 +30,10 @@ public class AbortBootstrap extends BaseCommand { @Option(names = "--node", description = "Node ID of the node that failed bootstrap") - private String nodeId = EMPTY; + public String nodeId = EMPTY; @Option(names = "--ip", description = "IP of the node that failed bootstrap") - private String endpoint = EMPTY; + public String endpoint = EMPTY; @Override public void execute(ServiceBridge probe) diff --git a/src/java/org/apache/cassandra/management/api/Assassinate.java b/src/java/org/apache/cassandra/management/api/Assassinate.java index 93bdb58b0de7..49a7914dd126 100644 --- a/src/java/org/apache/cassandra/management/api/Assassinate.java +++ b/src/java/org/apache/cassandra/management/api/Assassinate.java @@ -28,7 +28,7 @@ public class Assassinate extends BaseCommand { @CommandLine.Parameters(description = "IP address of the endpoint to assassinate", arity = "1") - private String ip_address = EMPTY; + public String ip_address = EMPTY; @Override public void execute(ServiceBridge probe) diff --git a/src/java/org/apache/cassandra/management/api/Compact.java b/src/java/org/apache/cassandra/management/api/Compact.java index a5596b8c357a..e74b81141c9f 100644 --- a/src/java/org/apache/cassandra/management/api/Compact.java +++ b/src/java/org/apache/cassandra/management/api/Compact.java @@ -20,7 +20,6 @@ import java.util.ArrayList; import java.util.List; -import org.apache.cassandra.db.compaction.CompactionManagerMBean; import org.apache.cassandra.management.BaseCommand; import org.apache.cassandra.management.ServiceBridge; import picocli.CommandLine; @@ -34,24 +33,24 @@ @CommandLine.Command(name = "compact", description = "Force a (major) compaction on one or more tables or user-defined compaction on given SSTables") public class Compact extends BaseCommand { - @CommandLine.Parameters(paramLabel = "[ ...] or ...", arity = "0..*", + @CommandLine.Parameters(paramLabel = "[ ...] or ...", description = "The keyspace followed by one or many tables or list of SSTable data files when using --user-defined") - private List args = new ArrayList<>(); + public List args = new ArrayList<>(); @CommandLine.Option(names = { "-s", "--split-output"}, description = "Use -s to not create a single big file") - private boolean splitOutput = false; + public boolean splitOutput = false; @CommandLine.Option(names = { "--user-defined"}, description = "Use --user-defined to submit listed files for user-defined compaction") - private boolean userDefined = false; + public boolean userDefined = false; @CommandLine.Option(names = { "-st", "--start-token"}, description = "Use -st to specify a token at which the compaction range starts (inclusive)") - private String startToken = EMPTY; + public String startToken = EMPTY; @CommandLine.Option(names = { "-et", "--end-token"}, description = "Use -et to specify a token at which compaction range ends (inclusive)") - private String endToken = EMPTY; + public String endToken = EMPTY; @CommandLine.Option(names = { "--partition"}, description = "String representation of the partition key") - private String partitionKey = EMPTY; + public String partitionKey = EMPTY; @Override public void execute(ServiceBridge probe) diff --git a/src/java/org/apache/cassandra/management/api/ForceCompact.java b/src/java/org/apache/cassandra/management/api/ForceCompact.java index 146aa345e80f..25d18d0e597c 100644 --- a/src/java/org/apache/cassandra/management/api/ForceCompact.java +++ b/src/java/org/apache/cassandra/management/api/ForceCompact.java @@ -37,16 +37,16 @@ public class ForceCompact extends BaseCommand @CommandLine.Parameters(hidden = true, arity = "0", paramLabel = "[
]", description = { CommandUtils.CASSANDRA_BACKWARD_COMPATIBLE_MARKER, "The keyspace, table, and a list of partition keys ignoring the gc_grace_seconds" }) - private List args; + public List args; @CommandLine.Parameters(index = "0", arity = "1", description = "The keyspace name to compact") - private String keyspace; + public String keyspace; @CommandLine.Parameters(index = "1", arity = "1", description = "The table name to compact") - private String table; + public String table; @CommandLine.Parameters(index = "2..*", arity = "1", description = "The partition keys to compact") - private String[] keys; + public String[] keys; @Override public void execute(ServiceBridge probe) diff --git a/src/java/org/apache/cassandra/management/api/JmxConnectionMixin.java b/src/java/org/apache/cassandra/management/api/JmxConnectionMixin.java index 4439f9afaf98..dc0ee5223f6a 100644 --- a/src/java/org/apache/cassandra/management/api/JmxConnectionMixin.java +++ b/src/java/org/apache/cassandra/management/api/JmxConnectionMixin.java @@ -41,7 +41,6 @@ /** * Command options for NodeTool commands that are executed via JMX. */ -@CommandLine.Command(name = "connect", description = "Connect NodeTool to a Cassandra node using JMX") public class JmxConnectionMixin { public static final String MIXIN_KEY = "jmx"; @@ -82,7 +81,8 @@ public static int executionStrategy(CommandLine.ParseResult parseResult) CommandLine.Model.CommandSpec jmx = lastParent.mixins().get(MIXIN_KEY); Preconditions.checkNotNull(jmx, "No JmxConnect mixin found in the command hierarchy"); - ((BaseCommand) lastParent.userObject()).setBridge(((JmxConnectionMixin) jmx.userObject()).init(lastParent)); + if (lastParent.userObject() instanceof BaseCommand) + ((BaseCommand) lastParent.userObject()).setBridge(((JmxConnectionMixin) jmx.userObject()).init(lastParent)); return new CommandLine.RunLast().execute(parseResult); } diff --git a/src/java/org/apache/cassandra/management/api/TopLevelCommand.java b/src/java/org/apache/cassandra/management/api/TopLevelCommand.java index 4716b6e7bc59..125d32fcedba 100644 --- a/src/java/org/apache/cassandra/management/api/TopLevelCommand.java +++ b/src/java/org/apache/cassandra/management/api/TopLevelCommand.java @@ -21,6 +21,8 @@ import org.apache.cassandra.management.CassandraHelpCommand; import picocli.CommandLine; +import static org.apache.cassandra.management.CassandraHelpCommand.printTopCommandUsage; + @CommandLine.Command(name = "nodetool", description = "Manage your Cassandra cluster", subcommands = { CassandraHelpCommand.class, @@ -28,6 +30,15 @@ Assassinate.class, ForceCompact.class, Compact.class }) -public class TopLevelCommand +public class TopLevelCommand implements Runnable { + @CommandLine.Spec + public CommandLine.Model.CommandSpec spec; + + public void run() + { + printTopCommandUsage(spec.commandLine(), + spec.commandLine().getColorScheme(), + spec.commandLine().getOut()); + } } diff --git a/src/java/org/apache/cassandra/tools/NodeToolV2.java b/src/java/org/apache/cassandra/tools/NodeToolV2.java index 7f12758e7f73..eea7686f8c6e 100644 --- a/src/java/org/apache/cassandra/tools/NodeToolV2.java +++ b/src/java/org/apache/cassandra/tools/NodeToolV2.java @@ -134,27 +134,4 @@ else if (field.getType().equals(Output.class)) return (K) bean; } } - -// public static SortedMap getOwnershipByDcWithPort(NodeProbe probe, boolean resolveIp, -// Map tokenToEndpoint, -// Map ownerships) -// { -// SortedMap ownershipByDc = Maps.newTreeMap(); -// EndpointSnitchInfoMBean epSnitchInfo = probe.getEndpointSnitchInfoProxy(); -// try -// { -// for (Entry tokenAndEndPoint : tokenToEndpoint.entrySet()) -// { -// String dc = epSnitchInfo.getDatacenter(tokenAndEndPoint.getValue()); -// if (!ownershipByDc.containsKey(dc)) -// ownershipByDc.put(dc, new SetHostStatWithPort(resolveIp)); -// ownershipByDc.get(dc).add(tokenAndEndPoint.getKey(), tokenAndEndPoint.getValue(), ownerships); -// } -// } -// catch (UnknownHostException e) -// { -// throw new RuntimeException(e); -// } -// return ownershipByDc; -// } } diff --git a/test/unit/org/apache/cassandra/tools/nodetool/CQLToolRunnerTester.java b/test/unit/org/apache/cassandra/tools/nodetool/CQLToolRunnerTester.java index 53f5c3a665a9..88718be27770 100644 --- a/test/unit/org/apache/cassandra/tools/nodetool/CQLToolRunnerTester.java +++ b/test/unit/org/apache/cassandra/tools/nodetool/CQLToolRunnerTester.java @@ -22,6 +22,7 @@ import java.util.Map; import java.util.Set; +import org.junit.BeforeClass; import org.junit.runner.RunWith; import org.junit.runners.Parameterized; @@ -45,6 +46,13 @@ public static Set runners() return runnersMap.keySet(); } + @BeforeClass + public static void setupNetwork() throws Throwable + { + requireNetwork(); + startJMXServer(); + } + protected ToolRunner.ToolResult invokeNodetool(String... args) { return runnersMap.get(runner).execute(args); diff --git a/test/unit/org/apache/cassandra/tools/nodetool/CompactTest.java b/test/unit/org/apache/cassandra/tools/nodetool/CompactTest.java index 60b55bf226f8..0f844ca247ba 100644 --- a/test/unit/org/apache/cassandra/tools/nodetool/CompactTest.java +++ b/test/unit/org/apache/cassandra/tools/nodetool/CompactTest.java @@ -19,7 +19,6 @@ import java.util.Arrays; -import org.junit.BeforeClass; import org.junit.Test; import org.apache.cassandra.db.ColumnFamilyStore; @@ -29,13 +28,6 @@ public class CompactTest extends CQLToolRunnerTester { - @BeforeClass - public static void setup() throws Throwable - { - requireNetwork(); - startJMXServer(); - } - @Test public void keyPresent() throws Throwable { diff --git a/test/unit/org/apache/cassandra/tools/nodetool/ForceCompactionTest.java b/test/unit/org/apache/cassandra/tools/nodetool/ForceCompactionTest.java index 878433d702cb..528cd939dbcc 100644 --- a/test/unit/org/apache/cassandra/tools/nodetool/ForceCompactionTest.java +++ b/test/unit/org/apache/cassandra/tools/nodetool/ForceCompactionTest.java @@ -24,14 +24,10 @@ import java.util.Random; import java.util.concurrent.TimeUnit; -import org.apache.cassandra.Util; - import org.junit.Before; import org.junit.Test; -import static org.junit.Assert.*; - -import org.apache.cassandra.cql3.CQLTester; +import org.apache.cassandra.Util; import org.apache.cassandra.db.ColumnFamilyStore; import org.apache.cassandra.db.Keyspace; import org.apache.cassandra.db.rows.Cell; @@ -41,7 +37,11 @@ import org.apache.cassandra.io.sstable.ISSTableScanner; import org.apache.cassandra.io.sstable.format.SSTableReader; -public class ForceCompactionTest extends CQLTester +import static org.apache.commons.lang3.ArrayUtils.addAll; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +public class ForceCompactionTest extends CQLToolRunnerTester { private final static int NUM_PARTITIONS = 10; private final static int NUM_ROWS = 100; @@ -241,7 +241,8 @@ private void forceCompact(String[] partitionKeysIgnoreGcGrace) if (cfs != null) { cfs.forceMajorCompaction(); - cfs.forceCompactionKeysIgnoringGcGrace(partitionKeysIgnoreGcGrace); + invokeNodetool(addAll(new String[]{ "forcecompact", cfs.keyspace.getName(), cfs.getTableName() }, + partitionKeysIgnoreGcGrace)).assertOnCleanExit(); } } From 85428874a86ceec97c75feafd0524851950e09ac Mon Sep 17 00:00:00 2001 From: Maxim Muzafarov Date: Fri, 21 Jun 2024 13:13:41 +0200 Subject: [PATCH 07/22] wip --- .../apache/cassandra/config/CassandraRelevantProperties.java | 2 +- src/java/org/apache/cassandra/management/api/Compact.java | 2 +- .../apache/cassandra/management/api/JmxConnectionMixin.java | 3 ++- src/java/org/apache/cassandra/tools/NodeToolV2.java | 2 +- 4 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/java/org/apache/cassandra/config/CassandraRelevantProperties.java b/src/java/org/apache/cassandra/config/CassandraRelevantProperties.java index 565d21e6ff39..cfec7274f1f8 100644 --- a/src/java/org/apache/cassandra/config/CassandraRelevantProperties.java +++ b/src/java/org/apache/cassandra/config/CassandraRelevantProperties.java @@ -77,7 +77,7 @@ public enum CassandraRelevantProperties CACHEABLE_MUTATION_SIZE_LIMIT("cassandra.cacheable_mutation_size_limit_bytes", convertToString(1_000_000)), CASSANDRA_ALLOW_SIMPLE_STRATEGY("cassandra.allow_simplestrategy"), CASSANDRA_AVAILABLE_PROCESSORS("cassandra.available_processors"), - CASSANDRA_CLI_DEFAULT_LAYOUT("cassandra.cli.picocli.layout", "false"), + CASSANDRA_CLI_PICOCLI_LAYOUT("cassandra.cli.picocli.layout", "false"), /** The classpath storage configuration file. */ CASSANDRA_CONFIG("cassandra.config", "cassandra.yaml"), /** diff --git a/src/java/org/apache/cassandra/management/api/Compact.java b/src/java/org/apache/cassandra/management/api/Compact.java index e74b81141c9f..c50ed999d78c 100644 --- a/src/java/org/apache/cassandra/management/api/Compact.java +++ b/src/java/org/apache/cassandra/management/api/Compact.java @@ -33,7 +33,7 @@ @CommandLine.Command(name = "compact", description = "Force a (major) compaction on one or more tables or user-defined compaction on given SSTables") public class Compact extends BaseCommand { - @CommandLine.Parameters(paramLabel = "[ ...] or ...", + @CommandLine.Parameters(paramLabel = " ...] or [", description = "The keyspace followed by one or many tables or list of SSTable data files when using --user-defined") public List args = new ArrayList<>(); diff --git a/src/java/org/apache/cassandra/management/api/JmxConnectionMixin.java b/src/java/org/apache/cassandra/management/api/JmxConnectionMixin.java index dc0ee5223f6a..16c59f895179 100644 --- a/src/java/org/apache/cassandra/management/api/JmxConnectionMixin.java +++ b/src/java/org/apache/cassandra/management/api/JmxConnectionMixin.java @@ -79,7 +79,8 @@ public static int executionStrategy(CommandLine.ParseResult parseResult) int start = indexOfLastSubcommandWithSameParent(parsedCommands); CommandLine.Model.CommandSpec lastParent = parsedCommands.get(start).getCommandSpec(); CommandLine.Model.CommandSpec jmx = lastParent.mixins().get(MIXIN_KEY); - Preconditions.checkNotNull(jmx, "No JmxConnect mixin found in the command hierarchy"); + if (jmx == null) + throw new CommandLine.InitializationException("No JmxConnect mixin found in the command hierarchy"); if (lastParent.userObject() instanceof BaseCommand) ((BaseCommand) lastParent.userObject()).setBridge(((JmxConnectionMixin) jmx.userObject()).init(lastParent)); diff --git a/src/java/org/apache/cassandra/tools/NodeToolV2.java b/src/java/org/apache/cassandra/tools/NodeToolV2.java index eea7686f8c6e..a1fa5691b7af 100644 --- a/src/java/org/apache/cassandra/tools/NodeToolV2.java +++ b/src/java/org/apache/cassandra/tools/NodeToolV2.java @@ -96,7 +96,7 @@ public int execute(String... args) private static void configureCliLayout(CommandLine commandLine) { - if (CassandraRelevantProperties.CASSANDRA_CLI_DEFAULT_LAYOUT.getBoolean()) + if (CassandraRelevantProperties.CASSANDRA_CLI_PICOCLI_LAYOUT.getBoolean()) return; commandLine.setHelpFactory(CassandraHelpLayout::new) From 63101e4c81bcd3318f59d63c8974cb76c3d8d213 Mon Sep 17 00:00:00 2001 From: Maxim Muzafarov Date: Wed, 26 Jun 2024 18:45:37 +0200 Subject: [PATCH 08/22] improve test coverage --- .../management/CassandraHelpLayout.java | 41 ++--- .../management/api/JmxConnectionMixin.java | 18 +- .../org/apache/cassandra/tools/NodeTool.java | 94 +++++++++- .../apache/cassandra/tools/NodeToolV2.java | 147 ++++++++++++---- .../cassandra/tools/NodeToolSynopsisTest.java | 162 ------------------ .../apache/cassandra/tools/ToolRunner.java | 52 ++++-- .../tools/nodetool/CQLToolRunnerTester.java | 34 +++- .../tools/nodetool/NodeToolMBeanTest.java | 133 ++++++++++++++ .../tools/nodetool/NodeToolSynopsisTest.java | 110 ++++++++++++ 9 files changed, 539 insertions(+), 252 deletions(-) delete mode 100644 test/unit/org/apache/cassandra/tools/NodeToolSynopsisTest.java create mode 100644 test/unit/org/apache/cassandra/tools/nodetool/NodeToolMBeanTest.java create mode 100644 test/unit/org/apache/cassandra/tools/nodetool/NodeToolSynopsisTest.java diff --git a/src/java/org/apache/cassandra/management/CassandraHelpLayout.java b/src/java/org/apache/cassandra/management/CassandraHelpLayout.java index c965f7826d57..85f74249a021 100644 --- a/src/java/org/apache/cassandra/management/CassandraHelpLayout.java +++ b/src/java/org/apache/cassandra/management/CassandraHelpLayout.java @@ -60,9 +60,9 @@ public class CassandraHelpLayout extends CommandLine.Help private static final String DESCRIPTION_HEADING = "NAME%n"; private static final String SYNOPSIS_HEADING = "SYNOPSIS%n"; private static final String OPTIONS_HEADING = "OPTIONS%n"; - private static final int COLUMN_INDENT = 8; private static final int DESCRIPTION_INDENT = 4; - private static final int SUBCOMMANDS_INDENT = 4; + public static final int COLUMN_INDENT = 8; + public static final int SUBCOMMANDS_INDENT = 4; private static final CommandLine.Model.OptionSpec CASSANDRA_END_OF_OPTIONS_OPTION = CommandLine.Model.OptionSpec.builder("--") .description("This option can be used to separate command-line options from the " + @@ -70,9 +70,10 @@ public class CassandraHelpLayout extends CommandLine.Help "command-line options") .arity("0") .build(); - private static final String TOP_LEVEL_SYNOPSIS_LIST_PREFIX = "usage:"; - private static final String TOP_LEVEL_COMMAND_HEADING = "The most commonly used nodetool commands are:%n"; - private static final String SYNOPSIS_SUBCOMMANDS_LABEL = " []"; + public static final String TOP_LEVEL_SYNOPSIS_LIST_PREFIX = "usage:"; + public static final String TOP_LEVEL_COMMAND_HEADING = "The most commonly used nodetool commands are:"; + public static final String USAGE_HELP_FOOTER = "See 'nodetool help ' for more information on a specific command."; + public static final String SYNOPSIS_SUBCOMMANDS_LABEL = " []"; public CassandraHelpLayout(CommandLine.Model.CommandSpec spec, ColorScheme scheme) { @@ -340,29 +341,29 @@ public String commandList(Map subcommands) return table.toString(); } - @Override - public String footerHeading(Object... params) - { - return createHeading("%n", params); - } +// @Override +// public String footerHeading(Object... params) +// { +// return createHeading("%n", params); +// } @Override public String footer(Object... params) { - - String[] footer = isEmpty( - commandSpec().usageMessage().footer()) ? new String[]{ "See 'nodetool help ' for more information on a specific command." } : + String[] footer = isEmpty(commandSpec().usageMessage().footer()) ? new String[]{ USAGE_HELP_FOOTER } : commandSpec().usageMessage().footer(); - return join(ansi(), - commandSpec().usageMessage().width(), - commandSpec().usageMessage().adjustLineBreaksForWideCJKCharacters(), - footer, - new StringBuilder(), - params).toString(); + StringBuilder sb = new StringBuilder(); + TextTable table = TextTable.forColumnWidths(ansi(), commandSpec().usageMessage().width()); + table.setAdjustLineBreaksForWideCJKCharacters(commandSpec().usageMessage().adjustLineBreaksForWideCJKCharacters()); + table.indentWrappedLines = 0; + for (String summaryLine : footer) + table.addRowValues(String.format(summaryLine, params)); + table.toString(sb); + return table.toString(); } public String topLevelCommandListHeading(Object... params) { - return createHeading(TOP_LEVEL_COMMAND_HEADING, params); + return createHeading(TOP_LEVEL_COMMAND_HEADING + "%n", params); } public String topLevelSynopsis(Object... params) diff --git a/src/java/org/apache/cassandra/management/api/JmxConnectionMixin.java b/src/java/org/apache/cassandra/management/api/JmxConnectionMixin.java index 16c59f895179..1d1fd5903c0f 100644 --- a/src/java/org/apache/cassandra/management/api/JmxConnectionMixin.java +++ b/src/java/org/apache/cassandra/management/api/JmxConnectionMixin.java @@ -22,7 +22,6 @@ import java.util.List; import javax.inject.Inject; -import com.google.common.base.Preconditions; import com.google.common.base.Throwables; import org.apache.cassandra.management.BaseCommand; @@ -34,6 +33,7 @@ import static java.lang.Integer.parseInt; import static org.apache.cassandra.tools.NodeTool.NodeToolCmd.promptAndReadPassword; import static org.apache.cassandra.tools.NodeTool.NodeToolCmd.readUserPasswordFromFile; +import static org.apache.cassandra.tools.NodeToolV2.lastExecutableSubcommandWithSameParent; import static org.apache.commons.lang3.StringUtils.EMPTY; import static org.apache.commons.lang3.StringUtils.isEmpty; import static org.apache.commons.lang3.StringUtils.isNotEmpty; @@ -75,9 +75,7 @@ public class JmxConnectionMixin */ public static int executionStrategy(CommandLine.ParseResult parseResult) { - List parsedCommands = parseResult.asCommandLineList(); - int start = indexOfLastSubcommandWithSameParent(parsedCommands); - CommandLine.Model.CommandSpec lastParent = parsedCommands.get(start).getCommandSpec(); + CommandLine.Model.CommandSpec lastParent = lastExecutableSubcommandWithSameParent(parseResult.asCommandLineList()); CommandLine.Model.CommandSpec jmx = lastParent.mixins().get(MIXIN_KEY); if (jmx == null) throw new CommandLine.InitializationException("No JmxConnect mixin found in the command hierarchy"); @@ -87,18 +85,6 @@ public static int executionStrategy(CommandLine.ParseResult parseResult) return new CommandLine.RunLast().execute(parseResult); } - private static int indexOfLastSubcommandWithSameParent(List parsedCommands) - { - int start = parsedCommands.size() - 1; - for (int i = parsedCommands.size() - 2; i >= 0; i--) - { - if (parsedCommands.get(i).getParent() != parsedCommands.get(i + 1).getParent()) - break; - start = i; - } - return start; - } - /** * Initialize the JMX connection to the Cassandra node. * @param spec The command specification to be executed after the initialization. diff --git a/src/java/org/apache/cassandra/tools/NodeTool.java b/src/java/org/apache/cassandra/tools/NodeTool.java index fe72e2ed0bb7..6d01faab384a 100644 --- a/src/java/org/apache/cassandra/tools/NodeTool.java +++ b/src/java/org/apache/cassandra/tools/NodeTool.java @@ -31,11 +31,13 @@ import java.util.Map.Entry; import java.util.Scanner; import java.util.SortedMap; +import java.util.TreeMap; import java.util.function.Consumer; import javax.management.InstanceNotFoundException; import com.google.common.base.Joiner; import com.google.common.base.Throwables; +import com.google.common.collect.ImmutableList; import com.google.common.collect.Maps; import io.airlift.airline.Cli; @@ -49,18 +51,28 @@ import io.airlift.airline.ParseOptionConversionException; import io.airlift.airline.ParseOptionMissingException; import io.airlift.airline.ParseOptionMissingValueException; +import io.airlift.airline.UsageHelper; +import io.airlift.airline.UsagePrinter; +import io.airlift.airline.model.CommandGroupMetadata; +import io.airlift.airline.model.CommandMetadata; +import io.airlift.airline.model.GlobalMetadata; import org.apache.cassandra.io.util.File; import org.apache.cassandra.io.util.FileWriter; import org.apache.cassandra.locator.EndpointSnitchInfoMBean; +import org.apache.cassandra.management.CassandraHelpLayout; import org.apache.cassandra.management.ServiceBridge; import org.apache.cassandra.tools.nodetool.*; import org.apache.cassandra.utils.FBUtilities; +import picocli.CommandLine; +import static com.google.common.base.MoreObjects.firstNonNull; import static com.google.common.base.Throwables.getStackTraceAsString; +import static com.google.common.collect.ImmutableList.toImmutableList; import static com.google.common.collect.Iterables.toArray; import static com.google.common.collect.Lists.newArrayList; import static java.lang.Integer.parseInt; import static java.lang.String.format; +import static java.util.stream.Collectors.toList; import static org.apache.cassandra.io.util.File.WriteMode.APPEND; import static org.apache.cassandra.management.CommandUtils.ssProxy; import static org.apache.commons.lang3.ArrayUtils.EMPTY_STRING_ARRAY; @@ -75,6 +87,7 @@ public class NodeTool FBUtilities.preventIllegalAccessWarnings(); } + private static final int FALLBACK_NODETOOL_V1 = -100; private static final String HISTORYFILE = "nodetool.history"; private final INodeProbeFactory nodeProbeFactory; @@ -101,7 +114,7 @@ public int execute(String... args) Cleanup.class, ClearSnapshot.class, ClientStats.class, - Compact.class, +// Compact.class, CompactionHistory.class, CompactionStats.class, DataPaths.class, @@ -272,9 +285,28 @@ public int execute(String... args) int status = 0; try { - NodeToolCmdRunnable parse = parser.parse(args); - printHistory(args); - parse.run(nodeProbeFactory, output); + // Try to run the command with the new parser, if it у fails, fallback to the old parser. + int result = new NodeToolV2(nodeProbeFactory, output) + // Filter out the help command, and nodetool command, and fallback to the old help message. + .withCommandNameFilter(cmd -> cmd.equals("help") || cmd.equals("nodetool"), FALLBACK_NODETOOL_V1) + .withParameterExceptionHandler((ex, arg) -> { + if (ex instanceof CommandLine.UnmatchedArgumentException) + return FALLBACK_NODETOOL_V1; + badUse(ex); + return 1; + }) + .withExecutionExceptionHandler((ex, c, arg) -> { + err(ex); + return 2; + }).execute(args); + + if (result >= 0) + return result; + + // Fallback to the old parser, and run the command. + assert result == FALLBACK_NODETOOL_V1; + NodeToolCmdRunnable cmd = parser.parse(args); + cmd.run(nodeProbeFactory, output); } catch (IllegalArgumentException | IllegalStateException | ParseArgumentsMissingException | @@ -348,9 +380,61 @@ public static class CassHelp extends Help implements NodeToolCmdRunnable public void run(INodeProbeFactory nodeProbeFactory, Output output) { StringBuilder sb = new StringBuilder(); - help(global, command, sb); + NodeToolV2 cmd = new NodeToolV2(nodeProbeFactory, output); + if (command.isEmpty()) + { + usage(global, cmd.getCommandsDescription(), sb); + } + else + { + if (cmd.isCommandPresent(command.get(0))) + cmd.execute("help", command.get(0)); + else + help(global, command, sb); + } + output.out.println(sb); } + + public static void usage(GlobalMetadata global, Map extraCommands, StringBuilder sb) + { + UsagePrinter out = new UsagePrinter(sb, CassandraHelpLayout.DEFAULT_USAGE_HELP_WIDTH); + List commandArguments = global.getOptions().stream() + .filter(option -> !option.isHidden()) + .map(UsageHelper::toUsage) + .collect(toImmutableList()); + + out.newPrinterWithHangingIndent(CassandraHelpLayout.COLUMN_INDENT) + .append(CassandraHelpLayout.TOP_LEVEL_SYNOPSIS_LIST_PREFIX) + .append(global.getName()) + .appendWords(commandArguments) + .append(CassandraHelpLayout.SYNOPSIS_SUBCOMMANDS_LABEL) + .newline() + .newline(); + + Map commands = getCommandsDescription(global); + // Remove the help command from the list of extra commands if exists, as it's not applicable for backward compatibility. + extraCommands.remove("help"); + commands.putAll(extraCommands); + + out.append(CassandraHelpLayout.TOP_LEVEL_COMMAND_HEADING).newline(); + out.newIndentedPrinter(CassandraHelpLayout.SUBCOMMANDS_INDENT) + .appendTable(commands.entrySet().stream() + .map(entry -> ImmutableList.of(entry.getKey(), firstNonNull(entry.getValue(), ""))) + .collect(toList())); + out.newline(); + out.append(CassandraHelpLayout.USAGE_HELP_FOOTER); + } + + private static Map getCommandsDescription(GlobalMetadata global) + { + Map commands = new TreeMap<>(); + for (CommandMetadata meta : global.getDefaultGroupCommands()) + commands.put(meta.getName(), meta.getDescription()); + for (CommandGroupMetadata grp : global.getCommandGroups()) + commands.put(grp.getName(), grp.getDescription()); + return commands; + } } public interface NodeToolCmdRunnable diff --git a/src/java/org/apache/cassandra/tools/NodeToolV2.java b/src/java/org/apache/cassandra/tools/NodeToolV2.java index a1fa5691b7af..adc1f2052382 100644 --- a/src/java/org/apache/cassandra/tools/NodeToolV2.java +++ b/src/java/org/apache/cassandra/tools/NodeToolV2.java @@ -19,6 +19,10 @@ import java.io.PrintWriter; import java.lang.reflect.Field; +import java.util.List; +import java.util.Map; +import java.util.TreeMap; +import java.util.function.Predicate; import javax.inject.Inject; import com.google.common.base.Throwables; @@ -43,6 +47,17 @@ public class NodeToolV2 private final INodeProbeFactory nodeProbeFactory; private final Output output; + private CommandLine.IParameterExceptionHandler parameterExceptionHandler = (ex, arg) -> { + badUse(ex.getCommandLine().getOut()::println, Throwables.getRootCause(ex)); + return 1; + }; + + private CommandLine.IExecutionExceptionHandler executionExceptionHandler = (ex, cmdLine, parseResult) -> { + err(cmdLine.getErr()::println, Throwables.getRootCause(ex)); + return 2; + }; + + private CommandLine.IExecutionStrategy strategy; public static void main(String... args) { @@ -62,38 +77,101 @@ public NodeToolV2(INodeProbeFactory nodeProbeFactory, Output output) */ public int execute(String... args) { - CommandLine.IFactory factory; - CommandLine commandLine = new CommandLine(new TopLevelCommand(), factory = new CassandraCliFactory(nodeProbeFactory, output)); - - configureCliLayout(commandLine); - commandLine.setOut(new PrintWriter(output.out, true)) - .setErr(new PrintWriter(output.err, true)) - .setExecutionExceptionHandler((ex, cmdLine, parseResult) -> { - err(cmdLine.getErr()::println, Throwables.getRootCause(ex)); - return 2; - }) - .setParameterExceptionHandler((ex, arg) -> { - badUse(commandLine.getOut()::println, Throwables.getRootCause(ex)); - return 1; - }); + return execute(createCommandLine(new CassandraCliFactory(nodeProbeFactory, output)), args); + } + protected int execute(CommandLine commandLine, String... args) + { try { - JmxConnectionMixin mixin = factory.create(JmxConnectionMixin.class); - commandLine.setExecutionStrategy(JmxConnectionMixin::executionStrategy); - commandLine.addMixin(JmxConnectionMixin.MIXIN_KEY, mixin); - commandLine.getSubcommands().values().forEach(sub -> sub.addMixin(JmxConnectionMixin.MIXIN_KEY, mixin)); + configureCliLayout(commandLine); + commandLine.setExecutionStrategy(strategy == null ? JmxConnectionMixin::executionStrategy : strategy) + .setExecutionExceptionHandler(executionExceptionHandler) + .setParameterExceptionHandler(parameterExceptionHandler); printHistory(args); return commandLine.execute(args); } catch (Exception e) { - err(commandLine.getErr()::println, e); + err(output.err::println, e); return 2; } } + /** + * Filter the command by its name and return the given exit code when the filter is matched. + * @param commandPredicate the predicate to filter the command name. + * @param exitCodeWhenMatched the exit code to return when the filter is matched. + * @return this instance. + */ + public NodeToolV2 withCommandNameFilter(Predicate commandPredicate, int exitCodeWhenMatched) + { + strategy = parsed -> { + CommandLine.Model.CommandSpec spec = lastExecutableSubcommandWithSameParent(parsed.asCommandLineList()); + if (commandPredicate.test(spec.name())) + return exitCodeWhenMatched; + return JmxConnectionMixin.executionStrategy(parsed); + }; + return this; + } + + public NodeToolV2 withParameterExceptionHandler(CommandLine.IParameterExceptionHandler handler) + { + parameterExceptionHandler = handler; + return this; + } + + public NodeToolV2 withExecutionExceptionHandler(CommandLine.IExecutionExceptionHandler handler) + { + executionExceptionHandler = handler; + return this; + } + + public boolean isCommandPresent(String commandName) + { + CommandLine commandLine = createCommandLine(new CassandraCliFactory(nodeProbeFactory, output)); + return commandLine.getSubcommands().values().stream() + .anyMatch(sub -> sub.getCommandName().equals(commandName)); + } + + public Map getCommandsDescription() + { + Map commands = new TreeMap<>(); + CommandLine commandLine = createCommandLine(new CassandraCliFactory(nodeProbeFactory, output)); + commandLine.getSubcommands() + .values() + .forEach(sub -> commands.put(sub.getCommandName(), + CommandLine.Help.join(commandLine.getColorScheme().ansi(), 1000, + sub.getCommandSpec().usageMessage().adjustLineBreaksForWideCJKCharacters(), + sub.getCommandSpec().usageMessage().description(), new StringBuilder()).toString())); + // Remove the help command from the list of commands, as it's not applicable for backward compatibility. + return commands; + } + + public static CommandLine.Model.CommandSpec lastExecutableSubcommandWithSameParent(List parsedCommands) + { + int start = parsedCommands.size() - 1; + for (int i = parsedCommands.size() - 2; i >= 0; i--) + { + if (parsedCommands.get(i).getParent() != parsedCommands.get(i + 1).getParent()) + break; + start = i; + } + return parsedCommands.get(start).getCommandSpec(); + } + + private static CommandLine createCommandLine(CassandraCliFactory factory) + { + CommandLine commandLine = new CommandLine(new TopLevelCommand(), factory); + JmxConnectionMixin mixin = factory.create(JmxConnectionMixin.class); + commandLine.addMixin(JmxConnectionMixin.MIXIN_KEY, mixin); + commandLine.getSubcommands().values().forEach(sub -> sub.addMixin(JmxConnectionMixin.MIXIN_KEY, mixin)); + commandLine.setOut(new PrintWriter(factory.output.out, true)); + commandLine.setErr(new PrintWriter(factory.output.err, true)); + return commandLine; + } + private static void configureCliLayout(CommandLine commandLine) { if (CassandraRelevantProperties.CASSANDRA_CLI_PICOCLI_LAYOUT.getBoolean()) @@ -117,21 +195,28 @@ public CassandraCliFactory(INodeProbeFactory nodeProbeFactory, Output output) this.output = output; } - public K create(Class cls) throws Exception + public K create(Class cls) { - Object bean = this.fallback.create(cls); - Field[] fields = bean.getClass().getDeclaredFields(); - for (Field field : fields) + try + { + Object bean = this.fallback.create(cls); + Field[] fields = bean.getClass().getDeclaredFields(); + for (Field field : fields) + { + if (!field.isAnnotationPresent(Inject.class)) + continue; + field.setAccessible(true); + if (field.getType().equals(INodeProbeFactory.class)) + field.set(bean, nodeProbeFactory); + else if (field.getType().equals(Output.class)) + field.set(bean, output); + } + return (K) bean; + } + catch (Exception e) { - if (!field.isAnnotationPresent(Inject.class)) - continue; - field.setAccessible(true); - if (field.getType().equals(INodeProbeFactory.class)) - field.set(bean, nodeProbeFactory); - else if (field.getType().equals(Output.class)) - field.set(bean, output); + throw new CommandLine.InitializationException("Failed to create instance of " + cls, e); } - return (K) bean; } } } diff --git a/test/unit/org/apache/cassandra/tools/NodeToolSynopsisTest.java b/test/unit/org/apache/cassandra/tools/NodeToolSynopsisTest.java deleted file mode 100644 index 884a77cdc966..000000000000 --- a/test/unit/org/apache/cassandra/tools/NodeToolSynopsisTest.java +++ /dev/null @@ -1,162 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.apache.cassandra.tools; - -import java.io.OutputStream; -import java.io.PrintStream; -import java.util.ArrayList; -import java.util.List; -import java.util.function.BiFunction; - -import org.apache.commons.lang3.StringUtils; -import org.junit.Test; - -import com.github.difflib.DiffUtils; -import com.github.difflib.patch.AbstractDelta; -import com.github.difflib.patch.Patch; -import org.apache.cassandra.cql3.CQLTester; - -import static org.junit.Assert.assertTrue; - -public class NodeToolSynopsisTest -{ - @Test - public void cliHelp() - { - List outNodeTool = invokeNodetool(NodeTool::new, "help"); - List outNodeToolV2 = invokeNodetool(NodeToolV2::new, "help"); - - String diff = computeDiff(outNodeTool, outNodeToolV2); - assertTrue(concatNodetoolOutput(outNodeTool) + - '\n' + "-----------------------------------------------------" + - '\n' + concatNodetoolOutput(outNodeToolV2) + - '\n' + "Difference for \"" + "help" + "\":" + diff, - StringUtils.isBlank(diff)); - } - - @Test - public void dummy() - { - List outNodeToolV2 = invokeNodetool(NodeToolV2::new, "help"); - System.out.println(concatNodetoolOutput(outNodeToolV2)); - } - - @Test - public void compareNodeToolHelpOutput() throws Exception - { -// runCommandHelpOutputComparison("abortbootstrap"); - runCommandHelpOutputComparison("assassinate"); - runCommandHelpOutputComparison("forcecompact"); - runCommandHelpOutputComparison("compact"); - } - - public void runCommandHelpOutputComparison(String commandName) - { - List outNodeTool = invokeNodetool(NodeTool::new, "help", commandName); - List outNodeToolV2 = invokeNodetool(NodeToolV2::new, "help", commandName); - String diff = computeDiff(outNodeTool, outNodeToolV2); - assertTrue(concatNodetoolOutput(outNodeTool) + - '\n' + "-----------------------------------------------------" + - '\n' + concatNodetoolOutput(outNodeToolV2) + - '\n' + " difference for \"" + commandName + "\":" + diff, - StringUtils.isBlank(diff)); - } - - private static String concatNodetoolOutput(List output) - { - return '\n' + String.join("\n", output); - } - - public static List invokeNodetool(BiFunction factory, String... commands) - { - ListOutputStream output = new ListOutputStream(); - List args = CQLTester.buildNodetoolArgs(List.of(commands)); - args.remove("bin/nodetool"); - try - { - Object runner = factory.apply(new NodeProbeFactory(), new Output(new PrintStream(output), new PrintStream(output))); - Object result = runner.getClass().getMethod("execute", String[].class) - .invoke(runner, new Object[] { args.toArray(new String[0]) }); - if (result instanceof Integer && (Integer) result != 0) - throw new RuntimeException("Command failed with exit code " + result); - return output.getOutputLines(); - } - catch (Exception e) - { - throw new RuntimeException(e); - } - } - - public static String computeDiff(List original, List revised) { - Patch patch = DiffUtils.diff(original, revised); - List diffLines = new ArrayList<>(); - - for (AbstractDelta delta : patch.getDeltas()) { - for (String line : delta.getSource().getLines()) { - diffLines.add(delta.getType().toString().toLowerCase() + " source: " + line); - } - for (String line : delta.getTarget().getLines()) { - diffLines.add(delta.getType().toString().toLowerCase() + " target: " + line); - } - } - - return '\n' + String.join("\n", diffLines); - } - - public static class ListOutputStream extends OutputStream - { - private final List outputLines = new ArrayList<>(); - private final StringBuilder buffer = new StringBuilder(); - - @Override - public void write(int b) - { - char c = (char) b; - if (c == '\n') - { - // Add the buffer to the list if it's a new line - outputLines.add(buffer.toString()); - buffer.setLength(0); // Clear the buffer - } - else - buffer.append(c); - } - - public void flush() - { - if (buffer.length() > 0) - { - outputLines.add(buffer.toString()); - buffer.setLength(0); - } - } - - public List getOutputLines() - { - flush(); - return new ArrayList<>(outputLines); - } - - public String getOutput() - { - flush(); - return String.join(System.lineSeparator(), outputLines); - } - } -} diff --git a/test/unit/org/apache/cassandra/tools/ToolRunner.java b/test/unit/org/apache/cassandra/tools/ToolRunner.java index c8dac8fe6ca1..4b4ddf92ab6f 100644 --- a/test/unit/org/apache/cassandra/tools/ToolRunner.java +++ b/test/unit/org/apache/cassandra/tools/ToolRunner.java @@ -39,7 +39,6 @@ import com.google.common.base.Throwables; import com.google.common.collect.ImmutableList; - import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -49,6 +48,7 @@ import org.apache.cassandra.utils.Pair; import org.assertj.core.util.Lists; +import static com.github.jknack.handlebars.internal.lang3.ArrayUtils.isEmpty; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; @@ -316,21 +316,11 @@ public NodeToolResult get() res.right.getException()); } - public static ToolResult invokeNodetoolInJvmV2(String... commands) - { - return ToolRunner.invokeNodetoolInJvm(NodeToolV2::new, commands); - } - - public static ToolResult invokeNodetoolInJvmV1(String... commands) - { - return ToolRunner.invokeNodetoolInJvm(NodeTool::new, commands); - } - public static ToolRunner.ToolResult invokeNodetoolInJvm(BiFunction factory, String... commands) { - NodeToolSynopsisTest.ListOutputStream out = new NodeToolSynopsisTest.ListOutputStream(); - NodeToolSynopsisTest.ListOutputStream err = new NodeToolSynopsisTest.ListOutputStream(); - List args = CQLTester.buildNodetoolArgs(List.of(commands)); + ListOutputStream out = new ListOutputStream(); + ListOutputStream err = new ListOutputStream(); + List args = CQLTester.buildNodetoolArgs(isEmpty(commands) ? new ArrayList<>() : List.of(commands)); args.remove("bin/nodetool"); try { @@ -781,4 +771,38 @@ public ToolResult invoke() return ToolRunner.invoke(env, stdin, args); } } + + private static class ListOutputStream extends OutputStream + { + private final List outputLines = new ArrayList<>(); + private final StringBuilder buffer = new StringBuilder(); + + @Override + public void write(int b) + { + char c = (char) b; + if (c == '\n') + { + // Add the buffer to the list if it's a new line + outputLines.add(buffer.toString()); + buffer.setLength(0); // Clear the buffer + } + else + buffer.append(c); + } + + public void flush() + { + if (buffer.length() > 0) + { + outputLines.add(buffer.toString()); + buffer.setLength(0); + } + } + + public String getOutput() + { + return String.join("\n", outputLines); + } + } } diff --git a/test/unit/org/apache/cassandra/tools/nodetool/CQLToolRunnerTester.java b/test/unit/org/apache/cassandra/tools/nodetool/CQLToolRunnerTester.java index 88718be27770..95e11969f8e5 100644 --- a/test/unit/org/apache/cassandra/tools/nodetool/CQLToolRunnerTester.java +++ b/test/unit/org/apache/cassandra/tools/nodetool/CQLToolRunnerTester.java @@ -18,6 +18,7 @@ package org.apache.cassandra.tools.nodetool; +import java.util.Arrays; import java.util.List; import java.util.Map; import java.util.Set; @@ -27,6 +28,8 @@ import org.junit.runners.Parameterized; import org.apache.cassandra.cql3.CQLTester; +import org.apache.cassandra.tools.NodeTool; +import org.apache.cassandra.tools.NodeToolV2; import org.apache.cassandra.tools.ToolRunner; @RunWith(Parameterized.class) @@ -34,8 +37,8 @@ public abstract class CQLToolRunnerTester extends CQLTester { public static final Map runnersMap = Map.of( "invokeNodetool", ToolRunner::invokeNodetool, - "invokeNodetoolInJvmV1", ToolRunner::invokeNodetoolInJvmV1, - "invokeNodetoolInJvmV2", ToolRunner::invokeNodetoolInJvmV2); + "invokeNodetoolInJvmV1", CQLToolRunnerTester::invokeNodetoolInJvmV1, + "invokeNodetoolInJvmV2", CQLToolRunnerTester::invokeNodetoolInJvmV2); @Parameterized.Parameter public String runner; @@ -47,10 +50,18 @@ public static Set runners() } @BeforeClass - public static void setupNetwork() throws Throwable + public static void setUpClass() { + CQLTester.setUpClass(); requireNetwork(); - startJMXServer(); + try + { + startJMXServer(); + } + catch (Exception e) + { + throw new RuntimeException(e); + } } protected ToolRunner.ToolResult invokeNodetool(String... args) @@ -58,6 +69,21 @@ protected ToolRunner.ToolResult invokeNodetool(String... args) return runnersMap.get(runner).execute(args); } + public static ToolRunner.ToolResult invokeNodetoolInJvmV2(String... commands) + { + return ToolRunner.invokeNodetoolInJvm(NodeToolV2::new, commands); + } + + public static ToolRunner.ToolResult invokeNodetoolInJvmV1(String... commands) + { + return ToolRunner.invokeNodetoolInJvm(NodeTool::new, commands); + } + + public static List sliceStdout(ToolRunner.ToolResult result) + { + return Arrays.asList(result.getStdout().split("\\R")); + } + public interface ToolHandler { ToolRunner.ToolResult execute(String... args); diff --git a/test/unit/org/apache/cassandra/tools/nodetool/NodeToolMBeanTest.java b/test/unit/org/apache/cassandra/tools/nodetool/NodeToolMBeanTest.java new file mode 100644 index 000000000000..a154e614082b --- /dev/null +++ b/test/unit/org/apache/cassandra/tools/nodetool/NodeToolMBeanTest.java @@ -0,0 +1,133 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.tools.nodetool; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import javax.management.MBeanServer; +import javax.management.ObjectName; +import javax.management.QueryExp; + +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.Test; + +import org.apache.cassandra.config.CassandraRelevantProperties; +import org.apache.cassandra.dht.Murmur3Partitioner; +import org.apache.cassandra.service.StorageServiceMBean; +import org.apache.cassandra.utils.MBeanWrapper; +import org.mockito.Mockito; + +import static org.apache.cassandra.config.CassandraRelevantProperties.MBEAN_REGISTRATION_CLASS; +import static org.mockito.Mockito.when; + +public class NodeToolMBeanTest extends CQLToolRunnerTester +{ + private static MBeanMockInstance mbeanMockInstance; + + @BeforeClass + public static void setUpClass() + { + CassandraRelevantProperties.DTEST_IS_IN_JVM_DTEST.setBoolean(true); + MBEAN_REGISTRATION_CLASS.setString(MBeanMockInstance.class.getName()); + mbeanMockInstance = (MBeanMockInstance) ((MBeanWrapper.DelegatingMbeanWrapper) MBeanWrapper.instance).getDelegate(); + CQLToolRunnerTester.setUpClass(); + } + + @AfterClass + public static void resetMBeanWrapper() + { + CassandraRelevantProperties.DTEST_IS_IN_JVM_DTEST.setBoolean(false); + } + + @Test + public void keyPresent() throws Throwable + { + long token = 42; + long key = Murmur3Partitioner.LongToken.keyForToken(token).getLong(); + String table = "table"; + StorageServiceMBean mock = getMock(mbeanMockInstance, StorageServiceMBean.class); + when(mock.getKeyspaces()).thenReturn(List.of(keyspace())); + when(mock.getNonSystemKeyspaces()).thenReturn(List.of(keyspace())); + + invokeNodetool("compact", "--partition", Long.toString(key), keyspace(), table).assertOnCleanExit(); + Mockito.verify(mock).forceKeyspaceCompactionForPartitionKey(keyspace(), Long.toString(key), table); + } + + private static T getMock(MBeanMockInstance mbeanMockInstance, Class clz) + { + return clz.cast(mbeanMockInstance.mocks.get(clz)); + } + + public static class MBeanMockInstance implements MBeanWrapper + { + private final Map mbeans = new HashMap<>(); + private final Map, Object> mocks = new HashMap<>(); + private final PlatformMBeanWrapper wrapper = new PlatformMBeanWrapper(); + + public MBeanMockInstance() + { + } + + @Override + public void registerMBean(Object obj, ObjectName mbeanName, OnException onException) + { + Class clz = obj.getClass(); + Object mock = obj; + if (mbeanName.toString().equals("org.apache.cassandra.db:type=StorageService")) + { + mbeans.put(mbeanName, mock = Mockito.mock(clz)); + mocks.put(StorageServiceMBean.class, mock); + } + + wrapper.registerMBean(mock, mbeanName, onException); + } + + @Override + public boolean isRegistered(ObjectName mbeanName, OnException onException) + { + if (mbeans.containsKey(mbeanName)) + return true; + return wrapper.isRegistered(mbeanName, onException); + } + + @Override + public void unregisterMBean(ObjectName mbeanName, OnException onException) + { + if (mbeans.containsKey(mbeanName)) + mbeans.remove(mbeanName); + else + wrapper.unregisterMBean(mbeanName, onException); + } + + @Override + public Set queryNames(ObjectName name, QueryExp query) + { + return wrapper.queryNames(name, query); + } + + @Override + public MBeanServer getMBeanServer() + { + return wrapper.getMBeanServer(); + } + } +} diff --git a/test/unit/org/apache/cassandra/tools/nodetool/NodeToolSynopsisTest.java b/test/unit/org/apache/cassandra/tools/nodetool/NodeToolSynopsisTest.java new file mode 100644 index 000000000000..e7a839cc9aac --- /dev/null +++ b/test/unit/org/apache/cassandra/tools/nodetool/NodeToolSynopsisTest.java @@ -0,0 +1,110 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.tools.nodetool; + +import java.util.ArrayList; +import java.util.List; + +import org.apache.commons.lang3.StringUtils; +import org.junit.Test; + +import com.github.difflib.DiffUtils; +import com.github.difflib.patch.AbstractDelta; +import com.github.difflib.patch.Patch; + +import static org.junit.Assert.assertTrue; + +public class NodeToolSynopsisTest extends CQLToolRunnerTester +{ + @Test + public void testCompareHelpCommand() + { + List outNodeTool = sliceStdout(invokeNodetoolInJvmV1("help")); + List outNodeToolV2 = sliceStdout(invokeNodetoolInJvmV2("help")); + + String diff = computeDiff(outNodeTool, outNodeToolV2); + assertTrue(printFormattedDiffsMessage(outNodeTool, outNodeToolV2, "help", diff), + StringUtils.isBlank(diff)); + } + + @Test + public void testBaseCommandOutput() + { + List outNodeToolV2 = sliceStdout(invokeNodetoolInJvmV1()); + System.out.println(printFormattedNodeToolOutput(outNodeToolV2)); + } + + @Test + public void testCompareCommandHelpOutputBetweenTools() + { + runCommandHelpOutputComparison("abortbootstrap"); + runCommandHelpOutputComparison("assassinate"); + runCommandHelpOutputComparison("forcecompact"); + runCommandHelpOutputComparison("compact"); + } + + public void runCommandHelpOutputComparison(String commandName) + { + List outNodeTool = sliceStdout(invokeNodetoolInJvmV1("help", commandName)); + List outNodeToolV2 = sliceStdout(invokeNodetoolInJvmV2("help", commandName)); + String diff = computeDiff(outNodeTool, outNodeToolV2); + assertTrue(printFormattedDiffsMessage(outNodeTool, outNodeToolV2, commandName, diff), + StringUtils.isBlank(diff)); + } + + private static String printFormattedDiffsMessage(List stdoutOrig, + List stdoutNew, + String commandName, + String diff) + { + return '\n' + "> NodeTool" + '\n' + + printFormattedNodeToolOutput(stdoutOrig) + + '\n' + "> NodeToolV2" + + '\n' + printFormattedNodeToolOutput(stdoutNew) + + '\n' + " difference for \"" + commandName + "\":" + diff; + } + + private static String printFormattedNodeToolOutput(List output) + { + StringBuilder sb = new StringBuilder(); + for(int i = 0; i < output.size(); i++) + { + sb.append(i).append(':').append(output.get(i)); + if(i < output.size() - 1) + sb.append('\n'); + } + return sb.toString(); + } + + public static String computeDiff(List original, List revised) { + Patch patch = DiffUtils.diff(original, revised); + List diffLines = new ArrayList<>(); + + for (AbstractDelta delta : patch.getDeltas()) { + for (String line : delta.getSource().getLines()) { + diffLines.add(delta.getType().toString().toLowerCase() + " source: " + line); + } + for (String line : delta.getTarget().getLines()) { + diffLines.add(delta.getType().toString().toLowerCase() + " target: " + line); + } + } + + return '\n' + String.join("\n", diffLines); + } +} From 4b29544c0c6a100df1df1c127b60b3e0ac3ed0d3 Mon Sep 17 00:00:00 2001 From: Maxim Muzafarov Date: Wed, 26 Jun 2024 20:28:54 +0200 Subject: [PATCH 09/22] add tests for missed commands --- .../management/api/JmxConnectionMixin.java | 1 - .../tools/nodetool/NodeToolMBeanTest.java | 121 +++++++++--------- 2 files changed, 61 insertions(+), 61 deletions(-) diff --git a/src/java/org/apache/cassandra/management/api/JmxConnectionMixin.java b/src/java/org/apache/cassandra/management/api/JmxConnectionMixin.java index 1d1fd5903c0f..e0a00a9d0c7d 100644 --- a/src/java/org/apache/cassandra/management/api/JmxConnectionMixin.java +++ b/src/java/org/apache/cassandra/management/api/JmxConnectionMixin.java @@ -19,7 +19,6 @@ package org.apache.cassandra.management.api; import java.io.IOException; -import java.util.List; import javax.inject.Inject; import com.google.common.base.Throwables; diff --git a/test/unit/org/apache/cassandra/tools/nodetool/NodeToolMBeanTest.java b/test/unit/org/apache/cassandra/tools/nodetool/NodeToolMBeanTest.java index a154e614082b..f2883216cdc4 100644 --- a/test/unit/org/apache/cassandra/tools/nodetool/NodeToolMBeanTest.java +++ b/test/unit/org/apache/cassandra/tools/nodetool/NodeToolMBeanTest.java @@ -21,113 +21,114 @@ import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.Set; -import javax.management.MBeanServer; -import javax.management.ObjectName; -import javax.management.QueryExp; +import javax.management.StandardMBean; -import org.junit.AfterClass; -import org.junit.BeforeClass; +import org.junit.After; +import org.junit.Before; import org.junit.Test; -import org.apache.cassandra.config.CassandraRelevantProperties; +import org.apache.cassandra.db.compaction.CompactionManagerMBean; import org.apache.cassandra.dht.Murmur3Partitioner; import org.apache.cassandra.service.StorageServiceMBean; import org.apache.cassandra.utils.MBeanWrapper; import org.mockito.Mockito; -import static org.apache.cassandra.config.CassandraRelevantProperties.MBEAN_REGISTRATION_CLASS; import static org.mockito.Mockito.when; public class NodeToolMBeanTest extends CQLToolRunnerTester { - private static MBeanMockInstance mbeanMockInstance; + public static final String STORAGE_SERVICE_MBEAN = "org.apache.cassandra.db:type=StorageService"; + public static final String COMPACTION_MANAGER_MBEAN = "org.apache.cassandra.db:type=CompactionManager"; + private static final MBeanWrapper mbeanServer = MBeanWrapper.instance; + private MBeanMockHodler mbeanMockHodler; - @BeforeClass - public static void setUpClass() + @Before + public void prepareMocks() { - CassandraRelevantProperties.DTEST_IS_IN_JVM_DTEST.setBoolean(true); - MBEAN_REGISTRATION_CLASS.setString(MBeanMockInstance.class.getName()); - mbeanMockInstance = (MBeanMockInstance) ((MBeanWrapper.DelegatingMbeanWrapper) MBeanWrapper.instance).getDelegate(); - CQLToolRunnerTester.setUpClass(); + mbeanMockHodler = new MBeanMockHodler(); + mbeanMockHodler.unregisterAll(mbeanServer); + mbeanMockHodler.registerAll(mbeanServer); } - @AfterClass - public static void resetMBeanWrapper() + @After + public void unregisterMocks() { - CassandraRelevantProperties.DTEST_IS_IN_JVM_DTEST.setBoolean(false); + mbeanMockHodler.unregisterAll(mbeanServer); } @Test - public void keyPresent() throws Throwable + public void testAssassinate() throws Throwable + { + String ip = "10.20.113.11"; + StorageServiceMBean mock = mbeanMockHodler.getMock(STORAGE_SERVICE_MBEAN); + when(mock.getNonSystemKeyspaces()).thenReturn(List.of(keyspace())); + invokeNodetool("assassinate", ip).assertOnCleanExit(); + Mockito.verify(mock).assassinateEndpoint(ip); + } + + @Test + public void testAbortBootstrap() throws Throwable + { + String nodeId = "1"; + String ip = "10.20.113.11"; + StorageServiceMBean mock = mbeanMockHodler.getMock(STORAGE_SERVICE_MBEAN); + invokeNodetool("abortbootstrap", "--node", nodeId).assertOnCleanExit(); + Mockito.verify(mock).abortBootstrap(nodeId, ""); + invokeNodetool("abortbootstrap", "--ip", ip).assertOnCleanExit(); + Mockito.verify(mock).abortBootstrap("", ip); + } + + @Test + public void testForceKeyspaceCompactionForPartitionKey() throws Throwable { long token = 42; long key = Murmur3Partitioner.LongToken.keyForToken(token).getLong(); String table = "table"; - StorageServiceMBean mock = getMock(mbeanMockInstance, StorageServiceMBean.class); + StorageServiceMBean mock = mbeanMockHodler.getMock(STORAGE_SERVICE_MBEAN); when(mock.getKeyspaces()).thenReturn(List.of(keyspace())); when(mock.getNonSystemKeyspaces()).thenReturn(List.of(keyspace())); - invokeNodetool("compact", "--partition", Long.toString(key), keyspace(), table).assertOnCleanExit(); Mockito.verify(mock).forceKeyspaceCompactionForPartitionKey(keyspace(), Long.toString(key), table); } - private static T getMock(MBeanMockInstance mbeanMockInstance, Class clz) + private static class MBeanMockHodler { - return clz.cast(mbeanMockInstance.mocks.get(clz)); - } - - public static class MBeanMockInstance implements MBeanWrapper - { - private final Map mbeans = new HashMap<>(); - private final Map, Object> mocks = new HashMap<>(); - private final PlatformMBeanWrapper wrapper = new PlatformMBeanWrapper(); + private static final Map> mbeans = Map.of( + STORAGE_SERVICE_MBEAN, StorageServiceMBean.class, + COMPACTION_MANAGER_MBEAN, CompactionManagerMBean.class); + private final Map mocks = new HashMap<>(); - public MBeanMockInstance() + MBeanMockHodler() { + mbeans.forEach((name, clz) -> mocks.put(name, newMock(clz))); } - @Override - public void registerMBean(Object obj, ObjectName mbeanName, OnException onException) + public static StandardMBean newMock(Class clz) { - Class clz = obj.getClass(); - Object mock = obj; - if (mbeanName.toString().equals("org.apache.cassandra.db:type=StorageService")) + try { - mbeans.put(mbeanName, mock = Mockito.mock(clz)); - mocks.put(StorageServiceMBean.class, mock); + return new StandardMBean(Mockito.mock(clz), clz); + } + catch (Exception e) + { + throw new RuntimeException(e); } - - wrapper.registerMBean(mock, mbeanName, onException); - } - - @Override - public boolean isRegistered(ObjectName mbeanName, OnException onException) - { - if (mbeans.containsKey(mbeanName)) - return true; - return wrapper.isRegistered(mbeanName, onException); } - @Override - public void unregisterMBean(ObjectName mbeanName, OnException onException) + @SuppressWarnings("unchecked") + public T getMock(String mBeanName) { - if (mbeans.containsKey(mbeanName)) - mbeans.remove(mbeanName); - else - wrapper.unregisterMBean(mbeanName, onException); + return (T) mocks.get(mBeanName).getImplementation(); } - @Override - public Set queryNames(ObjectName name, QueryExp query) + public void registerAll(MBeanWrapper mbeanMockInstance) { - return wrapper.queryNames(name, query); + mocks.forEach((name, mock) -> mbeanMockInstance.registerMBean(mock, name)); } - @Override - public MBeanServer getMBeanServer() + public void unregisterAll(MBeanWrapper mbeanMockInstance) { - return wrapper.getMBeanServer(); + mocks.keySet().forEach(name -> mbeanMockInstance.unregisterMBean(name, MBeanWrapper.OnException.IGNORE)); } } } From 3a91f470e7fb3ce00aaa6df27a679ad2b5197077 Mon Sep 17 00:00:00 2001 From: Maxim Muzafarov Date: Wed, 26 Jun 2024 20:37:30 +0200 Subject: [PATCH 10/22] delete commands that have been moved under new api --- .../org/apache/cassandra/tools/NodeTool.java | 6 +- .../tools/nodetool/AbortBootstrap.java | 48 -------- .../cassandra/tools/nodetool/Assassinate.java | 47 -------- .../cassandra/tools/nodetool/Compact.java | 106 ------------------ .../tools/nodetool/ForceCompact.java | 58 ---------- ...st.java => NodeToolCommandsMBeanTest.java} | 2 +- ...Test.java => NodeToolHelpMessageTest.java} | 2 +- 7 files changed, 3 insertions(+), 266 deletions(-) delete mode 100644 src/java/org/apache/cassandra/tools/nodetool/AbortBootstrap.java delete mode 100644 src/java/org/apache/cassandra/tools/nodetool/Assassinate.java delete mode 100644 src/java/org/apache/cassandra/tools/nodetool/Compact.java delete mode 100644 src/java/org/apache/cassandra/tools/nodetool/ForceCompact.java rename test/unit/org/apache/cassandra/tools/nodetool/{NodeToolMBeanTest.java => NodeToolCommandsMBeanTest.java} (98%) rename test/unit/org/apache/cassandra/tools/nodetool/{NodeToolSynopsisTest.java => NodeToolHelpMessageTest.java} (98%) diff --git a/src/java/org/apache/cassandra/tools/NodeTool.java b/src/java/org/apache/cassandra/tools/NodeTool.java index 6d01faab384a..c0cb4791e753 100644 --- a/src/java/org/apache/cassandra/tools/NodeTool.java +++ b/src/java/org/apache/cassandra/tools/NodeTool.java @@ -107,14 +107,11 @@ public NodeTool(INodeProbeFactory nodeProbeFactory, Output output) public int execute(String... args) { List> commands = newArrayList( - AbortBootstrap.class, - Assassinate.class, CassHelp.class, CIDRFilteringStats.class, Cleanup.class, ClearSnapshot.class, ClientStats.class, -// Compact.class, CompactionHistory.class, CompactionStats.class, DataPaths.class, @@ -246,8 +243,7 @@ public int execute(String... args) UpgradeSSTable.class, Verify.class, Version.class, - ViewBuildStatus.class, - ForceCompact.class + ViewBuildStatus.class ); Cli.CliBuilder builder = Cli.builder("nodetool"); diff --git a/src/java/org/apache/cassandra/tools/nodetool/AbortBootstrap.java b/src/java/org/apache/cassandra/tools/nodetool/AbortBootstrap.java deleted file mode 100644 index 4c180ad81cff..000000000000 --- a/src/java/org/apache/cassandra/tools/nodetool/AbortBootstrap.java +++ /dev/null @@ -1,48 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.cassandra.tools.nodetool; - - -import io.airlift.airline.Command; -import io.airlift.airline.Option; -import org.apache.cassandra.tools.NodeProbe; -import org.apache.cassandra.tools.NodeTool.NodeToolCmd; - -import static org.apache.commons.lang3.StringUtils.EMPTY; -import static org.apache.commons.lang3.StringUtils.isEmpty; - -@Command(name = "abortbootstrap", description = "Abort a failed bootstrap") -public class AbortBootstrap extends NodeToolCmd -{ - @Option(title = "node id", name = "--node", description = "Node ID of the node that failed bootstrap", required = false) - private String nodeId = EMPTY; - - @Option(title = "ip", name = "--ip", description = "IP of the node that failed bootstrap", required = false) - private String endpoint = EMPTY; - - - @Override - public void execute(NodeProbe probe) - { - if (isEmpty(nodeId) && isEmpty(endpoint)) - throw new IllegalArgumentException("Either --node or --ip needs to be set"); - if (!isEmpty(nodeId) && !isEmpty(endpoint)) - throw new IllegalArgumentException("Only one of --node or --ip need to be set"); - probe.abortBootstrap(nodeId, endpoint); - } -} diff --git a/src/java/org/apache/cassandra/tools/nodetool/Assassinate.java b/src/java/org/apache/cassandra/tools/nodetool/Assassinate.java deleted file mode 100644 index 2639ec81b32f..000000000000 --- a/src/java/org/apache/cassandra/tools/nodetool/Assassinate.java +++ /dev/null @@ -1,47 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.cassandra.tools.nodetool; - -import static org.apache.commons.lang3.StringUtils.EMPTY; -import io.airlift.airline.Arguments; -import io.airlift.airline.Command; - -import java.net.UnknownHostException; - -import org.apache.cassandra.tools.NodeProbe; -import org.apache.cassandra.tools.NodeTool.NodeToolCmd; - -@Command(name = "assassinate", description = "Forcefully remove a dead node without re-replicating any data. Use as a last resort if you cannot removenode") -public class Assassinate extends NodeToolCmd -{ - @Arguments(title = "ip address", usage = "", description = "IP address of the endpoint to assassinate", required = true) - private String endpoint = EMPTY; - - @Override - public void execute(NodeProbe probe) - { - try - { - probe.assassinateEndpoint(endpoint); - } - catch (UnknownHostException e) - { - throw new RuntimeException(e); - } - } -} diff --git a/src/java/org/apache/cassandra/tools/nodetool/Compact.java b/src/java/org/apache/cassandra/tools/nodetool/Compact.java deleted file mode 100644 index f5a83ed90475..000000000000 --- a/src/java/org/apache/cassandra/tools/nodetool/Compact.java +++ /dev/null @@ -1,106 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.cassandra.tools.nodetool; - -import static org.apache.commons.lang3.StringUtils.EMPTY; - -import io.airlift.airline.Arguments; -import io.airlift.airline.Command; -import io.airlift.airline.Option; - -import java.util.ArrayList; -import java.util.List; - -import org.apache.cassandra.tools.NodeProbe; -import org.apache.cassandra.tools.NodeTool.NodeToolCmd; - -@Command(name = "compact", description = "Force a (major) compaction on one or more tables or user-defined compaction on given SSTables") -public class Compact extends NodeToolCmd -{ - @Arguments(usage = "[ ...] or ...", description = "The keyspace followed by one or many tables or list of SSTable data files when using --user-defined") - private List args = new ArrayList<>(); - - @Option(title = "split_output", name = {"-s", "--split-output"}, description = "Use -s to not create a single big file") - private boolean splitOutput = false; - - @Option(title = "user-defined", name = {"--user-defined"}, description = "Use --user-defined to submit listed files for user-defined compaction") - private boolean userDefined = false; - - @Option(title = "start_token", name = {"-st", "--start-token"}, description = "Use -st to specify a token at which the compaction range starts (inclusive)") - private String startToken = EMPTY; - - @Option(title = "end_token", name = {"-et", "--end-token"}, description = "Use -et to specify a token at which compaction range ends (inclusive)") - private String endToken = EMPTY; - - @Option(title = "partition_key", name = {"--partition"}, description = "String representation of the partition key") - private String partitionKey = EMPTY; - - - @Override - public void execute(NodeProbe probe) - { - final boolean startEndTokenProvided = !(startToken.isEmpty() && endToken.isEmpty()); - final boolean partitionKeyProvided = !partitionKey.isEmpty(); - final boolean tokenProvided = startEndTokenProvided || partitionKeyProvided; - if (splitOutput && (userDefined || tokenProvided)) - { - throw new RuntimeException("Invalid option combination: Can not use split-output here"); - } - if (userDefined && tokenProvided) - { - throw new RuntimeException("Invalid option combination: Can not provide tokens when using user-defined"); - } - - if (userDefined) - { - try - { - String userDefinedFiles = String.join(",", args); - probe.forceUserDefinedCompaction(userDefinedFiles); - } catch (Exception e) { - throw new RuntimeException("Error occurred during user defined compaction", e); - } - return; - } - - List keyspaces = parseOptionalKeyspace(args, probe); - String[] tableNames = parseOptionalTables(args); - - for (String keyspace : keyspaces) - { - try - { - if (startEndTokenProvided) - { - probe.forceKeyspaceCompactionForTokenRange(keyspace, startToken, endToken, tableNames); - } - else if (partitionKeyProvided) - { - probe.forceKeyspaceCompactionForPartitionKey(keyspace, partitionKey, tableNames); - } - else - { - probe.forceKeyspaceCompaction(splitOutput, keyspace, tableNames); - } - } catch (Exception e) - { - throw new RuntimeException("Error occurred during compaction", e); - } - } - } -} diff --git a/src/java/org/apache/cassandra/tools/nodetool/ForceCompact.java b/src/java/org/apache/cassandra/tools/nodetool/ForceCompact.java deleted file mode 100644 index 99265e7bf0fa..000000000000 --- a/src/java/org/apache/cassandra/tools/nodetool/ForceCompact.java +++ /dev/null @@ -1,58 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.apache.cassandra.tools.nodetool; - -import io.airlift.airline.Arguments; -import io.airlift.airline.Command; - -import java.util.ArrayList; -import java.util.List; - -import org.apache.cassandra.tools.NodeProbe; -import org.apache.cassandra.tools.NodeTool.NodeToolCmd; - -import static com.google.common.base.Preconditions.checkArgument; - -@Command(name = "forcecompact", description = "Force a (major) compaction on a table") -public class ForceCompact extends NodeToolCmd -{ - @Arguments(usage = "[
]", description = "The keyspace, table, and a list of partition keys ignoring the gc_grace_seconds") - private List args = new ArrayList<>(); - - @Override - public void execute(NodeProbe probe) - { - // Check if the input has valid size - checkArgument(args.size() >= 3, "forcecompact requires keyspace, table and keys args"); - - // We rely on lower-level APIs to check and throw exceptions if the input keyspace or table name are invalid - String keyspaceName = args.get(0); - String tableName = args.get(1); - String[] partitionKeysIgnoreGcGrace = parsePartitionKeys(args); - - try - { - probe.forceCompactionKeysIgnoringGcGrace(keyspaceName, tableName, partitionKeysIgnoreGcGrace); - } - catch (Exception e) - { - throw new RuntimeException("Error occurred during compaction keys", e); - } - } -} \ No newline at end of file diff --git a/test/unit/org/apache/cassandra/tools/nodetool/NodeToolMBeanTest.java b/test/unit/org/apache/cassandra/tools/nodetool/NodeToolCommandsMBeanTest.java similarity index 98% rename from test/unit/org/apache/cassandra/tools/nodetool/NodeToolMBeanTest.java rename to test/unit/org/apache/cassandra/tools/nodetool/NodeToolCommandsMBeanTest.java index f2883216cdc4..4cb385de70bb 100644 --- a/test/unit/org/apache/cassandra/tools/nodetool/NodeToolMBeanTest.java +++ b/test/unit/org/apache/cassandra/tools/nodetool/NodeToolCommandsMBeanTest.java @@ -35,7 +35,7 @@ import static org.mockito.Mockito.when; -public class NodeToolMBeanTest extends CQLToolRunnerTester +public class NodeToolCommandsMBeanTest extends CQLToolRunnerTester { public static final String STORAGE_SERVICE_MBEAN = "org.apache.cassandra.db:type=StorageService"; public static final String COMPACTION_MANAGER_MBEAN = "org.apache.cassandra.db:type=CompactionManager"; diff --git a/test/unit/org/apache/cassandra/tools/nodetool/NodeToolSynopsisTest.java b/test/unit/org/apache/cassandra/tools/nodetool/NodeToolHelpMessageTest.java similarity index 98% rename from test/unit/org/apache/cassandra/tools/nodetool/NodeToolSynopsisTest.java rename to test/unit/org/apache/cassandra/tools/nodetool/NodeToolHelpMessageTest.java index e7a839cc9aac..5695318eaa56 100644 --- a/test/unit/org/apache/cassandra/tools/nodetool/NodeToolSynopsisTest.java +++ b/test/unit/org/apache/cassandra/tools/nodetool/NodeToolHelpMessageTest.java @@ -30,7 +30,7 @@ import static org.junit.Assert.assertTrue; -public class NodeToolSynopsisTest extends CQLToolRunnerTester +public class NodeToolHelpMessageTest extends CQLToolRunnerTester { @Test public void testCompareHelpCommand() From 08303a5881631c65e1c509d34356a14bbeec3899 Mon Sep 17 00:00:00 2001 From: Maxim Muzafarov Date: Thu, 27 Jun 2024 16:30:53 +0200 Subject: [PATCH 11/22] improve naming conventions --- .../cassandra/management/BaseCommand.java | 6 +++--- .../cassandra/management/CommandUtils.java | 16 +++------------ ...iceBridge.java => ServiceMBeanBridge.java} | 20 ++++++++++++++++--- .../management/api/AbortBootstrap.java | 7 +++---- .../cassandra/management/api/Assassinate.java | 7 +++---- .../cassandra/management/api/Compact.java | 14 ++++++------- .../management/api/ForceCompact.java | 7 +++---- .../management/api/JmxConnectionMixin.java | 4 ++-- .../org/apache/cassandra/tools/NodeProbe.java | 6 +++--- .../org/apache/cassandra/tools/NodeTool.java | 15 +++++++------- 10 files changed, 50 insertions(+), 52 deletions(-) rename src/java/org/apache/cassandra/management/{ServiceBridge.java => ServiceMBeanBridge.java} (57%) diff --git a/src/java/org/apache/cassandra/management/BaseCommand.java b/src/java/org/apache/cassandra/management/BaseCommand.java index 3de461bb12f5..3cfca1240c9d 100644 --- a/src/java/org/apache/cassandra/management/BaseCommand.java +++ b/src/java/org/apache/cassandra/management/BaseCommand.java @@ -29,14 +29,14 @@ public abstract class BaseCommand implements Runnable @CommandLine.Spec protected CommandLine.Model.CommandSpec spec; // injected by picocli /** The ServiceBridge instance to interact with the Cassandra node. */ - protected ServiceBridge bridge; + protected ServiceMBeanBridge bridge; /** * The ServiceBridge instance is injected by the picocli framework during command execution and is used to * interact with the Cassandra node. This method is called by picocli and used depending on the execution strategy. * @param bridge The ServiceBridge instance to inject. */ - public void setBridge(ServiceBridge bridge) + public void setBridge(ServiceMBeanBridge bridge) { this.bridge = bridge; } @@ -47,5 +47,5 @@ public void run() execute(bridge); } - protected abstract void execute(ServiceBridge probe); + protected abstract void execute(ServiceMBeanBridge probe); } diff --git a/src/java/org/apache/cassandra/management/CommandUtils.java b/src/java/org/apache/cassandra/management/CommandUtils.java index 58706f21e162..6eca667b7d73 100644 --- a/src/java/org/apache/cassandra/management/CommandUtils.java +++ b/src/java/org/apache/cassandra/management/CommandUtils.java @@ -21,9 +21,9 @@ import java.util.Arrays; import java.util.Collection; -import org.apache.cassandra.db.compaction.CompactionManagerMBean; -import org.apache.cassandra.service.StorageServiceMBean; - +/** + * Utility methods for nodetool commands. + */ public final class CommandUtils { public static final String CASSANDRA_BACKWARD_COMPATIBLE_MARKER = "cassandra-backward-compatible"; @@ -48,14 +48,4 @@ public static int maxLength(Collection any) result = Math.max(result, String.valueOf(value).length()); return result; } - - public static StorageServiceMBean ssProxy(ServiceBridge bridge) - { - return bridge.mBean(StorageServiceMBean.class); - } - - public static CompactionManagerMBean cmProxy(ServiceBridge bridge) - { - return bridge.mBean(CompactionManagerMBean.class); - } } diff --git a/src/java/org/apache/cassandra/management/ServiceBridge.java b/src/java/org/apache/cassandra/management/ServiceMBeanBridge.java similarity index 57% rename from src/java/org/apache/cassandra/management/ServiceBridge.java rename to src/java/org/apache/cassandra/management/ServiceMBeanBridge.java index 80edce06824a..d37cbaf8d4df 100644 --- a/src/java/org/apache/cassandra/management/ServiceBridge.java +++ b/src/java/org/apache/cassandra/management/ServiceMBeanBridge.java @@ -18,10 +18,24 @@ package org.apache.cassandra.management; +import org.apache.cassandra.db.compaction.CompactionManagerMBean; +import org.apache.cassandra.service.StorageServiceMBean; + /** - * Management context for nodetool commands to access management services like StorageServiceMBean etc. + * Service bridge for Cassandra management commands to access MBeans and other service-related functionality. + * This interface is used to bridge the gap between the nodetool commands and the Cassandra service MBeans. */ -public interface ServiceBridge +public interface ServiceMBeanBridge { - T mBean(Class clazz); + T getMBean(Class clazz); + + default StorageServiceMBean ssProxy() + { + return getMBean(StorageServiceMBean.class); + } + + default CompactionManagerMBean cmProxy() + { + return getMBean(CompactionManagerMBean.class); + } } diff --git a/src/java/org/apache/cassandra/management/api/AbortBootstrap.java b/src/java/org/apache/cassandra/management/api/AbortBootstrap.java index cbea96efedc5..e76e9668a045 100644 --- a/src/java/org/apache/cassandra/management/api/AbortBootstrap.java +++ b/src/java/org/apache/cassandra/management/api/AbortBootstrap.java @@ -18,11 +18,10 @@ package org.apache.cassandra.management.api; import org.apache.cassandra.management.BaseCommand; -import org.apache.cassandra.management.ServiceBridge; +import org.apache.cassandra.management.ServiceMBeanBridge; import picocli.CommandLine.Command; import picocli.CommandLine.Option; -import static org.apache.cassandra.management.CommandUtils.ssProxy; import static org.apache.commons.lang3.StringUtils.EMPTY; import static org.apache.commons.lang3.StringUtils.isEmpty; @@ -36,12 +35,12 @@ public class AbortBootstrap extends BaseCommand public String endpoint = EMPTY; @Override - public void execute(ServiceBridge probe) + public void execute(ServiceMBeanBridge probe) { if (isEmpty(nodeId) && isEmpty(endpoint)) throw new IllegalArgumentException("Either --node or --ip needs to be set"); if (!isEmpty(nodeId) && !isEmpty(endpoint)) throw new IllegalArgumentException("Only one of --node or --ip need to be set"); - ssProxy(probe).abortBootstrap(nodeId, endpoint); + probe.ssProxy().abortBootstrap(nodeId, endpoint); } } diff --git a/src/java/org/apache/cassandra/management/api/Assassinate.java b/src/java/org/apache/cassandra/management/api/Assassinate.java index 49a7914dd126..785aec4538ad 100644 --- a/src/java/org/apache/cassandra/management/api/Assassinate.java +++ b/src/java/org/apache/cassandra/management/api/Assassinate.java @@ -18,10 +18,9 @@ package org.apache.cassandra.management.api; import org.apache.cassandra.management.BaseCommand; -import org.apache.cassandra.management.ServiceBridge; +import org.apache.cassandra.management.ServiceMBeanBridge; import picocli.CommandLine; -import static org.apache.cassandra.management.CommandUtils.ssProxy; import static org.apache.commons.lang3.StringUtils.EMPTY; @CommandLine.Command(name = "assassinate", description = "Forcefully remove a dead node without re-replicating any data. Use as a last resort if you cannot removenode") @@ -31,8 +30,8 @@ public class Assassinate extends BaseCommand public String ip_address = EMPTY; @Override - public void execute(ServiceBridge probe) + public void execute(ServiceMBeanBridge probe) { - ssProxy(probe).assassinateEndpoint(ip_address); + probe.ssProxy().assassinateEndpoint(ip_address); } } diff --git a/src/java/org/apache/cassandra/management/api/Compact.java b/src/java/org/apache/cassandra/management/api/Compact.java index c50ed999d78c..8f883db72d43 100644 --- a/src/java/org/apache/cassandra/management/api/Compact.java +++ b/src/java/org/apache/cassandra/management/api/Compact.java @@ -21,11 +21,9 @@ import java.util.List; import org.apache.cassandra.management.BaseCommand; -import org.apache.cassandra.management.ServiceBridge; +import org.apache.cassandra.management.ServiceMBeanBridge; import picocli.CommandLine; -import static org.apache.cassandra.management.CommandUtils.cmProxy; -import static org.apache.cassandra.management.CommandUtils.ssProxy; import static org.apache.cassandra.tools.NodeTool.NodeToolCmd.parseOptionalKeyspace; import static org.apache.cassandra.tools.NodeTool.NodeToolCmd.parseOptionalTables; import static org.apache.commons.lang3.StringUtils.EMPTY; @@ -53,7 +51,7 @@ public class Compact extends BaseCommand public String partitionKey = EMPTY; @Override - public void execute(ServiceBridge probe) + public void execute(ServiceMBeanBridge probe) { final boolean startEndTokenProvided = !(startToken.isEmpty() && endToken.isEmpty()); final boolean partitionKeyProvided = !partitionKey.isEmpty(); @@ -72,7 +70,7 @@ public void execute(ServiceBridge probe) try { String userDefinedFiles = String.join(",", args); - cmProxy(probe).forceUserDefinedCompaction(userDefinedFiles); + probe.cmProxy().forceUserDefinedCompaction(userDefinedFiles); } catch (Exception e) { throw new RuntimeException("Error occurred during user defined compaction", e); } @@ -88,15 +86,15 @@ public void execute(ServiceBridge probe) { if (startEndTokenProvided) { - ssProxy(probe).forceKeyspaceCompactionForTokenRange(keyspace, startToken, endToken, tableNames); + probe.ssProxy().forceKeyspaceCompactionForTokenRange(keyspace, startToken, endToken, tableNames); } else if (partitionKeyProvided) { - ssProxy(probe).forceKeyspaceCompactionForPartitionKey(keyspace, partitionKey, tableNames); + probe.ssProxy().forceKeyspaceCompactionForPartitionKey(keyspace, partitionKey, tableNames); } else { - ssProxy(probe).forceKeyspaceCompaction(splitOutput, keyspace, tableNames); + probe.ssProxy().forceKeyspaceCompaction(splitOutput, keyspace, tableNames); } } catch (Exception e) { diff --git a/src/java/org/apache/cassandra/management/api/ForceCompact.java b/src/java/org/apache/cassandra/management/api/ForceCompact.java index 25d18d0e597c..781affee983a 100644 --- a/src/java/org/apache/cassandra/management/api/ForceCompact.java +++ b/src/java/org/apache/cassandra/management/api/ForceCompact.java @@ -24,11 +24,10 @@ import org.apache.cassandra.management.BaseCommand; import org.apache.cassandra.management.CommandUtils; -import org.apache.cassandra.management.ServiceBridge; +import org.apache.cassandra.management.ServiceMBeanBridge; import picocli.CommandLine; import static com.google.common.base.Preconditions.checkArgument; -import static org.apache.cassandra.management.CommandUtils.ssProxy; import static org.apache.cassandra.tools.NodeTool.NodeToolCmd.parsePartitionKeys; @CommandLine.Command(name = "forcecompact", description = "Force a (major) compaction on a table") @@ -49,7 +48,7 @@ public class ForceCompact extends BaseCommand public String[] keys; @Override - public void execute(ServiceBridge probe) + public void execute(ServiceMBeanBridge probe) { args = Lists.asList(keyspace, table, keys); // Check if the input has valid size @@ -62,7 +61,7 @@ public void execute(ServiceBridge probe) try { - ssProxy(probe).forceCompactionKeysIgnoringGcGrace(keyspaceName, tableName, partitionKeysIgnoreGcGrace); + probe.ssProxy().forceCompactionKeysIgnoringGcGrace(keyspaceName, tableName, partitionKeysIgnoreGcGrace); } catch (Exception e) { diff --git a/src/java/org/apache/cassandra/management/api/JmxConnectionMixin.java b/src/java/org/apache/cassandra/management/api/JmxConnectionMixin.java index e0a00a9d0c7d..d5c52e57b082 100644 --- a/src/java/org/apache/cassandra/management/api/JmxConnectionMixin.java +++ b/src/java/org/apache/cassandra/management/api/JmxConnectionMixin.java @@ -24,7 +24,7 @@ import com.google.common.base.Throwables; import org.apache.cassandra.management.BaseCommand; -import org.apache.cassandra.management.ServiceBridge; +import org.apache.cassandra.management.ServiceMBeanBridge; import org.apache.cassandra.tools.INodeProbeFactory; import org.apache.cassandra.tools.Output; import picocli.CommandLine; @@ -89,7 +89,7 @@ public static int executionStrategy(CommandLine.ParseResult parseResult) * @param spec The command specification to be executed after the initialization. * @return The ServiceBridge instance to interact with the Cassandra node. */ - private ServiceBridge init(CommandLine.Model.CommandSpec spec) + private ServiceMBeanBridge init(CommandLine.Model.CommandSpec spec) { try { diff --git a/src/java/org/apache/cassandra/tools/NodeProbe.java b/src/java/org/apache/cassandra/tools/NodeProbe.java index ee7a41640260..65c21a5f0b8b 100644 --- a/src/java/org/apache/cassandra/tools/NodeProbe.java +++ b/src/java/org/apache/cassandra/tools/NodeProbe.java @@ -106,7 +106,7 @@ import org.apache.cassandra.metrics.StorageMetrics; import org.apache.cassandra.metrics.TableMetrics; import org.apache.cassandra.metrics.ThreadPoolMetrics; -import org.apache.cassandra.management.ServiceBridge; +import org.apache.cassandra.management.ServiceMBeanBridge; import org.apache.cassandra.net.MessagingService; import org.apache.cassandra.net.MessagingServiceMBean; import org.apache.cassandra.service.ActiveRepairServiceMBean; @@ -132,7 +132,7 @@ /** * JMX client operations for Cassandra. */ -public class NodeProbe implements AutoCloseable, ServiceBridge +public class NodeProbe implements AutoCloseable, ServiceMBeanBridge { private static final String fmtUrl = "service:jmx:rmi:///jndi/rmi://%s:%d/jmxrmi"; private static final String ssObjName = "org.apache.cassandra.db:type=StorageService"; @@ -246,7 +246,7 @@ private static T cachedNewMBeanProxy(BiConsumer cache, } @Override - public T mBean(Class clazz) + public T getMBean(Class clazz) { return clazz.cast(Optional.ofNullable(cachedMBeans.get(clazz.getName())) .orElseThrow(() -> new IllegalArgumentException("No MBean found for " + clazz.getName()))); diff --git a/src/java/org/apache/cassandra/tools/NodeTool.java b/src/java/org/apache/cassandra/tools/NodeTool.java index c0cb4791e753..f9dc61eb3bb1 100644 --- a/src/java/org/apache/cassandra/tools/NodeTool.java +++ b/src/java/org/apache/cassandra/tools/NodeTool.java @@ -60,7 +60,7 @@ import org.apache.cassandra.io.util.FileWriter; import org.apache.cassandra.locator.EndpointSnitchInfoMBean; import org.apache.cassandra.management.CassandraHelpLayout; -import org.apache.cassandra.management.ServiceBridge; +import org.apache.cassandra.management.ServiceMBeanBridge; import org.apache.cassandra.tools.nodetool.*; import org.apache.cassandra.utils.FBUtilities; import picocli.CommandLine; @@ -74,7 +74,6 @@ import static java.lang.String.format; import static java.util.stream.Collectors.toList; import static org.apache.cassandra.io.util.File.WriteMode.APPEND; -import static org.apache.cassandra.management.CommandUtils.ssProxy; import static org.apache.commons.lang3.ArrayUtils.EMPTY_STRING_ARRAY; import static org.apache.commons.lang3.StringUtils.EMPTY; import static org.apache.commons.lang3.StringUtils.isEmpty; @@ -561,12 +560,12 @@ protected enum KeyspaceSet ALL, NON_SYSTEM, NON_LOCAL_STRATEGY } - public static List parseOptionalKeyspace(List cmdArgs, ServiceBridge nodeProbe) + public static List parseOptionalKeyspace(List cmdArgs, ServiceMBeanBridge nodeProbe) { return parseOptionalKeyspace(cmdArgs, nodeProbe, KeyspaceSet.ALL); } - public static List parseOptionalKeyspace(List cmdArgs, ServiceBridge nodeProbe, KeyspaceSet defaultKeyspaceSet) + public static List parseOptionalKeyspace(List cmdArgs, ServiceMBeanBridge nodeProbe, KeyspaceSet defaultKeyspaceSet) { List keyspaces = new ArrayList<>(); @@ -574,11 +573,11 @@ public static List parseOptionalKeyspace(List cmdArgs, ServiceBr if (cmdArgs == null || cmdArgs.isEmpty()) { if (defaultKeyspaceSet == KeyspaceSet.NON_LOCAL_STRATEGY) - keyspaces.addAll(keyspaces = ssProxy(nodeProbe).getNonLocalStrategyKeyspaces()); + keyspaces.addAll(keyspaces = nodeProbe.ssProxy().getNonLocalStrategyKeyspaces()); else if (defaultKeyspaceSet == KeyspaceSet.NON_SYSTEM) - keyspaces.addAll(keyspaces = ssProxy(nodeProbe).getNonSystemKeyspaces()); + keyspaces.addAll(keyspaces = nodeProbe.ssProxy().getNonSystemKeyspaces()); else - keyspaces.addAll(ssProxy(nodeProbe).getKeyspaces()); + keyspaces.addAll(nodeProbe.ssProxy().getKeyspaces()); } else { @@ -587,7 +586,7 @@ else if (defaultKeyspaceSet == KeyspaceSet.NON_SYSTEM) for (String keyspace : keyspaces) { - if (!ssProxy(nodeProbe).getKeyspaces().contains(keyspace)) + if (!nodeProbe.ssProxy().getKeyspaces().contains(keyspace)) throw new IllegalArgumentException("Keyspace [" + keyspace + "] does not exist."); } From 456580be78b4abf0ad38af17b73afbc43f7d8e16 Mon Sep 17 00:00:00 2001 From: Maxim Muzafarov Date: Mon, 1 Jul 2024 18:04:50 +0200 Subject: [PATCH 12/22] add cli argument to support backwards compatibility --- .../management/CassandraCliArgument.java | 36 +++++ .../management/CassandraHelpLayout.java | 125 ++++++++++-------- .../cassandra/management/CommandUtils.java | 19 ++- .../cassandra/management/api/Compact.java | 1 + .../management/api/ForceCompact.java | 6 +- .../nodetool/NodeToolHelpMessageTest.java | 2 +- 6 files changed, 131 insertions(+), 58 deletions(-) create mode 100644 src/java/org/apache/cassandra/management/CassandraCliArgument.java diff --git a/src/java/org/apache/cassandra/management/CassandraCliArgument.java b/src/java/org/apache/cassandra/management/CassandraCliArgument.java new file mode 100644 index 000000000000..12b8058678ce --- /dev/null +++ b/src/java/org/apache/cassandra/management/CassandraCliArgument.java @@ -0,0 +1,36 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.management; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Argument annotation for Cassandra commands, used to provide metadata + * for command-line argument for backward compatibility purposes. + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ ElementType.FIELD }) +public @interface CassandraCliArgument +{ + String description() default ""; + String usage() default ""; +} diff --git a/src/java/org/apache/cassandra/management/CassandraHelpLayout.java b/src/java/org/apache/cassandra/management/CassandraHelpLayout.java index 85f74249a021..65fd326ca5d5 100644 --- a/src/java/org/apache/cassandra/management/CassandraHelpLayout.java +++ b/src/java/org/apache/cassandra/management/CassandraHelpLayout.java @@ -27,10 +27,13 @@ import java.util.List; import java.util.Map; import java.util.Set; +import java.util.function.Function; +import org.apache.cassandra.utils.Pair; import picocli.CommandLine; import static org.apache.cassandra.management.CommandUtils.leadingSpaces; +import static org.apache.cassandra.management.CommandUtils.findBackwardCompatibleArgument; import static org.apache.commons.lang3.ArrayUtils.isEmpty; import static picocli.CommandLine.Model.UsageMessageSpec.SECTION_KEY_COMMAND_LIST; import static picocli.CommandLine.Model.UsageMessageSpec.SECTION_KEY_COMMAND_LIST_HEADING; @@ -204,8 +207,13 @@ private Ansi.Text createCassandraSynopsisPositionalsText(Collection positionals = cassandraPositionals(commandSpec()); positionals.removeAll(done); - IParamLabelRenderer parameterLabelRenderer = createMinimalSpacedParamLabelRenderer(); + Pair commandArgumensSpec = findBackwardCompatibleArgument(commandSpec().userObject()); Ansi.Text text = colorScheme().text(""); + // If the command has a backward compatible argument, use it to generate the synopsis based on the old format. + if (commandArgumensSpec != null) + return colorScheme().parameterText(commandArgumensSpec.left); + + IParamLabelRenderer parameterLabelRenderer = CassandraStyleParamLabelRender.create(); for (CommandLine.Model.PositionalParamSpec positionalParam : positionals) { Ansi.Text label = parameterLabelRenderer.renderParameterLabel(positionalParam, colorScheme().ansi(), colorScheme().parameterStyles()); @@ -223,7 +231,7 @@ private List createCassandraSynopsisOptionsText(Collection createCassandraSynopsisOptionsText(Collection styles) - { - ColorScheme colorScheme = CommandLine.Help.defaultColorScheme(ansi); - if (argSpec.equals(CASSANDRA_END_OF_OPTIONS_OPTION)) - return colorScheme.text(""); - if (argSpec instanceof CommandLine.Model.OptionSpec && argSpec.typeInfo().isBoolean()) - return colorScheme.text(""); - return argSpec.isOption() ? colorScheme.optionText(argSpec.paramLabel()) : - colorScheme.parameterText(argSpec.paramLabel()); - } - - public String separator() - { - return ""; - } - }; - } - @Override public String optionListHeading(Object... params) { @@ -285,32 +271,65 @@ public String optionList() options.sort(comparator); Layout layout = cassandraSingleColumnOptionsParametersLayout(); - layout.addAllOptions(options, createMinimalSpacedParamLabelRenderer()); + layout.addAllOptions(options, CassandraStyleParamLabelRender.create()); return layout.toString(); } @Override public String endOfOptionsList() { Layout layout = cassandraSingleColumnOptionsParametersLayout(); - layout.addOption(CASSANDRA_END_OF_OPTIONS_OPTION, createMinimalSpacedParamLabelRenderer()); + layout.addOption(CASSANDRA_END_OF_OPTIONS_OPTION, CassandraStyleParamLabelRender.create()); return layout.toString(); } private Layout cassandraSingleColumnOptionsParametersLayout() + { + return new Layout(colorScheme(), configureLayoutTextTable(), new CassandraStyleOptionRenderer(), new CassandraStyleParameterRenderer()); + } + + private TextTable configureLayoutTextTable() { TextTable table = TextTable.forColumns(colorScheme(), new Column(commandSpec().usageMessage().width() - COLUMN_INDENT, COLUMN_INDENT, Column.Overflow.WRAP)); table.setAdjustLineBreaksForWideCJKCharacters(commandSpec().usageMessage().adjustLineBreaksForWideCJKCharacters()); table.indentWrappedLines = DESCRIPTION_INDENT; - return new Layout(colorScheme(), table, new CassandraStyleOptionRenderer(), new CassandraStyleParameterRenderer()); + return table; } @Override public String parameterList() { + Pair cassandraArgument = findBackwardCompatibleArgument(commandSpec().userObject()); List positionalParams = cassandraPositionals(commandSpec()); - Layout layout = cassandraSingleColumnOptionsParametersLayout(); - layout.addAllPositionalParameters(positionalParams, createMinimalSpacedParamLabelRenderer()); + Layout layout = cassandraArgument == null ? + cassandraSingleColumnOptionsParametersLayout() : + new Layout(colorScheme(), + configureLayoutTextTable(), + new CassandraStyleOptionRenderer(), + new CassandraStyleParameterRenderer()) + { + // If the command has a backward compatible argument, use it to generate the synopsis + // based on the old format. + @Override + public void layout(CommandLine.Model.ArgSpec argSpec, Ansi.Text[][] cellValues) + { + Ansi.Text descPadding = Ansi.OFF.new Text(leadingSpaces(DESCRIPTION_INDENT), colorScheme); + cellValues[0] = new Ansi.Text[]{ colorScheme.parameterText(cassandraArgument.left) }; + cellValues[1] = new Ansi.Text[]{ descPadding.concat(colorScheme.parameterText(cassandraArgument.right)) }; + cellValues[2] = new Ansi.Text[]{ Ansi.OFF.new Text("", colorScheme) }; + for (Ansi.Text[] oneRow : cellValues) + table.addRowValues(oneRow); + } + + @Override + public void addAllPositionalParameters(List params, + IParamLabelRenderer paramLabelRenderer) + { + layout(null, new Ansi.Text[3][]); + } + }; + + layout.addAllPositionalParameters(positionalParams, CassandraStyleParamLabelRender.create()); return layout.toString(); } @@ -341,12 +360,6 @@ public String commandList(Map subcommands) return table.toString(); } -// @Override -// public String footerHeading(Object... params) -// { -// return createHeading("%n", params); -// } - @Override public String footer(Object... params) { @@ -374,20 +387,7 @@ public String topLevelSynopsis(Object... params) private static List cassandraPositionals(CommandLine.Model.CommandSpec commandSpec) { List positionals = new ArrayList<>(commandSpec.positionalParameters()); - for (CommandLine.Model.PositionalParamSpec param : positionals) - { - if (param.hidden()) - { - if (param.description()[0].equals(CommandUtils.CASSANDRA_BACKWARD_COMPATIBLE_MARKER)) - { - positionals.clear(); - positionals.add(param); - break; - } - else - positionals.remove(param); - } - } + positionals.removeIf(CommandLine.Model.ArgSpec::hidden); return positionals; } @@ -447,6 +447,29 @@ private static Ansi.Text spacedParamLabel(CommandLine.Model.OptionSpec optionSpe scheme.text(" ").concat(parameterLabelRenderer.renderParameterLabel(optionSpec, scheme.ansi(), scheme.optionParamStyles())); } + private static class CassandraStyleParamLabelRender implements IParamLabelRenderer + { + public static IParamLabelRenderer create() + { + return new CassandraStyleParamLabelRender(); + } + + @Override + public Ansi.Text renderParameterLabel(CommandLine.Model.ArgSpec argSpec, Ansi ansi, List styles) + { + ColorScheme colorScheme = CommandLine.Help.defaultColorScheme(ansi); + if (argSpec.equals(CASSANDRA_END_OF_OPTIONS_OPTION)) + return colorScheme.text(""); + if (argSpec instanceof CommandLine.Model.OptionSpec && argSpec.typeInfo().isBoolean()) + return colorScheme.text(""); + return argSpec.isOption() ? colorScheme.optionText(argSpec.paramLabel()) : + colorScheme.parameterText(argSpec.paramLabel()); + } + + @Override + public String separator() { return ""; } + } + private static class CassandraStyleOptionRenderer implements IOptionRenderer { public Ansi.Text[][] render(CommandLine.Model.OptionSpec option, IParamLabelRenderer parameterLabelRenderer, ColorScheme scheme) @@ -476,9 +499,7 @@ private static class CassandraStyleParameterRenderer implements IParameterRender @Override public Ansi.Text[][] render(CommandLine.Model.PositionalParamSpec param, IParamLabelRenderer parameterLabelRenderer, ColorScheme scheme) { - String descriptionString = param.description()[0].equals(CommandUtils.CASSANDRA_BACKWARD_COMPATIBLE_MARKER) ? - param.description()[1] : param.description()[0]; - + String descriptionString = param.description()[0]; Ansi.Text descPadding = Ansi.OFF.new Text(leadingSpaces(DESCRIPTION_INDENT), scheme); Ansi.Text[][] result = new Ansi.Text[3][]; result[0] = new Ansi.Text[]{ parameterLabelRenderer.renderParameterLabel(param, scheme.ansi(), scheme.parameterStyles()) }; diff --git a/src/java/org/apache/cassandra/management/CommandUtils.java b/src/java/org/apache/cassandra/management/CommandUtils.java index 6eca667b7d73..bfffd444cbbc 100644 --- a/src/java/org/apache/cassandra/management/CommandUtils.java +++ b/src/java/org/apache/cassandra/management/CommandUtils.java @@ -18,16 +18,17 @@ package org.apache.cassandra.management; +import java.lang.reflect.Field; import java.util.Arrays; import java.util.Collection; +import org.apache.cassandra.utils.Pair; + /** * Utility methods for nodetool commands. */ public final class CommandUtils { - public static final String CASSANDRA_BACKWARD_COMPATIBLE_MARKER = "cassandra-backward-compatible"; - /** * Returns a string with the given number of leading spaces. * @@ -48,4 +49,18 @@ public static int maxLength(Collection any) result = Math.max(result, String.valueOf(value).length()); return result; } + + public static Pair findBackwardCompatibleArgument(Object userObject) + { + Class clazz = userObject.getClass(); + for (Field field : clazz.getFields()) + { + if (field.isAnnotationPresent(CassandraCliArgument.class)) + { + CassandraCliArgument ann = field.getAnnotation(CassandraCliArgument.class); + return Pair.create(ann.usage(), ann.description()); + } + } + return null; + } } diff --git a/src/java/org/apache/cassandra/management/api/Compact.java b/src/java/org/apache/cassandra/management/api/Compact.java index 8f883db72d43..1e768e4dd102 100644 --- a/src/java/org/apache/cassandra/management/api/Compact.java +++ b/src/java/org/apache/cassandra/management/api/Compact.java @@ -21,6 +21,7 @@ import java.util.List; import org.apache.cassandra.management.BaseCommand; +import org.apache.cassandra.management.CassandraCliArgument; import org.apache.cassandra.management.ServiceMBeanBridge; import picocli.CommandLine; diff --git a/src/java/org/apache/cassandra/management/api/ForceCompact.java b/src/java/org/apache/cassandra/management/api/ForceCompact.java index 781affee983a..8479c9b50cc3 100644 --- a/src/java/org/apache/cassandra/management/api/ForceCompact.java +++ b/src/java/org/apache/cassandra/management/api/ForceCompact.java @@ -23,6 +23,7 @@ import com.google.common.collect.Lists; import org.apache.cassandra.management.BaseCommand; +import org.apache.cassandra.management.CassandraCliArgument; import org.apache.cassandra.management.CommandUtils; import org.apache.cassandra.management.ServiceMBeanBridge; import picocli.CommandLine; @@ -33,9 +34,8 @@ @CommandLine.Command(name = "forcecompact", description = "Force a (major) compaction on a table") public class ForceCompact extends BaseCommand { - @CommandLine.Parameters(hidden = true, arity = "0", paramLabel = "[
]", - description = { CommandUtils.CASSANDRA_BACKWARD_COMPATIBLE_MARKER, - "The keyspace, table, and a list of partition keys ignoring the gc_grace_seconds" }) + @CassandraCliArgument(usage = "[
]", + description = "The keyspace, table, and a list of partition keys ignoring the gc_grace_seconds") public List args; @CommandLine.Parameters(index = "0", arity = "1", description = "The keyspace name to compact") diff --git a/test/unit/org/apache/cassandra/tools/nodetool/NodeToolHelpMessageTest.java b/test/unit/org/apache/cassandra/tools/nodetool/NodeToolHelpMessageTest.java index 5695318eaa56..37561213b8b3 100644 --- a/test/unit/org/apache/cassandra/tools/nodetool/NodeToolHelpMessageTest.java +++ b/test/unit/org/apache/cassandra/tools/nodetool/NodeToolHelpMessageTest.java @@ -46,7 +46,7 @@ public void testCompareHelpCommand() @Test public void testBaseCommandOutput() { - List outNodeToolV2 = sliceStdout(invokeNodetoolInJvmV1()); + List outNodeToolV2 = sliceStdout(invokeNodetoolInJvmV1("help", "forcecompact")); System.out.println(printFormattedNodeToolOutput(outNodeToolV2)); } From 0de57ffaf1c9994856881020847401f3e31afbff Mon Sep 17 00:00:00 2001 From: Maxim Muzafarov Date: Mon, 1 Jul 2024 21:19:00 +0200 Subject: [PATCH 13/22] add nodetool v1 static help output --- .../management/CassandraHelpLayout.java | 3 +- .../cassandra/management/api/Compact.java | 47 +++++--- .../management/api/ForceCompact.java | 1 - test/resources/nodetool/help/abortbootstrap | 35 ++++++ test/resources/nodetool/help/assassinate | 38 ++++++ test/resources/nodetool/help/compact | 60 ++++++++++ test/resources/nodetool/help/forcecompact | 38 ++++++ .../tools/nodetool/CQLToolRunnerTester.java | 57 ++++++++- .../nodetool/NodeToolHelpCommandTest.java | 79 +++++++++++++ .../nodetool/NodeToolHelpMessageTest.java | 110 ------------------ .../tools/nodetool/NodeToolMessageTest.java | 47 ++++++++ 11 files changed, 383 insertions(+), 132 deletions(-) create mode 100644 test/resources/nodetool/help/abortbootstrap create mode 100644 test/resources/nodetool/help/assassinate create mode 100644 test/resources/nodetool/help/compact create mode 100644 test/resources/nodetool/help/forcecompact create mode 100644 test/unit/org/apache/cassandra/tools/nodetool/NodeToolHelpCommandTest.java delete mode 100644 test/unit/org/apache/cassandra/tools/nodetool/NodeToolHelpMessageTest.java create mode 100644 test/unit/org/apache/cassandra/tools/nodetool/NodeToolMessageTest.java diff --git a/src/java/org/apache/cassandra/management/CassandraHelpLayout.java b/src/java/org/apache/cassandra/management/CassandraHelpLayout.java index 65fd326ca5d5..5aabae235bf5 100644 --- a/src/java/org/apache/cassandra/management/CassandraHelpLayout.java +++ b/src/java/org/apache/cassandra/management/CassandraHelpLayout.java @@ -27,13 +27,12 @@ import java.util.List; import java.util.Map; import java.util.Set; -import java.util.function.Function; import org.apache.cassandra.utils.Pair; import picocli.CommandLine; -import static org.apache.cassandra.management.CommandUtils.leadingSpaces; import static org.apache.cassandra.management.CommandUtils.findBackwardCompatibleArgument; +import static org.apache.cassandra.management.CommandUtils.leadingSpaces; import static org.apache.commons.lang3.ArrayUtils.isEmpty; import static picocli.CommandLine.Model.UsageMessageSpec.SECTION_KEY_COMMAND_LIST; import static picocli.CommandLine.Model.UsageMessageSpec.SECTION_KEY_COMMAND_LIST_HEADING; diff --git a/src/java/org/apache/cassandra/management/api/Compact.java b/src/java/org/apache/cassandra/management/api/Compact.java index 1e768e4dd102..083e9ebd57e7 100644 --- a/src/java/org/apache/cassandra/management/api/Compact.java +++ b/src/java/org/apache/cassandra/management/api/Compact.java @@ -20,6 +20,8 @@ import java.util.ArrayList; import java.util.List; +import com.google.common.collect.Lists; + import org.apache.cassandra.management.BaseCommand; import org.apache.cassandra.management.CassandraCliArgument; import org.apache.cassandra.management.ServiceMBeanBridge; @@ -32,28 +34,47 @@ @CommandLine.Command(name = "compact", description = "Force a (major) compaction on one or more tables or user-defined compaction on given SSTables") public class Compact extends BaseCommand { - @CommandLine.Parameters(paramLabel = " ...] or [", + @CassandraCliArgument(usage = "[ ...] or []", description = "The keyspace followed by one or many tables or list of SSTable data files when using --user-defined") public List args = new ArrayList<>(); + @CommandLine.ArgGroup(exclusive = false) + public TablesArgs tablesArgs = new TablesArgs(); + @CommandLine.ArgGroup(exclusive = false) + public UserDefinedArgs userDefinedArgs = new UserDefinedArgs(); - @CommandLine.Option(names = { "-s", "--split-output"}, description = "Use -s to not create a single big file") - public boolean splitOutput = false; - - @CommandLine.Option(names = { "--user-defined"}, description = "Use --user-defined to submit listed files for user-defined compaction") - public boolean userDefined = false; - - @CommandLine.Option(names = { "-st", "--start-token"}, description = "Use -st to specify a token at which the compaction range starts (inclusive)") - public String startToken = EMPTY; + public static class TablesArgs + { + @CommandLine.Option(names = { "-st", "--start-token"}, description = "Use -st to specify a token at which the compaction range starts (inclusive)") + public String startToken = EMPTY; + @CommandLine.Option(names = { "-et", "--end-token"}, description = "Use -et to specify a token at which compaction range ends (inclusive)") + public String endToken = EMPTY; + @CommandLine.Option(names = { "--partition"}, description = "String representation of the partition key") + public String partitionKey = EMPTY; + @CommandLine.Parameters(index = "0", arity = "1", description = "The keyspace followed by one or many tables") + public String keyspace; + @CommandLine.Parameters(index = "1..*", arity = "1", description = "The tables to be compacted") + public String[] tables; + } - @CommandLine.Option(names = { "-et", "--end-token"}, description = "Use -et to specify a token at which compaction range ends (inclusive)") - public String endToken = EMPTY; + public static class UserDefinedArgs + { + @CommandLine.Option(names = { "--user-defined"}, description = "Use --user-defined to submit listed files for user-defined compaction") + public boolean userDefined = false; + @CommandLine.Parameters(index = "0..*", arity = "1", description = "The SSTable files to be compacted") + public String[] sstableFiles; + } - @CommandLine.Option(names = { "--partition"}, description = "String representation of the partition key") - public String partitionKey = EMPTY; + @CommandLine.Option(names = { "-s", "--split-output"}, description = "Use -s to not create a single big file") + public boolean splitOutput = false; @Override public void execute(ServiceMBeanBridge probe) { + final boolean userDefined = userDefinedArgs.userDefined; + final String startToken = tablesArgs.startToken; + final String endToken = tablesArgs.endToken; + final String partitionKey = tablesArgs.partitionKey; + args = userDefined ? List.of(userDefinedArgs.sstableFiles) : Lists.asList(tablesArgs.keyspace, tablesArgs.tables); final boolean startEndTokenProvided = !(startToken.isEmpty() && endToken.isEmpty()); final boolean partitionKeyProvided = !partitionKey.isEmpty(); final boolean tokenProvided = startEndTokenProvided || partitionKeyProvided; diff --git a/src/java/org/apache/cassandra/management/api/ForceCompact.java b/src/java/org/apache/cassandra/management/api/ForceCompact.java index 8479c9b50cc3..a22e109e8581 100644 --- a/src/java/org/apache/cassandra/management/api/ForceCompact.java +++ b/src/java/org/apache/cassandra/management/api/ForceCompact.java @@ -24,7 +24,6 @@ import org.apache.cassandra.management.BaseCommand; import org.apache.cassandra.management.CassandraCliArgument; -import org.apache.cassandra.management.CommandUtils; import org.apache.cassandra.management.ServiceMBeanBridge; import picocli.CommandLine; diff --git a/test/resources/nodetool/help/abortbootstrap b/test/resources/nodetool/help/abortbootstrap new file mode 100644 index 000000000000..5d1d382fbb0e --- /dev/null +++ b/test/resources/nodetool/help/abortbootstrap @@ -0,0 +1,35 @@ +NAME + nodetool abortbootstrap - Abort a failed bootstrap + +SYNOPSIS + nodetool [(-h | --host )] [(-p | --port )] + [(-pp | --print-port)] [(-pw | --password )] + [(-pwf | --password-file )] + [(-u | --username )] abortbootstrap [--ip ] + [--node ] + +OPTIONS + -h , --host + Node hostname or ip address + + --ip + IP of the node that failed bootstrap + + --node + Node ID of the node that failed bootstrap + + -p , --port + Remote jmx agent port number + + -pp, --print-port + Operate in 4.0 mode with hosts disambiguated by port number + + -pw , --password + Remote jmx agent password + + -pwf , --password-file + Path to the JMX password file + + -u , --username + Remote jmx agent username + diff --git a/test/resources/nodetool/help/assassinate b/test/resources/nodetool/help/assassinate new file mode 100644 index 000000000000..baa42ed8feae --- /dev/null +++ b/test/resources/nodetool/help/assassinate @@ -0,0 +1,38 @@ +NAME + nodetool assassinate - Forcefully remove a dead node without + re-replicating any data. Use as a last resort if you cannot removenode + +SYNOPSIS + nodetool [(-h | --host )] [(-p | --port )] + [(-pp | --print-port)] [(-pw | --password )] + [(-pwf | --password-file )] + [(-u | --username )] assassinate [--] + +OPTIONS + -h , --host + Node hostname or ip address + + -p , --port + Remote jmx agent port number + + -pp, --print-port + Operate in 4.0 mode with hosts disambiguated by port number + + -pw , --password + Remote jmx agent password + + -pwf , --password-file + Path to the JMX password file + + -u , --username + Remote jmx agent username + + -- + This option can be used to separate command-line options from the + list of argument, (useful when arguments might be mistaken for + command-line options + + + IP address of the endpoint to assassinate + + diff --git a/test/resources/nodetool/help/compact b/test/resources/nodetool/help/compact new file mode 100644 index 000000000000..b6269f16a6f4 --- /dev/null +++ b/test/resources/nodetool/help/compact @@ -0,0 +1,60 @@ +NAME + nodetool compact - Force a (major) compaction on one or more tables or + user-defined compaction on given SSTables + +SYNOPSIS + nodetool [(-h | --host )] [(-p | --port )] + [(-pp | --print-port)] [(-pw | --password )] + [(-pwf | --password-file )] + [(-u | --username )] compact + [(-et | --end-token )] + [--partition ] [(-s | --split-output)] + [(-st | --start-token )] [--user-defined] + [--] [ ...] or ... + +OPTIONS + -et , --end-token + Use -et to specify a token at which compaction range ends + (inclusive) + + -h , --host + Node hostname or ip address + + -p , --port + Remote jmx agent port number + + --partition + String representation of the partition key + + -pp, --print-port + Operate in 4.0 mode with hosts disambiguated by port number + + -pw , --password + Remote jmx agent password + + -pwf , --password-file + Path to the JMX password file + + -s, --split-output + Use -s to not create a single big file + + -st , --start-token + Use -st to specify a token at which the compaction range starts + (inclusive) + + -u , --username + Remote jmx agent username + + --user-defined + Use --user-defined to submit listed files for user-defined + compaction + + -- + This option can be used to separate command-line options from the + list of argument, (useful when arguments might be mistaken for + command-line options + + [ ...] or ... + The keyspace followed by one or many tables or list of SSTable data + files when using --user-defined + diff --git a/test/resources/nodetool/help/forcecompact b/test/resources/nodetool/help/forcecompact new file mode 100644 index 000000000000..53af21abb212 --- /dev/null +++ b/test/resources/nodetool/help/forcecompact @@ -0,0 +1,38 @@ +NAME + nodetool forcecompact - Force a (major) compaction on a table + +SYNOPSIS + nodetool [(-h | --host )] [(-p | --port )] + [(-pp | --print-port)] [(-pw | --password )] + [(-pwf | --password-file )] + [(-u | --username )] forcecompact [--] [ +
] + +OPTIONS + -h , --host + Node hostname or ip address + + -p , --port + Remote jmx agent port number + + -pp, --print-port + Operate in 4.0 mode with hosts disambiguated by port number + + -pw , --password + Remote jmx agent password + + -pwf , --password-file + Path to the JMX password file + + -u , --username + Remote jmx agent username + + -- + This option can be used to separate command-line options from the + list of argument, (useful when arguments might be mistaken for + command-line options + + [
] + The keyspace, table, and a list of partition keys ignoring the + gc_grace_seconds + diff --git a/test/unit/org/apache/cassandra/tools/nodetool/CQLToolRunnerTester.java b/test/unit/org/apache/cassandra/tools/nodetool/CQLToolRunnerTester.java index 95e11969f8e5..9b45848a582b 100644 --- a/test/unit/org/apache/cassandra/tools/nodetool/CQLToolRunnerTester.java +++ b/test/unit/org/apache/cassandra/tools/nodetool/CQLToolRunnerTester.java @@ -18,15 +18,19 @@ package org.apache.cassandra.tools.nodetool; +import java.util.ArrayList; import java.util.Arrays; +import java.util.Collection; import java.util.List; import java.util.Map; -import java.util.Set; import org.junit.BeforeClass; import org.junit.runner.RunWith; import org.junit.runners.Parameterized; +import com.github.difflib.DiffUtils; +import com.github.difflib.patch.AbstractDelta; +import com.github.difflib.patch.Patch; import org.apache.cassandra.cql3.CQLTester; import org.apache.cassandra.tools.NodeTool; import org.apache.cassandra.tools.NodeToolV2; @@ -36,17 +40,17 @@ public abstract class CQLToolRunnerTester extends CQLTester { public static final Map runnersMap = Map.of( - "invokeNodetool", ToolRunner::invokeNodetool, "invokeNodetoolInJvmV1", CQLToolRunnerTester::invokeNodetoolInJvmV1, "invokeNodetoolInJvmV2", CQLToolRunnerTester::invokeNodetoolInJvmV2); @Parameterized.Parameter public String runner; - @Parameterized.Parameters(name = "{0}") - public static Set runners() - { - return runnersMap.keySet(); + @Parameterized.Parameters + public static Collection data() { + List res = new ArrayList<>(); + runnersMap.forEach((k, v) -> res.add(new Object[]{ k })); + return res; } @BeforeClass @@ -89,4 +93,45 @@ public interface ToolHandler ToolRunner.ToolResult execute(String... args); default ToolRunner.ToolResult execute(List args) { return execute(args.toArray(new String[0])); } } + + + protected static String printFormattedDiffsMessage(List stdoutOrig, + List stdoutNew, + String commandName, + String diff) + { + return '\n' + ">> source <<" + '\n' + + printFormattedNodeToolOutput(stdoutOrig) + + '\n' + ">> target <<" + + '\n' + printFormattedNodeToolOutput(stdoutNew) + + '\n' + " difference for \"" + commandName + "\":" + diff; + } + + protected static String printFormattedNodeToolOutput(List output) + { + StringBuilder sb = new StringBuilder(); + for(int i = 0; i < output.size(); i++) + { + sb.append(i).append(':').append(output.get(i)); + if(i < output.size() - 1) + sb.append('\n'); + } + return sb.toString(); + } + + protected static String computeDiff(List original, List revised) { + Patch patch = DiffUtils.diff(original, revised); + List diffLines = new ArrayList<>(); + + for (AbstractDelta delta : patch.getDeltas()) { + for (String line : delta.getSource().getLines()) { + diffLines.add(delta.getType().toString().toLowerCase() + " source: " + line); + } + for (String line : delta.getTarget().getLines()) { + diffLines.add(delta.getType().toString().toLowerCase() + " target: " + line); + } + } + + return '\n' + String.join("\n", diffLines); + } } diff --git a/test/unit/org/apache/cassandra/tools/nodetool/NodeToolHelpCommandTest.java b/test/unit/org/apache/cassandra/tools/nodetool/NodeToolHelpCommandTest.java new file mode 100644 index 000000000000..52ab70a5524d --- /dev/null +++ b/test/unit/org/apache/cassandra/tools/nodetool/NodeToolHelpCommandTest.java @@ -0,0 +1,79 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.tools.nodetool; + +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.stream.Stream; + +import org.apache.commons.lang3.StringUtils; +import org.junit.Test; +import org.junit.runners.Parameterized; + +import static org.junit.Assert.assertTrue; + +public class NodeToolHelpCommandTest extends CQLToolRunnerTester +{ + private static final String NODETOOL_COMMAND_HELP_DIR = "nodetool/help"; + private static final List COMMANDS = List.of("abortbootstrap", "assassinate", "forcecompact", "compact"); + + @Parameterized.Parameter(1) + public String command; + + @Parameterized.Parameters(name = "runner={0}, command={1}") + public static Collection data() + { + List res = new ArrayList<>(); + for (String command : COMMANDS) + for (String tool : runnersMap.keySet()) + res.add(new Object[]{ tool, command }); + return res; + } + + @Test + public void testCompareCommandHelpOutputBetweenTools() throws Exception + { + compareCommandHelpOutput(command); + } + + private void compareCommandHelpOutput(String commandName) throws Exception + { + List origLines = readCommandHelpLines(commandName); + List targetLines = sliceStdout(invokeNodetool("help", commandName)); + String diff = computeDiff(targetLines, origLines); + assertTrue(printFormattedDiffsMessage(origLines, targetLines, commandName, diff), + StringUtils.isBlank(diff)); + } + + private static List readCommandHelpLines(String command) throws Exception + { + List lines = new ArrayList<>(); + try (Stream stream = Files.lines( + Paths.get(NodeToolHelpCommandTest.class.getClassLoader() + .getResource(NODETOOL_COMMAND_HELP_DIR + '/' + command) + .toURI()))) + { + stream.forEach(lines::add); + } + return lines; + } +} diff --git a/test/unit/org/apache/cassandra/tools/nodetool/NodeToolHelpMessageTest.java b/test/unit/org/apache/cassandra/tools/nodetool/NodeToolHelpMessageTest.java deleted file mode 100644 index 37561213b8b3..000000000000 --- a/test/unit/org/apache/cassandra/tools/nodetool/NodeToolHelpMessageTest.java +++ /dev/null @@ -1,110 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.apache.cassandra.tools.nodetool; - -import java.util.ArrayList; -import java.util.List; - -import org.apache.commons.lang3.StringUtils; -import org.junit.Test; - -import com.github.difflib.DiffUtils; -import com.github.difflib.patch.AbstractDelta; -import com.github.difflib.patch.Patch; - -import static org.junit.Assert.assertTrue; - -public class NodeToolHelpMessageTest extends CQLToolRunnerTester -{ - @Test - public void testCompareHelpCommand() - { - List outNodeTool = sliceStdout(invokeNodetoolInJvmV1("help")); - List outNodeToolV2 = sliceStdout(invokeNodetoolInJvmV2("help")); - - String diff = computeDiff(outNodeTool, outNodeToolV2); - assertTrue(printFormattedDiffsMessage(outNodeTool, outNodeToolV2, "help", diff), - StringUtils.isBlank(diff)); - } - - @Test - public void testBaseCommandOutput() - { - List outNodeToolV2 = sliceStdout(invokeNodetoolInJvmV1("help", "forcecompact")); - System.out.println(printFormattedNodeToolOutput(outNodeToolV2)); - } - - @Test - public void testCompareCommandHelpOutputBetweenTools() - { - runCommandHelpOutputComparison("abortbootstrap"); - runCommandHelpOutputComparison("assassinate"); - runCommandHelpOutputComparison("forcecompact"); - runCommandHelpOutputComparison("compact"); - } - - public void runCommandHelpOutputComparison(String commandName) - { - List outNodeTool = sliceStdout(invokeNodetoolInJvmV1("help", commandName)); - List outNodeToolV2 = sliceStdout(invokeNodetoolInJvmV2("help", commandName)); - String diff = computeDiff(outNodeTool, outNodeToolV2); - assertTrue(printFormattedDiffsMessage(outNodeTool, outNodeToolV2, commandName, diff), - StringUtils.isBlank(diff)); - } - - private static String printFormattedDiffsMessage(List stdoutOrig, - List stdoutNew, - String commandName, - String diff) - { - return '\n' + "> NodeTool" + '\n' + - printFormattedNodeToolOutput(stdoutOrig) + - '\n' + "> NodeToolV2" + - '\n' + printFormattedNodeToolOutput(stdoutNew) + - '\n' + " difference for \"" + commandName + "\":" + diff; - } - - private static String printFormattedNodeToolOutput(List output) - { - StringBuilder sb = new StringBuilder(); - for(int i = 0; i < output.size(); i++) - { - sb.append(i).append(':').append(output.get(i)); - if(i < output.size() - 1) - sb.append('\n'); - } - return sb.toString(); - } - - public static String computeDiff(List original, List revised) { - Patch patch = DiffUtils.diff(original, revised); - List diffLines = new ArrayList<>(); - - for (AbstractDelta delta : patch.getDeltas()) { - for (String line : delta.getSource().getLines()) { - diffLines.add(delta.getType().toString().toLowerCase() + " source: " + line); - } - for (String line : delta.getTarget().getLines()) { - diffLines.add(delta.getType().toString().toLowerCase() + " target: " + line); - } - } - - return '\n' + String.join("\n", diffLines); - } -} diff --git a/test/unit/org/apache/cassandra/tools/nodetool/NodeToolMessageTest.java b/test/unit/org/apache/cassandra/tools/nodetool/NodeToolMessageTest.java new file mode 100644 index 000000000000..bfdb051c3e5a --- /dev/null +++ b/test/unit/org/apache/cassandra/tools/nodetool/NodeToolMessageTest.java @@ -0,0 +1,47 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.tools.nodetool; + +import java.util.List; + +import org.apache.commons.lang3.StringUtils; +import org.junit.Test; + +import static org.junit.Assert.assertTrue; + +public class NodeToolMessageTest extends CQLToolRunnerTester +{ + @Test + public void testCompareHelpCommand() + { + List outNodeTool = sliceStdout(invokeNodetoolInJvmV1("help")); + List outNodeToolV2 = sliceStdout(invokeNodetoolInJvmV2("help")); + + String diff = computeDiff(outNodeTool, outNodeToolV2); + assertTrue(printFormattedDiffsMessage(outNodeTool, outNodeToolV2, "help", diff), + StringUtils.isBlank(diff)); + } + + @Test + public void testBaseCommandOutput() + { + List outNodeToolV2 = sliceStdout(invokeNodetoolInJvmV1()); + System.out.println(printFormattedNodeToolOutput(outNodeToolV2)); + } +} From 7695da400d8b389640aa0b93c0e6c94351c83b57 Mon Sep 17 00:00:00 2001 From: Maxim Muzafarov Date: Tue, 2 Jul 2024 17:59:40 +0200 Subject: [PATCH 14/22] update results view for running command tests --- src/java/org/apache/cassandra/management/api/Compact.java | 2 +- .../apache/cassandra/tools/nodetool/CQLToolRunnerTester.java | 2 +- .../cassandra/tools/nodetool/NodeToolHelpCommandTest.java | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/java/org/apache/cassandra/management/api/Compact.java b/src/java/org/apache/cassandra/management/api/Compact.java index 083e9ebd57e7..cda0b7cdb55e 100644 --- a/src/java/org/apache/cassandra/management/api/Compact.java +++ b/src/java/org/apache/cassandra/management/api/Compact.java @@ -34,7 +34,7 @@ @CommandLine.Command(name = "compact", description = "Force a (major) compaction on one or more tables or user-defined compaction on given SSTables") public class Compact extends BaseCommand { - @CassandraCliArgument(usage = "[ ...] or []", + @CassandraCliArgument(usage = "[ ...] or ...", description = "The keyspace followed by one or many tables or list of SSTable data files when using --user-defined") public List args = new ArrayList<>(); @CommandLine.ArgGroup(exclusive = false) diff --git a/test/unit/org/apache/cassandra/tools/nodetool/CQLToolRunnerTester.java b/test/unit/org/apache/cassandra/tools/nodetool/CQLToolRunnerTester.java index 9b45848a582b..dcd02f6c634c 100644 --- a/test/unit/org/apache/cassandra/tools/nodetool/CQLToolRunnerTester.java +++ b/test/unit/org/apache/cassandra/tools/nodetool/CQLToolRunnerTester.java @@ -46,7 +46,7 @@ public abstract class CQLToolRunnerTester extends CQLTester @Parameterized.Parameter public String runner; - @Parameterized.Parameters + @Parameterized.Parameters(name = "runner={0}") public static Collection data() { List res = new ArrayList<>(); runnersMap.forEach((k, v) -> res.add(new Object[]{ k })); diff --git a/test/unit/org/apache/cassandra/tools/nodetool/NodeToolHelpCommandTest.java b/test/unit/org/apache/cassandra/tools/nodetool/NodeToolHelpCommandTest.java index 52ab70a5524d..4e37b088a2d6 100644 --- a/test/unit/org/apache/cassandra/tools/nodetool/NodeToolHelpCommandTest.java +++ b/test/unit/org/apache/cassandra/tools/nodetool/NodeToolHelpCommandTest.java @@ -43,8 +43,8 @@ public class NodeToolHelpCommandTest extends CQLToolRunnerTester public static Collection data() { List res = new ArrayList<>(); - for (String command : COMMANDS) - for (String tool : runnersMap.keySet()) + for (String tool : runnersMap.keySet()) + for (String command : COMMANDS) res.add(new Object[]{ tool, command }); return res; } From ad0bcd6c2b6bd26ac7049c7718c2ee5232b3a994 Mon Sep 17 00:00:00 2001 From: Maxim Muzafarov Date: Tue, 2 Jul 2024 21:29:12 +0200 Subject: [PATCH 15/22] align help output for migrated commands --- .build/build-rat.xml | 1 + .../management/CassandraHelpLayout.java | 199 +++++++++++------- .../cassandra/management/CommandUtils.java | 7 + .../management/api/AbortBootstrap.java | 8 +- .../management/api/JmxConnectionMixin.java | 5 +- .../apache/cassandra/tools/NodeToolV2.java | 11 +- 6 files changed, 142 insertions(+), 89 deletions(-) diff --git a/.build/build-rat.xml b/.build/build-rat.xml index 27e8f63ae43d..5ce5e0dbd72c 100644 --- a/.build/build-rat.xml +++ b/.build/build-rat.xml @@ -55,6 +55,7 @@ + diff --git a/src/java/org/apache/cassandra/management/CassandraHelpLayout.java b/src/java/org/apache/cassandra/management/CassandraHelpLayout.java index 5aabae235bf5..04e13b3a3724 100644 --- a/src/java/org/apache/cassandra/management/CassandraHelpLayout.java +++ b/src/java/org/apache/cassandra/management/CassandraHelpLayout.java @@ -19,20 +19,19 @@ package org.apache.cassandra.management; import java.util.ArrayList; -import java.util.Collection; +import java.util.Collections; import java.util.Comparator; -import java.util.HashSet; import java.util.LinkedHashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; -import java.util.Set; import org.apache.cassandra.utils.Pair; import picocli.CommandLine; import static org.apache.cassandra.management.CommandUtils.findBackwardCompatibleArgument; import static org.apache.cassandra.management.CommandUtils.leadingSpaces; +import static org.apache.cassandra.management.CommandUtils.sortShortestFirst; import static org.apache.commons.lang3.ArrayUtils.isEmpty; import static picocli.CommandLine.Model.UsageMessageSpec.SECTION_KEY_COMMAND_LIST; import static picocli.CommandLine.Model.UsageMessageSpec.SECTION_KEY_COMMAND_LIST_HEADING; @@ -58,7 +57,7 @@ */ public class CassandraHelpLayout extends CommandLine.Help { - public static final int DEFAULT_USAGE_HELP_WIDTH = 90; + public static final int DEFAULT_USAGE_HELP_WIDTH = 85; private static final String DESCRIPTION_HEADING = "NAME%n"; private static final String SYNOPSIS_HEADING = "SYNOPSIS%n"; private static final String OPTIONS_HEADING = "OPTIONS%n"; @@ -138,11 +137,15 @@ private String printDetailedSynopsis(String synopsisPrefix, int columnIndent, bo CommandLine.Model.CommandSpec commandSpec = commandSpec(); ColorScheme colorScheme = colorScheme(); - Set argsInGroups = new HashSet<>(); - Ansi.Text groupsText = createDetailedSynopsisGroupsText(argsInGroups); - List optionsList = createCassandraSynopsisOptionsText(argsInGroups); - Ansi.Text endOfOptionsText = createDetailedSynopsisEndOfOptionsText(); - Ansi.Text positionalParamText = createCassandraSynopsisPositionalsText(argsInGroups); + List parentOptionsList = createCassandraSynopsisOptionsText(parentCommandOptions(commandSpec)); + List commandOptionsList = createCassandraSynopsisOptionsText(commandSpec.options()); + + Ansi.Text positionalParamText = createCassandraSynopsisPositionalsText(commandSpec, colorScheme); + Ansi.Text endOfOptionsText = positionalParamText.plainString().isEmpty() ? + colorScheme.text("") : + colorScheme.text("[") + .concat(commandSpec.parser().endOfOptionsDelimiter()) + .concat("]"); Ansi.Text commandText = createCassandraSynopsisCommandText(); int width = commandSpec.usageMessage().width(); @@ -153,81 +156,62 @@ private String printDetailedSynopsis(String synopsisPrefix, int columnIndent, bo textTable.indentWrappedLines = COLUMN_INDENT; textTable.setAdjustLineBreaksForWideCJKCharacters(commandSpec.usageMessage().adjustLineBreaksForWideCJKCharacters()); - // All other fields added to the synopsis are left-adjusted, so we don't need to align them. - Ansi.Text text = groupsText.concat(isEmptyParent ? Ansi.OFF.new Text(0) : - colorScheme.text(" ").concat(commandSpec.name())) - .concat(endOfOptionsText).concat(" ") - .concat(positionalParamText).concat(commandText); - Ansi.Text padding = Ansi.OFF.new Text(leadingSpaces(mainCommandText.plainString().length()), colorScheme); - List alignedOptions = alignByWidth(optionsList, - width - columnIndent - textTable.indentWrappedLines - synopsisPrefix.length(), - colorScheme); - // Align options by width - for (int i = 0; i < alignedOptions.size(); i++) - { - Ansi.Text option = alignedOptions.get(i); - if (i == 0) - option = colorScheme.text(synopsisPrefix).concat(synopsisPrefix.isEmpty() ? "" : " ") - .concat(mainCommandText).concat(" ") - .concat(option); - else - option = padding.concat(option); - - if (i == alignedOptions.size() - 1) - textTable.addRowValues(option.concat(text)); - else - textTable.addRowValues(option); - } + // List + new LineBreakingLayout(colorScheme, width, textTable) + .concatItem(synopsisPrefix.isEmpty() ? mainCommandText : colorScheme.text(synopsisPrefix).concat(" ").concat(mainCommandText)) + .concatItems(parentOptionsList) + .concatItem(isEmptyParent ? colorScheme.text("") : colorScheme.text(commandSpec.name())) + .concatItems(commandOptionsList) + .concatItem(endOfOptionsText) + // All other fields added to the synopsis are left-adjusted, so we don't need to add them one by one. + .flush(positionalParamText.concat(commandText)); textTable.addEmptyRow(); return textTable.toString(); } - private static List alignByWidth(List optionsList, int width, ColorScheme colorScheme) - { - List result = new ArrayList<>(); - Ansi.Text current = Ansi.OFF.new Text("", colorScheme); - for (Ansi.Text option : optionsList) - { - if (current.plainString().length() + option.plainString().length() >= width) - { - result.add(current); - current = Ansi.OFF.new Text("", colorScheme); - } - current = current.plainString().isEmpty() ? option : current.concat(" ").concat(option); - } - if (!current.plainString().isEmpty()) - result.add(current); - return result; - } - - private Ansi.Text createCassandraSynopsisPositionalsText(Collection done) + private static Ansi.Text createCassandraSynopsisPositionalsText(CommandLine.Model.CommandSpec spec, + ColorScheme colorScheme) { - List positionals = cassandraPositionals(commandSpec()); - positionals.removeAll(done); + List positionals = cassandraPositionals(spec); - Pair commandArgumensSpec = findBackwardCompatibleArgument(commandSpec().userObject()); - Ansi.Text text = colorScheme().text(""); + Pair commandArgumensSpec = findBackwardCompatibleArgument(spec.userObject()); + Ansi.Text text = colorScheme.text(""); // If the command has a backward compatible argument, use it to generate the synopsis based on the old format. if (commandArgumensSpec != null) - return colorScheme().parameterText(commandArgumensSpec.left); + return colorScheme.parameterText(commandArgumensSpec.left); IParamLabelRenderer parameterLabelRenderer = CassandraStyleParamLabelRender.create(); for (CommandLine.Model.PositionalParamSpec positionalParam : positionals) { - Ansi.Text label = parameterLabelRenderer.renderParameterLabel(positionalParam, colorScheme().ansi(), colorScheme().parameterStyles()); + Ansi.Text label = parameterLabelRenderer.renderParameterLabel(positionalParam, colorScheme.ansi(), colorScheme.parameterStyles()); text = text.plainString().isEmpty() ? label : text.concat(" ").concat(label); } return text; } - private List createCassandraSynopsisOptionsText(Collection done) + private static List parentCommandOptions(CommandLine.Model.CommandSpec commandSpec) + { + List hierarhy = new LinkedList<>(); + CommandLine.Model.CommandSpec curr; + while ((curr = commandSpec.parent()) != null) + { + hierarhy.add(curr); + commandSpec = curr; + } + Collections.reverse(hierarhy); + List options = new ArrayList<>(); + for (CommandLine.Model.CommandSpec spec : hierarhy) + options.addAll(spec.options()); + return options; + } + + private List createCassandraSynopsisOptionsText(List options) { // Cassandra uses alphabetical order for options, ordered by short name. - List optionList = new ArrayList<>(commandSpec().options()); + List optionList = new ArrayList<>(options); optionList.sort(createShortOptionNameComparator()); List result = new ArrayList<>(); - optionList.removeAll(done); ColorScheme colorScheme = colorScheme(); IParamLabelRenderer parameterLabelRenderer = CassandraStyleParamLabelRender.create(); @@ -238,16 +222,25 @@ private List createCassandraSynopsisOptionsText(Collection comparator = createShortOptionNameComparator(); - List optionList = commandSpec().options(); - - List options = new ArrayList<>(optionList); + List options = new LinkedList<>(parentCommandOptions(commandSpec())); + options.addAll(commandSpec().options()); options.sort(comparator); Layout layout = cassandraSingleColumnOptionsParametersLayout(); @@ -275,7 +267,11 @@ public String optionList() } @Override - public String endOfOptionsList() { + public String endOfOptionsList() + { + List positionals = cassandraPositionals(commandSpec()); + if (positionals.isEmpty()) + return ""; Layout layout = cassandraSingleColumnOptionsParametersLayout(); layout.addOption(CASSANDRA_END_OF_OPTIONS_OPTION, CassandraStyleParamLabelRender.create()); return layout.toString(); @@ -507,4 +503,55 @@ public Ansi.Text[][] render(CommandLine.Model.PositionalParamSpec param, IParamL return result; } } + + private static class LineBreakingLayout + { + private static final int spaceWidth = 1; + private final ColorScheme colorScheme; + private final int width; + private final int indentWrappedLines; + private final TextTable textTable; + /** Current line being built, always less than width. */ + private Ansi.Text current; + + public LineBreakingLayout(ColorScheme colorScheme, int width, TextTable textTable) + { + this.colorScheme = colorScheme; + this.width = width - textTable.columns()[0].indent; + this.indentWrappedLines = textTable.indentWrappedLines; + this.textTable = textTable; + current = colorScheme.text(""); + } + + public LineBreakingLayout concatItems(List items) + { + for (Ansi.Text item : items) + concatItem(item); + return this; + } + + public LineBreakingLayout concatItem(Ansi.Text item) + { + if (item.plainString().isEmpty()) + return this; + + if (current.plainString().length() + spaceWidth + item.plainString().length() >= width) + { + textTable.addRowValues(current); + current = colorScheme.text(leadingSpaces(indentWrappedLines)).concat(item); + } + else + current = current.plainString().length() == indentWrappedLines ? + current.concat(item) : + current.concat(" ").concat(item); + return this; + } + + public void flush(Ansi.Text end) + { + textTable.addRowValues(current.plainString().length() == indentWrappedLines ? + end : + current.concat(" ").concat(end)); + } + } } diff --git a/src/java/org/apache/cassandra/management/CommandUtils.java b/src/java/org/apache/cassandra/management/CommandUtils.java index bfffd444cbbc..505a95b40478 100644 --- a/src/java/org/apache/cassandra/management/CommandUtils.java +++ b/src/java/org/apache/cassandra/management/CommandUtils.java @@ -21,6 +21,7 @@ import java.lang.reflect.Field; import java.util.Arrays; import java.util.Collection; +import java.util.Comparator; import org.apache.cassandra.utils.Pair; @@ -63,4 +64,10 @@ public static Pair findBackwardCompatibleArgument(Object userObj } return null; } + + public static String[] sortShortestFirst(String[] names) + { + Arrays.sort(names, Comparator.comparing(String::length)); + return names; + } } diff --git a/src/java/org/apache/cassandra/management/api/AbortBootstrap.java b/src/java/org/apache/cassandra/management/api/AbortBootstrap.java index e76e9668a045..a9c1f7ddec27 100644 --- a/src/java/org/apache/cassandra/management/api/AbortBootstrap.java +++ b/src/java/org/apache/cassandra/management/api/AbortBootstrap.java @@ -32,15 +32,15 @@ public class AbortBootstrap extends BaseCommand public String nodeId = EMPTY; @Option(names = "--ip", description = "IP of the node that failed bootstrap") - public String endpoint = EMPTY; + public String ip = EMPTY; @Override public void execute(ServiceMBeanBridge probe) { - if (isEmpty(nodeId) && isEmpty(endpoint)) + if (isEmpty(nodeId) && isEmpty(ip)) throw new IllegalArgumentException("Either --node or --ip needs to be set"); - if (!isEmpty(nodeId) && !isEmpty(endpoint)) + if (!isEmpty(nodeId) && !isEmpty(ip)) throw new IllegalArgumentException("Only one of --node or --ip need to be set"); - probe.ssProxy().abortBootstrap(nodeId, endpoint); + probe.ssProxy().abortBootstrap(nodeId, ip); } } diff --git a/src/java/org/apache/cassandra/management/api/JmxConnectionMixin.java b/src/java/org/apache/cassandra/management/api/JmxConnectionMixin.java index d5c52e57b082..fc8b790fab2e 100644 --- a/src/java/org/apache/cassandra/management/api/JmxConnectionMixin.java +++ b/src/java/org/apache/cassandra/management/api/JmxConnectionMixin.java @@ -40,6 +40,7 @@ /** * Command options for NodeTool commands that are executed via JMX. */ +@CommandLine.Command(name = "connect", description = "Connect to a Cassandra node via JMX") public class JmxConnectionMixin { public static final String MIXIN_KEY = "jmx"; @@ -74,11 +75,11 @@ public class JmxConnectionMixin */ public static int executionStrategy(CommandLine.ParseResult parseResult) { - CommandLine.Model.CommandSpec lastParent = lastExecutableSubcommandWithSameParent(parseResult.asCommandLineList()); - CommandLine.Model.CommandSpec jmx = lastParent.mixins().get(MIXIN_KEY); + CommandLine.Model.CommandSpec jmx = parseResult.commandSpec().mixins().get(MIXIN_KEY); if (jmx == null) throw new CommandLine.InitializationException("No JmxConnect mixin found in the command hierarchy"); + CommandLine.Model.CommandSpec lastParent = lastExecutableSubcommandWithSameParent(parseResult.asCommandLineList()); if (lastParent.userObject() instanceof BaseCommand) ((BaseCommand) lastParent.userObject()).setBridge(((JmxConnectionMixin) jmx.userObject()).init(lastParent)); return new CommandLine.RunLast().execute(parseResult); diff --git a/src/java/org/apache/cassandra/tools/NodeToolV2.java b/src/java/org/apache/cassandra/tools/NodeToolV2.java index adc1f2052382..1d02b445d48d 100644 --- a/src/java/org/apache/cassandra/tools/NodeToolV2.java +++ b/src/java/org/apache/cassandra/tools/NodeToolV2.java @@ -163,13 +163,10 @@ public static CommandLine.Model.CommandSpec lastExecutableSubcommandWithSamePare private static CommandLine createCommandLine(CassandraCliFactory factory) { - CommandLine commandLine = new CommandLine(new TopLevelCommand(), factory); - JmxConnectionMixin mixin = factory.create(JmxConnectionMixin.class); - commandLine.addMixin(JmxConnectionMixin.MIXIN_KEY, mixin); - commandLine.getSubcommands().values().forEach(sub -> sub.addMixin(JmxConnectionMixin.MIXIN_KEY, mixin)); - commandLine.setOut(new PrintWriter(factory.output.out, true)); - commandLine.setErr(new PrintWriter(factory.output.err, true)); - return commandLine; + return new CommandLine(new TopLevelCommand(), factory) + .addMixin(JmxConnectionMixin.MIXIN_KEY, factory.create(JmxConnectionMixin.class)) + .setOut(new PrintWriter(factory.output.out, true)) + .setErr(new PrintWriter(factory.output.err, true)); } private static void configureCliLayout(CommandLine commandLine) From 31479650da452ffae018ed65424c71b48f0127d8 Mon Sep 17 00:00:00 2001 From: Maxim Muzafarov Date: Wed, 3 Jul 2024 18:45:05 +0200 Subject: [PATCH 16/22] command help output must match previously migrated commands --- .../management/CassandraHelpCommand.java | 34 +++++++++- .../management/CassandraHelpLayout.java | 66 ++++++++----------- .../cassandra/management/api/Assassinate.java | 4 +- .../cassandra/management/api/Compact.java | 15 +++-- test/resources/nodetool/help/abortbootstrap | 7 +- test/resources/nodetool/help/assassinate | 18 ++--- test/resources/nodetool/help/compact | 27 ++++---- test/resources/nodetool/help/forcecompact | 13 ++-- .../tools/nodetool/CQLToolRunnerTester.java | 16 +++-- .../nodetool/NodeToolCommandsMBeanTest.java | 41 +++++++++++- .../tools/nodetool/NodeToolMessageTest.java | 6 +- 11 files changed, 153 insertions(+), 94 deletions(-) diff --git a/src/java/org/apache/cassandra/management/CassandraHelpCommand.java b/src/java/org/apache/cassandra/management/CassandraHelpCommand.java index 6d8d6c912b87..23ab472d19b9 100644 --- a/src/java/org/apache/cassandra/management/CassandraHelpCommand.java +++ b/src/java/org/apache/cassandra/management/CassandraHelpCommand.java @@ -19,12 +19,23 @@ package org.apache.cassandra.management; import java.io.PrintWriter; +import java.util.LinkedHashMap; import java.util.Map; import com.google.common.base.Preconditions; import picocli.CommandLine; +import static picocli.CommandLine.Model.UsageMessageSpec.SECTION_KEY_COMMAND_LIST; +import static picocli.CommandLine.Model.UsageMessageSpec.SECTION_KEY_COMMAND_LIST_HEADING; +import static picocli.CommandLine.Model.UsageMessageSpec.SECTION_KEY_EXIT_CODE_LIST; +import static picocli.CommandLine.Model.UsageMessageSpec.SECTION_KEY_EXIT_CODE_LIST_HEADING; +import static picocli.CommandLine.Model.UsageMessageSpec.SECTION_KEY_FOOTER; +import static picocli.CommandLine.Model.UsageMessageSpec.SECTION_KEY_FOOTER_HEADING; +import static picocli.CommandLine.Model.UsageMessageSpec.SECTION_KEY_HEADER; +import static picocli.CommandLine.Model.UsageMessageSpec.SECTION_KEY_HEADER_HEADING; +import static picocli.CommandLine.Model.UsageMessageSpec.SECTION_KEY_SYNOPSIS; + @CommandLine.Command(name = "help", header = "Display help information about the specified command.", synopsisHeading = "%nUsage: ", @@ -91,7 +102,7 @@ public static void printTopCommandUsage(CommandLine command, CommandLine.Help.Co return; } - Map helpSectionMap = CassandraHelpLayout.cassandraTopLevelHelpSectionKeys((CassandraHelpLayout) help); + Map helpSectionMap = cassandraTopLevelHelpSectionKeys((CassandraHelpLayout) help); for (String key : command.getHelpSectionKeys()) { CommandLine.IHelpSectionRenderer renderer = helpSectionMap.get(key); @@ -104,6 +115,27 @@ public static void printTopCommandUsage(CommandLine command, CommandLine.Help.Co writer.flush(); } + /** + * Top-level help command (includes all the available nodetool commands) has a different layout, so we need to + * provide a different set of keys for the help sections. + * @param layout The help class layout. + * @return Map of supported keys for the help sections. + */ + public static Map cassandraTopLevelHelpSectionKeys(CassandraHelpLayout layout) + { + Map sectionMap = new LinkedHashMap<>(); + sectionMap.put(SECTION_KEY_HEADER_HEADING, CommandLine.Help::headerHeading); + sectionMap.put(SECTION_KEY_HEADER, CommandLine.Help::header); + sectionMap.put(SECTION_KEY_SYNOPSIS, layout::topLevelSynopsis); + sectionMap.put(SECTION_KEY_COMMAND_LIST_HEADING, layout::topLevelCommandListHeading); + sectionMap.put(SECTION_KEY_COMMAND_LIST, CommandLine.Help::commandList); + sectionMap.put(SECTION_KEY_EXIT_CODE_LIST_HEADING, CommandLine.Help::exitCodeListHeading); + sectionMap.put(SECTION_KEY_EXIT_CODE_LIST, CommandLine.Help::exitCodeList); + sectionMap.put(SECTION_KEY_FOOTER_HEADING, CommandLine.Help::footerHeading); + sectionMap.put(SECTION_KEY_FOOTER, CommandLine.Help::footer); + return sectionMap; + } + /** * The printHelpIfRequested method calls the init method on commands marked * as helpCommand before the help command's run or call method is called. diff --git a/src/java/org/apache/cassandra/management/CassandraHelpLayout.java b/src/java/org/apache/cassandra/management/CassandraHelpLayout.java index 04e13b3a3724..c292ee152889 100644 --- a/src/java/org/apache/cassandra/management/CassandraHelpLayout.java +++ b/src/java/org/apache/cassandra/management/CassandraHelpLayout.java @@ -21,7 +21,6 @@ import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; -import java.util.LinkedHashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; @@ -61,6 +60,7 @@ public class CassandraHelpLayout extends CommandLine.Help private static final String DESCRIPTION_HEADING = "NAME%n"; private static final String SYNOPSIS_HEADING = "SYNOPSIS%n"; private static final String OPTIONS_HEADING = "OPTIONS%n"; + private static final String FOOTER_HEADING = "%n"; private static final int DESCRIPTION_INDENT = 4; public static final int COLUMN_INDENT = 8; public static final int SUBCOMMANDS_INDENT = 4; @@ -75,6 +75,7 @@ public class CassandraHelpLayout extends CommandLine.Help public static final String TOP_LEVEL_COMMAND_HEADING = "The most commonly used nodetool commands are:"; public static final String USAGE_HELP_FOOTER = "See 'nodetool help ' for more information on a specific command."; public static final String SYNOPSIS_SUBCOMMANDS_LABEL = " []"; + private static final String[] EMPTY_FOOTER = new String[0]; public CassandraHelpLayout(CommandLine.Model.CommandSpec spec, ColorScheme scheme) { @@ -296,10 +297,11 @@ public String parameterList() { Pair cassandraArgument = findBackwardCompatibleArgument(commandSpec().userObject()); List positionalParams = cassandraPositionals(commandSpec()); + TextTable table = configureLayoutTextTable(); Layout layout = cassandraArgument == null ? cassandraSingleColumnOptionsParametersLayout() : new Layout(colorScheme(), - configureLayoutTextTable(), + table, new CassandraStyleOptionRenderer(), new CassandraStyleParameterRenderer()) { @@ -325,6 +327,7 @@ public void addAllPositionalParameters(List subcommands) return table.toString(); } + @Override + public String footerHeading(Object... params) + { + return createHeading(FOOTER_HEADING, params); + } + @Override public String footer(Object... params) { - String[] footer = isEmpty(commandSpec().usageMessage().footer()) ? new String[]{ USAGE_HELP_FOOTER } : - commandSpec().usageMessage().footer(); - StringBuilder sb = new StringBuilder(); - TextTable table = TextTable.forColumnWidths(ansi(), commandSpec().usageMessage().width()); + String[] footer; + if (commandSpec().parent() == null) + footer = isEmpty(commandSpec().usageMessage().footer()) ? new String[]{ USAGE_HELP_FOOTER } : + commandSpec().usageMessage().footer(); + else + footer = EMPTY_FOOTER; + + TextTable table = TextTable.forColumns(colorScheme(), new Column(commandSpec().usageMessage().width(), 0, Column.Overflow.WRAP)); table.setAdjustLineBreaksForWideCJKCharacters(commandSpec().usageMessage().adjustLineBreaksForWideCJKCharacters()); table.indentWrappedLines = 0; + for (String summaryLine : footer) table.addRowValues(String.format(summaryLine, params)); - table.toString(sb); + table.addEmptyRow(); return table.toString(); } - public String topLevelCommandListHeading(Object... params) { + public String topLevelCommandListHeading(Object... params) + { return createHeading(TOP_LEVEL_COMMAND_HEADING + "%n", params); } @@ -413,27 +428,6 @@ public static List cassandraHelpSectionKeys() return result; } - /** - * Top-level help command (includes all the available nodetool commands) has a different layout, so we need to - * provide a different set of keys for the help sections. - * @param layout The help class layout. - * @return Map of supported keys for the help sections. - */ - public static Map cassandraTopLevelHelpSectionKeys(CassandraHelpLayout layout) - { - Map sectionMap = new LinkedHashMap<>(); - sectionMap.put(SECTION_KEY_HEADER_HEADING, CommandLine.Help::headerHeading); - sectionMap.put(SECTION_KEY_HEADER, CommandLine.Help::header); - sectionMap.put(SECTION_KEY_SYNOPSIS, layout::topLevelSynopsis); - sectionMap.put(SECTION_KEY_COMMAND_LIST_HEADING, layout::topLevelCommandListHeading); - sectionMap.put(SECTION_KEY_COMMAND_LIST, CommandLine.Help::commandList); - sectionMap.put(SECTION_KEY_EXIT_CODE_LIST_HEADING, CommandLine.Help::exitCodeListHeading); - sectionMap.put(SECTION_KEY_EXIT_CODE_LIST, CommandLine.Help::exitCodeList); - sectionMap.put(SECTION_KEY_FOOTER_HEADING, CommandLine.Help::footerHeading); - sectionMap.put(SECTION_KEY_FOOTER, CommandLine.Help::footer); - return sectionMap; - } - private static Ansi.Text spacedParamLabel(CommandLine.Model.OptionSpec optionSpec, IParamLabelRenderer parameterLabelRenderer, ColorScheme scheme) @@ -507,18 +501,16 @@ public Ansi.Text[][] render(CommandLine.Model.PositionalParamSpec param, IParamL private static class LineBreakingLayout { private static final int spaceWidth = 1; - private final ColorScheme colorScheme; private final int width; - private final int indentWrappedLines; private final TextTable textTable; + private final Ansi.Text padding; /** Current line being built, always less than width. */ private Ansi.Text current; public LineBreakingLayout(ColorScheme colorScheme, int width, TextTable textTable) { - this.colorScheme = colorScheme; this.width = width - textTable.columns()[0].indent; - this.indentWrappedLines = textTable.indentWrappedLines; + this.padding = colorScheme.text(leadingSpaces(textTable.indentWrappedLines)); this.textTable = textTable; current = colorScheme.text(""); } @@ -538,10 +530,10 @@ public LineBreakingLayout concatItem(Ansi.Text item) if (current.plainString().length() + spaceWidth + item.plainString().length() >= width) { textTable.addRowValues(current); - current = colorScheme.text(leadingSpaces(indentWrappedLines)).concat(item); + current = padding.concat(item); } else - current = current.plainString().length() == indentWrappedLines ? + current = current == padding || current.plainString().isEmpty() ? current.concat(item) : current.concat(" ").concat(item); return this; @@ -549,9 +541,7 @@ public LineBreakingLayout concatItem(Ansi.Text item) public void flush(Ansi.Text end) { - textTable.addRowValues(current.plainString().length() == indentWrappedLines ? - end : - current.concat(" ").concat(end)); + textTable.addRowValues(current == padding ? end : current.concat(" ").concat(end)); } } } diff --git a/src/java/org/apache/cassandra/management/api/Assassinate.java b/src/java/org/apache/cassandra/management/api/Assassinate.java index 785aec4538ad..1da182a0ca10 100644 --- a/src/java/org/apache/cassandra/management/api/Assassinate.java +++ b/src/java/org/apache/cassandra/management/api/Assassinate.java @@ -27,11 +27,11 @@ public class Assassinate extends BaseCommand { @CommandLine.Parameters(description = "IP address of the endpoint to assassinate", arity = "1") - public String ip_address = EMPTY; + public String ipAddress = EMPTY; @Override public void execute(ServiceMBeanBridge probe) { - probe.ssProxy().assassinateEndpoint(ip_address); + probe.ssProxy().assassinateEndpoint(ipAddress); } } diff --git a/src/java/org/apache/cassandra/management/api/Compact.java b/src/java/org/apache/cassandra/management/api/Compact.java index cda0b7cdb55e..fd48bf9c0c6b 100644 --- a/src/java/org/apache/cassandra/management/api/Compact.java +++ b/src/java/org/apache/cassandra/management/api/Compact.java @@ -37,18 +37,21 @@ public class Compact extends BaseCommand @CassandraCliArgument(usage = "[ ...] or ...", description = "The keyspace followed by one or many tables or list of SSTable data files when using --user-defined") public List args = new ArrayList<>(); - @CommandLine.ArgGroup(exclusive = false) + @CommandLine.ArgGroup public TablesArgs tablesArgs = new TablesArgs(); - @CommandLine.ArgGroup(exclusive = false) + @CommandLine.ArgGroup public UserDefinedArgs userDefinedArgs = new UserDefinedArgs(); public static class TablesArgs { - @CommandLine.Option(names = { "-st", "--start-token"}, description = "Use -st to specify a token at which the compaction range starts (inclusive)") + @CommandLine.Option(names = { "-st", "--start-token"}, + description = "Use -st to specify a token at which the compaction range starts (inclusive)") public String startToken = EMPTY; - @CommandLine.Option(names = { "-et", "--end-token"}, description = "Use -et to specify a token at which compaction range ends (inclusive)") + @CommandLine.Option(names = { "-et", "--end-token"}, + description = "Use -et to specify a token at which compaction range ends (inclusive)") public String endToken = EMPTY; - @CommandLine.Option(names = { "--partition"}, description = "String representation of the partition key") + @CommandLine.Option(names = { "--partition"}, + description = "String representation of the partition key") public String partitionKey = EMPTY; @CommandLine.Parameters(index = "0", arity = "1", description = "The keyspace followed by one or many tables") public String keyspace; @@ -58,7 +61,7 @@ public static class TablesArgs public static class UserDefinedArgs { - @CommandLine.Option(names = { "--user-defined"}, description = "Use --user-defined to submit listed files for user-defined compaction") + @CommandLine.Option(names = { "--user-defined" }, description = "Use --user-defined to submit listed files for user-defined compaction") public boolean userDefined = false; @CommandLine.Parameters(index = "0..*", arity = "1", description = "The SSTable files to be compacted") public String[] sstableFiles; diff --git a/test/resources/nodetool/help/abortbootstrap b/test/resources/nodetool/help/abortbootstrap index 5d1d382fbb0e..c6493ba31d82 100644 --- a/test/resources/nodetool/help/abortbootstrap +++ b/test/resources/nodetool/help/abortbootstrap @@ -6,7 +6,7 @@ SYNOPSIS [(-pp | --print-port)] [(-pw | --password )] [(-pwf | --password-file )] [(-u | --username )] abortbootstrap [--ip ] - [--node ] + [--node ] OPTIONS -h , --host @@ -15,7 +15,7 @@ OPTIONS --ip IP of the node that failed bootstrap - --node + --node Node ID of the node that failed bootstrap -p , --port @@ -31,5 +31,4 @@ OPTIONS Path to the JMX password file -u , --username - Remote jmx agent username - + Remote jmx agent username \ No newline at end of file diff --git a/test/resources/nodetool/help/assassinate b/test/resources/nodetool/help/assassinate index baa42ed8feae..c8c34e401154 100644 --- a/test/resources/nodetool/help/assassinate +++ b/test/resources/nodetool/help/assassinate @@ -1,12 +1,14 @@ NAME nodetool assassinate - Forcefully remove a dead node without - re-replicating any data. Use as a last resort if you cannot removenode + re-replicating any data. Use as a last resort if you cannot + removenode SYNOPSIS nodetool [(-h | --host )] [(-p | --port )] [(-pp | --print-port)] [(-pw | --password )] [(-pwf | --password-file )] - [(-u | --username )] assassinate [--] + [(-u | --username )] assassinate [--] + OPTIONS -h , --host @@ -28,11 +30,9 @@ OPTIONS Remote jmx agent username -- - This option can be used to separate command-line options from the - list of argument, (useful when arguments might be mistaken for - command-line options - - - IP address of the endpoint to assassinate - + This option can be used to separate command-line options from + the list of argument, (useful when arguments might be mistaken + for command-line options + + IP address of the endpoint to assassinate \ No newline at end of file diff --git a/test/resources/nodetool/help/compact b/test/resources/nodetool/help/compact index b6269f16a6f4..a9ca45ccfba7 100644 --- a/test/resources/nodetool/help/compact +++ b/test/resources/nodetool/help/compact @@ -1,19 +1,19 @@ NAME - nodetool compact - Force a (major) compaction on one or more tables or - user-defined compaction on given SSTables + nodetool compact - Force a (major) compaction on one or more tables + or user-defined compaction on given SSTables SYNOPSIS nodetool [(-h | --host )] [(-p | --port )] [(-pp | --print-port)] [(-pw | --password )] [(-pwf | --password-file )] [(-u | --username )] compact - [(-et | --end-token )] - [--partition ] [(-s | --split-output)] - [(-st | --start-token )] [--user-defined] + [(-et | --end-token )] + [--partition ] [(-s | --split-output)] + [(-st | --start-token )] [--user-defined] [--] [ ...] or ... OPTIONS - -et , --end-token + -et , --end-token Use -et to specify a token at which compaction range ends (inclusive) @@ -23,7 +23,7 @@ OPTIONS -p , --port Remote jmx agent port number - --partition + --partition String representation of the partition key -pp, --print-port @@ -38,7 +38,7 @@ OPTIONS -s, --split-output Use -s to not create a single big file - -st , --start-token + -st , --start-token Use -st to specify a token at which the compaction range starts (inclusive) @@ -50,11 +50,10 @@ OPTIONS compaction -- - This option can be used to separate command-line options from the - list of argument, (useful when arguments might be mistaken for - command-line options + This option can be used to separate command-line options from + the list of argument, (useful when arguments might be mistaken + for command-line options [ ...] or ... - The keyspace followed by one or many tables or list of SSTable data - files when using --user-defined - + The keyspace followed by one or many tables or list of SSTable + data files when using --user-defined \ No newline at end of file diff --git a/test/resources/nodetool/help/forcecompact b/test/resources/nodetool/help/forcecompact index 53af21abb212..b248f0ac88f6 100644 --- a/test/resources/nodetool/help/forcecompact +++ b/test/resources/nodetool/help/forcecompact @@ -5,8 +5,8 @@ SYNOPSIS nodetool [(-h | --host )] [(-p | --port )] [(-pp | --print-port)] [(-pw | --password )] [(-pwf | --password-file )] - [(-u | --username )] forcecompact [--] [ -
] + [(-u | --username )] forcecompact [--] + [
] OPTIONS -h , --host @@ -28,11 +28,10 @@ OPTIONS Remote jmx agent username -- - This option can be used to separate command-line options from the - list of argument, (useful when arguments might be mistaken for - command-line options + This option can be used to separate command-line options from + the list of argument, (useful when arguments might be mistaken + for command-line options [
] The keyspace, table, and a list of partition keys ignoring the - gc_grace_seconds - + gc_grace_seconds \ No newline at end of file diff --git a/test/unit/org/apache/cassandra/tools/nodetool/CQLToolRunnerTester.java b/test/unit/org/apache/cassandra/tools/nodetool/CQLToolRunnerTester.java index dcd02f6c634c..1ee337042d45 100644 --- a/test/unit/org/apache/cassandra/tools/nodetool/CQLToolRunnerTester.java +++ b/test/unit/org/apache/cassandra/tools/nodetool/CQLToolRunnerTester.java @@ -18,6 +18,7 @@ package org.apache.cassandra.tools.nodetool; +import java.text.DecimalFormat; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; @@ -40,8 +41,8 @@ public abstract class CQLToolRunnerTester extends CQLTester { public static final Map runnersMap = Map.of( - "invokeNodetoolInJvmV1", CQLToolRunnerTester::invokeNodetoolInJvmV1, - "invokeNodetoolInJvmV2", CQLToolRunnerTester::invokeNodetoolInJvmV2); + "invokeNodetoolV1InJvm", CQLToolRunnerTester::invokeNodetoolV1InJvm, + "invokeNodetoolV2InJvm", CQLToolRunnerTester::invokeNodetoolV2InJvm); @Parameterized.Parameter public String runner; @@ -73,12 +74,12 @@ protected ToolRunner.ToolResult invokeNodetool(String... args) return runnersMap.get(runner).execute(args); } - public static ToolRunner.ToolResult invokeNodetoolInJvmV2(String... commands) + public static ToolRunner.ToolResult invokeNodetoolV2InJvm(String... commands) { return ToolRunner.invokeNodetoolInJvm(NodeToolV2::new, commands); } - public static ToolRunner.ToolResult invokeNodetoolInJvmV1(String... commands) + public static ToolRunner.ToolResult invokeNodetoolV1InJvm(String... commands) { return ToolRunner.invokeNodetoolInJvm(NodeTool::new, commands); } @@ -102,7 +103,7 @@ protected static String printFormattedDiffsMessage(List stdoutOrig, { return '\n' + ">> source <<" + '\n' + printFormattedNodeToolOutput(stdoutOrig) + - '\n' + ">> target <<" + + '\n' + ">> result <<" + '\n' + printFormattedNodeToolOutput(stdoutNew) + '\n' + " difference for \"" + commandName + "\":" + diff; } @@ -110,9 +111,10 @@ protected static String printFormattedDiffsMessage(List stdoutOrig, protected static String printFormattedNodeToolOutput(List output) { StringBuilder sb = new StringBuilder(); + DecimalFormat df = new DecimalFormat("000"); for(int i = 0; i < output.size(); i++) { - sb.append(i).append(':').append(output.get(i)); + sb.append(df.format(i)).append(':').append(output.get(i)); if(i < output.size() - 1) sb.append('\n'); } @@ -128,7 +130,7 @@ protected static String computeDiff(List original, List revised) diffLines.add(delta.getType().toString().toLowerCase() + " source: " + line); } for (String line : delta.getTarget().getLines()) { - diffLines.add(delta.getType().toString().toLowerCase() + " target: " + line); + diffLines.add(delta.getType().toString().toLowerCase() + " result: " + line); } } diff --git a/test/unit/org/apache/cassandra/tools/nodetool/NodeToolCommandsMBeanTest.java b/test/unit/org/apache/cassandra/tools/nodetool/NodeToolCommandsMBeanTest.java index 4cb385de70bb..7379b1c67f8b 100644 --- a/test/unit/org/apache/cassandra/tools/nodetool/NodeToolCommandsMBeanTest.java +++ b/test/unit/org/apache/cassandra/tools/nodetool/NodeToolCommandsMBeanTest.java @@ -57,7 +57,7 @@ public void unregisterMocks() } @Test - public void testAssassinate() throws Throwable + public void testAssassinate() { String ip = "10.20.113.11"; StorageServiceMBean mock = mbeanMockHodler.getMock(STORAGE_SERVICE_MBEAN); @@ -67,7 +67,7 @@ public void testAssassinate() throws Throwable } @Test - public void testAbortBootstrap() throws Throwable + public void testAbortBootstrap() { String nodeId = "1"; String ip = "10.20.113.11"; @@ -79,7 +79,7 @@ public void testAbortBootstrap() throws Throwable } @Test - public void testForceKeyspaceCompactionForPartitionKey() throws Throwable + public void testCompactForceKeyspaceCompactionForPartitionKey() throws Throwable { long token = 42; long key = Murmur3Partitioner.LongToken.keyForToken(token).getLong(); @@ -91,6 +91,41 @@ public void testForceKeyspaceCompactionForPartitionKey() throws Throwable Mockito.verify(mock).forceKeyspaceCompactionForPartitionKey(keyspace(), Long.toString(key), table); } + @Test + public void testCompactForceKeyspaceCompactionForTokenRange() throws Throwable + { + long token = 11; + long key = Murmur3Partitioner.LongToken.keyForToken(token).getLong(); + String startToken = Long.toString(key - 1); + String endToken = Long.toString(key + 1); + String table = "table"; + StorageServiceMBean mock = mbeanMockHodler.getMock(STORAGE_SERVICE_MBEAN); + when(mock.getKeyspaces()).thenReturn(List.of(keyspace())); + when(mock.getNonSystemKeyspaces()).thenReturn(List.of(keyspace())); + invokeNodetool("compact", "--start-token", startToken, "--end-token", endToken, keyspace(), table).assertOnCleanExit(); + Mockito.verify(mock).forceKeyspaceCompactionForTokenRange(keyspace(), startToken, endToken, table); + } + + @Test + public void testCompactForceKeyspaceCompaction() throws Throwable + { + String table = "table"; + StorageServiceMBean mock = mbeanMockHodler.getMock(STORAGE_SERVICE_MBEAN); + when(mock.getKeyspaces()).thenReturn(List.of(keyspace())); + when(mock.getNonSystemKeyspaces()).thenReturn(List.of(keyspace())); + invokeNodetool("compact", "--split-output", keyspace(), table).assertOnCleanExit(); + Mockito.verify(mock).forceKeyspaceCompaction(true, keyspace(), table); + } + + @Test + public void testCompactForceUserDefinedCompaction() throws Throwable + { + String[] ssTables = new String[] { "ssTable1", "ssTable2" }; + CompactionManagerMBean mock = mbeanMockHodler.getMock(COMPACTION_MANAGER_MBEAN); + invokeNodetool("compact", "--user-defined", ssTables[0], ssTables[1]).assertOnCleanExit(); + Mockito.verify(mock).forceUserDefinedCompaction(String.join(",", ssTables)); + } + private static class MBeanMockHodler { private static final Map> mbeans = Map.of( diff --git a/test/unit/org/apache/cassandra/tools/nodetool/NodeToolMessageTest.java b/test/unit/org/apache/cassandra/tools/nodetool/NodeToolMessageTest.java index bfdb051c3e5a..6002e0f33544 100644 --- a/test/unit/org/apache/cassandra/tools/nodetool/NodeToolMessageTest.java +++ b/test/unit/org/apache/cassandra/tools/nodetool/NodeToolMessageTest.java @@ -30,8 +30,8 @@ public class NodeToolMessageTest extends CQLToolRunnerTester @Test public void testCompareHelpCommand() { - List outNodeTool = sliceStdout(invokeNodetoolInJvmV1("help")); - List outNodeToolV2 = sliceStdout(invokeNodetoolInJvmV2("help")); + List outNodeTool = sliceStdout(invokeNodetoolV1InJvm("help")); + List outNodeToolV2 = sliceStdout(invokeNodetoolV2InJvm("help")); String diff = computeDiff(outNodeTool, outNodeToolV2); assertTrue(printFormattedDiffsMessage(outNodeTool, outNodeToolV2, "help", diff), @@ -41,7 +41,7 @@ public void testCompareHelpCommand() @Test public void testBaseCommandOutput() { - List outNodeToolV2 = sliceStdout(invokeNodetoolInJvmV1()); + List outNodeToolV2 = sliceStdout(invokeNodetoolV1InJvm("help", "abortbootstrap")); System.out.println(printFormattedNodeToolOutput(outNodeToolV2)); } } From 5e6e50750d5fc5b4ae90bd1c14e3f8541cac7170 Mon Sep 17 00:00:00 2001 From: Maxim Muzafarov Date: Wed, 3 Jul 2024 19:56:40 +0200 Subject: [PATCH 17/22] fix parse args for compact command --- .../cassandra/management/api/Compact.java | 61 +++++++------------ 1 file changed, 23 insertions(+), 38 deletions(-) diff --git a/src/java/org/apache/cassandra/management/api/Compact.java b/src/java/org/apache/cassandra/management/api/Compact.java index fd48bf9c0c6b..2093d9120cfc 100644 --- a/src/java/org/apache/cassandra/management/api/Compact.java +++ b/src/java/org/apache/cassandra/management/api/Compact.java @@ -20,8 +20,6 @@ import java.util.ArrayList; import java.util.List; -import com.google.common.collect.Lists; - import org.apache.cassandra.management.BaseCommand; import org.apache.cassandra.management.CassandraCliArgument; import org.apache.cassandra.management.ServiceMBeanBridge; @@ -36,48 +34,32 @@ public class Compact extends BaseCommand { @CassandraCliArgument(usage = "[ ...] or ...", description = "The keyspace followed by one or many tables or list of SSTable data files when using --user-defined") + @CommandLine.Parameters(index = "0..*", description = "The keyspace followed by one or many tables or " + + "list of SSTable data files when using --user-defined") public List args = new ArrayList<>(); - @CommandLine.ArgGroup - public TablesArgs tablesArgs = new TablesArgs(); - @CommandLine.ArgGroup - public UserDefinedArgs userDefinedArgs = new UserDefinedArgs(); - public static class TablesArgs - { - @CommandLine.Option(names = { "-st", "--start-token"}, - description = "Use -st to specify a token at which the compaction range starts (inclusive)") - public String startToken = EMPTY; - @CommandLine.Option(names = { "-et", "--end-token"}, - description = "Use -et to specify a token at which compaction range ends (inclusive)") - public String endToken = EMPTY; - @CommandLine.Option(names = { "--partition"}, - description = "String representation of the partition key") - public String partitionKey = EMPTY; - @CommandLine.Parameters(index = "0", arity = "1", description = "The keyspace followed by one or many tables") - public String keyspace; - @CommandLine.Parameters(index = "1..*", arity = "1", description = "The tables to be compacted") - public String[] tables; - } + @CommandLine.Option(names = { "-s", "--split-output" }, description = "Use -s to not create a single big file") + public boolean splitOutput = false; - public static class UserDefinedArgs - { - @CommandLine.Option(names = { "--user-defined" }, description = "Use --user-defined to submit listed files for user-defined compaction") - public boolean userDefined = false; - @CommandLine.Parameters(index = "0..*", arity = "1", description = "The SSTable files to be compacted") - public String[] sstableFiles; - } + @CommandLine.Option(names = { "--user-defined" }, + description = "Use --user-defined to submit listed files for user-defined compaction") + public boolean userDefined = false; - @CommandLine.Option(names = { "-s", "--split-output"}, description = "Use -s to not create a single big file") - public boolean splitOutput = false; + @CommandLine.Option(names = { "-st", "--start-token" }, + description = "Use -st to specify a token at which the compaction range starts (inclusive)") + public String startToken = EMPTY; + + @CommandLine.Option(names = { "-et", "--end-token" }, + description = "Use -et to specify a token at which compaction range ends (inclusive)") + public String endToken = EMPTY; + + @CommandLine.Option(names = { "--partition" }, + description = "String representation of the partition key") + public String partitionKey = EMPTY; @Override public void execute(ServiceMBeanBridge probe) { - final boolean userDefined = userDefinedArgs.userDefined; - final String startToken = tablesArgs.startToken; - final String endToken = tablesArgs.endToken; - final String partitionKey = tablesArgs.partitionKey; - args = userDefined ? List.of(userDefinedArgs.sstableFiles) : Lists.asList(tablesArgs.keyspace, tablesArgs.tables); final boolean startEndTokenProvided = !(startToken.isEmpty() && endToken.isEmpty()); final boolean partitionKeyProvided = !partitionKey.isEmpty(); final boolean tokenProvided = startEndTokenProvided || partitionKeyProvided; @@ -96,7 +78,9 @@ public void execute(ServiceMBeanBridge probe) { String userDefinedFiles = String.join(",", args); probe.cmProxy().forceUserDefinedCompaction(userDefinedFiles); - } catch (Exception e) { + } + catch (Exception e) + { throw new RuntimeException("Error occurred during user defined compaction", e); } return; @@ -121,7 +105,8 @@ else if (partitionKeyProvided) { probe.ssProxy().forceKeyspaceCompaction(splitOutput, keyspace, tableNames); } - } catch (Exception e) + } + catch (Exception e) { throw new RuntimeException("Error occurred during compaction", e); } From 520ee15229def904139f6af62f18d05b65a6a063 Mon Sep 17 00:00:00 2001 From: Maxim Muzafarov Date: Wed, 3 Jul 2024 20:04:17 +0200 Subject: [PATCH 18/22] rename parameter usage helper --- src/java/org/apache/cassandra/management/CommandUtils.java | 4 ++-- .../{CassandraCliArgument.java => ParameterUsage.java} | 6 +++--- src/java/org/apache/cassandra/management/api/Compact.java | 4 ++-- .../org/apache/cassandra/management/api/ForceCompact.java | 4 ++-- 4 files changed, 9 insertions(+), 9 deletions(-) rename src/java/org/apache/cassandra/management/{CassandraCliArgument.java => ParameterUsage.java} (86%) diff --git a/src/java/org/apache/cassandra/management/CommandUtils.java b/src/java/org/apache/cassandra/management/CommandUtils.java index 505a95b40478..5363c941b298 100644 --- a/src/java/org/apache/cassandra/management/CommandUtils.java +++ b/src/java/org/apache/cassandra/management/CommandUtils.java @@ -56,9 +56,9 @@ public static Pair findBackwardCompatibleArgument(Object userObj Class clazz = userObject.getClass(); for (Field field : clazz.getFields()) { - if (field.isAnnotationPresent(CassandraCliArgument.class)) + if (field.isAnnotationPresent(ParameterUsage.class)) { - CassandraCliArgument ann = field.getAnnotation(CassandraCliArgument.class); + ParameterUsage ann = field.getAnnotation(ParameterUsage.class); return Pair.create(ann.usage(), ann.description()); } } diff --git a/src/java/org/apache/cassandra/management/CassandraCliArgument.java b/src/java/org/apache/cassandra/management/ParameterUsage.java similarity index 86% rename from src/java/org/apache/cassandra/management/CassandraCliArgument.java rename to src/java/org/apache/cassandra/management/ParameterUsage.java index 12b8058678ce..1f541dfe58f1 100644 --- a/src/java/org/apache/cassandra/management/CassandraCliArgument.java +++ b/src/java/org/apache/cassandra/management/ParameterUsage.java @@ -24,12 +24,12 @@ import java.lang.annotation.Target; /** - * Argument annotation for Cassandra commands, used to provide metadata - * for command-line argument for backward compatibility purposes. + * Argument annotation for Cassandra commands, used to provide a message + * for command-line argument for backward compatibility when help is requested. */ @Retention(RetentionPolicy.RUNTIME) @Target({ ElementType.FIELD }) -public @interface CassandraCliArgument +public @interface ParameterUsage { String description() default ""; String usage() default ""; diff --git a/src/java/org/apache/cassandra/management/api/Compact.java b/src/java/org/apache/cassandra/management/api/Compact.java index 2093d9120cfc..078c3f7b8fba 100644 --- a/src/java/org/apache/cassandra/management/api/Compact.java +++ b/src/java/org/apache/cassandra/management/api/Compact.java @@ -21,7 +21,7 @@ import java.util.List; import org.apache.cassandra.management.BaseCommand; -import org.apache.cassandra.management.CassandraCliArgument; +import org.apache.cassandra.management.ParameterUsage; import org.apache.cassandra.management.ServiceMBeanBridge; import picocli.CommandLine; @@ -32,7 +32,7 @@ @CommandLine.Command(name = "compact", description = "Force a (major) compaction on one or more tables or user-defined compaction on given SSTables") public class Compact extends BaseCommand { - @CassandraCliArgument(usage = "[ ...] or ...", + @ParameterUsage(usage = "[ ...] or ...", description = "The keyspace followed by one or many tables or list of SSTable data files when using --user-defined") @CommandLine.Parameters(index = "0..*", description = "The keyspace followed by one or many tables or " + "list of SSTable data files when using --user-defined") diff --git a/src/java/org/apache/cassandra/management/api/ForceCompact.java b/src/java/org/apache/cassandra/management/api/ForceCompact.java index a22e109e8581..cba5148f3e89 100644 --- a/src/java/org/apache/cassandra/management/api/ForceCompact.java +++ b/src/java/org/apache/cassandra/management/api/ForceCompact.java @@ -23,7 +23,7 @@ import com.google.common.collect.Lists; import org.apache.cassandra.management.BaseCommand; -import org.apache.cassandra.management.CassandraCliArgument; +import org.apache.cassandra.management.ParameterUsage; import org.apache.cassandra.management.ServiceMBeanBridge; import picocli.CommandLine; @@ -33,7 +33,7 @@ @CommandLine.Command(name = "forcecompact", description = "Force a (major) compaction on a table") public class ForceCompact extends BaseCommand { - @CassandraCliArgument(usage = "[
]", + @ParameterUsage(usage = "[
]", description = "The keyspace, table, and a list of partition keys ignoring the gc_grace_seconds") public List args; From d77f3509e66331e933074fc9495b9c60a1751e27 Mon Sep 17 00:00:00 2001 From: Maxim Muzafarov Date: Thu, 4 Jul 2024 17:09:48 +0200 Subject: [PATCH 19/22] add command invoker to close connection --- .../cassandra/management/BaseCommand.java | 14 ++-- ...mxConnectionMixin.java => JmxConnect.java} | 84 ++++++++++++++++--- .../apache/cassandra/tools/NodeToolV2.java | 8 +- 3 files changed, 86 insertions(+), 20 deletions(-) rename src/java/org/apache/cassandra/management/api/{JmxConnectionMixin.java => JmxConnect.java} (61%) diff --git a/src/java/org/apache/cassandra/management/BaseCommand.java b/src/java/org/apache/cassandra/management/BaseCommand.java index 3cfca1240c9d..50a2909df714 100644 --- a/src/java/org/apache/cassandra/management/BaseCommand.java +++ b/src/java/org/apache/cassandra/management/BaseCommand.java @@ -18,16 +18,11 @@ package org.apache.cassandra.management; -import picocli.CommandLine; - /** * Base class for all nodetool commands. */ public abstract class BaseCommand implements Runnable { - /** The command specification, used to access command-specific properties. */ - @CommandLine.Spec - protected CommandLine.Model.CommandSpec spec; // injected by picocli /** The ServiceBridge instance to interact with the Cassandra node. */ protected ServiceMBeanBridge bridge; @@ -41,6 +36,15 @@ public void setBridge(ServiceMBeanBridge bridge) this.bridge = bridge; } + /** + * Returns the ServiceBridge instance used to interact with the Cassandra node. + * @return The ServiceBridge instance. + */ + public ServiceMBeanBridge getBridge() + { + return bridge; + } + @Override public void run() { diff --git a/src/java/org/apache/cassandra/management/api/JmxConnectionMixin.java b/src/java/org/apache/cassandra/management/api/JmxConnect.java similarity index 61% rename from src/java/org/apache/cassandra/management/api/JmxConnectionMixin.java rename to src/java/org/apache/cassandra/management/api/JmxConnect.java index fc8b790fab2e..bec5ba3cecc8 100644 --- a/src/java/org/apache/cassandra/management/api/JmxConnectionMixin.java +++ b/src/java/org/apache/cassandra/management/api/JmxConnect.java @@ -41,10 +41,14 @@ * Command options for NodeTool commands that are executed via JMX. */ @CommandLine.Command(name = "connect", description = "Connect to a Cassandra node via JMX") -public class JmxConnectionMixin +public class JmxConnect extends BaseCommand implements AutoCloseable { public static final String MIXIN_KEY = "jmx"; + /** The command specification, used to access command-specific properties. */ + @CommandLine.Spec + protected CommandLine.Model.CommandSpec spec; // injected by picocli + @CommandLine.Option(names = { "-h", "--host" }, description = "Node hostname or ip address") public String host = "127.0.0.1"; @@ -77,21 +81,28 @@ public static int executionStrategy(CommandLine.ParseResult parseResult) { CommandLine.Model.CommandSpec jmx = parseResult.commandSpec().mixins().get(MIXIN_KEY); if (jmx == null) - throw new CommandLine.InitializationException("No JmxConnect mixin found in the command hierarchy"); + throw new CommandLine.InitializationException("No JmxConnect command found in the top-level hierarchy"); - CommandLine.Model.CommandSpec lastParent = lastExecutableSubcommandWithSameParent(parseResult.asCommandLineList()); - if (lastParent.userObject() instanceof BaseCommand) - ((BaseCommand) lastParent.userObject()).setBridge(((JmxConnectionMixin) jmx.userObject()).init(lastParent)); - return new CommandLine.RunLast().execute(parseResult); + try (CommandInvoker invoker = new CommandInvoker((JmxConnect) jmx.userObject())) + { + return invoker.execute(parseResult); + } + catch (CommandInvoker.CloseException e) + { + jmx.commandLine() + .getErr() + .println("Failed to connect to JMX: " + e.getMessage()); + return CommandLine.ExitCode.SOFTWARE; + } } /** - * Initialize the JMX connection to the Cassandra node. - * @param spec The command specification to be executed after the initialization. - * @return The ServiceBridge instance to interact with the Cassandra node. + * Initialize the JMX connection to the Cassandra node using the provided options. */ - private ServiceMBeanBridge init(CommandLine.Model.CommandSpec spec) + @Override + protected void execute(ServiceMBeanBridge probe) { + assert probe == null; try { if (isNotEmpty(username)) { @@ -102,7 +113,7 @@ private ServiceMBeanBridge init(CommandLine.Model.CommandSpec spec) password = promptAndReadPassword(); } - return username.isEmpty() ? nodeProbeFactory.create(host, parseInt(port)) + bridge = username.isEmpty() ? nodeProbeFactory.create(host, parseInt(port)) : nodeProbeFactory.create(host, parseInt(port), username, password); } catch (IOException | SecurityException e) @@ -113,4 +124,55 @@ private ServiceMBeanBridge init(CommandLine.Model.CommandSpec spec) throw new CommandLine.ExecutionException(spec.commandLine(), "Failed to connect to JMX", e); } } + + @Override + public void close() throws Exception + { + if (bridge instanceof AutoCloseable) + ((AutoCloseable) bridge).close(); + } + + private static class CommandInvoker implements CommandLine.IExecutionStrategy, AutoCloseable + { + private final JmxConnect connect; + + public CommandInvoker(JmxConnect connect) + { + this.connect = connect; + } + + @Override + public int execute(CommandLine.ParseResult parseResult) throws CommandLine.ExecutionException, CommandLine.ParameterException + { + CommandLine.Model.CommandSpec lastParent = lastExecutableSubcommandWithSameParent(parseResult.asCommandLineList()); + if (lastParent.userObject() instanceof BaseCommand) + { + connect.run(); + ((BaseCommand) lastParent.userObject()).setBridge(connect.getBridge()); + } + return new CommandLine.RunLast().execute(parseResult); + } + + @Override + public void close() throws CloseException + { + try + { + if (connect.getBridge() instanceof AutoCloseable) + ((AutoCloseable) connect.getBridge()).close(); + } + catch (Exception e) + { + throw new CloseException("Failed to close JMX connection", e); + } + } + + private static class CloseException extends RuntimeException + { + public CloseException(String message, Throwable cause) + { + super(message, cause); + } + } + } } diff --git a/src/java/org/apache/cassandra/tools/NodeToolV2.java b/src/java/org/apache/cassandra/tools/NodeToolV2.java index 1d02b445d48d..529e89807a7b 100644 --- a/src/java/org/apache/cassandra/tools/NodeToolV2.java +++ b/src/java/org/apache/cassandra/tools/NodeToolV2.java @@ -29,7 +29,7 @@ import org.apache.cassandra.config.CassandraRelevantProperties; import org.apache.cassandra.management.CassandraHelpLayout; -import org.apache.cassandra.management.api.JmxConnectionMixin; +import org.apache.cassandra.management.api.JmxConnect; import org.apache.cassandra.management.api.TopLevelCommand; import org.apache.cassandra.utils.FBUtilities; import picocli.CommandLine; @@ -85,7 +85,7 @@ protected int execute(CommandLine commandLine, String... args) try { configureCliLayout(commandLine); - commandLine.setExecutionStrategy(strategy == null ? JmxConnectionMixin::executionStrategy : strategy) + commandLine.setExecutionStrategy(strategy == null ? JmxConnect::executionStrategy : strategy) .setExecutionExceptionHandler(executionExceptionHandler) .setParameterExceptionHandler(parameterExceptionHandler); @@ -111,7 +111,7 @@ public NodeToolV2 withCommandNameFilter(Predicate commandPredicate, int CommandLine.Model.CommandSpec spec = lastExecutableSubcommandWithSameParent(parsed.asCommandLineList()); if (commandPredicate.test(spec.name())) return exitCodeWhenMatched; - return JmxConnectionMixin.executionStrategy(parsed); + return JmxConnect.executionStrategy(parsed); }; return this; } @@ -164,7 +164,7 @@ public static CommandLine.Model.CommandSpec lastExecutableSubcommandWithSamePare private static CommandLine createCommandLine(CassandraCliFactory factory) { return new CommandLine(new TopLevelCommand(), factory) - .addMixin(JmxConnectionMixin.MIXIN_KEY, factory.create(JmxConnectionMixin.class)) + .addMixin(JmxConnect.MIXIN_KEY, factory.create(JmxConnect.class)) .setOut(new PrintWriter(factory.output.out, true)) .setErr(new PrintWriter(factory.output.err, true)); } From 91bb01f7e4c47fdc9ceda663cfb6dd5236beb767 Mon Sep 17 00:00:00 2001 From: Maxim Muzafarov Date: Thu, 4 Jul 2024 20:36:35 +0200 Subject: [PATCH 20/22] fix log output for tool runner --- .../cassandra/management/BaseCommand.java | 7 +++++ .../cassandra/management/api/JmxConnect.java | 7 ++--- .../apache/cassandra/tools/NodeToolV2.java | 27 +++++++++++-------- .../org/apache/cassandra/tools/Output.java | 10 +++++++ .../apache/cassandra/tools/ToolRunner.java | 15 ++++++++--- 5 files changed, 47 insertions(+), 19 deletions(-) diff --git a/src/java/org/apache/cassandra/management/BaseCommand.java b/src/java/org/apache/cassandra/management/BaseCommand.java index 50a2909df714..71534320a7bb 100644 --- a/src/java/org/apache/cassandra/management/BaseCommand.java +++ b/src/java/org/apache/cassandra/management/BaseCommand.java @@ -18,11 +18,18 @@ package org.apache.cassandra.management; +import javax.inject.Inject; + +import org.apache.cassandra.tools.Output; + /** * Base class for all nodetool commands. */ public abstract class BaseCommand implements Runnable { + @Inject + protected Output logger; + /** The ServiceBridge instance to interact with the Cassandra node. */ protected ServiceMBeanBridge bridge; diff --git a/src/java/org/apache/cassandra/management/api/JmxConnect.java b/src/java/org/apache/cassandra/management/api/JmxConnect.java index bec5ba3cecc8..e808c54e5a3c 100644 --- a/src/java/org/apache/cassandra/management/api/JmxConnect.java +++ b/src/java/org/apache/cassandra/management/api/JmxConnect.java @@ -26,7 +26,6 @@ import org.apache.cassandra.management.BaseCommand; import org.apache.cassandra.management.ServiceMBeanBridge; import org.apache.cassandra.tools.INodeProbeFactory; -import org.apache.cassandra.tools.Output; import picocli.CommandLine; import static java.lang.Integer.parseInt; @@ -69,8 +68,6 @@ public class JmxConnect extends BaseCommand implements AutoCloseable @Inject private INodeProbeFactory nodeProbeFactory; - @Inject - private Output output; /** * This method is called by picocli and used depending on the execution strategy. @@ -92,7 +89,7 @@ public static int executionStrategy(CommandLine.ParseResult parseResult) jmx.commandLine() .getErr() .println("Failed to connect to JMX: " + e.getMessage()); - return CommandLine.ExitCode.SOFTWARE; + return jmx.commandLine().getExitCodeExceptionMapper().getExitCode(e); } } @@ -119,7 +116,7 @@ protected void execute(ServiceMBeanBridge probe) catch (IOException | SecurityException e) { Throwable rootCause = Throwables.getRootCause(e); - output.err.printf("nodetool: Failed to connect to '%s:%s' - %s: '%s'.%n", host, port, + logger.error("nodetool: Failed to connect to '%s:%s' - %s: '%s'.%n", host, port, rootCause.getClass().getSimpleName(), rootCause.getMessage()); throw new CommandLine.ExecutionException(spec.commandLine(), "Failed to connect to JMX", e); } diff --git a/src/java/org/apache/cassandra/tools/NodeToolV2.java b/src/java/org/apache/cassandra/tools/NodeToolV2.java index 529e89807a7b..8b3400e68057 100644 --- a/src/java/org/apache/cassandra/tools/NodeToolV2.java +++ b/src/java/org/apache/cassandra/tools/NodeToolV2.java @@ -196,19 +196,24 @@ public K create(Class cls) { try { - Object bean = this.fallback.create(cls); - Field[] fields = bean.getClass().getDeclaredFields(); - for (Field field : fields) + K bean = this.fallback.create(cls); + Class beanClass = bean.getClass(); + do { - if (!field.isAnnotationPresent(Inject.class)) - continue; - field.setAccessible(true); - if (field.getType().equals(INodeProbeFactory.class)) - field.set(bean, nodeProbeFactory); - else if (field.getType().equals(Output.class)) - field.set(bean, output); + Field[] fields = beanClass.getDeclaredFields(); + for (Field field : fields) + { + if (!field.isAnnotationPresent(Inject.class)) + continue; + field.setAccessible(true); + if (field.getType().equals(INodeProbeFactory.class)) + field.set(bean, nodeProbeFactory); + else if (field.getType().equals(Output.class)) + field.set(bean, output); + } } - return (K) bean; + while ((beanClass = beanClass.getSuperclass()) != null); + return bean; } catch (Exception e) { diff --git a/src/java/org/apache/cassandra/tools/Output.java b/src/java/org/apache/cassandra/tools/Output.java index 1d2fcb3a6325..db0b22fad652 100644 --- a/src/java/org/apache/cassandra/tools/Output.java +++ b/src/java/org/apache/cassandra/tools/Output.java @@ -32,4 +32,14 @@ public Output(PrintStream out, PrintStream err) this.out = out; this.err = err; } + + public void info(String msg, Object... args) + { + out.printf(msg, args); + } + + public void error(String msg, Object... args) + { + err.printf(msg, args); + } } diff --git a/test/unit/org/apache/cassandra/tools/ToolRunner.java b/test/unit/org/apache/cassandra/tools/ToolRunner.java index 4b4ddf92ab6f..90aa8e34c78c 100644 --- a/test/unit/org/apache/cassandra/tools/ToolRunner.java +++ b/test/unit/org/apache/cassandra/tools/ToolRunner.java @@ -33,6 +33,7 @@ import java.util.Map; import java.util.Objects; import java.util.function.BiFunction; +import java.util.function.Consumer; import java.util.function.Supplier; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -318,8 +319,8 @@ public NodeToolResult get() public static ToolRunner.ToolResult invokeNodetoolInJvm(BiFunction factory, String... commands) { - ListOutputStream out = new ListOutputStream(); - ListOutputStream err = new ListOutputStream(); + LinesOutputStream out = new LinesOutputStream(logger::info); + LinesOutputStream err = new LinesOutputStream(logger::error); List args = CQLTester.buildNodetoolArgs(isEmpty(commands) ? new ArrayList<>() : List.of(commands)); args.remove("bin/nodetool"); try @@ -772,10 +773,16 @@ public ToolResult invoke() } } - private static class ListOutputStream extends OutputStream + private static class LinesOutputStream extends OutputStream { private final List outputLines = new ArrayList<>(); private final StringBuilder buffer = new StringBuilder(); + private final Consumer logger; + + public LinesOutputStream(Consumer logger) + { + this.logger = logger; + } @Override public void write(int b) @@ -785,6 +792,7 @@ public void write(int b) { // Add the buffer to the list if it's a new line outputLines.add(buffer.toString()); + logger.accept(buffer.toString()); buffer.setLength(0); // Clear the buffer } else @@ -796,6 +804,7 @@ public void flush() if (buffer.length() > 0) { outputLines.add(buffer.toString()); + logger.accept(buffer.toString()); buffer.setLength(0); } } From dc0fbf1bd6fdb31e2a033e2f12dadac33d721735 Mon Sep 17 00:00:00 2001 From: Maxim Muzafarov Date: Fri, 5 Jul 2024 14:32:03 +0200 Subject: [PATCH 21/22] rename command invoker --- .../org/apache/cassandra/management/api/JmxConnect.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/java/org/apache/cassandra/management/api/JmxConnect.java b/src/java/org/apache/cassandra/management/api/JmxConnect.java index e808c54e5a3c..142e418eb6d8 100644 --- a/src/java/org/apache/cassandra/management/api/JmxConnect.java +++ b/src/java/org/apache/cassandra/management/api/JmxConnect.java @@ -80,11 +80,11 @@ public static int executionStrategy(CommandLine.ParseResult parseResult) if (jmx == null) throw new CommandLine.InitializationException("No JmxConnect command found in the top-level hierarchy"); - try (CommandInvoker invoker = new CommandInvoker((JmxConnect) jmx.userObject())) + try (JmxConnectionCommandInvoker invoker = new JmxConnectionCommandInvoker((JmxConnect) jmx.userObject())) { return invoker.execute(parseResult); } - catch (CommandInvoker.CloseException e) + catch (JmxConnectionCommandInvoker.CloseException e) { jmx.commandLine() .getErr() @@ -129,11 +129,11 @@ public void close() throws Exception ((AutoCloseable) bridge).close(); } - private static class CommandInvoker implements CommandLine.IExecutionStrategy, AutoCloseable + private static class JmxConnectionCommandInvoker implements CommandLine.IExecutionStrategy, AutoCloseable { private final JmxConnect connect; - public CommandInvoker(JmxConnect connect) + public JmxConnectionCommandInvoker(JmxConnect connect) { this.connect = connect; } From 2671e3d075c31898d01ea93a74064455fbe7887a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tibor=20R=C3=A9p=C3=A1si?= Date: Fri, 5 Jul 2024 14:41:13 +0200 Subject: [PATCH 22/22] migrate version command --- .../management/api/TopLevelCommand.java | 3 +- .../nodetool => management/api}/Version.java | 28 ++--- .../org/apache/cassandra/tools/NodeTool.java | 1 - .../apache/cassandra/tools/NodeToolV2.java | 1 + .../apache/cassandra/tools/ToolRunner.java | 39 +++++++ .../cassandra/tools/nodetool/VersionTest.java | 100 ++++++++++++++++++ 6 files changed, 156 insertions(+), 16 deletions(-) rename src/java/org/apache/cassandra/{tools/nodetool => management/api}/Version.java (56%) create mode 100644 test/unit/org/apache/cassandra/tools/nodetool/VersionTest.java diff --git a/src/java/org/apache/cassandra/management/api/TopLevelCommand.java b/src/java/org/apache/cassandra/management/api/TopLevelCommand.java index 125d32fcedba..54eb6b628a5c 100644 --- a/src/java/org/apache/cassandra/management/api/TopLevelCommand.java +++ b/src/java/org/apache/cassandra/management/api/TopLevelCommand.java @@ -29,7 +29,8 @@ AbortBootstrap.class, Assassinate.class, ForceCompact.class, - Compact.class }) + Compact.class, + Version.class}) public class TopLevelCommand implements Runnable { @CommandLine.Spec diff --git a/src/java/org/apache/cassandra/tools/nodetool/Version.java b/src/java/org/apache/cassandra/management/api/Version.java similarity index 56% rename from src/java/org/apache/cassandra/tools/nodetool/Version.java rename to src/java/org/apache/cassandra/management/api/Version.java index 6556a046272f..fc8d24a009bc 100644 --- a/src/java/org/apache/cassandra/tools/nodetool/Version.java +++ b/src/java/org/apache/cassandra/management/api/Version.java @@ -15,26 +15,26 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.apache.cassandra.tools.nodetool; +package org.apache.cassandra.management.api; -import io.airlift.airline.Command; -import io.airlift.airline.Option; -import org.apache.cassandra.tools.NodeProbe; -import org.apache.cassandra.tools.NodeTool.NodeToolCmd; +import org.apache.cassandra.management.BaseCommand; +import org.apache.cassandra.management.ServiceMBeanBridge; +import picocli.CommandLine; +import picocli.CommandLine.Option; -@Command(name = "version", description = "Print cassandra version") -public class Version extends NodeToolCmd +@CommandLine.Command(name = "version", description = "Print cassandra version") +public class Version extends BaseCommand { - @Option(title = "verbose", - name = {"-v", "--verbose"}, - description = "Include additional information") + @Option(names = {"-v", "--verbose"}, description = "Include addtitional information") private boolean verbose = false; @Override - public void execute(NodeProbe probe) + protected void execute(ServiceMBeanBridge probe) { - probe.output().out.println("ReleaseVersion: " + probe.getReleaseVersion()); - if (verbose) - probe.output().out.println("GitSHA: " + probe.getGitSHA()); + logger.out.println("ReleaseVersion: " + probe.ssProxy().getReleaseVersion()); + if (verbose) { + logger.out.println("GitSHA: " + probe.ssProxy().getGitSHA()); + } + } } diff --git a/src/java/org/apache/cassandra/tools/NodeTool.java b/src/java/org/apache/cassandra/tools/NodeTool.java index f9dc61eb3bb1..8df3a1625b8c 100644 --- a/src/java/org/apache/cassandra/tools/NodeTool.java +++ b/src/java/org/apache/cassandra/tools/NodeTool.java @@ -241,7 +241,6 @@ public int execute(String... args) UpdateCIDRGroup.class, UpgradeSSTable.class, Verify.class, - Version.class, ViewBuildStatus.class ); diff --git a/src/java/org/apache/cassandra/tools/NodeToolV2.java b/src/java/org/apache/cassandra/tools/NodeToolV2.java index 8b3400e68057..a19404729e4c 100644 --- a/src/java/org/apache/cassandra/tools/NodeToolV2.java +++ b/src/java/org/apache/cassandra/tools/NodeToolV2.java @@ -192,6 +192,7 @@ public CassandraCliFactory(INodeProbeFactory nodeProbeFactory, Output output) this.output = output; } + @Override public K create(Class cls) { try diff --git a/test/unit/org/apache/cassandra/tools/ToolRunner.java b/test/unit/org/apache/cassandra/tools/ToolRunner.java index 90aa8e34c78c..fd5042e0d243 100644 --- a/test/unit/org/apache/cassandra/tools/ToolRunner.java +++ b/test/unit/org/apache/cassandra/tools/ToolRunner.java @@ -40,6 +40,7 @@ import com.google.common.base.Throwables; import com.google.common.collect.ImmutableList; + import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -51,6 +52,7 @@ import static com.github.jknack.handlebars.internal.lang3.ArrayUtils.isEmpty; import static org.assertj.core.api.Assertions.assertThat; + import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; @@ -561,6 +563,43 @@ public AssertHelp errorContainsAny(String... messages) return this; } + public AssertHelp outputContains(String messages) + { + return outputContainsAny(messages); + } + + public AssertHelp outputContainsAny(String... messages) + { + assertThat(messages).hasSizeGreaterThan(0); + assertThat(stdout).isNotNull(); + if (!Stream.of(messages).anyMatch(stdout::contains)) + fail("stdout does not contain " + Arrays.toString(messages)); + return this; + } + + public AssertHelp outputMatches(String messages) + { + return outputMatchesAny(messages); + } + + public AssertHelp outputMatchesAny(String... messages) + { + assertThat(messages).hasSizeGreaterThan(0); + assertThat(stdout).isNotNull(); + if (!Stream.of(messages).anyMatch(stdout::matches)) + fail("stdout does not contain " + Arrays.toString(messages)); + return this; + } + + public AssertHelp outputLinesMatchesAll(String... messages) + { + assertThat(messages).hasSizeGreaterThan(0); + assertThat(stdout).isNotNull(); + if (!Stream.of(messages).allMatch((String msg) -> stdout.lines().anyMatch((String line) -> line.matches(msg)))) + fail("stdout does not match " + Arrays.toString(messages)); + return this; + } + private void fail(String msg) { StringBuilder sb = new StringBuilder(); diff --git a/test/unit/org/apache/cassandra/tools/nodetool/VersionTest.java b/test/unit/org/apache/cassandra/tools/nodetool/VersionTest.java new file mode 100644 index 000000000000..2e064b5291b0 --- /dev/null +++ b/test/unit/org/apache/cassandra/tools/nodetool/VersionTest.java @@ -0,0 +1,100 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.cassandra.tools.nodetool; + +import org.junit.Test; + +public class VersionTest extends CQLToolRunnerTester +{ + private static final String SUBCOMMAND = "version"; + + @Test + public void testHelp() + { + String help = """ + NAME + nodetool version - Print cassandra version + + SYNOPSIS + nodetool [(-h | --host )] [(-p | --port )] + [(-pp | --print-port)] [(-pw | --password )] + [(-pwf | --password-file )] + [(-u | --username )] version [(-v | --verbose)] + + OPTIONS + -h , --host + Node hostname or ip address + + -p , --port + Remote jmx agent port number + + -pp, --print-port + Operate in 4.0 mode with hosts disambiguated by port number + + -pw , --password + Remote jmx agent password + + -pwf , --password-file + Path to the JMX password file + + -u , --username + Remote jmx agent username + + -v, --verbose + Include addtitional information + """; + + invokeNodetool("help", SUBCOMMAND) + .asserts() + .success() + .outputContains(help); + } + + @Test + public void testOutput() + { + invokeNodetool(SUBCOMMAND) + .asserts() + .success() + .outputMatches("ReleaseVersion: \\d+.*"); + } + + @Test + public void testVerboseOutput1() + { + invokeNodetool(SUBCOMMAND, "-v") + .asserts() + .success() + .outputLinesMatchesAll( + "ReleaseVersion: \\d+.*", + "GitSHA: [0-9a-f]{40}.*" + ); + } + + @Test + public void testVerboseOutput2() + { + invokeNodetool(SUBCOMMAND, "--verbose") + .asserts() + .success() + .outputLinesMatchesAll( + "ReleaseVersion: \\d+.*", + "GitSHA: [0-9a-f]{40}.*" + ); + } +}