This is an automated email from the ASF dual-hosted git repository.

namelchev pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/ignite.git


The following commit(s) were added to refs/heads/master by this push:
     new fc8a07e0f0e IGNITE-20418 Added the multiple nodes argument to the 
'indexes_force_rebuild' command (#10941)
fc8a07e0f0e is described below

commit fc8a07e0f0ebe27d4ed17612da18f90707ae60a2
Author: Vladimir Steshin <vlads...@gmail.com>
AuthorDate: Fri Sep 29 14:51:12 2023 +0300

    IGNITE-20418 Added the multiple nodes argument to the 
'indexes_force_rebuild' command (#10941)
---
 docs/_docs/tools/control-script.adoc               |   4 +-
 .../internal/commandline/ArgumentParser.java       |  11 +-
 .../GridCommandHandlerIndexForceRebuildTest.java   | 255 +++++++++++++++++++--
 .../internal/management/api/ArgumentGroup.java     |   3 +
 ...rgumentGroup.java => ArgumentGroupsHolder.java} |  21 +-
 .../internal/management/api/CommandUtils.java      | 126 +++++++---
 .../cache/CacheIndexesForceRebuildCommand.java     | 179 +++++++++++++--
 .../cache/CacheIndexesForceRebuildCommandArg.java  |  38 ++-
 .../management/cache/IndexForceRebuildTask.java    |  26 ++-
 ...mandHandlerClusterByClassTest_cache_help.output |   6 +-
 ...dlerClusterByClassWithSSLTest_cache_help.output |   6 +-
 11 files changed, 584 insertions(+), 91 deletions(-)

diff --git a/docs/_docs/tools/control-script.adoc 
b/docs/_docs/tools/control-script.adoc
index 490c55e7f37..843878b6a5f 100644
--- a/docs/_docs/tools/control-script.adoc
+++ b/docs/_docs/tools/control-script.adoc
@@ -1071,12 +1071,12 @@ To trigger the rebuild process of all indexes for the 
specified caches or the ca
 tab:Unix[]
 [source,shell]
 ----
-control.sh --cache indexes_force_rebuild --node-id nodeId --cache-names 
cacheName1,...cacheNameN|--group-names groupName1,...groupNameN
+control.sh --cache indexes_force_rebuild --node-ids 
nodeId1,...nodeIdN|--all-nodes --cache-names 
cacheName1,...cacheNameN|--group-names groupName1,...groupNameN
 ----
 tab:Window[]
 [source,shell]
 ----
-control.bat --cache indexes_force_rebuild --node-id nodeId --cache-names 
cacheName1,...cacheNameN|--group-names groupName1,...groupNameN
+control.bat --cache indexes_force_rebuild --node-ids 
nodeId1,...nodeIdN|--all-nodes --cache-names 
cacheName1,...cacheNameN|--group-names groupName1,...groupNameN
 ----
 --
 
diff --git 
a/modules/control-utility/src/main/java/org/apache/ignite/internal/commandline/ArgumentParser.java
 
b/modules/control-utility/src/main/java/org/apache/ignite/internal/commandline/ArgumentParser.java
index 604f3a47d24..1a6e1d19cb9 100644
--- 
a/modules/control-utility/src/main/java/org/apache/ignite/internal/commandline/ArgumentParser.java
+++ 
b/modules/control-utility/src/main/java/org/apache/ignite/internal/commandline/ArgumentParser.java
@@ -21,8 +21,6 @@ package org.apache.ignite.internal.commandline;
 import java.lang.reflect.Field;
 import java.util.ArrayDeque;
 import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Collections;
 import java.util.Deque;
 import java.util.HashSet;
 import java.util.Iterator;
@@ -42,6 +40,7 @@ import org.apache.ignite.internal.management.api.Argument;
 import org.apache.ignite.internal.management.api.ArgumentGroup;
 import org.apache.ignite.internal.management.api.CliSubcommandsWithPrefix;
 import org.apache.ignite.internal.management.api.Command;
+import org.apache.ignite.internal.management.api.CommandUtils;
 import org.apache.ignite.internal.management.api.CommandsRegistry;
 import org.apache.ignite.internal.management.api.Positional;
 import org.apache.ignite.internal.util.typedef.internal.U;
@@ -350,13 +349,11 @@ public class ArgumentParser {
             (name, val) -> {}
         );
 
-        ArgumentGroup argGrp = 
cmdPath.peek().argClass().getAnnotation(ArgumentGroup.class);
-        Set<String> grpdFlds = argGrp == null
-            ? Collections.emptySet()
-            : new HashSet<>(Arrays.asList(argGrp.value()));
+        List<Set<String>> grpdFlds = 
CommandUtils.argumentGroupsValues(cmdPath.peek().argClass());
 
         Consumer<Field> namedArgCb = fld -> namedArgs.add(
-            toArg.apply(fld, grpdFlds.contains(fld.getName()) || 
fld.getAnnotation(Argument.class).optional())
+            toArg.apply(fld, CommandUtils.argumentGroupIdx(grpdFlds, 
fld.getName()) >= 0
+                || fld.getAnnotation(Argument.class).optional())
         );
 
         Consumer<Field> positionalArgCb = fld -> positionalArgs.add(new 
CLIArgument<>(
diff --git 
a/modules/control-utility/src/test/java/org/apache/ignite/util/GridCommandHandlerIndexForceRebuildTest.java
 
b/modules/control-utility/src/test/java/org/apache/ignite/util/GridCommandHandlerIndexForceRebuildTest.java
index 3486d746826..34aa9b51239 100644
--- 
a/modules/control-utility/src/test/java/org/apache/ignite/util/GridCommandHandlerIndexForceRebuildTest.java
+++ 
b/modules/control-utility/src/test/java/org/apache/ignite/util/GridCommandHandlerIndexForceRebuildTest.java
@@ -26,12 +26,17 @@ import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.ThreadLocalRandom;
 import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.stream.Collectors;
+import org.apache.ignite.Ignite;
+import org.apache.ignite.cache.CacheAtomicityMode;
+import org.apache.ignite.cache.CacheWriteSynchronizationMode;
 import org.apache.ignite.cluster.ClusterState;
+import org.apache.ignite.configuration.CacheConfiguration;
 import org.apache.ignite.configuration.IgniteConfiguration;
 import org.apache.ignite.internal.IgniteEx;
 import org.apache.ignite.internal.IgniteInternalFuture;
 import org.apache.ignite.internal.IgniteInterruptedCheckedException;
 import org.apache.ignite.internal.cache.query.index.IndexProcessor;
+import 
org.apache.ignite.internal.management.cache.CacheIndexesForceRebuildCommand;
 import org.apache.ignite.internal.managers.indexing.IndexesRebuildTask;
 import org.apache.ignite.internal.processors.cache.GridCacheContext;
 import org.apache.ignite.internal.processors.cache.IgniteCacheProxy;
@@ -40,6 +45,7 @@ import 
org.apache.ignite.internal.processors.query.schema.SchemaIndexCacheFuture
 import 
org.apache.ignite.internal.processors.query.schema.SchemaIndexCacheVisitorClosure;
 import org.apache.ignite.internal.util.future.GridFutureAdapter;
 import org.apache.ignite.internal.util.typedef.F;
+import org.apache.ignite.internal.util.typedef.G;
 import org.apache.ignite.internal.util.typedef.internal.CU;
 import org.apache.ignite.internal.util.typedef.internal.SB;
 import org.apache.ignite.internal.util.typedef.internal.U;
@@ -55,6 +61,7 @@ import static java.lang.String.valueOf;
 import static 
org.apache.ignite.internal.commandline.CommandHandler.EXIT_CODE_INVALID_ARGUMENTS;
 import static 
org.apache.ignite.internal.commandline.CommandHandler.EXIT_CODE_OK;
 import static org.apache.ignite.internal.management.api.CommandUtils.INDENT;
+import static 
org.apache.ignite.internal.processors.cache.persistence.file.FilePageStoreManager.DFLT_STORE_DIR;
 import static org.apache.ignite.internal.util.IgniteUtils.max;
 import static org.apache.ignite.testframework.GridTestUtils.assertContains;
 import static org.apache.ignite.testframework.GridTestUtils.getFieldValue;
@@ -64,6 +71,7 @@ import static 
org.apache.ignite.util.GridCommandHandlerIndexingUtils.breakSqlInd
 import static 
org.apache.ignite.util.GridCommandHandlerIndexingUtils.complexIndexEntity;
 import static 
org.apache.ignite.util.GridCommandHandlerIndexingUtils.createAndFillCache;
 import static 
org.apache.ignite.util.GridCommandHandlerIndexingUtils.createAndFillThreeFieldsEntryCache;
+import static 
org.apache.ignite.util.GridCommandHandlerIndexingUtils.personEntity;
 
 /**
  * Test for --cache indexes_force_rebuild command. Uses single cluster per 
suite.
@@ -176,7 +184,7 @@ public class GridCommandHandlerIndexForceRebuildTest 
extends GridCommandHandlerA
 
         String cacheNamesOutputStr = testOut.toString();
 
-        assertTrue(cacheNamesOutputStr.contains("WARNING: Indexes rebuild was 
not started for any cache. Check command input."));
+        
assertTrue(cacheNamesOutputStr.contains(CacheIndexesForceRebuildCommand.PREF_REBUILD_NOT_STARTED_SINGLE));
 
         testOut.reset();
 
@@ -186,7 +194,83 @@ public class GridCommandHandlerIndexForceRebuildTest 
extends GridCommandHandlerA
 
         String grpNamesOutputStr = testOut.toString();
 
-        assertTrue(grpNamesOutputStr.contains("WARNING: Indexes rebuild was 
not started for any cache. Check command input."));
+        
assertTrue(grpNamesOutputStr.contains(CacheIndexesForceRebuildCommand.PREF_REBUILD_NOT_STARTED_SINGLE));
+    }
+
+    /**
+     * Test the command output on a cache with node filter.
+     */
+    @Test
+    public void testWithNodeFilter() throws Exception {
+        injectTestSystemOut();
+
+        try {
+            grid(1).createCache(new CacheConfiguration<>("cacheWithNodeFilter")
+                .setNodeFilter(n -> n.consistentId().toString().endsWith("1"))
+                
.setWriteSynchronizationMode(CacheWriteSynchronizationMode.FULL_SYNC)
+                .setBackups(1)
+                .setAtomicityMode(CacheAtomicityMode.ATOMIC)
+                .setQueryEntities(Collections.singletonList(personEntity())));
+
+            for (int i = 0; i < 100; ++i)
+                grid(1).cache("cacheWithNodeFilter").put(i, new Person(i * 10, 
"Name_" + 1));
+
+            assertEquals(EXIT_CODE_OK, execute("--cache", 
"indexes_force_rebuild", "--all-nodes", "--cache-names",
+                "cacheWithNodeFilter"));
+
+            String cacheNamesOutputStr = testOut.toString();
+
+            validateMultiNodeOutput(cacheNamesOutputStr, 
CacheIndexesForceRebuildCommand.PREF_REBUILD_STARTED,
+                grid(1).localNode().id().toString());
+
+            validateMultiNodeOutput(cacheNamesOutputStr, 
CacheIndexesForceRebuildCommand.PREF_CACHES_NOT_FOUND,
+                grid(LAST_NODE_NUM).localNode().id().toString());
+            validateMultiNodeOutput(cacheNamesOutputStr, 
CacheIndexesForceRebuildCommand.PREF_CACHES_NOT_FOUND,
+                grid(0).localNode().id().toString());
+
+            validateMultiNodeOutput(cacheNamesOutputStr, 
CacheIndexesForceRebuildCommand.PREF_REBUILD_NOT_STARTED,
+                grid(0).localNode().id().toString());
+            validateMultiNodeOutput(cacheNamesOutputStr, 
CacheIndexesForceRebuildCommand.PREF_REBUILD_NOT_STARTED,
+                grid(LAST_NODE_NUM).localNode().id().toString());
+
+            waitForIndexesRebuild(grid(1));
+        }
+        finally {
+            grid(LAST_NODE_NUM).destroyCache("cacheWithNodeFilter");
+
+            awaitPartitionMapExchange();
+
+            // TODO Remove after IGNITE-20507.
+            // Cleaning cache meta being kept.
+            for (Ignite ig : G.allGrids()) {
+                U.delete(U.resolveWorkDirectory(U.defaultWorkDirectory(), 
DFLT_STORE_DIR + '/' + ig.name()
+                    + "/cache-cacheWithNodeFilter", false));
+            }
+        }
+    }
+
+    /**
+     * Checks error messages when trying to rebuild indexes for non-existent 
cache of group on several nodes
+     * using '--node-ids'.
+     */
+    @Test
+    public void testEmptyResultTwoNodes() {
+        injectTestSystemOut();
+
+        String nids = grid(LAST_NODE_NUM).localNode().id().toString() + ',' + 
grid(0).localNode().id().toString();
+
+        assertEquals(EXIT_CODE_OK, execute("--cache", "indexes_force_rebuild", 
"--node-ids", nids,
+            "--cache-names", CACHE_NAME_NON_EXISTING));
+
+        String cacheNamesOutputStr = testOut.toString();
+
+        
assertFalse(cacheNamesOutputStr.contains(CacheIndexesForceRebuildCommand.PREF_REBUILD_STARTED));
+        
assertFalse(cacheNamesOutputStr.contains(CacheIndexesForceRebuildCommand.PREF_REBUILD_NOT_STARTED_SINGLE));
+
+        validateMultiNodeOutput(cacheNamesOutputStr, 
CacheIndexesForceRebuildCommand.PREF_REBUILD_NOT_STARTED,
+            grid(LAST_NODE_NUM).localNode().id().toString());
+        validateMultiNodeOutput(cacheNamesOutputStr, 
CacheIndexesForceRebuildCommand.PREF_REBUILD_NOT_STARTED,
+            grid(0).localNode().id().toString());
     }
 
     /**
@@ -209,6 +293,37 @@ public class GridCommandHandlerIndexForceRebuildTest 
extends GridCommandHandlerA
         removeLogListener(grid(LAST_NODE_NUM), lsnr);
     }
 
+    /**
+     * Checks two arguments in the group are not allowed.
+     */
+    @Test
+    public void testInvalidArgumentGroups() {
+        injectTestSystemOut();
+
+        assertContains(log, executeCommand(EXIT_CODE_INVALID_ARGUMENTS, 
"--cache", "indexes_force_rebuild",
+                "--node-ids", grid(LAST_NODE_NUM).localNode().id().toString() 
+ ',' + grid(0).localNode().id().toString(),
+                "--node-id", grid(LAST_NODE_NUM).localNode().id().toString(),
+                "--cache-names", CACHE_NAME_NO_GRP),
+            "Only one of [--node-ids, --all-nodes, --node-id] allowed");
+
+        assertContains(log, executeCommand(EXIT_CODE_INVALID_ARGUMENTS, 
"--cache", "indexes_force_rebuild",
+                "--node-ids", grid(LAST_NODE_NUM).localNode().id().toString() 
+ ',' + grid(0).localNode().id().toString(),
+                "--all-nodes",
+                "--cache-names", CACHE_NAME_NO_GRP),
+            "Only one of [--node-ids, --all-nodes, --node-id] allowed");
+
+        assertContains(log, executeCommand(EXIT_CODE_INVALID_ARGUMENTS, 
"--cache", "indexes_force_rebuild",
+                "--all-nodes",
+                "--node-id", grid(LAST_NODE_NUM).localNode().id().toString(),
+                "--cache-names", CACHE_NAME_NO_GRP),
+            "Only one of [--node-ids, --all-nodes, --node-id] allowed");
+
+        assertContains(log, executeCommand(EXIT_CODE_INVALID_ARGUMENTS, 
"--cache", "indexes_force_rebuild",
+                "--node-id", grid(LAST_NODE_NUM).localNode().id().toString(),
+                "--cache-names", CACHE_NAME_NO_GRP, "--group-names", 
CACHE_NAME_NO_GRP),
+            "Only one of [--group-names, --cache-names] allowed");
+    }
+
     /**
      * Checks --node-id and --cache-names options,
      * correctness of utility output and the fact that indexes were actually 
rebuilt.
@@ -268,6 +383,84 @@ public class GridCommandHandlerIndexForceRebuildTest 
extends GridCommandHandlerA
         }
     }
 
+    /**
+     * Checks output of index rebuilding launched on several nodes using 
'--nodes-ids'.
+     */
+    @Test
+    public void testIndexRebuildOutputTwoNodes() throws Exception {
+        blockRebuildIdx.put(CACHE_NAME_2_1, new GridFutureAdapter<>());
+
+        injectTestSystemOut();
+
+        try {
+            triggerIndexRebuild(LAST_NODE_NUM, 
Collections.singletonList(CACHE_NAME_2_1));
+
+            assertEquals(EXIT_CODE_OK, execute("--cache", 
"indexes_force_rebuild",
+                "--node-ids", grid(LAST_NODE_NUM).localNode().id().toString() 
+ ',' + grid(0).localNode().id().toString(),
+                "--cache-names", CACHE_NAME_1_1 + ',' + CACHE_NAME_2_1 + ',' + 
CACHE_NAME_NON_EXISTING));
+
+            String outputStr = testOut.toString();
+
+            validateMultiNodeOutput(outputStr, 
CacheIndexesForceRebuildCommand.PREF_CACHES_NOT_FOUND, CACHE_NAME_NON_EXISTING);
+            validateMultiNodeOutput(outputStr, 
CacheIndexesForceRebuildCommand.PREF_CACHES_NOT_FOUND,
+                grid(LAST_NODE_NUM).localNode().id().toString());
+            validateMultiNodeOutput(outputStr, 
CacheIndexesForceRebuildCommand.PREF_CACHES_NOT_FOUND,
+                grid(0).localNode().id().toString());
+
+            validateMultiNodeOutput(outputStr, 
CacheIndexesForceRebuildCommand.PREF_REBUILDING, CACHE_NAME_2_1);
+            validateMultiNodeOutput(outputStr, 
CacheIndexesForceRebuildCommand.PREF_REBUILDING,
+                grid(LAST_NODE_NUM).localNode().id().toString());
+
+            validateMultiNodeOutput(outputStr, 
CacheIndexesForceRebuildCommand.PREF_REBUILD_STARTED, CACHE_NAME_1_1);
+            validateMultiNodeOutput(outputStr, 
CacheIndexesForceRebuildCommand.PREF_REBUILD_STARTED,
+                grid(LAST_NODE_NUM).localNode().id().toString());
+            validateMultiNodeOutput(outputStr, 
CacheIndexesForceRebuildCommand.PREF_REBUILD_STARTED,
+                grid(0).localNode().id().toString());
+        }
+        finally {
+            blockRebuildIdx.remove(CACHE_NAME_2_1);
+
+            assertTrue(waitForIndexesRebuild(grid(LAST_NODE_NUM)));
+        }
+    }
+
+    /**
+     * Checks output of index rebuilding launched on all nodes using 
'--all-nodes'.
+     */
+    @Test
+    public void testIndexRebuildAllNodes() throws 
IgniteInterruptedCheckedException {
+        injectTestSystemOut();
+
+        LogListener[] cacheLsnrs = new LogListener[GRIDS_NUM];
+
+        try {
+            for (int i = 0; i < GRIDS_NUM; i++)
+                cacheLsnrs[i] = installRebuildCheckListener(grid(i), 
CACHE_NAME_1_1);
+
+            assertEquals(EXIT_CODE_OK, execute("--cache", 
"indexes_force_rebuild", "--all-nodes",
+                "--cache-names", CACHE_NAME_1_1));
+
+            String outputStr = testOut.toString();
+
+            validateMultiNodeOutput(outputStr, 
CacheIndexesForceRebuildCommand.PREF_REBUILD_STARTED, CACHE_NAME_1_1);
+
+            for (int i = 0; i < GRIDS_NUM; i++) {
+                validateMultiNodeOutput(outputStr, 
CacheIndexesForceRebuildCommand.PREF_REBUILD_STARTED,
+                    grid(i).localNode().id().toString());
+            }
+
+            for (Ignite ig : G.allGrids())
+                waitForIndexesRebuild((IgniteEx)ig);
+
+            for (LogListener lsnr : cacheLsnrs)
+                assertTrue(lsnr.check());
+        }
+        finally {
+            for (int i = 0; i < GRIDS_NUM; i++)
+                removeLogListener(grid(i), cacheLsnrs[i]);
+        }
+    }
+
     /**
      * Checks --node-id and --group-names options,
      * correctness of utility output and the fact that indexes were actually 
rebuilt.
@@ -514,25 +707,25 @@ public class GridCommandHandlerIndexForceRebuildTest 
extends GridCommandHandlerA
      */
     @Test
     public void testSequentialForceRebuildIndexes() throws Exception {
-        IgniteEx grid = grid(0);
+        Collection<IgniteEx> grids = Collections.singletonList(grid(0));
 
         injectTestSystemOut();
 
         String outputStr;
 
-        forceRebuildIndices(F.asList(CACHE_NAME_1_1), grid);
+        forceRebuildIndices(F.asList(CACHE_NAME_1_1), grids);
 
         outputStr = testOut.toString();
 
         validateOutputIndicesRebuildWasStarted(outputStr, F.asMap(GRP_NAME_1, 
F.asList(CACHE_NAME_1_1)));
 
-        assertFalse(outputStr.contains("WARNING: These caches have indexes 
rebuilding in progress:"));
+        
assertFalse(outputStr.contains(CacheIndexesForceRebuildCommand.PREF_REBUILDING));
 
-        forceRebuildIndices(F.asList(CACHE_NAME_1_1), grid);
+        forceRebuildIndices(F.asList(CACHE_NAME_1_1), grids);
 
         validateOutputIndicesRebuildWasStarted(outputStr, F.asMap(GRP_NAME_1, 
F.asList(CACHE_NAME_1_1)));
 
-        assertFalse(outputStr.contains("WARNING: These caches have indexes 
rebuilding in progress:"));
+        
assertFalse(outputStr.contains(CacheIndexesForceRebuildCommand.PREF_REBUILDING));
     }
 
     /**
@@ -541,7 +734,7 @@ public class GridCommandHandlerIndexForceRebuildTest 
extends GridCommandHandlerA
      * @param outputStr CLI {@code control.sh} utility output.
      * @param cacheNames Cache names to print.
      */
-    private void validateOutputCacheNamesNotFound(String outputStr, String... 
cacheNames) {
+    private static void validateOutputCacheNamesNotFound(String outputStr, 
String... cacheNames) {
         assertContains(
             log,
             outputStr,
@@ -568,7 +761,7 @@ public class GridCommandHandlerIndexForceRebuildTest 
extends GridCommandHandlerA
      * @param strings List of strings.
      * @return Formated text.
      */
-    private String makeStringListWithIndent(String... strings) {
+    private static String makeStringListWithIndent(String... strings) {
         return INDENT + String.join(U.nl() + INDENT, strings);
     }
 
@@ -578,7 +771,7 @@ public class GridCommandHandlerIndexForceRebuildTest 
extends GridCommandHandlerA
      * @param cacheGroputToNames Cache groups mapping to non-existing cache 
names.
      * @return Text for CLI print output for given caches.
      */
-    private String makeStringListForCacheGroupsAndNames(Map<String, 
List<String>> cacheGroputToNames) {
+    private static String makeStringListForCacheGroupsAndNames(Map<String, 
List<String>> cacheGroputToNames) {
         SB sb = new SB();
 
         for (Map.Entry<String, List<String>> entry : 
cacheGroputToNames.entrySet()) {
@@ -597,7 +790,7 @@ public class GridCommandHandlerIndexForceRebuildTest 
extends GridCommandHandlerA
      * @param outputStr CLI {@code control.sh} utility output.
      * @param cacheGroputToNames Cache groups mapping to non-existing cache 
names.
      */
-    private void validateOutputIndicesRebuildingInProgress(String outputStr, 
Map<String, List<String>> cacheGroputToNames) {
+    private static void validateOutputIndicesRebuildingInProgress(String 
outputStr, Map<String, List<String>> cacheGroputToNames) {
         String caches = 
makeStringListForCacheGroupsAndNames(cacheGroputToNames);
 
         assertContains(
@@ -623,6 +816,35 @@ public class GridCommandHandlerIndexForceRebuildTest 
extends GridCommandHandlerA
         );
     }
 
+    /**
+     * Validates the multi-node command output. Searches for the passed 
prefix/header and the target strings below it.
+     *
+     * @param outputStr The output.
+     * @param prefix    Prefix or header to search.
+     * @param targetStr Target string to search after {@code prefix}.
+     */
+    private static void validateMultiNodeOutput(String outputStr, String 
prefix, String targetStr) {
+        String[] lines = outputStr.split(U.nl());
+
+        for (int i = 0, heraderIdx = -1; i < lines.length; ++i) {
+            String line = lines[i];
+
+            if (heraderIdx < 0) {
+                if (line.contains(prefix))
+                    heraderIdx = i;
+
+                continue;
+            }
+
+            // Search next line after the header.
+            if (i == heraderIdx + 1 && line.contains(targetStr))
+                return;
+        }
+
+        throw new IllegalStateException("Target string '" + targetStr + "' not 
found after header '" + prefix
+            + "' in the command output.");
+    }
+
     /**
      * Triggers indexes rebuild for ALL caches on grid node with index {@code 
igniteIdx}.
      *
@@ -772,21 +994,24 @@ public class GridCommandHandlerIndexForceRebuildTest 
extends GridCommandHandlerA
      * Force rebuilds indices for chosen caches, and waits until rebuild 
process is complete.
      *
      * @param cacheNames Cache names need indices to rebuild.
-     * @param grid Ignite node.
+     * @param grids Ignite nodes.
      * @throws Exception If failed.
      */
-    private void forceRebuildIndices(Iterable<String> cacheNames, IgniteEx 
grid) throws Exception {
+    private void forceRebuildIndices(Iterable<String> cacheNames, 
Collection<IgniteEx> grids) throws Exception {
         String cacheNamesArg = String.join(",", cacheNames);
 
         assertEquals(
             EXIT_CODE_OK,
             execute(
                 "--cache", "indexes_force_rebuild",
-                "--node-id", grid.localNode().id().toString(),
+                grids.size() == 1 ? "--node-id" : "--node-ids",
+                grids.size() == 1 ? 
grids.iterator().next().localNode().id().toString()
+                    : grids.stream().map(g -> 
g.localNode().id().toString()).collect(Collectors.joining(",")),
                 "--cache-names", cacheNamesArg
             )
         );
 
-        waitForIndexesRebuild(grid, getTestTimeout(), Collections.emptyList());
+        for (IgniteEx g : grids)
+            waitForIndexesRebuild(g, getTestTimeout(), 
Collections.emptyList());
     }
 }
diff --git 
a/modules/core/src/main/java/org/apache/ignite/internal/management/api/ArgumentGroup.java
 
b/modules/core/src/main/java/org/apache/ignite/internal/management/api/ArgumentGroup.java
index f55c684892a..8f48a04a4e2 100644
--- 
a/modules/core/src/main/java/org/apache/ignite/internal/management/api/ArgumentGroup.java
+++ 
b/modules/core/src/main/java/org/apache/ignite/internal/management/api/ArgumentGroup.java
@@ -18,6 +18,7 @@
 package org.apache.ignite.internal.management.api;
 
 import java.lang.annotation.ElementType;
+import java.lang.annotation.Repeatable;
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
 import java.lang.annotation.Target;
@@ -28,9 +29,11 @@ import java.lang.annotation.Target;
  * If values from {@link #value()} not conform restrictions then error will be 
thrown.
  *
  * @see org.apache.ignite.internal.management.SystemViewCommandArg
+ * @see ArgumentGroupsHolder
  */
 @Retention(RetentionPolicy.RUNTIME)
 @Target(ElementType.TYPE)
+@Repeatable(ArgumentGroupsHolder.class)
 public @interface ArgumentGroup {
     /** @return Names of argument class fields to forms "group" restriction. */
     public String[] value();
diff --git 
a/modules/core/src/main/java/org/apache/ignite/internal/management/api/ArgumentGroup.java
 
b/modules/core/src/main/java/org/apache/ignite/internal/management/api/ArgumentGroupsHolder.java
similarity index 61%
copy from 
modules/core/src/main/java/org/apache/ignite/internal/management/api/ArgumentGroup.java
copy to 
modules/core/src/main/java/org/apache/ignite/internal/management/api/ArgumentGroupsHolder.java
index f55c684892a..d39a740200d 100644
--- 
a/modules/core/src/main/java/org/apache/ignite/internal/management/api/ArgumentGroup.java
+++ 
b/modules/core/src/main/java/org/apache/ignite/internal/management/api/ArgumentGroupsHolder.java
@@ -18,26 +18,17 @@
 package org.apache.ignite.internal.management.api;
 
 import java.lang.annotation.ElementType;
+import java.lang.annotation.Repeatable;
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
 import java.lang.annotation.Target;
 
 /**
- * Defines commands arguments restriction.
- * Group of {@link #value()} fields must be presented in Arguments.
- * If values from {@link #value()} not conform restrictions then error will be 
thrown.
- *
- * @see org.apache.ignite.internal.management.SystemViewCommandArg
+ * {@link Repeatable} container for {@link ArgumentGroup}.
  */
 @Retention(RetentionPolicy.RUNTIME)
-@Target(ElementType.TYPE)
-public @interface ArgumentGroup {
-    /** @return Names of argument class fields to forms "group" restriction. */
-    public String[] value();
-
-    /** @return {@code True} if arguments is optional, {@code false} if 
required. */
-    public boolean optional();
-
-    /** @return {@code True} if only one of argument from group allowed. */
-    public boolean onlyOneOf() default false;
+@Target({ElementType.TYPE})
+public @interface ArgumentGroupsHolder {
+    /** Array of {@link ArgumentGroup} annotations. */
+    ArgumentGroup[] value();
 }
diff --git 
a/modules/core/src/main/java/org/apache/ignite/internal/management/api/CommandUtils.java
 
b/modules/core/src/main/java/org/apache/ignite/internal/management/api/CommandUtils.java
index ad6d5ac724b..76e37070e52 100644
--- 
a/modules/core/src/main/java/org/apache/ignite/internal/management/api/CommandUtils.java
+++ 
b/modules/core/src/main/java/org/apache/ignite/internal/management/api/CommandUtils.java
@@ -369,21 +369,27 @@ public class CommandUtils {
         List<Field> positionalParams = new ArrayList<>();
         List<Field> namedParams = new ArrayList<>();
 
-        ArgumentGroup argGrp = argCls.getAnnotation(ArgumentGroup.class);
+        List<ArgumentGroup> argGprs = argumentGroups(argCls);
 
-        Set<String> grpNames = argGrp != null
-            ? new HashSet<>(Arrays.asList(argGrp.value()))
-            : Collections.emptySet();
+        List<Set<String>> grpNames = argumentGroupsValues(argGprs);
 
-        List<Field> grpFlds = new ArrayList<>();
+        List<List<Field>> grpFlds = grpNames.isEmpty() ? 
Collections.emptyList() : new ArrayList<>(grpNames.size());
+
+        grpNames.forEach(gf -> grpFlds.add(grpFlds.size(), null));
 
         // Iterates classes from the roots.
         for (int i = classes.size() - 1; i >= 0; i--) {
             Field[] flds = classes.get(i).getDeclaredFields();
 
             for (Field fld : flds) {
-                if (grpNames.contains(fld.getName()))
-                    grpFlds.add(fld);
+                int argGrpIdx = argumentGroupIdx(grpNames, fld.getName());
+
+                if (argGrpIdx >= 0) {
+                    if (grpFlds.get(argGrpIdx) == null)
+                        grpFlds.set(argGrpIdx, new ArrayList<>());
+
+                    grpFlds.get(argGrpIdx).add(fld);
+                }
                 else if (fld.isAnnotationPresent(Positional.class))
                     positionalParams.add(fld);
                 else if (fld.isAnnotationPresent(Argument.class))
@@ -395,8 +401,61 @@ public class CommandUtils {
 
         namedParams.forEach(namedParamVisitor);
 
-        if (argGrp != null)
-            argumentGroupVisitor.accept(argGrp, grpFlds);
+        for (int i = 0; i < grpFlds.size(); ++i)
+            argumentGroupVisitor.accept(argGprs.get(i), grpFlds.get(i));
+    }
+
+    /**
+     * @return List of declared {@link ArgumentGroup} at {@code cls}. 
Singleton list if only one argument group is
+     * declared. Empty list if no argument group is declared.
+     */
+    private static List<ArgumentGroup> argumentGroups(Class<?> cls) {
+        ArgumentGroup singleGrp = cls.getAnnotation(ArgumentGroup.class);
+
+        if (singleGrp != null) {
+            assert cls.getAnnotation(ArgumentGroupsHolder.class) == null;
+
+            return Collections.singletonList(singleGrp);
+        }
+
+        ArgumentGroupsHolder grps = 
cls.getAnnotation(ArgumentGroupsHolder.class);
+
+        return grps == null ? Collections.emptyList() : 
Arrays.asList(grps.value());
+    }
+
+    /**
+     * @return Sets list of {@link ArgumentGroup#value()} declared at {@code 
cls}.
+     */
+    public static List<Set<String>> argumentGroupsValues(Class<?> cls) {
+        return argumentGroupsValues(argumentGroups(cls));
+    }
+
+    /**
+     * @return Sets list of {@link ArgumentGroup#value()} holding in {@code 
argGrps}.
+     * @see #argumentGroupsValues(Class)
+     */
+    public static List<Set<String>> argumentGroupsValues(List<ArgumentGroup> 
argGrps) {
+        List<Set<String>> res = argGrps.stream().map(grp -> new 
HashSet<>(Arrays.asList(grp.value())))
+            .collect(Collectors.toList());
+
+        // Checks that argument groups only unique values.
+        assert 
F.flatCollections(res).stream().collect(Collectors.groupingBy(Function.identity(),
 Collectors.counting()))
+            .entrySet().stream().noneMatch(e -> e.getValue() > 1) : "Argument 
groups " + argGrps + " have not unique arguments";
+
+        return res;
+    }
+
+    /**
+     * @return Index of first value set in {@code argGrpValues} containing 
{@code name}. -1 if not found.
+     * @see #argumentGroupsValues(Class)
+     */
+    public static int argumentGroupIdx(List<Set<String>> argGrpValues, String 
name) {
+        for (int i = 0; i < argGrpValues.size(); ++i) {
+            if (argGrpValues.get(i).contains(name))
+                return i;
+        }
+
+        return -1;
     }
 
     /**
@@ -690,8 +749,12 @@ public class CommandUtils {
                 })
             );
 
-            if (arg.argGrp != null && (!arg.grpOptional() && 
!arg.grpFldExists))
-                throw new IllegalArgumentException("One of " + 
toFormattedNames(argCls, arg.grpdFlds) + " required");
+            for (int grpIdx = 0; grpIdx < arg.argGrps.size(); ++grpIdx) {
+                if (!arg.argGrps.get(grpIdx).optional() && 
!arg.grpFldExists[grpIdx]) {
+                    throw new IllegalArgumentException("One of " + 
toFormattedNames(argCls, arg.grpdFlds.get(grpIdx))
+                        + " required");
+                }
+            }
 
             return arg.res;
         }
@@ -737,32 +800,29 @@ public class CommandUtils {
     /** */
     private static class ArgumentState<A extends IgniteDataTransferObject> 
implements BiConsumer<Field, Object> {
         /** */
-        final A res;
+        private final A res;
 
         /** */
-        final ArgumentGroup argGrp;
+        private final List<ArgumentGroup> argGrps;
 
         /** */
-        boolean grpFldExists;
+        private final @Nullable boolean[] grpFldExists;
 
         /** */
-        int idx;
+        private int idx;
 
         /** */
-        final Set<String> grpdFlds;
+        private final List<Set<String>> grpdFlds;
 
         /** */
         public ArgumentState(Class<A> argCls) throws InstantiationException, 
IllegalAccessException {
             res = argCls.newInstance();
-            argGrp = argCls.getAnnotation(ArgumentGroup.class);
-            grpdFlds = argGrp == null
-                ? Collections.emptySet()
-                : new HashSet<>(Arrays.asList(argGrp.value()));
-        }
 
-        /** */
-        public boolean grpOptional() {
-            return argGrp == null || argGrp.optional();
+            argGrps = argumentGroups(argCls);
+
+            grpdFlds = argumentGroupsValues(argGrps);
+
+            grpFldExists = argGrps.isEmpty() ? null : new 
boolean[argGrps.size()];
         }
 
         /** */
@@ -776,10 +836,14 @@ public class CommandUtils {
 
         /** {@inheritDoc} */
         @Override public void accept(Field fld, Object val) {
-            boolean grpdFld = grpdFlds.contains(fld.getName());
+            int argGrpIdx = argumentGroupIdx(grpdFlds, fld.getName());
+
+            assert argGrpIdx < argGrps.size();
+
+            ArgumentGroup argGrp = argGrpIdx < 0 ? null : 
argGrps.get(argGrpIdx);
 
             if (val == null) {
-                if (grpdFld || fld.getAnnotation(Argument.class).optional())
+                if (argGrp != null || 
fld.getAnnotation(Argument.class).optional())
                     return;
 
                 String name = fld.isAnnotationPresent(Positional.class)
@@ -792,14 +856,16 @@ public class CommandUtils {
             if (Objects.equals(val, get(fld)))
                 return;
 
-            if (grpdFld) {
-                if (grpFldExists && (argGrp != null && argGrp.onlyOneOf())) {
+            if (argGrp != null) {
+                assert grpFldExists != null;
+
+                if (grpFldExists[argGrpIdx] && argGrp.onlyOneOf()) {
                     throw new IllegalArgumentException(
-                        "Only one of " + toFormattedNames(res.getClass(), 
grpdFlds) + " allowed"
+                        "Only one of " + toFormattedNames(res.getClass(), 
grpdFlds.get(argGrpIdx)) + " allowed"
                     );
                 }
 
-                grpFldExists = true;
+                grpFldExists[argGrpIdx] = true;
             }
 
             set(fld, val);
diff --git 
a/modules/core/src/main/java/org/apache/ignite/internal/management/cache/CacheIndexesForceRebuildCommand.java
 
b/modules/core/src/main/java/org/apache/ignite/internal/management/cache/CacheIndexesForceRebuildCommand.java
index 575e1e0d085..224c957159f 100644
--- 
a/modules/core/src/main/java/org/apache/ignite/internal/management/cache/CacheIndexesForceRebuildCommand.java
+++ 
b/modules/core/src/main/java/org/apache/ignite/internal/management/cache/CacheIndexesForceRebuildCommand.java
@@ -18,17 +18,45 @@
 package org.apache.ignite.internal.management.cache;
 
 import java.util.Collection;
-import java.util.List;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+import java.util.UUID;
 import java.util.function.Consumer;
+import java.util.stream.Collectors;
 import org.apache.ignite.internal.client.GridClientNode;
 import org.apache.ignite.internal.management.api.CommandUtils;
 import org.apache.ignite.internal.management.api.ComputeCommand;
 import org.apache.ignite.internal.util.typedef.F;
+import org.apache.ignite.internal.util.typedef.internal.SB;
+import org.apache.ignite.internal.util.typedef.internal.U;
 
 import static org.apache.ignite.internal.management.api.CommandUtils.INDENT;
 
 /** Index force rebuild. */
-public class CacheIndexesForceRebuildCommand implements 
ComputeCommand<CacheIndexesForceRebuildCommandArg, IndexForceRebuildTaskRes> {
+public class CacheIndexesForceRebuildCommand
+    implements ComputeCommand<CacheIndexesForceRebuildCommandArg, Map<UUID, 
IndexForceRebuildTaskRes>> {
+    /** */
+    public static final String PREF_REBUILDING = "WARNING: These caches have 
indexes rebuilding in progress:";
+
+    /** */
+    public static final String PREF_CACHES_NOT_FOUND = "WARNING: These caches 
were not found:";
+
+    /** */
+    private static final String PREF_GROUPS_NOT_FOUND = "WARNING: These cache 
groups were not found:";
+
+    /** */
+    public static final String PREF_REBUILD_STARTED = "Indexes rebuild was 
started for these caches:";
+
+    /** */
+    public static final String PREF_REBUILD_NOT_STARTED_SINGLE = "WARNING: 
Indexes rebuild was not started for " +
+        "any cache. Check command input";
+
+    /** */
+    public static final String PREF_REBUILD_NOT_STARTED = "WARNING: Indexes 
rebuild was not started for " +
+        "any cache on the following nodes. Check the command input:";
+
     /** {@inheritDoc} */
     @Override public String description() {
         return "Triggers rebuild of all indexes for specified caches or cache 
groups";
@@ -46,27 +74,89 @@ public class CacheIndexesForceRebuildCommand implements 
ComputeCommand<CacheInde
 
     /** {@inheritDoc} */
     @Override public Collection<GridClientNode> 
nodes(Collection<GridClientNode> nodes, CacheIndexesForceRebuildCommandArg arg) 
{
+        Collection<GridClientNode> res;
+
+        if (arg.allNodes())
+            res = nodes.stream().filter(n -> 
!n.isClient()).collect(Collectors.toList());
+        else {
+            res = arg.nodeIds() != null
+                ? CommandUtils.nodes(arg.nodeIds(), nodes)
+                : CommandUtils.node(arg.nodeId(), nodes);
+
+            if (!F.isEmpty(res)) {
+                for (GridClientNode n : res) {
+                    if (n != null && n.isClient())
+                        throw new IllegalArgumentException("Please, specify 
only server node ids");
+                }
+            }
+        }
+
+        if (F.isEmpty(res))
+            throw new IllegalArgumentException("Please, specify oat least one 
server node");
+
+        return res;
+    }
+
+    /** {@inheritDoc} */
+    @Override public void printResult(
+        CacheIndexesForceRebuildCommandArg arg,
+        Map<UUID, IndexForceRebuildTaskRes> results,
+        Consumer<String> printer
+    ) {
         if (arg.nodeId() != null) {
-            List<GridClientNode> node = CommandUtils.node(arg.nodeId(), nodes);
+            printSingleResult(arg, results.values().iterator().next(), 
printer);
+
+            return;
+        }
 
-            if (node.get(0) != null && node.get(0).isClient())
-                throw new IllegalArgumentException("Please, specify server 
node id");
+        Map<String, Set<UUID>> notFound = new HashMap<>();
+        Map<IndexRebuildStatusInfoContainer, Set<UUID>> rebuilding = new 
HashMap<>();
+        Map<IndexRebuildStatusInfoContainer, Set<UUID>> started = new 
HashMap<>();
+        Set<UUID> notStarted = new HashSet<>();
 
-            return node;
+        results.forEach((nodeId, res) -> {
+            storeCacheResults(notFound, res.notFoundCacheNames(), nodeId);
+
+            storeCacheResults(rebuilding, res.cachesWithRebuildInProgress(), 
nodeId);
+
+            if (!F.isEmpty(res.cachesWithStartedRebuild()))
+                storeCacheResults(started, res.cachesWithStartedRebuild(), 
nodeId);
+            else
+                notStarted.add(nodeId);
+        });
+
+        SB b = new SB();
+
+        if (!F.isEmpty(notFound))
+            printBlock(b, arg.groupNames() == null ? PREF_CACHES_NOT_FOUND : 
PREF_GROUPS_NOT_FOUND, notFound);
+
+        if (!F.isEmpty(notStarted)) {
+            printHeader(b, PREF_REBUILD_NOT_STARTED);
+
+            printEntryNewLine(b);
+
+            b.a(nodeIdsString(notStarted));
         }
 
-        return null;
+        if (!F.isEmpty(rebuilding))
+            printBlock(b, PREF_REBUILDING, rebuilding);
+
+        if (!F.isEmpty(started))
+            printBlock(b, PREF_REBUILD_STARTED, started);
+
+        printer.accept(b.toString().trim());
     }
 
-    /** {@inheritDoc} */
-    @Override public void printResult(
+    /**
+     * Prints result if only single node was requested with '--node-id' 
instead of '--node-ids'.
+     */
+    private static void printSingleResult(
         CacheIndexesForceRebuildCommandArg arg,
         IndexForceRebuildTaskRes res,
         Consumer<String> printer
     ) {
         if (!F.isEmpty(res.notFoundCacheNames())) {
-            String warning = arg.groupNames() == null ?
-                "WARNING: These caches were not found:" : "WARNING: These 
cache groups were not found:";
+            String warning = arg.groupNames() == null ? PREF_CACHES_NOT_FOUND 
: PREF_GROUPS_NOT_FOUND;
 
             printer.accept(warning);
 
@@ -79,7 +169,7 @@ public class CacheIndexesForceRebuildCommand implements 
ComputeCommand<CacheInde
         }
 
         if (!F.isEmpty(res.cachesWithRebuildInProgress())) {
-            printer.accept("WARNING: These caches have indexes rebuilding in 
progress:");
+            printer.accept(PREF_REBUILDING);
 
             printInfos(res.cachesWithRebuildInProgress(), printer);
 
@@ -87,20 +177,79 @@ public class CacheIndexesForceRebuildCommand implements 
ComputeCommand<CacheInde
         }
 
         if (!F.isEmpty(res.cachesWithStartedRebuild())) {
-            printer.accept("Indexes rebuild was started for these caches:");
+            printer.accept(PREF_REBUILD_STARTED);
 
             printInfos(res.cachesWithStartedRebuild(), printer);
         }
         else
-            printer.accept("WARNING: Indexes rebuild was not started for any 
cache. Check command input.");
+            printer.accept(PREF_REBUILD_NOT_STARTED_SINGLE);
 
         printer.accept("");
     }
 
     /** */
-    private void printInfos(Collection<IndexRebuildStatusInfoContainer> infos, 
Consumer<String> printer) {
+    private static <T> void storeCacheResults(Map<T, Set<UUID>> to, 
Collection<T> keys, UUID nodeId) {
+        if (F.isEmpty(keys))
+            return;
+
+        for (T kv : keys) {
+            to.compute(kv, (kv0, nodeIds0) -> {
+                if (nodeIds0 == null)
+                    nodeIds0 = new HashSet<>();
+
+                nodeIds0.add(nodeId);
+
+                return nodeIds0;
+            });
+        }
+    }
+
+    /** */
+    private static void printInfos(Collection<IndexRebuildStatusInfoContainer> 
infos, Consumer<String> printer) {
         infos.stream()
             .sorted(IndexRebuildStatusInfoContainer.comparator())
             .forEach(rebuildStatusInfo -> printer.accept(INDENT + 
rebuildStatusInfo.toString()));
     }
+
+    /** */
+    private static void printBlock(SB b, String header, Map<?, ? extends 
Collection<UUID>> data) {
+        printHeader(b, header);
+
+        data.forEach((cacheInfo, nodes) -> {
+            printEntryNewLine(b);
+
+            printCacheInfo(b, cacheInfo);
+
+            b.a(" on nodes ").a(nodeIdsString(nodes)).a('.');
+        });
+    }
+
+    /** */
+    private static void printEntryNewLine(SB b) {
+        b.a(U.nl()).a(INDENT);
+    }
+
+    /** */
+    private static String nodeIdsString(Collection<UUID> nodes) {
+        return nodes.stream().map(uuid -> '\'' + uuid.toString() + 
'\'').collect(Collectors.joining(", "));
+    }
+
+    /** */
+    private static void printHeader(SB b, String header) {
+        b.a(U.nl()).a(U.nl()).a(header);
+    }
+
+    /** */
+    private static void printCacheInfo(SB b, Object info) {
+        if (info.getClass() == String.class)
+            b.a('\'').a(info).a('\'');
+        else if (info instanceof IndexRebuildStatusInfoContainer) {
+            IndexRebuildStatusInfoContainer status = 
(IndexRebuildStatusInfoContainer)info;
+
+            b.a('\'').a(status.cacheName()).a('\'');
+
+            if (!F.isEmpty(status.groupName()))
+                b.a(" (groupName='").a(status.groupName()).a("')");
+        }
+    }
 }
diff --git 
a/modules/core/src/main/java/org/apache/ignite/internal/management/cache/CacheIndexesForceRebuildCommandArg.java
 
b/modules/core/src/main/java/org/apache/ignite/internal/management/cache/CacheIndexesForceRebuildCommandArg.java
index 7531b157871..111e9f24d22 100644
--- 
a/modules/core/src/main/java/org/apache/ignite/internal/management/cache/CacheIndexesForceRebuildCommandArg.java
+++ 
b/modules/core/src/main/java/org/apache/ignite/internal/management/cache/CacheIndexesForceRebuildCommandArg.java
@@ -27,15 +27,27 @@ import 
org.apache.ignite.internal.management.api.ArgumentGroup;
 import org.apache.ignite.internal.util.typedef.internal.U;
 
 /** */
+@ArgumentGroup(value = {"nodeIds", "allNodes", "nodeId"}, onlyOneOf = true, 
optional = false)
 @ArgumentGroup(value = {"cacheNames", "groupNames"}, onlyOneOf = true, 
optional = false)
 public class CacheIndexesForceRebuildCommandArg extends 
IgniteDataTransferObject {
     /** */
     private static final long serialVersionUID = 0;
 
     /** */
-    @Argument(description = "Specify node for indexes rebuild", example = 
"nodeId")
+    @Argument(description = "Specify node for indexes rebuild (deprecated. Use 
--node-ids instead)", example = "nodeId")
     private UUID nodeId;
 
+    /** */
+    @Argument(
+        description = "Comma-separated list of nodes ids to run index rebuild 
on",
+        example = "nodeId1,...nodeIdN"
+    )
+    private UUID[] nodeIds;
+
+    /** Flag to launch index rebuild on all nodes. */
+    @Argument(description = "Rebuild index on all nodes")
+    private boolean allNodes;
+
     /** */
     @Argument(description = "Comma-separated list of cache names for which 
indexes should be rebuilt",
         example = "cacheName1,...cacheNameN")
@@ -51,6 +63,8 @@ public class CacheIndexesForceRebuildCommandArg extends 
IgniteDataTransferObject
         U.writeUuid(out, nodeId);
         U.writeArray(out, cacheNames);
         U.writeArray(out, groupNames);
+        U.writeArray(out, nodeIds);
+        out.writeBoolean(allNodes);
     }
 
     /** {@inheritDoc} */
@@ -58,6 +72,8 @@ public class CacheIndexesForceRebuildCommandArg extends 
IgniteDataTransferObject
         nodeId = U.readUuid(in);
         cacheNames = U.readArray(in, String.class);
         groupNames = U.readArray(in, String.class);
+        nodeIds = U.readArray(in, UUID.class);
+        allNodes = in.readBoolean();
     }
 
     /** */
@@ -70,6 +86,26 @@ public class CacheIndexesForceRebuildCommandArg extends 
IgniteDataTransferObject
         this.nodeId = nodeId;
     }
 
+    /** */
+    public UUID[] nodeIds() {
+        return nodeIds;
+    }
+
+    /** */
+    public void allNodes(boolean allNodes) {
+        this.allNodes = allNodes;
+    }
+
+    /** */
+    public boolean allNodes() {
+        return allNodes;
+    }
+
+    /** */
+    public void nodeIds(UUID[] nodeIds) {
+        this.nodeIds = nodeIds;
+    }
+
     /** */
     public String[] cacheNames() {
         return cacheNames;
diff --git 
a/modules/core/src/main/java/org/apache/ignite/internal/management/cache/IndexForceRebuildTask.java
 
b/modules/core/src/main/java/org/apache/ignite/internal/management/cache/IndexForceRebuildTask.java
index f02afe358d8..74ce6c787d5 100644
--- 
a/modules/core/src/main/java/org/apache/ignite/internal/management/cache/IndexForceRebuildTask.java
+++ 
b/modules/core/src/main/java/org/apache/ignite/internal/management/cache/IndexForceRebuildTask.java
@@ -18,10 +18,15 @@
 package org.apache.ignite.internal.management.cache;
 
 import java.util.Collection;
+import java.util.HashMap;
 import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
 import java.util.Set;
+import java.util.UUID;
 import java.util.stream.Collectors;
 import org.apache.ignite.IgniteException;
+import org.apache.ignite.compute.ComputeJobResult;
 import org.apache.ignite.internal.processors.cache.CacheGroupContext;
 import org.apache.ignite.internal.processors.cache.GridCacheContext;
 import org.apache.ignite.internal.processors.cache.GridCacheProcessor;
@@ -29,13 +34,14 @@ import 
org.apache.ignite.internal.processors.cache.IgniteInternalCache;
 import org.apache.ignite.internal.processors.task.GridInternal;
 import org.apache.ignite.internal.util.typedef.internal.CU;
 import org.apache.ignite.internal.visor.VisorJob;
-import org.apache.ignite.internal.visor.VisorOneNodeTask;
+import org.apache.ignite.internal.visor.VisorMultiNodeTask;
 
 /**
  * Task that triggers indexes force rebuild for specified caches or cache 
groups.
  */
 @GridInternal
-public class IndexForceRebuildTask extends 
VisorOneNodeTask<CacheIndexesForceRebuildCommandArg, IndexForceRebuildTaskRes> {
+public class IndexForceRebuildTask extends 
VisorMultiNodeTask<CacheIndexesForceRebuildCommandArg,
+    Map<UUID, IndexForceRebuildTaskRes>, IndexForceRebuildTaskRes> {
     /** */
     private static final long serialVersionUID = 0L;
 
@@ -111,4 +117,20 @@ public class IndexForceRebuildTask extends 
VisorOneNodeTask<CacheIndexesForceReb
             );
         }
     }
+
+    /** {@inheritDoc} */
+    @Override protected Map<UUID, IndexForceRebuildTaskRes> 
reduce0(List<ComputeJobResult> results)
+        throws IgniteException {
+
+        Map<UUID, IndexForceRebuildTaskRes> res = new HashMap<>();
+
+        for (ComputeJobResult jobRes : results) {
+            if (jobRes.getException() != null)
+                throw jobRes.getException();
+
+            res.put(jobRes.getNode().id(), jobRes.getData());
+        }
+
+        return res;
+    }
 }
diff --git 
a/modules/core/src/test/resources/org.apache.ignite.util/GridCommandHandlerClusterByClassTest_cache_help.output
 
b/modules/core/src/test/resources/org.apache.ignite.util/GridCommandHandlerClusterByClassTest_cache_help.output
index ab64a2ff145..3b7b9111815 100644
--- 
a/modules/core/src/test/resources/org.apache.ignite.util/GridCommandHandlerClusterByClassTest_cache_help.output
+++ 
b/modules/core/src/test/resources/org.apache.ignite.util/GridCommandHandlerClusterByClassTest_cache_help.output
@@ -97,10 +97,12 @@ Arguments: --cache help --yes
       --node-id nodeId  - Specify node for job execution. If not specified 
explicitly, info will be gathered from all nodes.
 
   Triggers rebuild of all indexes for specified caches or cache groups:
-    control.(sh|bat) --cache indexes_force_rebuild --node-id nodeId 
--cache-names cacheName1,...cacheNameN|--group-names groupName1,...groupNameN
+    control.(sh|bat) --cache indexes_force_rebuild --node-id nodeId|--node-ids 
nodeId1,...nodeIdN|--all-nodes --cache-names 
cacheName1,...cacheNameN|--group-names groupName1,...groupNameN
 
     Parameters:
-      --node-id nodeId                        - Specify node for indexes 
rebuild.
+      --node-id nodeId                        - Specify node for indexes 
rebuild (deprecated. Use --node-ids instead).
+      --node-ids nodeId1,...nodeIdN           - Comma-separated list of nodes 
ids to run index rebuild on.
+      --all-nodes                             - Rebuild index on all nodes.
       --cache-names cacheName1,...cacheNameN  - Comma-separated list of cache 
names for which indexes should be rebuilt.
       --group-names groupName1,...groupNameN  - Comma-separated list of cache 
group names for which indexes should be rebuilt.
 
diff --git 
a/modules/core/src/test/resources/org.apache.ignite.util/GridCommandHandlerClusterByClassWithSSLTest_cache_help.output
 
b/modules/core/src/test/resources/org.apache.ignite.util/GridCommandHandlerClusterByClassWithSSLTest_cache_help.output
index ab64a2ff145..3b7b9111815 100644
--- 
a/modules/core/src/test/resources/org.apache.ignite.util/GridCommandHandlerClusterByClassWithSSLTest_cache_help.output
+++ 
b/modules/core/src/test/resources/org.apache.ignite.util/GridCommandHandlerClusterByClassWithSSLTest_cache_help.output
@@ -97,10 +97,12 @@ Arguments: --cache help --yes
       --node-id nodeId  - Specify node for job execution. If not specified 
explicitly, info will be gathered from all nodes.
 
   Triggers rebuild of all indexes for specified caches or cache groups:
-    control.(sh|bat) --cache indexes_force_rebuild --node-id nodeId 
--cache-names cacheName1,...cacheNameN|--group-names groupName1,...groupNameN
+    control.(sh|bat) --cache indexes_force_rebuild --node-id nodeId|--node-ids 
nodeId1,...nodeIdN|--all-nodes --cache-names 
cacheName1,...cacheNameN|--group-names groupName1,...groupNameN
 
     Parameters:
-      --node-id nodeId                        - Specify node for indexes 
rebuild.
+      --node-id nodeId                        - Specify node for indexes 
rebuild (deprecated. Use --node-ids instead).
+      --node-ids nodeId1,...nodeIdN           - Comma-separated list of nodes 
ids to run index rebuild on.
+      --all-nodes                             - Rebuild index on all nodes.
       --cache-names cacheName1,...cacheNameN  - Comma-separated list of cache 
names for which indexes should be rebuilt.
       --group-names groupName1,...groupNameN  - Comma-separated list of cache 
group names for which indexes should be rebuilt.
 

Reply via email to