Browse Source

Reroute when new repository is registered (#73761)

Today we fail to allocate searchable snapshot shards if the repository
containing their underlying data is not registered with the cluster.
This failure is somewhat messy, we allocate them and let the recovery
fail, and furthermore we don't automatically retry the allocation if the
repository is subsequently registered.

This commit introduces an allocation decider to prevent the allocation
of such shards, and explain more clearly why, and also a cluster state
listener that performs a reroute when a new repository is registered.

Relates #73669
Relates #73714
David Turner 4 years ago
parent
commit
7a3a45a184

+ 123 - 0
x-pack/plugin/searchable-snapshots/src/internalClusterTest/java/org/elasticsearch/xpack/searchablesnapshots/SearchableSnapshotsIntegTests.java

@@ -9,6 +9,7 @@ package org.elasticsearch.xpack.searchablesnapshots;
 import org.apache.lucene.search.TotalHits;
 import org.elasticsearch.ExceptionsHelper;
 import org.elasticsearch.ResourceNotFoundException;
+import org.elasticsearch.action.admin.cluster.allocation.ClusterAllocationExplanation;
 import org.elasticsearch.action.admin.cluster.snapshots.restore.RestoreSnapshotResponse;
 import org.elasticsearch.action.admin.cluster.snapshots.status.SnapshotIndexShardStatus;
 import org.elasticsearch.action.admin.cluster.snapshots.status.SnapshotStats;
@@ -20,11 +21,16 @@ import org.elasticsearch.action.admin.indices.stats.IndicesStatsResponse;
 import org.elasticsearch.action.admin.indices.stats.ShardStats;
 import org.elasticsearch.action.index.IndexRequestBuilder;
 import org.elasticsearch.cluster.ClusterState;
+import org.elasticsearch.cluster.RestoreInProgress;
 import org.elasticsearch.cluster.metadata.DataStream;
 import org.elasticsearch.cluster.metadata.IndexMetadata;
 import org.elasticsearch.cluster.metadata.RepositoryMetadata;
 import org.elasticsearch.cluster.node.DiscoveryNode;
 import org.elasticsearch.cluster.routing.ShardRouting;
+import org.elasticsearch.cluster.routing.allocation.AllocateUnassignedDecision;
+import org.elasticsearch.cluster.routing.allocation.AllocationDecision;
+import org.elasticsearch.cluster.routing.allocation.NodeAllocationResult;
+import org.elasticsearch.cluster.routing.allocation.decider.Decision;
 import org.elasticsearch.common.Priority;
 import org.elasticsearch.common.Strings;
 import org.elasticsearch.common.settings.Settings;
@@ -971,6 +977,123 @@ public class SearchableSnapshotsIntegTests extends BaseSearchableSnapshotsIntegT
         );
     }
 
+    public void testSnapshotOfSearchableSnapshotCanBeRestoredBeforeRepositoryRegistered() throws Exception {
+        final String indexName = randomAlphaOfLength(10).toLowerCase(Locale.ROOT);
+        createAndPopulateIndex(
+            indexName,
+            Settings.builder().put(INDEX_NUMBER_OF_SHARDS_SETTING.getKey(), 1).put(INDEX_SOFT_DELETES_SETTING.getKey(), true)
+        );
+
+        final TotalHits originalAllHits = internalCluster().client()
+            .prepareSearch(indexName)
+            .setTrackTotalHits(true)
+            .get()
+            .getHits()
+            .getTotalHits();
+        final TotalHits originalBarHits = internalCluster().client()
+            .prepareSearch(indexName)
+            .setTrackTotalHits(true)
+            .setQuery(matchQuery("foo", "bar"))
+            .get()
+            .getHits()
+            .getTotalHits();
+        logger.info("--> [{}] in total, of which [{}] match the query", originalAllHits, originalBarHits);
+
+        // Take snapshot containing the actual data to one repository
+        final String dataRepoName = randomAlphaOfLength(10).toLowerCase(Locale.ROOT);
+        createRepository(dataRepoName, "fs");
+
+        final SnapshotId dataSnapshot = createSnapshot(dataRepoName, "data-snapshot", List.of(indexName)).snapshotId();
+        assertAcked(client().admin().indices().prepareDelete(indexName));
+
+        final String restoredIndexName = randomValueOtherThan(indexName, () -> randomAlphaOfLength(10).toLowerCase(Locale.ROOT));
+        mountSnapshot(dataRepoName, dataSnapshot.getName(), indexName, restoredIndexName, Settings.EMPTY);
+        ensureGreen(restoredIndexName);
+
+        if (randomBoolean()) {
+            logger.info("--> closing index before snapshot");
+            assertAcked(client().admin().indices().prepareClose(restoredIndexName));
+        }
+
+        // Back up the cluster to a different repo
+        final String backupRepoName = randomValueOtherThan(dataRepoName, () -> randomAlphaOfLength(10).toLowerCase(Locale.ROOT));
+        createRepository(backupRepoName, "fs");
+        final SnapshotId backupSnapshot = createSnapshot(backupRepoName, "backup-snapshot", List.of(restoredIndexName)).snapshotId();
+
+        // Clear out data & the repo that contains it
+        final RepositoryMetadata dataRepoMetadata = client().admin()
+            .cluster()
+            .prepareGetRepositories(dataRepoName)
+            .get()
+            .repositories()
+            .get(0);
+        assertAcked(client().admin().indices().prepareDelete(restoredIndexName));
+        assertAcked(client().admin().cluster().prepareDeleteRepository(dataRepoName));
+
+        // Restore the backup snapshot
+        assertThat(
+            client().admin()
+                .cluster()
+                .prepareRestoreSnapshot(backupRepoName, backupSnapshot.getName())
+                .setIndices(restoredIndexName)
+                .get()
+                .status(),
+            equalTo(RestStatus.ACCEPTED)
+        );
+
+        assertBusy(() -> {
+            final ClusterAllocationExplanation clusterAllocationExplanation = client().admin()
+                .cluster()
+                .prepareAllocationExplain()
+                .setIndex(restoredIndexName)
+                .setShard(0)
+                .setPrimary(true)
+                .get()
+                .getExplanation();
+
+            final String description = Strings.toString(clusterAllocationExplanation);
+            final AllocateUnassignedDecision allocateDecision = clusterAllocationExplanation.getShardAllocationDecision()
+                .getAllocateDecision();
+            assertTrue(description, allocateDecision.isDecisionTaken());
+            assertThat(description, allocateDecision.getAllocationDecision(), equalTo(AllocationDecision.NO));
+            for (NodeAllocationResult nodeAllocationResult : allocateDecision.getNodeDecisions()) {
+                for (Decision decision : nodeAllocationResult.getCanAllocateDecision().getDecisions()) {
+                    final String explanation = decision.getExplanation();
+                    if (explanation.contains("this index is backed by a searchable snapshot")
+                        && explanation.contains("no such repository is registered")
+                        && explanation.contains("the required repository was originally named [" + dataRepoName + "]")) {
+                        return;
+                    }
+                }
+            }
+
+            fail(description);
+        });
+
+        assertBusy(() -> {
+            final RestoreInProgress restoreInProgress = client().admin()
+                .cluster()
+                .prepareState()
+                .clear()
+                .setCustoms(true)
+                .get()
+                .getState()
+                .custom(RestoreInProgress.TYPE, RestoreInProgress.EMPTY);
+            assertTrue(Strings.toString(restoreInProgress, true, true), restoreInProgress.isEmpty());
+        });
+
+        // Re-register the repository containing the actual data & verify that the shards are now allocated
+        final String newRepositoryName = randomAlphaOfLength(10).toLowerCase(Locale.ROOT);
+        final Settings.Builder settings = Settings.builder().put(dataRepoMetadata.settings());
+        if (randomBoolean()) {
+            settings.put(READONLY_SETTING_KEY, "true");
+        }
+        assertAcked(clusterAdmin().preparePutRepository(newRepositoryName).setType("fs").setSettings(settings));
+
+        ensureGreen(restoredIndexName);
+        assertTotalHits(restoredIndexName, originalAllHits, originalBarHits);
+    }
+
     private void assertSearchableSnapshotStats(String indexName, boolean cacheEnabled, List<String> nonCachedExtensions) {
         final SearchableSnapshotsStatsResponse statsResponse = client().execute(
             SearchableSnapshotsStatsAction.INSTANCE,

+ 47 - 1
x-pack/plugin/searchable-snapshots/src/main/java/org/elasticsearch/xpack/searchablesnapshots/SearchableSnapshots.java

@@ -9,11 +9,20 @@ package org.elasticsearch.xpack.searchablesnapshots;
 import org.apache.lucene.store.BufferedIndexInput;
 import org.apache.lucene.util.SetOnce;
 import org.elasticsearch.Version;
+import org.elasticsearch.action.ActionListener;
 import org.elasticsearch.action.ActionRequest;
 import org.elasticsearch.action.ActionResponse;
+import org.elasticsearch.cluster.ClusterChangedEvent;
+import org.elasticsearch.cluster.ClusterStateListener;
+import org.elasticsearch.cluster.metadata.RepositoriesMetadata;
+import org.elasticsearch.cluster.metadata.RepositoryMetadata;
+import org.elasticsearch.cluster.routing.RerouteService;
+import org.elasticsearch.common.Priority;
 import org.elasticsearch.common.util.concurrent.EsExecutors;
+import org.elasticsearch.repositories.RepositoryData;
 import org.elasticsearch.xpack.searchablesnapshots.action.cache.TransportSearchableSnapshotsNodeCachesStatsAction;
 import org.elasticsearch.xpack.searchablesnapshots.allocation.decider.DedicatedFrozenNodeAllocationDecider;
+import org.elasticsearch.xpack.searchablesnapshots.allocation.decider.SearchableSnapshotRepositoryExistsAllocationDecider;
 import org.elasticsearch.xpack.searchablesnapshots.cache.blob.BlobStoreCacheService;
 import org.elasticsearch.client.Client;
 import org.elasticsearch.cluster.metadata.IndexMetadata;
@@ -108,12 +117,15 @@ import java.io.UncheckedIOException;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
+import java.util.HashSet;
 import java.util.List;
 import java.util.Locale;
 import java.util.Map;
 import java.util.Optional;
+import java.util.Set;
 import java.util.function.Function;
 import java.util.function.Supplier;
+import java.util.stream.Collectors;
 
 import static org.elasticsearch.common.xcontent.XContentFactory.jsonBuilder;
 import static org.elasticsearch.index.mapper.MapperService.SINGLE_MAPPING_NAME;
@@ -344,7 +356,10 @@ public class SearchableSnapshots extends Plugin implements IndexStorePlugin, Eng
         this.allocator.set(new SearchableSnapshotAllocator(client, clusterService.getRerouteService(), frozenCacheInfoService));
         components.add(new FrozenCacheServiceSupplier(frozenCacheService.get()));
         components.add(new CacheServiceSupplier(cacheService.get()));
-        new SearchableSnapshotIndexMetadataUpgrader(clusterService, threadPool).initialize();
+        if (DiscoveryNode.isMasterNode(settings)) {
+            new SearchableSnapshotIndexMetadataUpgrader(clusterService, threadPool).initialize();
+            clusterService.addListener(new RepositoryUuidWatcher(clusterService.getRerouteService()));
+        }
         return Collections.unmodifiableList(components);
     }
 
@@ -519,6 +534,7 @@ public class SearchableSnapshots extends Plugin implements IndexStorePlugin, Eng
     public Collection<AllocationDecider> createAllocationDeciders(Settings settings, ClusterSettings clusterSettings) {
         return List.of(
             new SearchableSnapshotAllocationDecider(() -> getLicenseState().isAllowed(XPackLicenseState.Feature.SEARCHABLE_SNAPSHOTS)),
+            new SearchableSnapshotRepositoryExistsAllocationDecider(),
             new SearchableSnapshotEnableAllocationDecider(settings, clusterSettings),
             new HasFrozenCacheAllocationDecider(frozenCacheInfoService),
             new DedicatedFrozenNodeAllocationDecider()
@@ -697,4 +713,34 @@ public class SearchableSnapshots extends Plugin implements IndexStorePlugin, Eng
             return frozenCacheService;
         }
     }
+
+    private static final class RepositoryUuidWatcher implements ClusterStateListener {
+
+        private final RerouteService rerouteService;
+        private final HashSet<String> knownUuids = new HashSet<>();
+
+        RepositoryUuidWatcher(RerouteService rerouteService) {
+            this.rerouteService = rerouteService;
+        }
+
+        @Override
+        public void clusterChanged(ClusterChangedEvent event) {
+            final RepositoriesMetadata repositoriesMetadata = event.state().metadata().custom(RepositoriesMetadata.TYPE);
+            if (repositoriesMetadata == null) {
+                knownUuids.clear();
+                return;
+            }
+
+            final Set<String> newUuids = repositoriesMetadata.repositories()
+                .stream()
+                .map(RepositoryMetadata::uuid)
+                .filter(s -> s.equals(RepositoryData.MISSING_UUID) == false)
+                .collect(Collectors.toSet());
+            if (knownUuids.addAll(newUuids)) {
+                rerouteService.reroute("repository UUIDs changed", Priority.NORMAL, ActionListener.wrap((() -> {})));
+            }
+            knownUuids.retainAll(newUuids);
+            assert knownUuids.equals(newUuids) : knownUuids + " vs " + newUuids;
+        }
+    }
 }

+ 104 - 40
x-pack/plugin/searchable-snapshots/src/main/java/org/elasticsearch/xpack/searchablesnapshots/allocation/SearchableSnapshotAllocator.java

@@ -13,6 +13,7 @@ import org.elasticsearch.action.ActionListener;
 import org.elasticsearch.action.FailedNodeException;
 import org.elasticsearch.client.Client;
 import org.elasticsearch.cluster.ClusterState;
+import org.elasticsearch.cluster.RestoreInProgress;
 import org.elasticsearch.cluster.metadata.IndexMetadata;
 import org.elasticsearch.cluster.metadata.RepositoriesMetadata;
 import org.elasticsearch.cluster.metadata.RepositoryMetadata;
@@ -136,52 +137,61 @@ public class SearchableSnapshotAllocator implements ExistingShardsAllocator {
         UnassignedAllocationHandler unassignedAllocationHandler
     ) {
         // TODO: cancel and jump to better available allocations?
-        if (shardRouting.primary()
-            && (shardRouting.recoverySource().getType() == RecoverySource.Type.EXISTING_STORE
-                || shardRouting.recoverySource().getType() == RecoverySource.Type.EMPTY_STORE)) {
-            // we always force snapshot recovery source to use the snapshot-based recovery process on the node
-            final Settings indexSettings = allocation.metadata().index(shardRouting.index()).getSettings();
-            final IndexId indexId = new IndexId(
-                SNAPSHOT_INDEX_NAME_SETTING.get(indexSettings),
-                SNAPSHOT_INDEX_ID_SETTING.get(indexSettings)
-            );
-            final SnapshotId snapshotId = new SnapshotId(
-                SNAPSHOT_SNAPSHOT_NAME_SETTING.get(indexSettings),
-                SNAPSHOT_SNAPSHOT_ID_SETTING.get(indexSettings)
-            );
+        if (shardRouting.primary()) {
+            final String recoveryUuid = getRecoverySourceRestoreUuid(shardRouting, allocation);
+            if (recoveryUuid != null) {
+
+                // we always force snapshot recovery source to use the snapshot-based recovery process on the node
+                final Settings indexSettings = allocation.metadata().index(shardRouting.index()).getSettings();
+                final IndexId indexId = new IndexId(
+                    SNAPSHOT_INDEX_NAME_SETTING.get(indexSettings),
+                    SNAPSHOT_INDEX_ID_SETTING.get(indexSettings)
+                );
+                final SnapshotId snapshotId = new SnapshotId(
+                    SNAPSHOT_SNAPSHOT_NAME_SETTING.get(indexSettings),
+                    SNAPSHOT_SNAPSHOT_ID_SETTING.get(indexSettings)
+                );
 
-            final String repositoryUuid = SNAPSHOT_REPOSITORY_UUID_SETTING.get(indexSettings);
-            final String repositoryName;
-            if (Strings.hasLength(repositoryUuid) == false) {
-                repositoryName = SNAPSHOT_REPOSITORY_NAME_SETTING.get(indexSettings);
-            } else {
-                final RepositoriesMetadata repoMetadata = allocation.metadata().custom(RepositoriesMetadata.TYPE);
-                final List<RepositoryMetadata> repositories = repoMetadata == null ? emptyList() : repoMetadata.repositories();
-                repositoryName = repositories.stream()
-                    .filter(r -> repositoryUuid.equals(r.uuid()))
-                    .map(RepositoryMetadata::name)
-                    .findFirst()
-                    .orElse(null);
-            }
+                final String repositoryUuid = SNAPSHOT_REPOSITORY_UUID_SETTING.get(indexSettings);
+                final String repositoryName;
+                if (Strings.hasLength(repositoryUuid) == false) {
+                    repositoryName = SNAPSHOT_REPOSITORY_NAME_SETTING.get(indexSettings);
+                } else {
+                    final RepositoriesMetadata repoMetadata = allocation.metadata().custom(RepositoriesMetadata.TYPE);
+                    final List<RepositoryMetadata> repositories = repoMetadata == null ? emptyList() : repoMetadata.repositories();
+                    repositoryName = repositories.stream()
+                        .filter(r -> repositoryUuid.equals(r.uuid()))
+                        .map(RepositoryMetadata::name)
+                        .findFirst()
+                        .orElse(null);
+                }
 
-            if (repositoryName == null) {
-                // TODO if the repository we seek appears later, we will need to get its UUID (usually automatic) and then reroute
-                unassignedAllocationHandler.removeAndIgnore(UnassignedInfo.AllocationStatus.DECIDERS_NO, allocation.changes());
-                return;
-            }
+                if (repositoryName == null) {
+                    unassignedAllocationHandler.removeAndIgnore(UnassignedInfo.AllocationStatus.DECIDERS_NO, allocation.changes());
+                    return;
+                }
 
-            final Snapshot snapshot = new Snapshot(repositoryName, snapshotId);
+                final Snapshot snapshot = new Snapshot(repositoryName, snapshotId);
 
-            shardRouting = unassignedAllocationHandler.updateUnassigned(
-                shardRouting.unassignedInfo(),
-                new RecoverySource.SnapshotRecoverySource(
-                    RecoverySource.SnapshotRecoverySource.NO_API_RESTORE_UUID,
+                final Version version = shardRouting.recoverySource().getType() == RecoverySource.Type.SNAPSHOT
+                    ? ((RecoverySource.SnapshotRecoverySource) shardRouting.recoverySource()).version()
+                    : Version.CURRENT;
+
+                final RecoverySource.SnapshotRecoverySource recoverySource = new RecoverySource.SnapshotRecoverySource(
+                    recoveryUuid,
                     snapshot,
-                    Version.CURRENT,
+                    version,
                     indexId
-                ),
-                allocation.changes()
-            );
+                );
+
+                if (shardRouting.recoverySource().equals(recoverySource) == false) {
+                    shardRouting = unassignedAllocationHandler.updateUnassigned(
+                        shardRouting.unassignedInfo(),
+                        recoverySource,
+                        allocation.changes()
+                    );
+                }
+            }
         }
 
         final AllocateUnassignedDecision allocateUnassignedDecision = decideAllocation(allocation, shardRouting);
@@ -207,6 +217,60 @@ public class SearchableSnapshotAllocator implements ExistingShardsAllocator {
         }
     }
 
+    /**
+     * @return the restore UUID to use when adjusting the recovery source of the shard to be a snapshot recovery source from a
+     * repository of the correct name. Returns {@code null} if the recovery source should not be adjusted.
+     */
+    @Nullable
+    private static String getRecoverySourceRestoreUuid(ShardRouting shardRouting, RoutingAllocation allocation) {
+        switch (shardRouting.recoverySource().getType()) {
+            case EXISTING_STORE:
+            case EMPTY_STORE:
+                // this shard previously failed and/or was force-allocated, so the recovery source must be changed to reflect that it will
+                // be recovered from the snapshot again
+                return RecoverySource.SnapshotRecoverySource.NO_API_RESTORE_UUID;
+            case SNAPSHOT:
+                // the recovery source may be correct, or we may be recovering it from a real snapshot (i.e. a backup)
+
+                final RecoverySource.SnapshotRecoverySource recoverySource = (RecoverySource.SnapshotRecoverySource) shardRouting
+                    .recoverySource();
+                if (recoverySource.restoreUUID().equals(RecoverySource.SnapshotRecoverySource.NO_API_RESTORE_UUID)) {
+                    // this shard already has the right recovery ID, but maybe the repository name is now different, so check if it needs
+                    // fixing up again
+                    return RecoverySource.SnapshotRecoverySource.NO_API_RESTORE_UUID;
+                }
+
+                // else we're recovering from a real snapshot, in which case we can only fix up the recovery source once the "real"
+                // recovery attempt has completed. It might succeed, but if it doesn't then we replace it with a dummy restore to bypass
+                // the RestoreInProgressAllocationDecider
+
+                final RestoreInProgress restoreInProgress = allocation.custom(RestoreInProgress.TYPE);
+                if (restoreInProgress == null) {
+                    // no ongoing restores, so this shard definitely completed
+                    return RecoverySource.SnapshotRecoverySource.NO_API_RESTORE_UUID;
+                }
+
+                final RestoreInProgress.Entry entry = restoreInProgress.get(recoverySource.restoreUUID());
+                if (entry == null) {
+                    // this specific restore is not ongoing, so this shard definitely completed
+                    return RecoverySource.SnapshotRecoverySource.NO_API_RESTORE_UUID;
+                }
+
+                // else this specific restore is still ongoing, so check whether this shard has completed its attempt yet
+                final RestoreInProgress.ShardRestoreStatus shardRestoreStatus = entry.shards().get(shardRouting.shardId());
+                if (shardRestoreStatus == null || shardRestoreStatus.state().completed()) {
+                    // this shard is not still pending in its specific restore so we can fix up its restore UUID
+                    return RecoverySource.SnapshotRecoverySource.NO_API_RESTORE_UUID;
+                } else {
+                    // this shard is still pending in its specific restore so we must preserve its restore UUID, but we can fix up
+                    // the repository name anyway
+                    return recoverySource.restoreUUID();
+                }
+            default:
+                return null;
+        }
+    }
+
     private AllocateUnassignedDecision decideAllocation(RoutingAllocation allocation, ShardRouting shardRouting) {
         assert shardRouting.unassigned();
         assert ExistingShardsAllocator.EXISTING_SHARDS_ALLOCATOR_SETTING.get(

+ 101 - 0
x-pack/plugin/searchable-snapshots/src/main/java/org/elasticsearch/xpack/searchablesnapshots/allocation/decider/SearchableSnapshotRepositoryExistsAllocationDecider.java

@@ -0,0 +1,101 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+package org.elasticsearch.xpack.searchablesnapshots.allocation.decider;
+
+import org.elasticsearch.cluster.metadata.IndexMetadata;
+import org.elasticsearch.cluster.metadata.RepositoriesMetadata;
+import org.elasticsearch.cluster.metadata.RepositoryMetadata;
+import org.elasticsearch.cluster.routing.RoutingNode;
+import org.elasticsearch.cluster.routing.ShardRouting;
+import org.elasticsearch.cluster.routing.allocation.RoutingAllocation;
+import org.elasticsearch.cluster.routing.allocation.decider.AllocationDecider;
+import org.elasticsearch.cluster.routing.allocation.decider.Decision;
+import org.elasticsearch.common.Strings;
+import org.elasticsearch.common.settings.Settings;
+import org.elasticsearch.xpack.searchablesnapshots.SearchableSnapshotsConstants;
+
+import java.util.List;
+
+import static org.elasticsearch.xpack.searchablesnapshots.SearchableSnapshots.SNAPSHOT_REPOSITORY_NAME_SETTING;
+import static org.elasticsearch.xpack.searchablesnapshots.SearchableSnapshots.SNAPSHOT_REPOSITORY_UUID_SETTING;
+
+public class SearchableSnapshotRepositoryExistsAllocationDecider extends AllocationDecider {
+
+    private static final String NAME = "searchable_snapshot_repository_exists";
+
+    private static final Decision YES_INAPPLICABLE = Decision.single(
+        Decision.Type.YES,
+        NAME,
+        "this decider only applies to indices backed by searchable snapshots"
+    );
+
+    private static final Decision YES_REPOSITORY_EXISTS = Decision.single(
+        Decision.Type.YES,
+        NAME,
+        "the repository containing the data for this index exists"
+    );
+
+    @Override
+    public Decision canAllocate(ShardRouting shardRouting, RoutingNode node, RoutingAllocation allocation) {
+        return allowAllocation(allocation.metadata().getIndexSafe(shardRouting.index()), allocation);
+    }
+
+    @Override
+    public Decision canAllocate(ShardRouting shardRouting, RoutingAllocation allocation) {
+        return allowAllocation(allocation.metadata().getIndexSafe(shardRouting.index()), allocation);
+    }
+
+    @Override
+    public Decision canAllocate(IndexMetadata indexMetadata, RoutingNode node, RoutingAllocation allocation) {
+        return allowAllocation(indexMetadata, allocation);
+    }
+
+    private static Decision allowAllocation(IndexMetadata indexMetadata, RoutingAllocation allocation) {
+        final Settings settings = indexMetadata.getSettings();
+        if (SearchableSnapshotsConstants.isSearchableSnapshotStore(settings)) {
+
+            final RepositoriesMetadata repositoriesMetadata = allocation.metadata().custom(RepositoriesMetadata.TYPE);
+            if (repositoriesMetadata == null || repositoriesMetadata.repositories().isEmpty()) {
+                return allocation.decision(Decision.NO, NAME, "there are no repositories registered in this cluster");
+            }
+
+            final String repositoryUuid = SNAPSHOT_REPOSITORY_UUID_SETTING.get(settings);
+            if (Strings.hasLength(repositoryUuid)) {
+                final List<RepositoryMetadata> repositories = repositoriesMetadata.repositories();
+                if (repositories.stream().anyMatch(r -> repositoryUuid.equals(r.uuid()))) {
+                    return YES_REPOSITORY_EXISTS;
+                }
+
+                return allocation.decision(
+                    Decision.NO,
+                    NAME,
+                    "this index is backed by a searchable snapshot in a repository with UUID [%s] but no such repository is registered "
+                        + "with this cluster; the required repository was originally named [%s]",
+                    repositoryUuid,
+                    SNAPSHOT_REPOSITORY_NAME_SETTING.get(settings)
+                );
+
+            } else {
+                final String repositoryName = SNAPSHOT_REPOSITORY_NAME_SETTING.get(settings);
+                if (repositoriesMetadata.repository(repositoryName) != null) {
+                    return YES_REPOSITORY_EXISTS;
+                }
+
+                return allocation.decision(
+                    Decision.NO,
+                    NAME,
+                    "this index is backed by a searchable snapshot in a repository named [%s] but no such repository is registered "
+                        + "with this cluster",
+                    repositoryName
+                );
+            }
+        } else {
+            return YES_INAPPLICABLE;
+        }
+    }
+}

+ 5 - 2
x-pack/plugin/searchable-snapshots/src/test/java/org/elasticsearch/xpack/searchablesnapshots/allocation/SearchableSnapshotAllocatorTests.java

@@ -120,7 +120,11 @@ public class SearchableSnapshotAllocatorTests extends ESAllocationTestCase {
 
         assertEquals(1, reroutesTriggered.get());
         if (existingCacheSizes.values().stream().allMatch(size -> size == 0L)) {
-            assertFalse("If there are no existing caches the allocator should not take a decision", allocation.routingNodesChanged());
+            assertThat(
+                "If there are no existing caches the allocator should not take a decision",
+                allocation.routingNodes().assignedShards(shardId),
+                empty()
+            );
         } else {
             assertTrue(allocation.routingNodesChanged());
             final long bestCacheSize = existingCacheSizes.values().stream().mapToLong(l -> l).max().orElseThrow();
@@ -211,7 +215,6 @@ public class SearchableSnapshotAllocatorTests extends ESAllocationTestCase {
             testFrozenCacheSizeService()
         );
         allocateAllUnassigned(allocation, allocator);
-        assertFalse(allocation.routingNodesChanged());
         assertThat(allocation.routingNodes().assignedShards(shardId), empty());
         assertTrue(allocation.routingTable().index(shardId.getIndex()).allPrimaryShardsUnassigned());
     }

+ 5 - 0
x-pack/qa/rolling-upgrade/build.gradle

@@ -97,6 +97,11 @@ for (Version bwcVersion : BuildParams.bwcVersions.wireCompatible) {
     useCluster testClusters."${baseName}"
     mustRunAfter("precommit")
     dependsOn "copyTestNodeKeyMaterial"
+    doFirst {
+      delete("${buildDir}/cluster/shared/repo/${baseName}")
+      delete("${searchableSnapshotRepository}")
+    }
+
     systemProperty 'tests.rest.suite', 'old_cluster'
     systemProperty 'tests.upgrade_from_version', version.toString().replace('-SNAPSHOT', '')
     systemProperty 'tests.path.searchable.snapshots.repo', searchableSnapshotRepository