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

mpochatkin pushed a commit to branch IGNITE-23054
in repository https://gitbox.apache.org/repos/asf/ignite-3.git

commit e40a9e8576439097ae9b8facb694fc21af59e63a
Author: Mikhail Pochatkin <[email protected]>
AuthorDate: Tue Oct 22 14:32:40 2024 +0300

    IGNITE-23054 Improve cluster status REST endpoint
---
 .../ItClusterStatusCommandInitializedTest.java     | 64 +++++++++++++++----
 .../cli/commands/sql/ItSqlCommandTest.java         |  2 +
 .../{ClusterStatus.java => ClusterState.java}      | 71 ++++++++++++++++------
 .../cli/call/cluster/status/ClusterStatusCall.java | 24 ++++----
 .../cli/decorators/ClusterStatusDecorator.java     | 24 ++++++--
 .../cli/decorators/DefaultDecoratorRegistry.java   |  4 +-
 .../internal/rest/api/cluster/ClusterState.java    | 30 +++++++--
 .../internal/rest/api/cluster/ClusterStatus.java   | 40 ++++++++++++
 .../rest/cluster/ClusterManagementController.java  | 53 ++++++++++++++--
 .../java/org/apache/ignite/internal/Cluster.java   | 36 +++++++++--
 .../internal/ClusterPerClassIntegrationTest.java   | 14 ++++-
 11 files changed, 297 insertions(+), 65 deletions(-)

diff --git 
a/modules/cli/src/integrationTest/java/org/apache/ignite/internal/cli/commands/cluster/status/ItClusterStatusCommandInitializedTest.java
 
b/modules/cli/src/integrationTest/java/org/apache/ignite/internal/cli/commands/cluster/status/ItClusterStatusCommandInitializedTest.java
index b3a17a77a4..181deccbcc 100644
--- 
a/modules/cli/src/integrationTest/java/org/apache/ignite/internal/cli/commands/cluster/status/ItClusterStatusCommandInitializedTest.java
+++ 
b/modules/cli/src/integrationTest/java/org/apache/ignite/internal/cli/commands/cluster/status/ItClusterStatusCommandInitializedTest.java
@@ -17,12 +17,17 @@
 
 package org.apache.ignite.internal.cli.commands.cluster.status;
 
+import static java.util.function.Function.identity;
 import static java.util.stream.Collectors.joining;
 import static org.junit.jupiter.api.Assertions.assertAll;
 
 import java.util.Arrays;
-import org.apache.ignite.Ignite;
+import java.util.Map;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+import java.util.stream.IntStream;
 import org.apache.ignite.internal.cli.CliIntegrationTest;
+import org.jetbrains.annotations.Nullable;
 import org.junit.jupiter.api.DisplayName;
 import org.junit.jupiter.api.Test;
 
@@ -30,24 +35,61 @@ import org.junit.jupiter.api.Test;
  * Tests for {@link ClusterStatusCommand} for the cluster that is initialized.
  */
 class ItClusterStatusCommandInitializedTest extends CliIntegrationTest {
+    private Function<int[], String> mapper;
+
+    @Override
+    protected @Nullable int[] metastoreNodes() {
+        return new int[] { 0 };
+    }
+
+    @Override
+    protected @Nullable int[] cmgNodes() {
+        return new int[] { 1 };
+    }
+
     @Test
     @DisplayName("Should print status when valid cluster url is given but 
cluster is initialized")
-    void printStatus() {
-        String cmgNodes = Arrays.stream(cmgMetastoreNodes())
-                .mapToObj(CLUSTER::node)
-                .map(Ignite::name)
+    void printStatus() throws InterruptedException {
+        String node0Url = NODE_URL;
+        String node1Url = "http://localhost:"; + CLUSTER.httpPort(1);
+
+        Map<Integer, String> nodeNames = IntStream.range(0, initialNodes())
+                .boxed()
+                .collect(Collectors.toMap(identity(), i ->  
CLUSTER.node(i).name()));
+
+        mapper = nodes -> Arrays.stream(nodes)
+                .mapToObj(nodeNames::get)
                 .collect(joining(", ", "[", "]"));
 
-        execute("cluster", "status", "--url", NODE_URL);
+        CLUSTER.stopNode(0);
+        execute("cluster", "status", "--url", node1Url);
+        assertOutput("cluster", 2, "Metastore majority lost", cmgNodes(), 
metastoreNodes());
+
+        CLUSTER.startNode(0);
+        Thread.sleep(10000);
+        execute("cluster", "status", "--url", node1Url);
+        assertOutput("cluster", 3, "active", cmgNodes(), metastoreNodes());
+
+        CLUSTER.stopNode(1);
+        execute("cluster", "status", "--url", node0Url);
+        assertOutput("N/A", 2, "CMG majority lost", new int[0], new int[0]);
+    }
 
+    private void assertOutput(
+            String name,
+            int nodesCount,
+            String statusStr,
+            int[] cmgNodes,
+            int[] metastoreNodes
+    ) {
         assertAll(
                 this::assertExitCodeIsZero,
                 this::assertErrOutputIsEmpty,
-                () -> assertOutputContains("name: cluster"),
-                () -> assertOutputContains("nodes: 3"),
-                () -> assertOutputContains("status: active"),
-                () -> assertOutputContains("cmgNodes: " + cmgNodes),
-                () -> assertOutputContains("msNodes: " + cmgNodes)
+                () -> assertOutputContains("name: " + name),
+                () -> assertOutputContains("nodes: " + nodesCount),
+                () -> assertOutputContains("status: " + statusStr),
+                () -> assertOutputContains("cmgNodes: " + 
mapper.apply(cmgNodes)),
+                () -> assertOutputContains("msNodes: " + 
mapper.apply(metastoreNodes))
         );
     }
 }
diff --git 
a/modules/cli/src/integrationTest/java/org/apache/ignite/internal/cli/commands/sql/ItSqlCommandTest.java
 
b/modules/cli/src/integrationTest/java/org/apache/ignite/internal/cli/commands/sql/ItSqlCommandTest.java
index 416319d22d..7b83d2758b 100644
--- 
a/modules/cli/src/integrationTest/java/org/apache/ignite/internal/cli/commands/sql/ItSqlCommandTest.java
+++ 
b/modules/cli/src/integrationTest/java/org/apache/ignite/internal/cli/commands/sql/ItSqlCommandTest.java
@@ -33,6 +33,8 @@ class ItSqlCommandTest extends CliSqlCommandTestBase {
     void nonExistingFile() {
         execute("sql", "--file", "nonexisting", "--jdbc-url", JDBC_URL);
 
+        CLUSTER.stopNode(0);
+
         assertAll(
                 () -> assertExitCodeIs(1),
                 this::assertOutputIsEmpty,
diff --git 
a/modules/cli/src/main/java/org/apache/ignite/internal/cli/call/cluster/status/ClusterStatus.java
 
b/modules/cli/src/main/java/org/apache/ignite/internal/cli/call/cluster/status/ClusterState.java
similarity index 59%
rename from 
modules/cli/src/main/java/org/apache/ignite/internal/cli/call/cluster/status/ClusterStatus.java
rename to 
modules/cli/src/main/java/org/apache/ignite/internal/cli/call/cluster/status/ClusterState.java
index a358486c5a..6dd24752ae 100644
--- 
a/modules/cli/src/main/java/org/apache/ignite/internal/cli/call/cluster/status/ClusterStatus.java
+++ 
b/modules/cli/src/main/java/org/apache/ignite/internal/cli/call/cluster/status/ClusterState.java
@@ -18,11 +18,12 @@
 package org.apache.ignite.internal.cli.call.cluster.status;
 
 import java.util.List;
+import org.apache.ignite.rest.client.model.ClusterStatus;
 
 /**
  * Class that represents the cluster status.
  */
-public class ClusterStatus {
+public class ClusterState {
 
     private final int nodeCount;
 
@@ -38,8 +39,18 @@ public class ClusterStatus {
 
     private final List<String> metadataStorageNodes;
 
-    private ClusterStatus(int nodeCount, boolean initialized, String name,
-            boolean connected, String nodeUrl, List<String> cmgNodes, 
List<String> metadataStorageNodes) {
+    private final ClusterStatus clusterStatus;
+
+    private ClusterState(
+            int nodeCount,
+            boolean initialized,
+            String name,
+            boolean connected,
+            String nodeUrl,
+            List<String> cmgNodes,
+            List<String> metadataStorageNodes,
+            ClusterStatus clusterStatus
+    ) {
         this.nodeCount = nodeCount;
         this.initialized = initialized;
         this.name = name;
@@ -47,6 +58,7 @@ public class ClusterStatus {
         this.nodeUrl = nodeUrl;
         this.cmgNodes = cmgNodes;
         this.metadataStorageNodes = metadataStorageNodes;
+        this.clusterStatus = clusterStatus;
     }
 
     public String nodeCount() {
@@ -77,17 +89,21 @@ public class ClusterStatus {
         return metadataStorageNodes;
     }
 
+    public ClusterStatus clusterStatus() {
+        return clusterStatus;
+    }
+
     /**
-     * Builder for {@link ClusterStatus}.
+     * Builder for {@link ClusterState}.
      */
-    public static ClusterStatusBuilder builder() {
-        return new ClusterStatusBuilder();
+    public static ClusterStateBuilder builder() {
+        return new ClusterStateBuilder();
     }
 
     /**
-     * Builder for {@link ClusterStatus}.
+     * Builder for {@link ClusterState}.
      */
-    public static class ClusterStatusBuilder {
+    public static class ClusterStateBuilder {
         private int nodeCount;
 
         private boolean initialized;
@@ -102,47 +118,66 @@ public class ClusterStatus {
 
         private List<String> metadataStorageNodes;
 
-        private ClusterStatusBuilder() {
+        private ClusterStatus clusterStatus;
+
+        private ClusterStateBuilder() {
 
         }
 
-        public ClusterStatusBuilder nodeCount(int nodeCount) {
+        public ClusterStateBuilder nodeCount(int nodeCount) {
             this.nodeCount = nodeCount;
             return this;
         }
 
-        public ClusterStatusBuilder initialized(boolean initialized) {
+        public ClusterStateBuilder initialized(boolean initialized) {
             this.initialized = initialized;
             return this;
         }
 
-        public ClusterStatusBuilder name(String name) {
+        public ClusterStateBuilder name(String name) {
             this.name = name;
             return this;
         }
 
-        public ClusterStatusBuilder connected(boolean connected) {
+        public ClusterStateBuilder connected(boolean connected) {
             this.connected = connected;
             return this;
         }
 
-        public ClusterStatusBuilder connectedNodeUrl(String connectedNodeUrl) {
+        public ClusterStateBuilder connectedNodeUrl(String connectedNodeUrl) {
             this.connectedNodeUrl = connectedNodeUrl;
             return this;
         }
 
-        public ClusterStatusBuilder cmgNodes(List<String> cmgNodes) {
+        public ClusterStateBuilder cmgNodes(List<String> cmgNodes) {
             this.cmgNodes = cmgNodes;
             return this;
         }
 
-        public ClusterStatusBuilder metadataStorageNodes(List<String> 
metadataStorageNodes) {
+        public ClusterStateBuilder metadataStorageNodes(List<String> 
metadataStorageNodes) {
             this.metadataStorageNodes = metadataStorageNodes;
             return this;
         }
 
-        public ClusterStatus build() {
-            return new ClusterStatus(nodeCount, initialized, name, connected, 
connectedNodeUrl, cmgNodes, metadataStorageNodes);
+        public ClusterStateBuilder clusterStatus(ClusterStatus clusterStatus) {
+            this.clusterStatus = clusterStatus;
+            return this;
+        }
+
+        /**
+         * Returns new cluster state instance.
+         */
+        public ClusterState build() {
+            return new ClusterState(
+                    nodeCount,
+                    initialized,
+                    name,
+                    connected,
+                    connectedNodeUrl,
+                    cmgNodes,
+                    metadataStorageNodes,
+                    clusterStatus
+            );
         }
     }
 }
diff --git 
a/modules/cli/src/main/java/org/apache/ignite/internal/cli/call/cluster/status/ClusterStatusCall.java
 
b/modules/cli/src/main/java/org/apache/ignite/internal/cli/call/cluster/status/ClusterStatusCall.java
index 9c0fb22968..262b02e765 100644
--- 
a/modules/cli/src/main/java/org/apache/ignite/internal/cli/call/cluster/status/ClusterStatusCall.java
+++ 
b/modules/cli/src/main/java/org/apache/ignite/internal/cli/call/cluster/status/ClusterStatusCall.java
@@ -19,7 +19,7 @@ package org.apache.ignite.internal.cli.call.cluster.status;
 
 import jakarta.inject.Singleton;
 import java.util.List;
-import 
org.apache.ignite.internal.cli.call.cluster.status.ClusterStatus.ClusterStatusBuilder;
+import 
org.apache.ignite.internal.cli.call.cluster.status.ClusterState.ClusterStateBuilder;
 import 
org.apache.ignite.internal.cli.call.cluster.topology.PhysicalTopologyCall;
 import org.apache.ignite.internal.cli.core.call.Call;
 import org.apache.ignite.internal.cli.core.call.CallOutput;
@@ -30,13 +30,12 @@ import 
org.apache.ignite.internal.cli.core.rest.ApiClientFactory;
 import org.apache.ignite.rest.client.api.ClusterManagementApi;
 import org.apache.ignite.rest.client.invoker.ApiException;
 import org.apache.ignite.rest.client.model.ClusterNode;
-import org.apache.ignite.rest.client.model.ClusterState;
 
 /**
  * Call to get cluster status.
  */
 @Singleton
-public class ClusterStatusCall implements Call<UrlCallInput, ClusterStatus> {
+public class ClusterStatusCall implements Call<UrlCallInput, ClusterState> {
 
     private final PhysicalTopologyCall physicalTopologyCall;
 
@@ -48,20 +47,21 @@ public class ClusterStatusCall implements 
Call<UrlCallInput, ClusterStatus> {
     }
 
     @Override
-    public CallOutput<ClusterStatus> execute(UrlCallInput input) {
-        ClusterStatusBuilder clusterStatusBuilder = ClusterStatus.builder();
+    public CallOutput<ClusterState> execute(UrlCallInput input) {
+        ClusterStateBuilder clusterStateBuilder = ClusterState.builder();
         String clusterUrl = input.getUrl();
         try {
-            ClusterState clusterState = fetchClusterState(clusterUrl);
-            clusterStatusBuilder
+            org.apache.ignite.rest.client.model.ClusterState clusterState = 
fetchClusterState(clusterUrl);
+            clusterStateBuilder
                     .nodeCount(fetchNumberOfAllNodes(input))
                     .initialized(true)
                     .name(clusterState.getClusterTag().getClusterName())
                     .metadataStorageNodes(clusterState.getMsNodes())
-                    .cmgNodes(clusterState.getCmgNodes());
+                    .cmgNodes(clusterState.getCmgNodes())
+                    .clusterStatus(clusterState.getClusterStatus());
         } catch (ApiException e) {
             if (e.getCode() == 409) { // CONFLICT means the cluster is not 
initialized yet
-                
clusterStatusBuilder.initialized(false).nodeCount(fetchNumberOfAllNodes(input));
+                
clusterStateBuilder.initialized(false).nodeCount(fetchNumberOfAllNodes(input));
             } else {
                 return DefaultCallOutput.failure(new IgniteCliApiException(e, 
clusterUrl));
             }
@@ -69,7 +69,7 @@ public class ClusterStatusCall implements Call<UrlCallInput, 
ClusterStatus> {
             return DefaultCallOutput.failure(new IgniteCliApiException(e, 
clusterUrl));
         }
 
-        return DefaultCallOutput.success(clusterStatusBuilder.build());
+        return DefaultCallOutput.success(clusterStateBuilder.build());
     }
 
     private int fetchNumberOfAllNodes(UrlCallInput input) {
@@ -80,7 +80,7 @@ public class ClusterStatusCall implements Call<UrlCallInput, 
ClusterStatus> {
         return body.size();
     }
 
-    private ClusterState fetchClusterState(String url) throws ApiException {
-        return new 
ClusterManagementApi(clientFactory.getClient(url)).clusterState();
+    private org.apache.ignite.rest.client.model.ClusterState 
fetchClusterState(String url) throws ApiException {
+        return new 
ClusterManagementApi(clientFactory.getClient(url).setConnectTimeout(100_000).setReadTimeout(100_000)).clusterState();
     }
 }
diff --git 
a/modules/cli/src/main/java/org/apache/ignite/internal/cli/decorators/ClusterStatusDecorator.java
 
b/modules/cli/src/main/java/org/apache/ignite/internal/cli/decorators/ClusterStatusDecorator.java
index a722c03483..0d073c0b2a 100644
--- 
a/modules/cli/src/main/java/org/apache/ignite/internal/cli/decorators/ClusterStatusDecorator.java
+++ 
b/modules/cli/src/main/java/org/apache/ignite/internal/cli/decorators/ClusterStatusDecorator.java
@@ -20,23 +20,24 @@ package org.apache.ignite.internal.cli.decorators;
 import static org.apache.ignite.internal.cli.core.style.AnsiStringSupport.ansi;
 import static org.apache.ignite.internal.cli.core.style.AnsiStringSupport.fg;
 
-import org.apache.ignite.internal.cli.call.cluster.status.ClusterStatus;
+import org.apache.ignite.internal.cli.call.cluster.status.ClusterState;
 import org.apache.ignite.internal.cli.core.decorator.Decorator;
 import org.apache.ignite.internal.cli.core.decorator.TerminalOutput;
 import org.apache.ignite.internal.cli.core.style.AnsiStringSupport.Color;
+import org.apache.ignite.rest.client.model.ClusterStatus;
 
 /**
- * Decorator for {@link ClusterStatus}.
+ * Decorator for {@link ClusterState}.
  */
-public class ClusterStatusDecorator implements Decorator<ClusterStatus, 
TerminalOutput> {
+public class ClusterStatusDecorator implements Decorator<ClusterState, 
TerminalOutput> {
     @Override
-    public TerminalOutput decorate(ClusterStatus data) {
+    public TerminalOutput decorate(ClusterState data) {
         return data.isInitialized()
                 ? () -> ansi(String.format(
                 "[name: %s, nodes: %s, status: %s, cmgNodes: %s, msNodes: %s]",
                 data.getName(),
                 data.nodeCount(),
-                fg(Color.GREEN).mark("active"),
+                status(data.clusterStatus()),
                 data.getCmgNodes(),
                 data.getMsNodes()
         ))
@@ -45,4 +46,17 @@ public class ClusterStatusDecorator implements 
Decorator<ClusterStatus, Terminal
                         data.nodeCount(), fg(Color.RED).mark("not initialized")
                 ));
     }
+
+    private static String status(ClusterStatus status) {
+        switch (status) {
+            case MS_MAJORITY_LOST:
+                return fg(Color.RED).mark("Metastore majority lost");
+            case HEALTHY:
+                return fg(Color.GREEN).mark("active");
+            case CMG_MAJORITY_LOST:
+                return fg(Color.RED).mark("CMG majority lost");
+            default:
+                return "";
+        }
+    }
 }
diff --git 
a/modules/cli/src/main/java/org/apache/ignite/internal/cli/decorators/DefaultDecoratorRegistry.java
 
b/modules/cli/src/main/java/org/apache/ignite/internal/cli/decorators/DefaultDecoratorRegistry.java
index c4b6b8604a..e14c9dddcb 100644
--- 
a/modules/cli/src/main/java/org/apache/ignite/internal/cli/decorators/DefaultDecoratorRegistry.java
+++ 
b/modules/cli/src/main/java/org/apache/ignite/internal/cli/decorators/DefaultDecoratorRegistry.java
@@ -18,7 +18,7 @@
 package org.apache.ignite.internal.cli.decorators;
 
 import org.apache.ignite.internal.cli.call.cliconfig.profile.ProfileList;
-import org.apache.ignite.internal.cli.call.cluster.status.ClusterStatus;
+import org.apache.ignite.internal.cli.call.cluster.status.ClusterState;
 import org.apache.ignite.internal.cli.call.configuration.JsonString;
 import org.apache.ignite.internal.cli.call.node.status.NodeStatus;
 import org.apache.ignite.internal.cli.config.Profile;
@@ -43,7 +43,7 @@ public class DefaultDecoratorRegistry extends 
DecoratorRegistry {
         add(ProfileList.class, new ProfileListDecorator());
         add(Table.class, new TableDecorator(false));
         add(SqlQueryResult.class, new SqlQueryResultDecorator(false));
-        add(ClusterStatus.class, new ClusterStatusDecorator());
+        add(ClusterState.class, new ClusterStatusDecorator());
         add(NodeStatus.class, new NodeStatusDecorator());
         add(NodeVersion.class, new NodeVersionDecorator());
     }
diff --git 
a/modules/rest-api/src/main/java/org/apache/ignite/internal/rest/api/cluster/ClusterState.java
 
b/modules/rest-api/src/main/java/org/apache/ignite/internal/rest/api/cluster/ClusterState.java
index e70f2ece32..930901ed8b 100644
--- 
a/modules/rest-api/src/main/java/org/apache/ignite/internal/rest/api/cluster/ClusterState.java
+++ 
b/modules/rest-api/src/main/java/org/apache/ignite/internal/rest/api/cluster/ClusterState.java
@@ -19,8 +19,10 @@ package org.apache.ignite.internal.rest.api.cluster;
 
 import com.fasterxml.jackson.annotation.JsonCreator;
 import com.fasterxml.jackson.annotation.JsonGetter;
+import com.fasterxml.jackson.annotation.JsonInclude;
 import com.fasterxml.jackson.annotation.JsonProperty;
 import io.swagger.v3.oas.annotations.media.Schema;
+import io.swagger.v3.oas.annotations.media.Schema.RequiredMode;
 import java.util.Collection;
 import java.util.List;
 import java.util.Objects;
@@ -34,24 +36,31 @@ import org.jetbrains.annotations.Nullable;
  */
 @Schema(description = "Information about current cluster state.")
 public class ClusterState {
-    @Schema(description = "List of cluster management group nodes. These nodes 
are responsible for maintaining RAFT cluster topology.")
+    @Schema(description = "List of cluster management group nodes. These nodes 
are responsible for maintaining RAFT cluster topology.",
+            requiredMode = RequiredMode.REQUIRED)
     @IgniteToStringInclude
     private final Collection<String> cmgNodes;
 
-    @Schema(description = "List of metastorage nodes. These nodes are 
responsible for storing RAFT cluster metadata.")
+    @Schema(description = "List of metastorage nodes. These nodes are 
responsible for storing RAFT cluster metadata.",
+            requiredMode = RequiredMode.REQUIRED)
     @IgniteToStringInclude
     private final Collection<String> msNodes;
 
-    @Schema(description = "Version of Apache Ignite that the cluster was 
created on.")
+    @Schema(description = "Version of Apache Ignite that the cluster was 
created on.", requiredMode = RequiredMode.REQUIRED)
     private final String igniteVersion;
 
-    @Schema(description = "Unique tag that identifies the cluster.")
+    @Schema(description = "Unique tag that identifies the cluster.",
+            requiredMode = RequiredMode.REQUIRED)
     private final ClusterTag clusterTag;
 
     @Schema(description = "IDs the cluster had before.")
     @IgniteToStringInclude
     private final @Nullable List<UUID> formerClusterIds;
 
+    @Schema(description = "Cluster status.",
+            requiredMode = RequiredMode.REQUIRED)
+    private final ClusterStatus clusterStatus;
+
     /**
      * Creates a new cluster state.
      *
@@ -64,24 +73,28 @@ public class ClusterState {
     @JsonCreator
     public ClusterState(
             @JsonProperty("cmgNodes") Collection<String> cmgNodes,
-            @JsonProperty("msNodes") Collection<String> msNodes,
+            @JsonProperty("msNodes")  Collection<String> msNodes,
             @JsonProperty("igniteVersion") String igniteVersion,
             @JsonProperty("clusterTag") ClusterTag clusterTag,
-            @JsonProperty("formerClusterIds") @Nullable List<UUID> 
formerClusterIds
+            @JsonProperty("formerClusterIds") @Nullable List<UUID> 
formerClusterIds,
+            @JsonProperty("clusterStatus") ClusterStatus clusterStatus
     ) {
         this.cmgNodes = List.copyOf(cmgNodes);
         this.msNodes = List.copyOf(msNodes);
         this.igniteVersion = igniteVersion;
         this.clusterTag = clusterTag;
         this.formerClusterIds = formerClusterIds == null ? null : 
List.copyOf(formerClusterIds);
+        this.clusterStatus = clusterStatus;
     }
 
     @JsonGetter("cmgNodes")
+    @JsonInclude
     public Collection<String> cmgNodes() {
         return cmgNodes;
     }
 
     @JsonGetter("msNodes")
+    @JsonInclude
     public Collection<String> msNodes() {
         return msNodes;
     }
@@ -101,6 +114,11 @@ public class ClusterState {
         return formerClusterIds;
     }
 
+    @JsonGetter("clusterStatus")
+    public ClusterStatus clusterStatus() {
+        return clusterStatus;
+    }
+
     @Override
     public boolean equals(Object o) {
         if (this == o) {
diff --git 
a/modules/rest-api/src/main/java/org/apache/ignite/internal/rest/api/cluster/ClusterStatus.java
 
b/modules/rest-api/src/main/java/org/apache/ignite/internal/rest/api/cluster/ClusterStatus.java
new file mode 100644
index 0000000000..32c42b4246
--- /dev/null
+++ 
b/modules/rest-api/src/main/java/org/apache/ignite/internal/rest/api/cluster/ClusterStatus.java
@@ -0,0 +1,40 @@
+/*
+ * 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.ignite.internal.rest.api.cluster;
+
+/**
+ * Current health status of the cluster.
+ */
+public enum ClusterStatus {
+    /**
+     * The cluster is completely healthy. Minor losses in any of the groups 
are possible,
+     * but this does not affect the operation of the cluster.
+     */
+    HEALTHY,
+
+    /**
+     * The metastore group has lost its majority. Almost all of the cluster 
functions are inoperative.
+     * To restore their operation, it is necessary to return the majority to 
the metastore group.
+     */
+    MS_MAJORITY_LOST,
+
+    /**
+     * The cluster management group has lost its majority. The cluster is 
completely inoperative until the majority is returned.
+     */
+    CMG_MAJORITY_LOST,
+}
diff --git 
a/modules/rest/src/main/java/org/apache/ignite/internal/rest/cluster/ClusterManagementController.java
 
b/modules/rest/src/main/java/org/apache/ignite/internal/rest/cluster/ClusterManagementController.java
index a9ae750979..207284f19a 100644
--- 
a/modules/rest/src/main/java/org/apache/ignite/internal/rest/cluster/ClusterManagementController.java
+++ 
b/modules/rest/src/main/java/org/apache/ignite/internal/rest/cluster/ClusterManagementController.java
@@ -17,23 +17,31 @@
 
 package org.apache.ignite.internal.rest.cluster;
 
+import static java.util.Collections.emptyList;
+
 import io.micronaut.http.annotation.Body;
 import io.micronaut.http.annotation.Controller;
+import java.util.Set;
 import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.CompletionException;
+import java.util.concurrent.TimeoutException;
 import 
org.apache.ignite.configuration.validation.ConfigurationValidationException;
 import org.apache.ignite.internal.cluster.management.ClusterInitializer;
 import 
org.apache.ignite.internal.cluster.management.ClusterManagementGroupManager;
 import org.apache.ignite.internal.lang.IgniteInternalException;
 import org.apache.ignite.internal.logger.IgniteLogger;
 import org.apache.ignite.internal.logger.Loggers;
+import org.apache.ignite.internal.network.TopologyService;
 import org.apache.ignite.internal.rest.ResourceHolder;
 import org.apache.ignite.internal.rest.api.cluster.ClusterManagementApi;
 import org.apache.ignite.internal.rest.api.cluster.ClusterState;
+import org.apache.ignite.internal.rest.api.cluster.ClusterStatus;
 import org.apache.ignite.internal.rest.api.cluster.ClusterTag;
 import org.apache.ignite.internal.rest.api.cluster.InitCommand;
 import 
org.apache.ignite.internal.rest.cluster.exception.InvalidArgumentClusterInitializationException;
 import org.apache.ignite.internal.util.ExceptionUtils;
 import org.apache.ignite.lang.IgniteException;
+import org.apache.ignite.network.ClusterNode;
 
 /**
  * Cluster management controller implementation.
@@ -46,6 +54,8 @@ public class ClusterManagementController implements 
ClusterManagementApi, Resour
 
     private ClusterManagementGroupManager clusterManagementGroupManager;
 
+    private TopologyService topologyService;
+
     /**
      * Cluster management controller constructor.
      *
@@ -54,16 +64,35 @@ public class ClusterManagementController implements 
ClusterManagementApi, Resour
      */
     public ClusterManagementController(
             ClusterInitializer clusterInitializer,
-            ClusterManagementGroupManager clusterManagementGroupManager
+            ClusterManagementGroupManager clusterManagementGroupManager,
+            TopologyService topologyService
     ) {
         this.clusterInitializer = clusterInitializer;
         this.clusterManagementGroupManager = clusterManagementGroupManager;
+        this.topologyService = topologyService;
     }
 
     /** {@inheritDoc} */
     @Override
     public CompletableFuture<ClusterState> clusterState() {
-        return 
clusterManagementGroupManager.clusterState().thenApply(ClusterManagementController::mapClusterState);
+        return clusterManagementGroupManager.clusterState().handle((state, t) 
-> {
+            if (t != null) {
+                if (ExceptionUtils.unwrapCause(t) instanceof TimeoutException) 
{
+                    return new ClusterState(
+                            emptyList(),
+                            emptyList(),
+                            "N/A",
+                            new ClusterTag("N/A", null),
+                            null,
+                            ClusterStatus.CMG_MAJORITY_LOST
+                    );
+                } else {
+                    throw new CompletionException(t);
+                }
+            } else {
+                return mapClusterState(state);
+            }
+        });
     }
 
     /** {@inheritDoc} */
@@ -84,16 +113,31 @@ public class ClusterManagementController implements 
ClusterManagementApi, Resour
         });
     }
 
-    private static ClusterState 
mapClusterState(org.apache.ignite.internal.cluster.management.ClusterState 
clusterState) {
+    private ClusterState 
mapClusterState(org.apache.ignite.internal.cluster.management.ClusterState 
clusterState) {
         return new ClusterState(
                 clusterState.cmgNodes(),
                 clusterState.metaStorageNodes(),
                 clusterState.igniteVersion().toString(),
                 new ClusterTag(clusterState.clusterTag().clusterName(), 
clusterState.clusterTag().clusterId()),
-                clusterState.formerClusterIds()
+                clusterState.formerClusterIds(),
+                mapClusterStatus(clusterState)
         );
     }
 
+    private ClusterStatus 
mapClusterStatus(org.apache.ignite.internal.cluster.management.ClusterState 
clusterState) {
+        Set<String> metaStorageNodes = clusterState.metaStorageNodes();
+        long presentedMetaStorageNodes = topologyService.allMembers().stream()
+                .map(ClusterNode::name)
+                .filter(metaStorageNodes::contains)
+                .count();
+
+        if (presentedMetaStorageNodes <= metaStorageNodes.size() / 2) {
+            return ClusterStatus.MS_MAJORITY_LOST;
+        } else {
+            return ClusterStatus.HEALTHY;
+        }
+    }
+
     private static RuntimeException mapException(Throwable ex) {
         var cause = ExceptionUtils.unwrapCause(ex);
         if (cause instanceof IgniteInternalException) {
@@ -111,5 +155,6 @@ public class ClusterManagementController implements 
ClusterManagementApi, Resour
     public void cleanResources() {
         clusterInitializer = null;
         clusterManagementGroupManager = null;
+        topologyService = null;
     }
 }
diff --git 
a/modules/runner/src/testFixtures/java/org/apache/ignite/internal/Cluster.java 
b/modules/runner/src/testFixtures/java/org/apache/ignite/internal/Cluster.java
index 881185456d..eca72f7fa0 100644
--- 
a/modules/runner/src/testFixtures/java/org/apache/ignite/internal/Cluster.java
+++ 
b/modules/runner/src/testFixtures/java/org/apache/ignite/internal/Cluster.java
@@ -173,6 +173,17 @@ public class Cluster {
         startAndInit(nodeCount, new int[]{0}, initParametersConfigurator);
     }
 
+    /**
+     * Starts the cluster with the given number of nodes and initializes it.
+     *
+     * @param nodeCount Number of nodes in the cluster.
+     * @param cmgMetastoreNodes Indices of CMG and Metastore nodes.
+     * @param initParametersConfigurator Configure {@link InitParameters} 
before initializing the cluster.
+     */
+    public void startAndInit(int nodeCount, int[] cmgMetastoreNodes, 
Consumer<InitParametersBuilder> initParametersConfigurator) {
+        startAndInit(nodeCount, cmgMetastoreNodes, cmgMetastoreNodes, 
defaultNodeBootstrapConfigTemplate, initParametersConfigurator);
+    }
+
     /**
      * Starts the cluster with the given number of nodes and initializes it.
      *
@@ -180,8 +191,13 @@ public class Cluster {
      * @param cmgNodes Indices of CMG nodes.
      * @param initParametersConfigurator Configure {@link InitParameters} 
before initializing the cluster.
      */
-    public void startAndInit(int nodeCount, int[] cmgNodes, 
Consumer<InitParametersBuilder> initParametersConfigurator) {
-        startAndInit(nodeCount, cmgNodes, defaultNodeBootstrapConfigTemplate, 
initParametersConfigurator);
+    public void startAndInit(
+            int nodeCount,
+            int[] cmgNodes,
+            int[] metastoreNodes,
+            Consumer<InitParametersBuilder> initParametersConfigurator
+    ) {
+        startAndInit(nodeCount, cmgNodes, metastoreNodes, 
defaultNodeBootstrapConfigTemplate, initParametersConfigurator);
     }
 
     /**
@@ -197,7 +213,7 @@ public class Cluster {
             String nodeBootstrapConfigTemplate,
             Consumer<InitParametersBuilder> initParametersConfigurator
     ) {
-        startAndInit(nodeCount, new int[] { 0 }, nodeBootstrapConfigTemplate, 
initParametersConfigurator);
+        startAndInit(nodeCount, new int[] { 0 }, new int[] { 0 }, 
nodeBootstrapConfigTemplate, initParametersConfigurator);
     }
 
     /**
@@ -212,6 +228,7 @@ public class Cluster {
     private void startAndInit(
             int nodeCount,
             int[] cmgNodes,
+            int[] metastoreNodes,
             String nodeBootstrapConfigTemplate,
             Consumer<InitParametersBuilder> initParametersConfigurator
     ) {
@@ -225,18 +242,25 @@ public class Cluster {
                 .mapToObj(nodeIndex -> startEmbeddedNode(nodeIndex, 
nodeBootstrapConfigTemplate))
                 .collect(toList());
 
-        List<IgniteServer> metaStorageAndCmgNodes = Arrays.stream(cmgNodes)
+        List<IgniteServer> cmgNodeServers = Arrays.stream(cmgNodes)
+                .mapToObj(nodeRegistrations::get)
+                .map(ServerRegistration::server)
+                .collect(toList());
+
+        List<IgniteServer> metastoreNodeServers = Arrays.stream(metastoreNodes)
                 .mapToObj(nodeRegistrations::get)
                 .map(ServerRegistration::server)
                 .collect(toList());
 
         InitParametersBuilder builder = InitParameters.builder()
-                .metaStorageNodes(metaStorageAndCmgNodes)
+                .metaStorageNodes(metastoreNodeServers)
+                .cmgNodes(cmgNodeServers)
+                .cmgNodeNames(nodeRegistrations.get(1).server().name())
                 .clusterName("cluster");
 
         initParametersConfigurator.accept(builder);
 
-        TestIgnitionManager.init(metaStorageAndCmgNodes.get(0), 
builder.build());
+        TestIgnitionManager.init(cmgNodeServers.get(0), builder.build());
 
         for (ServerRegistration registration : nodeRegistrations) {
             assertThat(registration.registrationFuture(), 
willCompleteSuccessfully());
diff --git 
a/modules/runner/src/testFixtures/java/org/apache/ignite/internal/ClusterPerClassIntegrationTest.java
 
b/modules/runner/src/testFixtures/java/org/apache/ignite/internal/ClusterPerClassIntegrationTest.java
index 9c2ddae217..4014766864 100644
--- 
a/modules/runner/src/testFixtures/java/org/apache/ignite/internal/ClusterPerClassIntegrationTest.java
+++ 
b/modules/runner/src/testFixtures/java/org/apache/ignite/internal/ClusterPerClassIntegrationTest.java
@@ -106,7 +106,9 @@ public abstract class ClusterPerClassIntegrationTest 
extends BaseIgniteAbstractT
         CLUSTER = new Cluster(testInfo, WORK_DIR, 
getNodeBootstrapConfigTemplate());
 
         if (initialNodes() > 0 && needInitializeCluster()) {
-            CLUSTER.startAndInit(initialNodes(), cmgMetastoreNodes(), 
this::configureInitParameters);
+            int[] cmgNodes = cmgNodes() != null ? cmgNodes() : 
cmgMetastoreNodes();
+            int[] metastoreNodes = metastoreNodes() != null ? metastoreNodes() 
: cmgMetastoreNodes();
+            CLUSTER.startAndInit(initialNodes(), cmgNodes, metastoreNodes, 
this::configureInitParameters);
         }
     }
 
@@ -123,6 +125,16 @@ public abstract class ClusterPerClassIntegrationTest 
extends BaseIgniteAbstractT
         return new int[] { 0 };
     }
 
+    @Nullable
+    protected int[] metastoreNodes() {
+        return null;
+    }
+
+    @Nullable
+    protected int[] cmgNodes() {
+        return null;
+    }
+
     protected boolean needInitializeCluster() {
         return true;
     }


Reply via email to