diff --git a/server/src/internalClusterTest/java/org/elasticsearch/cluster/ClusterHealthIT.java b/server/src/internalClusterTest/java/org/elasticsearch/cluster/ClusterHealthIT.java index 8c5cb264aafc3..8c0614534221d 100644 --- a/server/src/internalClusterTest/java/org/elasticsearch/cluster/ClusterHealthIT.java +++ b/server/src/internalClusterTest/java/org/elasticsearch/cluster/ClusterHealthIT.java @@ -16,6 +16,7 @@ import org.elasticsearch.action.support.PlainActionFuture; import org.elasticsearch.cluster.health.ClusterHealthStatus; import org.elasticsearch.cluster.metadata.IndexMetadata; +import org.elasticsearch.cluster.metadata.Metadata; import org.elasticsearch.cluster.routing.UnassignedInfo; import org.elasticsearch.cluster.service.ClusterService; import org.elasticsearch.common.Priority; @@ -56,8 +57,14 @@ public void testSimpleLocalHealth() { } public void testHealth() { + logger.info("--> running cluster health for '_all' on an empty cluster"); + ClusterHealthResponse healthResponse = clusterAdmin().prepareHealth(TEST_REQUEST_TIMEOUT, Metadata.ALL).get(); + assertThat(healthResponse.isTimedOut(), equalTo(false)); + assertThat(healthResponse.getStatus(), equalTo(ClusterHealthStatus.GREEN)); + assertThat(healthResponse.getIndices().isEmpty(), equalTo(true)); + logger.info("--> running cluster health on an index that does not exists"); - ClusterHealthResponse healthResponse = clusterAdmin().prepareHealth(TEST_REQUEST_TIMEOUT, "test1") + healthResponse = clusterAdmin().prepareHealth(TEST_REQUEST_TIMEOUT, "test1") .setWaitForYellowStatus() .setTimeout(TimeValue.timeValueSeconds(1)) .get(); diff --git a/server/src/internalClusterTest/java/org/elasticsearch/synonyms/SynonymsManagementAPIServiceIT.java b/server/src/internalClusterTest/java/org/elasticsearch/synonyms/SynonymsManagementAPIServiceIT.java index 1872714b51820..9350cecb2d697 100644 --- a/server/src/internalClusterTest/java/org/elasticsearch/synonyms/SynonymsManagementAPIServiceIT.java +++ b/server/src/internalClusterTest/java/org/elasticsearch/synonyms/SynonymsManagementAPIServiceIT.java @@ -15,6 +15,7 @@ import org.elasticsearch.action.bulk.BulkResponse; import org.elasticsearch.cluster.ClusterName; import org.elasticsearch.cluster.ClusterState; +import org.elasticsearch.common.Strings; import org.elasticsearch.index.mapper.extras.MapperExtrasPlugin; import org.elasticsearch.indices.IndexCreationException; import org.elasticsearch.plugins.Plugin; @@ -316,11 +317,7 @@ public void testCreateSynonymsWithYellowSynonymsIndex() throws Exception { @Override void checkSynonymsIndexHealth(ActionListener listener) { ClusterState clusterState = ClusterState.builder(ClusterName.DEFAULT).build(); - ClusterHealthResponse response = new ClusterHealthResponse( - randomIdentifier(), - new String[] { SynonymsManagementAPIService.SYNONYMS_INDEX_CONCRETE_NAME }, - clusterState - ); + ClusterHealthResponse response = new ClusterHealthResponse(randomIdentifier(), Strings.EMPTY_ARRAY, clusterState); response.setTimedOut(true); listener.onResponse(response); } diff --git a/server/src/main/java/org/elasticsearch/cluster/health/ClusterIndexHealth.java b/server/src/main/java/org/elasticsearch/cluster/health/ClusterIndexHealth.java index 9cf567c219660..e8d8edaf90283 100644 --- a/server/src/main/java/org/elasticsearch/cluster/health/ClusterIndexHealth.java +++ b/server/src/main/java/org/elasticsearch/cluster/health/ClusterIndexHealth.java @@ -56,10 +56,29 @@ public ClusterIndexHealth(final IndexMetadata indexMetadata, final IndexRoutingT this.numberOfReplicas = indexMetadata.getNumberOfReplicas(); shards = new HashMap<>(); - for (int i = 0; i < indexRoutingTable.size(); i++) { - IndexShardRoutingTable shardRoutingTable = indexRoutingTable.shard(i); - int shardId = shardRoutingTable.shardId().id(); - shards.put(shardId, new ClusterShardHealth(shardId, shardRoutingTable)); + if (indexRoutingTable != null) { + for (int i = 0; i < indexRoutingTable.size(); i++) { + IndexShardRoutingTable shardRoutingTable = indexRoutingTable.shard(i); + int shardId = shardRoutingTable.shardId().id(); + shards.put(shardId, new ClusterShardHealth(shardId, shardRoutingTable)); + } + } else { + for (int shardId = 0; shardId < numberOfShards; shardId++) { + // Create a shard health representing completely unassigned shard + // All replicas for this shard are unassigned, including the primary + int replicasCount = numberOfReplicas + 1; + ClusterShardHealth clusterShardHealth = new ClusterShardHealth( + shardId, + ClusterHealthStatus.RED, + 0, + 0, + 0, + replicasCount, + 1, + false + ); + shards.put(shardId, clusterShardHealth); + } } // update the index status diff --git a/server/src/main/java/org/elasticsearch/cluster/health/ClusterStateHealth.java b/server/src/main/java/org/elasticsearch/cluster/health/ClusterStateHealth.java index e335232074df1..1d6006d1ce6fa 100644 --- a/server/src/main/java/org/elasticsearch/cluster/health/ClusterStateHealth.java +++ b/server/src/main/java/org/elasticsearch/cluster/health/ClusterStateHealth.java @@ -84,12 +84,16 @@ public ClusterStateHealth( int totalShardCount = 0; for (String index : concreteIndices) { - IndexRoutingTable indexRoutingTable = routingTable.index(index); IndexMetadata indexMetadata = project.index(index); - if (indexRoutingTable == null) { + if (indexMetadata == null) { + // should not happen, concreteIndices ought to have been resolved against the project metadata + assert false : "concrete index [" + index + "] not found in project [" + project.id() + "]"; + computeStatus = ClusterHealthStatus.RED; continue; } + IndexRoutingTable indexRoutingTable = routingTable.index(index); + ClusterIndexHealth indexHealth = new ClusterIndexHealth(indexMetadata, indexRoutingTable); indices.put(indexHealth.getIndex(), indexHealth); @@ -121,7 +125,7 @@ public ClusterStateHealth( // shortcut on green if (computeStatus.equals(ClusterHealthStatus.GREEN)) { - this.activeShardsPercent = 100; + this.activeShardsPercent = 100.0; } else { this.activeShardsPercent = (((double) this.activeShards) / totalShardCount) * 100; } diff --git a/server/src/test/java/org/elasticsearch/action/admin/cluster/health/ClusterHealthResponsesTests.java b/server/src/test/java/org/elasticsearch/action/admin/cluster/health/ClusterHealthResponsesTests.java index 57d040ecb9d1e..20a37a30e3764 100644 --- a/server/src/test/java/org/elasticsearch/action/admin/cluster/health/ClusterHealthResponsesTests.java +++ b/server/src/test/java/org/elasticsearch/action/admin/cluster/health/ClusterHealthResponsesTests.java @@ -16,7 +16,6 @@ import org.elasticsearch.cluster.health.ClusterIndexHealth; import org.elasticsearch.cluster.health.ClusterIndexHealthTests; import org.elasticsearch.cluster.health.ClusterStateHealth; -import org.elasticsearch.cluster.metadata.Metadata; import org.elasticsearch.common.io.stream.BytesStreamOutput; import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.Writeable; @@ -161,7 +160,7 @@ public void testClusterHealth() throws IOException { TimeValue pendingTaskInQueueTime = TimeValue.timeValueMillis(randomIntBetween(1000, 100000)); ClusterHealthResponse clusterHealth = new ClusterHealthResponse( "bla", - new String[] { Metadata.ALL }, + new String[0], // Use empty array since clusterState has no indices clusterState, clusterState.metadata().getProject().id(), pendingTasks, diff --git a/server/src/test/java/org/elasticsearch/action/admin/cluster/health/TransportClusterHealthActionTests.java b/server/src/test/java/org/elasticsearch/action/admin/cluster/health/TransportClusterHealthActionTests.java index bc06ed2356469..d44dacfc1ff4e 100644 --- a/server/src/test/java/org/elasticsearch/action/admin/cluster/health/TransportClusterHealthActionTests.java +++ b/server/src/test/java/org/elasticsearch/action/admin/cluster/health/TransportClusterHealthActionTests.java @@ -23,6 +23,7 @@ import org.elasticsearch.cluster.routing.ShardRoutingState; import org.elasticsearch.cluster.routing.TestShardRouting; import org.elasticsearch.common.Randomness; +import org.elasticsearch.common.Strings; import org.elasticsearch.core.TimeValue; import org.elasticsearch.core.Tuple; import org.elasticsearch.index.IndexVersion; @@ -75,7 +76,7 @@ public void testWaitForAllShards() { clusterState = ClusterState.builder(ClusterName.DEFAULT).build(); project = clusterState.metadata().getProject(Metadata.DEFAULT_PROJECT_ID); - response = createResponse(indices, clusterState, project); + response = createResponse(Strings.EMPTY_ARRAY /* no indices */ , clusterState, project); assertThat(TransportClusterHealthAction.prepareResponse(request, response, project, null), equalTo(1)); } diff --git a/server/src/test/java/org/elasticsearch/cluster/health/ClusterStateHealthTests.java b/server/src/test/java/org/elasticsearch/cluster/health/ClusterStateHealthTests.java index cd00b7749e899..6a496e2f8fd34 100644 --- a/server/src/test/java/org/elasticsearch/cluster/health/ClusterStateHealthTests.java +++ b/server/src/test/java/org/elasticsearch/cluster/health/ClusterStateHealthTests.java @@ -19,6 +19,9 @@ import org.elasticsearch.cluster.ClusterName; import org.elasticsearch.cluster.ClusterState; import org.elasticsearch.cluster.TestShardRoutingRoleStrategies; +import org.elasticsearch.cluster.block.ClusterBlock; +import org.elasticsearch.cluster.block.ClusterBlockLevel; +import org.elasticsearch.cluster.block.ClusterBlocks; import org.elasticsearch.cluster.metadata.IndexMetadata; import org.elasticsearch.cluster.metadata.IndexNameExpressionResolver; import org.elasticsearch.cluster.metadata.Metadata; @@ -41,6 +44,7 @@ import org.elasticsearch.common.util.set.Sets; import org.elasticsearch.index.IndexVersion; import org.elasticsearch.indices.TestIndexNameExpressionResolver; +import org.elasticsearch.rest.RestStatus; import org.elasticsearch.tasks.CancellableTask; import org.elasticsearch.tasks.TaskId; import org.elasticsearch.test.ESTestCase; @@ -633,6 +637,64 @@ private boolean primaryInactiveDueToRecovery(final String indexName, final Clust return true; } + /** + * Tests the case where indices exist in metadata but their routing tables are missing. + * This happens during cluster restart where metadata is loaded but routing table is not yet built. + * All shards should be considered completely unassigned and the cluster should be RED. + */ + public void testActiveShardsPercentDuringClusterRestart() { + final String indexName = "test-idx"; + ProjectId projectId = randomUniqueProjectId(); + + final IndexMetadata indexMetadata = IndexMetadata.builder(indexName) + .settings(settings(IndexVersion.current()).put(IndexMetadata.SETTING_INDEX_UUID, UUIDs.randomBase64UUID())) + .numberOfShards(3) + .numberOfReplicas(1) + .build(); + + // Create cluster state with index metadata but WITHOUT routing table entry + // This simulates cluster restart where metadata is loaded but routing table is not yet built + final var mdBuilder = Metadata.builder().put(ProjectMetadata.builder(projectId).put(indexMetadata, true).build()); + final var rtBuilder = GlobalRoutingTable.builder().put(projectId, RoutingTable.EMPTY_ROUTING_TABLE); + + ClusterState clusterState = ClusterState.builder(new ClusterName("test_cluster")) + .metadata(mdBuilder.build()) + .routingTable(rtBuilder.build()) + .blocks( + ClusterBlocks.builder() + .addGlobalBlock(new ClusterBlock(1, "test", true, true, true, RestStatus.SERVICE_UNAVAILABLE, ClusterBlockLevel.ALL)) + ) + .build(); + + String[] concreteIndices = new String[] { indexName }; + ClusterStateHealth clusterStateHealth = new ClusterStateHealth(clusterState, concreteIndices, projectId); + + // The cluster should be RED because all shards are unassigned + assertThat(clusterStateHealth.getStatus(), equalTo(ClusterHealthStatus.RED)); + + // All shards are unassigned, so activeShardsPercent should be 0.0 + assertThat( + "activeShardsPercent should be 0.0 when all shards are unassigned", + clusterStateHealth.getActiveShardsPercent(), + equalTo(0.0) + ); + + // Verify that totalShardCount is correctly calculated + int expectedTotalShards = indexMetadata.getTotalNumberOfShards(); + assertThat("All shards should be counted as unassigned", clusterStateHealth.getUnassignedShards(), equalTo(expectedTotalShards)); + + // All primary shards should be unassigned + assertThat( + "All primary shards should be unassigned", + clusterStateHealth.getUnassignedPrimaryShards(), + equalTo(indexMetadata.getNumberOfShards()) + ); + + // No active shards + assertThat(clusterStateHealth.getActiveShards(), equalTo(0)); + assertThat(clusterStateHealth.getActivePrimaryShards(), equalTo(0)); + } + private void assertClusterHealth(ClusterStateHealth clusterStateHealth, RoutingTableGenerator.ShardCounter counter) { assertThat(clusterStateHealth.getStatus(), equalTo(counter.status())); assertThat(clusterStateHealth.getActiveShards(), equalTo(counter.active)); diff --git a/server/src/test/java/org/elasticsearch/rest/action/cat/RestIndicesActionTests.java b/server/src/test/java/org/elasticsearch/rest/action/cat/RestIndicesActionTests.java index 2103f7dc0857f..2e11c38bf8155 100644 --- a/server/src/test/java/org/elasticsearch/rest/action/cat/RestIndicesActionTests.java +++ b/server/src/test/java/org/elasticsearch/rest/action/cat/RestIndicesActionTests.java @@ -151,7 +151,8 @@ public void testBuildTable() { IndexStats indexStats = indicesStats.get(indexName); IndexMetadata indexMetadata = project.index(indexName); - if (indexHealth != null) { + IndexRoutingTable indexRoutingTable = clusterState.routingTable(project.id()).index(indexName); + if (indexRoutingTable != null) { assertThat(row.get(0).value, equalTo(indexHealth.getStatus().toString().toLowerCase(Locale.ROOT))); } else if (indexStats != null) { assertThat(row.get(0).value, equalTo("red*"));