diff --git a/benchmarks/src/main/java/org/elasticsearch/benchmark/routing/allocation/ShardsAvailabilityHealthIndicatorBenchmark.java b/benchmarks/src/main/java/org/elasticsearch/benchmark/routing/allocation/ShardsAvailabilityHealthIndicatorBenchmark.java index 20e8106d93c02..d402ad47fbd06 100644 --- a/benchmarks/src/main/java/org/elasticsearch/benchmark/routing/allocation/ShardsAvailabilityHealthIndicatorBenchmark.java +++ b/benchmarks/src/main/java/org/elasticsearch/benchmark/routing/allocation/ShardsAvailabilityHealthIndicatorBenchmark.java @@ -29,6 +29,7 @@ import org.elasticsearch.health.HealthIndicatorResult; import org.elasticsearch.health.node.HealthInfo; import org.elasticsearch.index.shard.ShardId; +import org.elasticsearch.indices.SystemIndices; import org.elasticsearch.tasks.TaskManager; import org.elasticsearch.threadpool.ThreadPool; import org.openjdk.jmh.annotations.Benchmark; @@ -45,6 +46,7 @@ import java.util.Collections; import java.util.HashSet; +import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.TimeUnit; @@ -171,7 +173,7 @@ public void setUp() throws Exception { new TaskManager(Settings.EMPTY, threadPool, Collections.emptySet()) ); clusterService.getClusterApplierService().setInitialState(initialClusterState); - indicatorService = new ShardsAvailabilityHealthIndicatorService(clusterService, allocationService); + indicatorService = new ShardsAvailabilityHealthIndicatorService(clusterService, allocationService, new SystemIndices(List.of())); } private int toInt(String v) { diff --git a/docs/changelog/92296.yaml b/docs/changelog/92296.yaml new file mode 100644 index 0000000000000..75ecd5885edb0 --- /dev/null +++ b/docs/changelog/92296.yaml @@ -0,0 +1,6 @@ +pr: 92296 +summary: "[HealthAPI] Add support for the FEATURE_STATE affected resource" +area: Health +type: feature +issues: + - 91353 diff --git a/docs/reference/tab-widgets/troubleshooting/data/restore-from-snapshot.asciidoc b/docs/reference/tab-widgets/troubleshooting/data/restore-from-snapshot.asciidoc index 0a199bd6e48d9..589965d8ab079 100644 --- a/docs/reference/tab-widgets/troubleshooting/data/restore-from-snapshot.asciidoc +++ b/docs/reference/tab-widgets/troubleshooting/data/restore-from-snapshot.asciidoc @@ -210,6 +210,21 @@ POST _snapshot/my_repository/snapshot-20200617/_restore <1> The indices to restore. + <2> We also want to restore the aliases. ++ +NOTE: If any <> need to be restored we'll need to specify them using the +`feature_states` field and the indices that belong to the feature states we restore must not be specified under `indices`. +The <> returns both the `indices` and `feature_states` that need to be restored for the restore from snapshot diagnosis. e.g.: ++ +[source,console] +---- +POST _snapshot/my_repository/snapshot-20200617/_restore +{ + "feature_states": [ "geoip" ], + "indices": "kibana_sample_data_flights,.ds-my-data-stream-2022.06.17-000001", + "include_aliases": true +} +---- +// TEST[skip:illustration purposes only] . Finally we can verify that the indices health is now `green` via the <>. + @@ -430,6 +445,21 @@ POST _snapshot/my_repository/snapshot-20200617/_restore <1> The indices to restore. + <2> We also want to restore the aliases. ++ +NOTE: If any <> need to be restored we'll need to specify them using the +`feature_states` field and the indices that belong to the feature states we restore must not be specified under `indices`. +The <> returns both the `indices` and `feature_states` that need to be restored for the restore from snapshot diagnosis. e.g.: ++ +[source,console] +---- +POST _snapshot/my_repository/snapshot-20200617/_restore +{ + "feature_states": [ "geoip" ], + "indices": "kibana_sample_data_flights,.ds-my-data-stream-2022.06.17-000001", + "include_aliases": true +} +---- +// TEST[skip:illustration purposes only] . Finally we can verify that the indices health is now `green` via the <>. + diff --git a/server/src/main/java/org/elasticsearch/cluster/routing/allocation/ShardsAvailabilityHealthIndicatorService.java b/server/src/main/java/org/elasticsearch/cluster/routing/allocation/ShardsAvailabilityHealthIndicatorService.java index 00863ba2d59d0..51e53d0867396 100644 --- a/server/src/main/java/org/elasticsearch/cluster/routing/allocation/ShardsAvailabilityHealthIndicatorService.java +++ b/server/src/main/java/org/elasticsearch/cluster/routing/allocation/ShardsAvailabilityHealthIndicatorService.java @@ -43,6 +43,7 @@ import org.elasticsearch.health.ImpactArea; import org.elasticsearch.health.SimpleHealthIndicatorDetails; import org.elasticsearch.health.node.HealthInfo; +import org.elasticsearch.indices.SystemIndices; import org.elasticsearch.snapshots.SnapshotShardSizeInfo; import java.util.ArrayList; @@ -59,6 +60,8 @@ import java.util.stream.Stream; import static java.util.stream.Collectors.joining; +import static java.util.stream.Collectors.toMap; +import static java.util.stream.Collectors.toSet; import static org.elasticsearch.cluster.health.ClusterShardHealth.getInactivePrimaryHealth; import static org.elasticsearch.cluster.metadata.IndexMetadata.INDEX_ROUTING_INCLUDE_GROUP_PREFIX; import static org.elasticsearch.cluster.metadata.IndexMetadata.INDEX_ROUTING_INCLUDE_GROUP_SETTING; @@ -67,6 +70,7 @@ import static org.elasticsearch.cluster.routing.allocation.decider.EnableAllocationDecider.INDEX_ROUTING_ALLOCATION_ENABLE_SETTING; import static org.elasticsearch.cluster.routing.allocation.decider.ShardsLimitAllocationDecider.CLUSTER_TOTAL_SHARDS_PER_NODE_SETTING; import static org.elasticsearch.cluster.routing.allocation.decider.ShardsLimitAllocationDecider.INDEX_TOTAL_SHARDS_PER_NODE_SETTING; +import static org.elasticsearch.health.Diagnosis.Resource.Type.FEATURE_STATE; import static org.elasticsearch.health.Diagnosis.Resource.Type.INDEX; import static org.elasticsearch.health.HealthStatus.GREEN; import static org.elasticsearch.health.HealthStatus.RED; @@ -96,9 +100,16 @@ public class ShardsAvailabilityHealthIndicatorService implements HealthIndicator private final ClusterService clusterService; private final AllocationService allocationService; - public ShardsAvailabilityHealthIndicatorService(ClusterService clusterService, AllocationService allocationService) { + private final SystemIndices systemIndices; + + public ShardsAvailabilityHealthIndicatorService( + ClusterService clusterService, + AllocationService allocationService, + SystemIndices systemIndices + ) { this.clusterService = clusterService; this.allocationService = allocationService; + this.systemIndices = systemIndices; } @Override @@ -760,7 +771,7 @@ private Optional checkNotEnoughNodesInDataTier( } } - private class ShardAllocationStatus { + class ShardAllocationStatus { private final ShardAllocationCounts primaries = new ShardAllocationCounts(); private final ShardAllocationCounts replicas = new ShardAllocationCounts(); private final Metadata clusterMetadata; @@ -908,28 +919,108 @@ public List getDiagnosis(boolean verbose, int maxAffectedResourcesCou if (diagnosisToAffectedIndices.isEmpty()) { return List.of(); } else { - return diagnosisToAffectedIndices.entrySet() - .stream() - .map( - e -> new Diagnosis( - e.getKey(), - List.of( - new Diagnosis.Resource( - INDEX, - e.getValue() - .stream() - .sorted(indicesComparatorByPriorityAndName(clusterMetadata)) - .limit(Math.min(e.getValue().size(), maxAffectedResourcesCount)) - .collect(Collectors.toList()) - ) + + return diagnosisToAffectedIndices.entrySet().stream().map(e -> { + List affectedResources = new ArrayList<>(1); + if (e.getKey().equals(ACTION_RESTORE_FROM_SNAPSHOT)) { + Set restoreFromSnapshotIndices = e.getValue(); + if (restoreFromSnapshotIndices != null && restoreFromSnapshotIndices.isEmpty() == false) { + affectedResources = getRestoreFromSnapshotAffectedResources( + clusterMetadata, + systemIndices, + restoreFromSnapshotIndices, + maxAffectedResourcesCount + ); + } + } else { + affectedResources.add( + new Diagnosis.Resource( + INDEX, + e.getValue() + .stream() + .sorted(indicesComparatorByPriorityAndName(clusterMetadata)) + .limit(Math.min(e.getValue().size(), maxAffectedResourcesCount)) + .collect(Collectors.toList()) ) - ) - ) - .collect(Collectors.toList()); + ); + } + return new Diagnosis(e.getKey(), affectedResources); + }).collect(Collectors.toList()); } } else { return List.of(); } } + + /** + * The restore from snapshot operation requires the user to specify indices and feature states. + * The indices that are part of the feature states must not be specified. This method loops through all the + * identified unassigned indices and returns the affected {@link Diagnosis.Resource}s of type `INDEX` + * and if applicable `FEATURE_STATE` + */ + static List getRestoreFromSnapshotAffectedResources( + Metadata metadata, + SystemIndices systemIndices, + Set restoreFromSnapshotIndices, + int maxAffectedResourcesCount + ) { + List affectedResources = new ArrayList<>(2); + + Set affectedIndices = new HashSet<>(restoreFromSnapshotIndices); + Set affectedFeatureStates = new HashSet<>(); + Map> featureToSystemIndices = systemIndices.getFeatures() + .stream() + .collect( + toMap( + SystemIndices.Feature::getName, + feature -> feature.getIndexDescriptors() + .stream() + .flatMap(descriptor -> descriptor.getMatchingIndices(metadata).stream()) + .collect(toSet()) + ) + ); + + for (Map.Entry> featureToIndices : featureToSystemIndices.entrySet()) { + for (String featureIndex : featureToIndices.getValue()) { + if (restoreFromSnapshotIndices.contains(featureIndex)) { + affectedFeatureStates.add(featureToIndices.getKey()); + affectedIndices.remove(featureIndex); + } + } + } + + Map> featureToDsBackingIndices = systemIndices.getFeatures() + .stream() + .collect( + toMap( + SystemIndices.Feature::getName, + feature -> feature.getDataStreamDescriptors() + .stream() + .flatMap(descriptor -> descriptor.getBackingIndexNames(metadata).stream()) + .collect(toSet()) + ) + ); + + // the shards_availability indicator works with indices so let's remove the feature states data streams backing indices from + // the list of affected indices (the feature state will cover the restore of these indices too) + for (Map.Entry> featureToBackingIndices : featureToDsBackingIndices.entrySet()) { + for (String featureIndex : featureToBackingIndices.getValue()) { + if (restoreFromSnapshotIndices.contains(featureIndex)) { + affectedFeatureStates.add(featureToBackingIndices.getKey()); + affectedIndices.remove(featureIndex); + } + } + } + + if (affectedIndices.isEmpty() == false) { + affectedResources.add(new Diagnosis.Resource(INDEX, affectedIndices.stream().limit(maxAffectedResourcesCount).toList())); + } + if (affectedFeatureStates.isEmpty() == false) { + affectedResources.add( + new Diagnosis.Resource(FEATURE_STATE, affectedFeatureStates.stream().limit(maxAffectedResourcesCount).toList()) + ); + } + return affectedResources; + } } } diff --git a/server/src/main/java/org/elasticsearch/health/Diagnosis.java b/server/src/main/java/org/elasticsearch/health/Diagnosis.java index 343fe86d87456..a190dd3d5df0a 100644 --- a/server/src/main/java/org/elasticsearch/health/Diagnosis.java +++ b/server/src/main/java/org/elasticsearch/health/Diagnosis.java @@ -44,6 +44,7 @@ public enum Type { INDEX("indices"), NODE("nodes"), SLM_POLICY("slm_policies"), + FEATURE_STATE("feature_states"), SNAPSHOT_REPOSITORY("snapshot_repositories"); private final String displayValue; diff --git a/server/src/main/java/org/elasticsearch/node/Node.java b/server/src/main/java/org/elasticsearch/node/Node.java index 58b089940b215..8b6bb425b28a4 100644 --- a/server/src/main/java/org/elasticsearch/node/Node.java +++ b/server/src/main/java/org/elasticsearch/node/Node.java @@ -997,7 +997,13 @@ protected Node( discoveryModule.getCoordinator(), masterHistoryService ); - HealthService healthService = createHealthService(clusterService, clusterModule, coordinationDiagnosticsService, threadPool); + HealthService healthService = createHealthService( + clusterService, + clusterModule, + coordinationDiagnosticsService, + threadPool, + systemIndices + ); HealthMetadataService healthMetadataService = HealthMetadataService.create(clusterService, settings); LocalHealthMonitor localHealthMonitor = LocalHealthMonitor.create(settings, clusterService, nodeService, threadPool, client); HealthInfoCache nodeHealthOverview = HealthInfoCache.create(clusterService); @@ -1199,7 +1205,8 @@ private HealthService createHealthService( ClusterService clusterService, ClusterModule clusterModule, CoordinationDiagnosticsService coordinationDiagnosticsService, - ThreadPool threadPool + ThreadPool threadPool, + SystemIndices systemIndices ) { List preflightHealthIndicatorServices = Collections.singletonList( new StableMasterHealthIndicatorService(coordinationDiagnosticsService, clusterService) @@ -1207,7 +1214,7 @@ private HealthService createHealthService( var serverHealthIndicatorServices = new ArrayList<>( List.of( new RepositoryIntegrityHealthIndicatorService(clusterService), - new ShardsAvailabilityHealthIndicatorService(clusterService, clusterModule.getAllocationService()) + new ShardsAvailabilityHealthIndicatorService(clusterService, clusterModule.getAllocationService(), systemIndices) ) ); serverHealthIndicatorServices.add(new DiskHealthIndicatorService(clusterService)); diff --git a/server/src/test/java/org/elasticsearch/cluster/routing/allocation/ShardsAvailabilityHealthIndicatorServiceTests.java b/server/src/test/java/org/elasticsearch/cluster/routing/allocation/ShardsAvailabilityHealthIndicatorServiceTests.java index 7682a10483e7c..b3439b202b7ad 100644 --- a/server/src/test/java/org/elasticsearch/cluster/routing/allocation/ShardsAvailabilityHealthIndicatorServiceTests.java +++ b/server/src/test/java/org/elasticsearch/cluster/routing/allocation/ShardsAvailabilityHealthIndicatorServiceTests.java @@ -11,6 +11,7 @@ import org.elasticsearch.Version; import org.elasticsearch.cluster.ClusterName; import org.elasticsearch.cluster.ClusterState; +import org.elasticsearch.cluster.metadata.ComposableIndexTemplate; import org.elasticsearch.cluster.metadata.IndexMetadata; import org.elasticsearch.cluster.metadata.Metadata; import org.elasticsearch.cluster.metadata.NodesShutdownMetadata; @@ -23,6 +24,7 @@ import org.elasticsearch.cluster.routing.RoutingTable; import org.elasticsearch.cluster.routing.ShardRouting; import org.elasticsearch.cluster.routing.UnassignedInfo; +import org.elasticsearch.cluster.routing.allocation.ShardsAvailabilityHealthIndicatorService.ShardAllocationStatus; import org.elasticsearch.cluster.routing.allocation.decider.AwarenessAllocationDecider; import org.elasticsearch.cluster.routing.allocation.decider.Decision; import org.elasticsearch.cluster.routing.allocation.decider.EnableAllocationDecider; @@ -43,7 +45,12 @@ import org.elasticsearch.health.node.HealthInfo; import org.elasticsearch.index.Index; import org.elasticsearch.index.shard.ShardId; +import org.elasticsearch.indices.ExecutorNames; +import org.elasticsearch.indices.SystemDataStreamDescriptor; +import org.elasticsearch.indices.SystemIndexDescriptor; +import org.elasticsearch.indices.SystemIndices; import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.threadpool.ThreadPool; import org.mockito.stubbing.Answer; import java.util.ArrayList; @@ -56,7 +63,10 @@ import java.util.UUID; import java.util.stream.Collectors; +import static java.util.Collections.emptyList; import static java.util.stream.Collectors.toMap; +import static org.elasticsearch.cluster.metadata.DataStreamTestHelper.createBackingIndex; +import static org.elasticsearch.cluster.metadata.DataStreamTestHelper.newInstance; import static org.elasticsearch.cluster.metadata.IndexMetadata.INDEX_ROUTING_INCLUDE_GROUP_PREFIX; import static org.elasticsearch.cluster.metadata.IndexMetadata.INDEX_ROUTING_REQUIRE_GROUP_PREFIX; import static org.elasticsearch.cluster.metadata.SingleNodeShutdownMetadata.Type.RESTART; @@ -83,14 +93,18 @@ import static org.elasticsearch.cluster.routing.allocation.decider.ShardsLimitAllocationDecider.CLUSTER_TOTAL_SHARDS_PER_NODE_SETTING; import static org.elasticsearch.common.util.CollectionUtils.concatLists; import static org.elasticsearch.core.TimeValue.timeValueSeconds; +import static org.elasticsearch.health.Diagnosis.Resource.Type.FEATURE_STATE; import static org.elasticsearch.health.Diagnosis.Resource.Type.INDEX; import static org.elasticsearch.health.HealthStatus.GREEN; import static org.elasticsearch.health.HealthStatus.RED; import static org.elasticsearch.health.HealthStatus.YELLOW; import static org.hamcrest.Matchers.contains; import static org.hamcrest.Matchers.containsInAnyOrder; +import static org.hamcrest.Matchers.emptyCollectionOf; import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.hasItems; import static org.hamcrest.Matchers.hasSize; +import static org.hamcrest.Matchers.is; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -114,8 +128,8 @@ public void testShouldBeGreenWhenAllPrimariesAndReplicasAreStarted() { GREEN, "This cluster has all shards available.", Map.of("started_primaries", 2, "started_replicas", 1), - Collections.emptyList(), - Collections.emptyList() + emptyList(), + emptyList() ) ) ); @@ -353,8 +367,8 @@ public void testShouldBeGreenWhenThereAreRestartingReplicas() { GREEN, "This cluster has 1 restarting replica shard.", Map.of("started_primaries", 1, "restarting_replicas", 1), - Collections.emptyList(), - Collections.emptyList() + emptyList(), + emptyList() ) ) ); @@ -374,8 +388,8 @@ public void testShouldBeGreenWhenThereAreNoReplicasExpected() { GREEN, "This cluster has all shards available.", Map.of("started_primaries", 1), - Collections.emptyList(), - Collections.emptyList() + emptyList(), + emptyList() ) ) ); @@ -436,8 +450,8 @@ public void testShouldBeGreenWhenThereAreInitializingPrimaries() { GREEN, "This cluster has 1 creating primary shard.", Map.of("creating_primaries", 1), - Collections.emptyList(), - Collections.emptyList() + emptyList(), + emptyList() ) ) ); @@ -457,8 +471,8 @@ public void testShouldBeGreenWhenThereAreRestartingPrimaries() { GREEN, "This cluster has 1 restarting primary shard.", Map.of("restarting_primaries", 1), - Collections.emptyList(), - Collections.emptyList() + emptyList(), + emptyList() ) ) ); @@ -503,7 +517,7 @@ public void testShouldBeRedWhenRestartingPrimariesReachedAllocationDelayAndNoRep ); } - public void testUserActionsNotGeneratedWhenNotDrillingDown() { + public void testDiagnosisNotGeneratedWhenNotDrillingDown() { // Index definition, 1 primary no replicas IndexMetadata indexMetadata = IndexMetadata.builder("red-index") .settings(Settings.builder().put(IndexMetadata.SETTING_VERSION_CREATED, Version.CURRENT).build()) @@ -562,6 +576,149 @@ public void testDiagnoseRestoreIndexAfterDataLoss() { assertThat(definitions, contains(ACTION_RESTORE_FROM_SNAPSHOT)); } + public void testRestoreFromSnapshotReportsFeatureStates() { + // this test adds a mix of regular and system indices and data streams + // we'll test the `shards_availability` indicator correctly reports the + // affected feature states and indices + + IndexMetadata featureIndex = IndexMetadata.builder(".feature-index") + .settings(Settings.builder().put(IndexMetadata.SETTING_VERSION_CREATED, Version.CURRENT).build()) + .numberOfShards(1) + .numberOfReplicas(0) + .build(); + + IndexMetadata regularIndex = IndexMetadata.builder("regular-index") + .settings(Settings.builder().put(IndexMetadata.SETTING_VERSION_CREATED, Version.CURRENT).build()) + .numberOfShards(1) + .numberOfReplicas(0) + .build(); + + String featureDataStreamName = ".test-ds-feature"; + IndexMetadata backingIndex = createBackingIndex(featureDataStreamName, 1).build(); + + ShardRouting featureIndexRouting = createShardRouting( + new ShardId(featureIndex.getIndex(), 0), + true, + new ShardAllocation(randomNodeId(), UNAVAILABLE, noShardCopy()) + ); + + ShardRouting regularIndexRouting = createShardRouting( + new ShardId(regularIndex.getIndex(), 0), + true, + new ShardAllocation(randomNodeId(), UNAVAILABLE, noShardCopy()) + ); + + ShardRouting backingIndexRouting = createShardRouting( + new ShardId(backingIndex.getIndex(), 0), + true, + new ShardAllocation(randomNodeId(), UNAVAILABLE, noShardCopy()) + ); + + var clusterState = createClusterStateWith( + List.of(featureIndex, regularIndex, backingIndex), + List.of( + IndexRoutingTable.builder(featureIndex.getIndex()).addShard(featureIndexRouting).build(), + IndexRoutingTable.builder(regularIndex.getIndex()).addShard(regularIndexRouting).build(), + IndexRoutingTable.builder(backingIndex.getIndex()).addShard(backingIndexRouting).build() + ), + List.of(), + List.of() + ); + + // add the data stream to the cluster state + Metadata.Builder mdBuilder = Metadata.builder(clusterState.metadata()) + .put(newInstance(featureDataStreamName, List.of(backingIndex.getIndex()))); + ClusterState state = ClusterState.builder(clusterState).metadata(mdBuilder).build(); + + var service = createAllocationHealthIndicatorService( + Settings.EMPTY, + state, + Map.of(), + getSystemIndices(featureDataStreamName, ".test-ds-*", ".feature-*") + ); + HealthIndicatorResult result = service.calculate(true, HealthInfo.EMPTY_HEALTH_INFO); + + assertThat(result.status(), is(HealthStatus.RED)); + assertThat(result.diagnosisList().size(), is(1)); + Diagnosis diagnosis = result.diagnosisList().get(0); + List affectedResources = diagnosis.affectedResources(); + assertThat("expecting we report a resource of type INDEX and one of type FEATURE_STATE", affectedResources.size(), is(2)); + for (Diagnosis.Resource resource : affectedResources) { + if (resource.getType() == INDEX) { + assertThat(resource.getValues(), hasItems("regular-index")); + } else { + assertThat(resource.getType(), is(FEATURE_STATE)); + assertThat(resource.getValues(), hasItems("feature-with-system-data-stream", "feature-with-system-index")); + } + } + } + + public void testGetRestoreFromSnapshotAffectedResources() { + String featureDataStreamName = ".test-ds-feature"; + IndexMetadata backingIndex = createBackingIndex(featureDataStreamName, 1).build(); + + List indexMetadataList = List.of( + IndexMetadata.builder(".feature-index") + .settings(Settings.builder().put(IndexMetadata.SETTING_VERSION_CREATED, Version.CURRENT).build()) + .numberOfShards(1) + .numberOfReplicas(0) + .build(), + IndexMetadata.builder("regular-index") + .settings(Settings.builder().put(IndexMetadata.SETTING_VERSION_CREATED, Version.CURRENT).build()) + .numberOfShards(1) + .numberOfReplicas(0) + .build(), + backingIndex + ); + + Metadata.Builder metadataBuilder = Metadata.builder(); + Map indexMetadataMap = new HashMap<>(); + for (IndexMetadata indexMetadata : indexMetadataList) { + indexMetadataMap.put(indexMetadata.getIndex().getName(), indexMetadata); + } + metadataBuilder.indices(indexMetadataMap); + metadataBuilder.put(newInstance(featureDataStreamName, List.of(backingIndex.getIndex()))); + Metadata metadata = metadataBuilder.build(); + { + List affectedResources = ShardAllocationStatus.getRestoreFromSnapshotAffectedResources( + metadata, + getSystemIndices(featureDataStreamName, ".test-ds-*", ".feature-*"), + Set.of(backingIndex.getIndex().getName(), ".feature-index", "regular-index"), + 10 + ); + + assertThat(affectedResources.size(), is(2)); + for (Diagnosis.Resource resource : affectedResources) { + if (resource.getType() == INDEX) { + assertThat(resource.getValues(), hasItems("regular-index")); + } else { + assertThat(resource.getType(), is(FEATURE_STATE)); + assertThat(resource.getValues(), hasItems("feature-with-system-data-stream", "feature-with-system-index")); + } + } + } + + { + List affectedResources = ShardAllocationStatus.getRestoreFromSnapshotAffectedResources( + metadata, + getSystemIndices(featureDataStreamName, ".test-ds-*", ".feature-*"), + Set.of(backingIndex.getIndex().getName(), ".feature-index", "regular-index"), + 0 + ); + + assertThat(affectedResources.size(), is(2)); + for (Diagnosis.Resource resource : affectedResources) { + if (resource.getType() == INDEX) { + assertThat(resource.getValues(), emptyCollectionOf(String.class)); + } else { + assertThat(resource.getType(), is(FEATURE_STATE)); + assertThat(resource.getValues(), emptyCollectionOf(String.class)); + } + } + } + + } + public void testDiagnoseUnknownAllocationDeciderIssue() { // Index definition, 1 primary no replicas IndexMetadata indexMetadata = IndexMetadata.builder("red-index") @@ -1253,6 +1410,53 @@ public void testLimitNumberOfAffectedResources() { } } + /** + * Creates the {@link SystemIndices} with one standalone system index and a system data stream + */ + private SystemIndices getSystemIndices( + String featureDataStreamName, + String systemDataStreamPattern, + String standaloneSystemIndexPattern + ) { + return new SystemIndices( + List.of( + new SystemIndices.Feature( + "feature-with-system-index", + "testing", + List.of(new SystemIndexDescriptor(standaloneSystemIndexPattern, "feature with index")) + ), + new SystemIndices.Feature( + "feature-with-system-data-stream", + "feature with data stream", + List.of(), + List.of( + new SystemDataStreamDescriptor( + featureDataStreamName, + "description", + SystemDataStreamDescriptor.Type.EXTERNAL, + new ComposableIndexTemplate( + List.of(systemDataStreamPattern), + null, + null, + null, + null, + null, + new ComposableIndexTemplate.DataStreamTemplate() + ), + Map.of(), + List.of("test"), + new ExecutorNames( + ThreadPool.Names.SYSTEM_CRITICAL_READ, + ThreadPool.Names.SYSTEM_READ, + ThreadPool.Names.SYSTEM_WRITE + ) + ) + ) + ) + ) + ); + } + private HealthIndicatorResult createExpectedResult( HealthStatus status, String symptom, @@ -1271,7 +1475,7 @@ private HealthIndicatorResult createExpectedResult( } private HealthIndicatorResult createExpectedTruncatedResult(HealthStatus status, String symptom, List impacts) { - return new HealthIndicatorResult(NAME, status, symptom, HealthIndicatorDetails.EMPTY, impacts, Collections.emptyList()); + return new HealthIndicatorResult(NAME, status, symptom, HealthIndicatorDetails.EMPTY, impacts, emptyList()); } private static ClusterState createClusterStateWith(List indexRoutes, List nodeShutdowns) { @@ -1534,13 +1738,22 @@ private static ShardsAvailabilityHealthIndicatorService createShardsAvailability ClusterState clusterState, final Map decisions ) { - return createShardsAvailabilityIndicatorService(Settings.EMPTY, clusterState, decisions); + return createAllocationHealthIndicatorService(Settings.EMPTY, clusterState, decisions, new SystemIndices(List.of())); } private static ShardsAvailabilityHealthIndicatorService createShardsAvailabilityIndicatorService( Settings nodeSettings, ClusterState clusterState, final Map decisions + ) { + return createAllocationHealthIndicatorService(nodeSettings, clusterState, decisions, new SystemIndices(List.of())); + } + + private static ShardsAvailabilityHealthIndicatorService createAllocationHealthIndicatorService( + Settings nodeSettings, + ClusterState clusterState, + final Map decisions, + SystemIndices systemIndices ) { var clusterService = mock(ClusterService.class); when(clusterService.state()).thenReturn(clusterState); @@ -1552,6 +1765,6 @@ private static ShardsAvailabilityHealthIndicatorService createShardsAvailability var key = new ShardRoutingKey(shardRouting.getIndexName(), shardRouting.getId(), shardRouting.primary()); return decisions.getOrDefault(key, ShardAllocationDecision.NOT_TAKEN); }); - return new ShardsAvailabilityHealthIndicatorService(clusterService, allocationService); + return new ShardsAvailabilityHealthIndicatorService(clusterService, allocationService, systemIndices); } }