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; }
