wombatu-kun commented on code in PR #19970:
URL: https://github.com/apache/hudi/pull/19970#discussion_r4023844461
##########
hudi-cli/src/main/java/org/apache/hudi/cli/commands/SparkMain.java:
##########
@@ -139,8 +171,96 @@ public static void addAppArgs(SparkLauncher sparkLauncher,
SparkMain.SparkComman
sparkLauncher.addAppArgs(args);
}
- public static void main(String[] args) {
- ValidationUtils.checkArgument(args.length >= 4);
+ /**
+ * Adds the command invocation as fully named options: {@code --command X
--master Y --memory Z}
+ * plus {@code -Dkey=value} pairs for every command argument. This makes the
launcher command
+ * self-describing and removes the risk of mis-aligned positional arguments
at call sites.
+ *
+ * @param namedArgs flat name/value pairs; {@code null} values are skipped,
preserving the
+ * legacy semantics of absent trailing arguments (e.g.
propsFilePath)
+ */
+ public static void addNamedAppArgs(SparkLauncher sparkLauncher,
SparkMain.SparkCommand cmd, String master, String memory, String... namedArgs) {
+ ValidationUtils.checkArgument(namedArgs.length % 2 == 0, "namedArgs must
be flat name/value pairs");
+ sparkLauncher.addAppArgs("--command", cmd.toString());
+ sparkLauncher.addAppArgs("--master", master);
+ sparkLauncher.addAppArgs("--memory", memory);
+ for (int i = 0; i < namedArgs.length; i += 2) {
+ if (namedArgs[i + 1] != null) {
+ sparkLauncher.addAppArgs("-D" + namedArgs[i] + "=" + namedArgs[i + 1]);
+ }
+ }
+ }
+
+ private static boolean isNamedInvocation(String[] argv) {
+ return argv.length > 0 && argv[0].startsWith("-");
+ }
+
+ /**
+ * Translates {@code --command X --master Y --memory Z -Dkey=value...} into
the positional
+ * form {@code [X, Y, Z, arg1, arg2, ...]} expected by the legacy invocation
path. The named
+ * -D arguments are mapped onto the command's positional argument names;
unknown -D keys are
+ * passed through as trailing configs, matching {@link
SparkCommand#makeConfigs(String[])}.
+ */
+ private static String[] namedArgsToPositional(String[] argv) {
+ NamedArgs namedArgs = new NamedArgs();
+ JCommander commander = JCommander.newBuilder()
+ .addObject(namedArgs)
+ .build();
+ commander.parse(argv);
+
+ SparkCommand command = SparkCommand.valueOf(namedArgs.command);
+ Map<String, String> params = namedArgs.params;
+ List<String> positionalArgs = new ArrayList<>(command.paramNames.length);
+ for (String paramName : command.paramNames) {
+ String value = params.get(paramName);
+ // absent named args map to empty strings to keep the positional slots
aligned; trailing
+ // empty slots are handled by getPropsFilePath/makeConfigs exactly like
the legacy form
+ positionalArgs.add(value != null ? value.trim() : "");
+ }
+ // extra -D keys are forwarded as trailing configs, mirroring makeConfigs
behavior
+ List<String> extraConfigs = new ArrayList<>();
+ for (Map.Entry<String, String> entry : params.entrySet()) {
+ if (!Arrays.asList(command.paramNames).contains(entry.getKey())) {
+ extraConfigs.add(entry.getKey() + "=" + entry.getValue());
+ }
+ }
+
+ List<String> positional = new ArrayList<>();
+ positional.add(namedArgs.command);
+ positional.add(namedArgs.master);
+ positional.add(namedArgs.memory);
+ positional.addAll(positionalArgs);
+ positional.addAll(extraConfigs);
+ return positional.toArray(new String[0]);
+ }
+
+ /**
+ * Named-argument form of a SparkMain invocation. Kept separate from the
legacy
+ * positional protocol so that existing call sites and scripts keep working
unchanged:
+ * if the first argument does not look like a named option the legacy path
is used.
+ */
+ private static class NamedArgs {
+ @Parameter(names = {"--command", "-command"}, description = "Name of the
Spark command to run (e.g. ROLLBACK)", required = true)
+ private String command;
+
+ @Parameter(names = {"--master", "-master"}, description = "Spark master
URL (was positional arg 2)", required = true)
+ private String master;
+
+ @Parameter(names = {"--memory", "-memory"}, description = "Spark driver
memory (was positional arg 3)", required = true)
+ private String memory;
+
+ @DynamicParameter(names = "-D", description = "Command arguments and spark
configs as -Dkey=value pairs")
+ private Map<String, String> params = new HashMap<>();
Review Comment:
NamedArgs declares no main parameter, so the bare `key=value` app args that
UtilHelpers.validateAndAddProperties appends right after addNamedAppArgs make
JCommander throw "no main parameter was defined in your arg class", breaking
`--hoodieConfigs` for bootstrap, cleans run, and every compaction and
clustering command. Add a main `@Parameter List<String>` to NamedArgs and
append it to the positional tail where makeConfigs already expects trailing
configs.
##########
hudi-cli/pom.xml:
##########
@@ -236,6 +236,12 @@
<version>${project.version}</version>
</dependency>
+ <!-- JCommander for SparkMain named-argument CLI (HUDI-96) -->
+ <dependency>
+ <groupId>com.beust</groupId>
+ <artifactId>jcommander</artifactId>
Review Comment:
The new dependency is not carried into packaging/hudi-cli-bundle/pom.xml,
whose shade includes omit com.beust:jcommander while every other bundle that
uses it lists and relocates it - and SparkUtil.initLauncher launches SparkMain
out of that bundle with no lib directory beside it. Add the include and the
matching com.beust.jcommander relocation so the named path also works outside
the dev layout.
##########
hudi-cli/src/main/java/org/apache/hudi/cli/commands/SparkMain.java:
##########
@@ -139,8 +171,96 @@ public static void addAppArgs(SparkLauncher sparkLauncher,
SparkMain.SparkComman
sparkLauncher.addAppArgs(args);
}
- public static void main(String[] args) {
- ValidationUtils.checkArgument(args.length >= 4);
+ /**
+ * Adds the command invocation as fully named options: {@code --command X
--master Y --memory Z}
+ * plus {@code -Dkey=value} pairs for every command argument. This makes the
launcher command
+ * self-describing and removes the risk of mis-aligned positional arguments
at call sites.
+ *
+ * @param namedArgs flat name/value pairs; {@code null} values are skipped,
preserving the
+ * legacy semantics of absent trailing arguments (e.g.
propsFilePath)
+ */
+ public static void addNamedAppArgs(SparkLauncher sparkLauncher,
SparkMain.SparkCommand cmd, String master, String memory, String... namedArgs) {
+ ValidationUtils.checkArgument(namedArgs.length % 2 == 0, "namedArgs must
be flat name/value pairs");
Review Comment:
This only checks that the array has even length, and namedArgsToPositional
sizes the positional array from cmd.paramNames rather than from what was
passed, so dropping a whole name/value pair or misspelling a name leaves an
empty slot no assertEq can see - dropping "basePath" from the CLEAN call site
would run HoodieCleaner against an empty base path. Check each supplied name
against cmd.paramNames here, the way addAppArgs checks arity.
##########
hudi-cli/src/main/java/org/apache/hudi/cli/commands/SparkMain.java:
##########
@@ -139,8 +171,96 @@ public static void addAppArgs(SparkLauncher sparkLauncher,
SparkMain.SparkComman
sparkLauncher.addAppArgs(args);
}
- public static void main(String[] args) {
- ValidationUtils.checkArgument(args.length >= 4);
+ /**
+ * Adds the command invocation as fully named options: {@code --command X
--master Y --memory Z}
+ * plus {@code -Dkey=value} pairs for every command argument. This makes the
launcher command
+ * self-describing and removes the risk of mis-aligned positional arguments
at call sites.
+ *
+ * @param namedArgs flat name/value pairs; {@code null} values are skipped,
preserving the
+ * legacy semantics of absent trailing arguments (e.g.
propsFilePath)
+ */
+ public static void addNamedAppArgs(SparkLauncher sparkLauncher,
SparkMain.SparkCommand cmd, String master, String memory, String... namedArgs) {
+ ValidationUtils.checkArgument(namedArgs.length % 2 == 0, "namedArgs must
be flat name/value pairs");
+ sparkLauncher.addAppArgs("--command", cmd.toString());
+ sparkLauncher.addAppArgs("--master", master);
+ sparkLauncher.addAppArgs("--memory", memory);
+ for (int i = 0; i < namedArgs.length; i += 2) {
+ if (namedArgs[i + 1] != null) {
+ sparkLauncher.addAppArgs("-D" + namedArgs[i] + "=" + namedArgs[i + 1]);
+ }
+ }
+ }
+
+ private static boolean isNamedInvocation(String[] argv) {
+ return argv.length > 0 && argv[0].startsWith("-");
+ }
+
+ /**
+ * Translates {@code --command X --master Y --memory Z -Dkey=value...} into
the positional
+ * form {@code [X, Y, Z, arg1, arg2, ...]} expected by the legacy invocation
path. The named
+ * -D arguments are mapped onto the command's positional argument names;
unknown -D keys are
+ * passed through as trailing configs, matching {@link
SparkCommand#makeConfigs(String[])}.
+ */
+ private static String[] namedArgsToPositional(String[] argv) {
Review Comment:
namedArgsToPositional is private and nothing exercises main's argv handling,
so the only new logic here ships untested and the 3 + paramNames.length ==
minArgsCount relation holds for 23 enum constants purely by hand-counting. Make
it @VisibleForTesting static and add a test that walks SparkCommand and asserts
the translated length against minArgsCount.
##########
hudi-cli/src/main/java/org/apache/hudi/cli/commands/SparkMain.java:
##########
@@ -139,8 +171,96 @@ public static void addAppArgs(SparkLauncher sparkLauncher,
SparkMain.SparkComman
sparkLauncher.addAppArgs(args);
}
- public static void main(String[] args) {
- ValidationUtils.checkArgument(args.length >= 4);
+ /**
+ * Adds the command invocation as fully named options: {@code --command X
--master Y --memory Z}
+ * plus {@code -Dkey=value} pairs for every command argument. This makes the
launcher command
+ * self-describing and removes the risk of mis-aligned positional arguments
at call sites.
+ *
+ * @param namedArgs flat name/value pairs; {@code null} values are skipped,
preserving the
+ * legacy semantics of absent trailing arguments (e.g.
propsFilePath)
+ */
+ public static void addNamedAppArgs(SparkLauncher sparkLauncher,
SparkMain.SparkCommand cmd, String master, String memory, String... namedArgs) {
+ ValidationUtils.checkArgument(namedArgs.length % 2 == 0, "namedArgs must
be flat name/value pairs");
+ sparkLauncher.addAppArgs("--command", cmd.toString());
+ sparkLauncher.addAppArgs("--master", master);
+ sparkLauncher.addAppArgs("--memory", memory);
+ for (int i = 0; i < namedArgs.length; i += 2) {
+ if (namedArgs[i + 1] != null) {
+ sparkLauncher.addAppArgs("-D" + namedArgs[i] + "=" + namedArgs[i + 1]);
+ }
+ }
+ }
+
+ private static boolean isNamedInvocation(String[] argv) {
+ return argv.length > 0 && argv[0].startsWith("-");
+ }
+
+ /**
+ * Translates {@code --command X --master Y --memory Z -Dkey=value...} into
the positional
+ * form {@code [X, Y, Z, arg1, arg2, ...]} expected by the legacy invocation
path. The named
+ * -D arguments are mapped onto the command's positional argument names;
unknown -D keys are
+ * passed through as trailing configs, matching {@link
SparkCommand#makeConfigs(String[])}.
+ */
+ private static String[] namedArgsToPositional(String[] argv) {
+ NamedArgs namedArgs = new NamedArgs();
+ JCommander commander = JCommander.newBuilder()
+ .addObject(namedArgs)
+ .build();
+ commander.parse(argv);
+
+ SparkCommand command = SparkCommand.valueOf(namedArgs.command);
+ Map<String, String> params = namedArgs.params;
+ List<String> positionalArgs = new ArrayList<>(command.paramNames.length);
+ for (String paramName : command.paramNames) {
+ String value = params.get(paramName);
+ // absent named args map to empty strings to keep the positional slots
aligned; trailing
+ // empty slots are handled by getPropsFilePath/makeConfigs exactly like
the legacy form
+ positionalArgs.add(value != null ? value.trim() : "");
+ }
+ // extra -D keys are forwarded as trailing configs, mirroring makeConfigs
behavior
+ List<String> extraConfigs = new ArrayList<>();
+ for (Map.Entry<String, String> entry : params.entrySet()) {
+ if (!Arrays.asList(command.paramNames).contains(entry.getKey())) {
+ extraConfigs.add(entry.getKey() + "=" + entry.getValue());
+ }
+ }
+
+ List<String> positional = new ArrayList<>();
+ positional.add(namedArgs.command);
+ positional.add(namedArgs.master);
+ positional.add(namedArgs.memory);
+ positional.addAll(positionalArgs);
+ positional.addAll(extraConfigs);
+ return positional.toArray(new String[0]);
+ }
+
+ /**
+ * Named-argument form of a SparkMain invocation. Kept separate from the
legacy
+ * positional protocol so that existing call sites and scripts keep working
unchanged:
+ * if the first argument does not look like a named option the legacy path
is used.
+ */
+ private static class NamedArgs {
+ @Parameter(names = {"--command", "-command"}, description = "Name of the
Spark command to run (e.g. ROLLBACK)", required = true)
+ private String command;
+
+ @Parameter(names = {"--master", "-master"}, description = "Spark master
URL (was positional arg 2)", required = true)
+ private String master;
+
+ @Parameter(names = {"--memory", "-memory"}, description = "Spark driver
memory (was positional arg 3)", required = true)
Review Comment:
This says driver memory, but SparkUtil.initJavaSparkContext puts the value
into HoodieCliSparkConfig.CLI_EXECUTOR_MEMORY and every call site feeds it the
--sparkMemory shell option. Reword to executor memory.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]