From b0c009ae769877594ada8a2b05267463caebf274 Mon Sep 17 00:00:00 2001 From: Ali Beyad Date: Fri, 6 Jan 2017 17:48:57 -0500 Subject: [PATCH] Gracefully handles pre 2.x compressed snapshots In pre 2.x versions, if the repository was set to compress snapshots, then snapshots would be compressed with the LZF algorithm. In 5.x, Elasticsearch no longer supports the LZF compression algorithm. This presents an issue when retrieving snapshots in a repository or upgrading repository data to the 5.x version, because Elasticsearch throws an exception when it tries to read the snapshot metadata because it was compressed using LZF. This commit gracefully handles the situation by introducing a new incompatible-snapshots blob to the repository. For any pre-2.x snapshot that cannot be read, that snapshot is removed from the list of active snapshots, because the snapshot could not be restored anyway. Instead, the snapshot is recorded in the incompatible-snapshots blob. When listing snapshots, both active snapshots and incompatible snapshots will be listed, with incompatible snapshots showing a `INCOMPATIBLE` state. Any attempt to restore an incompatible snapshot will result in an exception. --- .../get/TransportGetSnapshotsAction.java | 2 +- .../TransportSnapshotsStatusAction.java | 9 +- .../repositories/RepositoryData.java | 130 +++++++++++++++--- .../blobstore/BlobStoreRepository.java | 63 +++++++-- .../snapshots/RestoreService.java | 5 + .../elasticsearch/snapshots/SnapshotInfo.java | 54 +++++++- .../snapshots/SnapshotState.java | 10 +- .../snapshots/SnapshotsService.java | 13 +- .../index/shard/IndexShardTests.java | 3 +- .../repositories/RepositoryDataTests.java | 30 +++- .../blobstore/BlobStoreRepositoryTests.java | 35 ++++- .../indices/bwc/compressed-repo-1.7.4.zip | Bin 0 -> 22789 bytes 12 files changed, 297 insertions(+), 57 deletions(-) create mode 100644 core/src/test/resources/indices/bwc/compressed-repo-1.7.4.zip diff --git a/core/src/main/java/org/elasticsearch/action/admin/cluster/snapshots/get/TransportGetSnapshotsAction.java b/core/src/main/java/org/elasticsearch/action/admin/cluster/snapshots/get/TransportGetSnapshotsAction.java index 573bb0ea263..ad8541ce9fd 100644 --- a/core/src/main/java/org/elasticsearch/action/admin/cluster/snapshots/get/TransportGetSnapshotsAction.java +++ b/core/src/main/java/org/elasticsearch/action/admin/cluster/snapshots/get/TransportGetSnapshotsAction.java @@ -88,7 +88,7 @@ public class TransportGetSnapshotsAction extends TransportMasterNodeAction 0) { final Set requestedSnapshotNames = Sets.newHashSet(request.snapshots()); - final Map matchedSnapshotIds = snapshotsService.snapshotIds(repositoryName).stream() + final RepositoryData repositoryData = snapshotsService.getRepositoryData(repositoryName); + final Map matchedSnapshotIds = repositoryData.getAllSnapshotIds().stream() .filter(s -> requestedSnapshotNames.contains(s.getName())) .collect(Collectors.toMap(SnapshotId::getName, Function.identity())); for (final String snapshotName : request.snapshots()) { @@ -220,6 +223,8 @@ public class TransportSnapshotsStatusAction extends TransportMasterNodeAction shardStatusBuilder = new ArrayList<>(); @@ -243,7 +248,7 @@ public class TransportSnapshotsStatusAction extends TransportMasterNodeAction> indexSnapshots; + /** + * The snapshots that are no longer compatible with the current cluster ES version. + */ + private final List incompatibleSnapshotIds; - private RepositoryData(long genId, List snapshotIds, Map> indexSnapshots) { + public RepositoryData(long genId, List snapshotIds, Map> indexSnapshots, + List incompatibleSnapshotIds) { this.genId = genId; this.snapshotIds = Collections.unmodifiableList(snapshotIds); this.indices = Collections.unmodifiableMap(indexSnapshots.keySet() .stream() .collect(Collectors.toMap(IndexId::getName, Function.identity()))); this.indexSnapshots = Collections.unmodifiableMap(indexSnapshots); - } - - /** - * Creates an instance of {@link RepositoryData} on a fresh repository (one that has no index-N files). - */ - public static RepositoryData initRepositoryData(List snapshotIds, Map> indexSnapshots) { - return new RepositoryData(EMPTY_REPO_GEN, snapshotIds, indexSnapshots); + this.incompatibleSnapshotIds = Collections.unmodifiableList(incompatibleSnapshotIds); } protected RepositoryData copy() { - return new RepositoryData(genId, snapshotIds, indexSnapshots); + return new RepositoryData(genId, snapshotIds, indexSnapshots, incompatibleSnapshotIds); } /** @@ -104,6 +104,25 @@ public final class RepositoryData implements ToXContent { return snapshotIds; } + /** + * Returns an immutable collection of the snapshot ids in the repository that are incompatible with the + * current ES version. + */ + public List getIncompatibleSnapshotIds() { + return incompatibleSnapshotIds; + } + + /** + * Returns an immutable collection of all the snapshot ids in the repository, both active and + * incompatible snapshots. + */ + public List getAllSnapshotIds() { + List allSnapshotIds = new ArrayList<>(snapshotIds.size() + incompatibleSnapshotIds.size()); + allSnapshotIds.addAll(snapshotIds); + allSnapshotIds.addAll(incompatibleSnapshotIds); + return Collections.unmodifiableList(allSnapshotIds); + } + /** * Returns an unmodifiable map of the index names to {@link IndexId} in the repository. */ @@ -139,7 +158,7 @@ public final class RepositoryData implements ToXContent { allIndexSnapshots.put(indexId, ids); } } - return new RepositoryData(genId, snapshots, allIndexSnapshots); + return new RepositoryData(genId, snapshots, allIndexSnapshots, incompatibleSnapshotIds); } /** @@ -168,7 +187,21 @@ public final class RepositoryData implements ToXContent { indexSnapshots.put(indexId, set); } - return new RepositoryData(genId, newSnapshotIds, indexSnapshots); + return new RepositoryData(genId, newSnapshotIds, indexSnapshots, incompatibleSnapshotIds); + } + + /** + * Returns a new {@link RepositoryData} instance containing the same snapshot data as the + * invoking instance, with the given incompatible snapshots added to the new instance. + */ + public RepositoryData addIncompatibleSnapshots(final List incompatibleSnapshotIds) { + List newSnapshotIds = new ArrayList<>(this.snapshotIds); + List newIncompatibleSnapshotIds = new ArrayList<>(this.incompatibleSnapshotIds); + for (SnapshotId snapshotId : incompatibleSnapshotIds) { + newSnapshotIds.remove(snapshotId); + newIncompatibleSnapshotIds.add(snapshotId); + } + return new RepositoryData(this.genId, newSnapshotIds, this.indexSnapshots, newIncompatibleSnapshotIds); } /** @@ -182,6 +215,13 @@ public final class RepositoryData implements ToXContent { return snapshotIds; } + /** + * Initializes the indices in the repository metadata; returns a new instance. + */ + public RepositoryData initIndices(final Map> indexSnapshots) { + return new RepositoryData(genId, snapshotIds, indexSnapshots, incompatibleSnapshotIds); + } + @Override public boolean equals(Object obj) { if (this == obj) { @@ -193,12 +233,13 @@ public final class RepositoryData implements ToXContent { @SuppressWarnings("unchecked") RepositoryData that = (RepositoryData) obj; return snapshotIds.equals(that.snapshotIds) && indices.equals(that.indices) - && indexSnapshots.equals(that.indexSnapshots); + && indexSnapshots.equals(that.indexSnapshots) + && incompatibleSnapshotIds.equals(that.incompatibleSnapshotIds); } @Override public int hashCode() { - return Objects.hash(snapshotIds, indices, indexSnapshots); + return Objects.hash(snapshotIds, indices, indexSnapshots, incompatibleSnapshotIds); } /** @@ -247,11 +288,15 @@ public final class RepositoryData implements ToXContent { } private static final String SNAPSHOTS = "snapshots"; + private static final String INCOMPATIBLE_SNAPSHOTS = "incompatible-snapshots"; private static final String INDICES = "indices"; private static final String INDEX_ID = "id"; - @Override - public XContentBuilder toXContent(final XContentBuilder builder, final Params params) throws IOException { + /** + * Writes the snapshots metadata and the related indices metadata to x-content, omitting the + * incompatible snapshots. + */ + public XContentBuilder snapshotsToXContent(final XContentBuilder builder, final ToXContent.Params params) throws IOException { builder.startObject(); // write the snapshots list builder.startArray(SNAPSHOTS); @@ -278,7 +323,10 @@ public final class RepositoryData implements ToXContent { return builder; } - public static RepositoryData fromXContent(final XContentParser parser, final long genId) throws IOException { + /** + * Reads an instance of {@link RepositoryData} from x-content, loading the snapshots and indices metadata. + */ + public static RepositoryData snapshotsFromXContent(final XContentParser parser, long genId) throws IOException { List snapshots = new ArrayList<>(); Map> indexSnapshots = new HashMap<>(); if (parser.nextToken() == XContentParser.Token.START_OBJECT) { @@ -327,7 +375,51 @@ public final class RepositoryData implements ToXContent { } else { throw new ElasticsearchParseException("start object expected"); } - return new RepositoryData(genId, snapshots, indexSnapshots); + return new RepositoryData(genId, snapshots, indexSnapshots, Collections.emptyList()); + } + + /** + * Writes the incompatible snapshot ids to x-content. + */ + public XContentBuilder incompatibleSnapshotsToXContent(final XContentBuilder builder, final ToXContent.Params params) + throws IOException { + + builder.startObject(); + // write the incompatible snapshots list + builder.startArray(INCOMPATIBLE_SNAPSHOTS); + for (final SnapshotId snapshot : getIncompatibleSnapshotIds()) { + snapshot.toXContent(builder, params); + } + builder.endArray(); + builder.endObject(); + return builder; + } + + /** + * Reads the incompatible snapshot ids from x-content, loading them into a new instance of {@link RepositoryData} + * that is created from the invoking instance, plus the incompatible snapshots that are read from x-content. + */ + public RepositoryData incompatibleSnapshotsFromXContent(final XContentParser parser) throws IOException { + List incompatibleSnapshotIds = new ArrayList<>(); + if (parser.nextToken() == XContentParser.Token.START_OBJECT) { + while (parser.nextToken() == XContentParser.Token.FIELD_NAME) { + String currentFieldName = parser.currentName(); + if (INCOMPATIBLE_SNAPSHOTS.equals(currentFieldName)) { + if (parser.nextToken() == XContentParser.Token.START_ARRAY) { + while (parser.nextToken() != XContentParser.Token.END_ARRAY) { + incompatibleSnapshotIds.add(SnapshotId.fromXContent(parser)); + } + } else { + throw new ElasticsearchParseException("expected array for [" + currentFieldName + "]"); + } + } else { + throw new ElasticsearchParseException("unknown field name [" + currentFieldName + "]"); + } + } + } else { + throw new ElasticsearchParseException("start object expected"); + } + return new RepositoryData(this.genId, this.snapshotIds, this.indexSnapshots, incompatibleSnapshotIds); } } diff --git a/core/src/main/java/org/elasticsearch/repositories/blobstore/BlobStoreRepository.java b/core/src/main/java/org/elasticsearch/repositories/blobstore/BlobStoreRepository.java index 6935d277eed..0ac2d7bade1 100644 --- a/core/src/main/java/org/elasticsearch/repositories/blobstore/BlobStoreRepository.java +++ b/core/src/main/java/org/elasticsearch/repositories/blobstore/BlobStoreRepository.java @@ -128,8 +128,9 @@ import static java.util.Collections.unmodifiableMap; *
  * {@code
  *   STORE_ROOT
- *   |- index-N           - list of all snapshot name as JSON array, N is the generation of the file
+ *   |- index-N           - list of all snapshot ids and the indices belonging to each snapshot, N is the generation of the file
  *   |- index.latest      - contains the numeric value of the latest generation of the index file (i.e. N from above)
+ *   |- incompatible-snapshots - list of all snapshot ids that are no longer compatible with the current version of the cluster
  *   |- snap-20131010 - JSON serialized Snapshot for snapshot "20131010"
  *   |- meta-20131010.dat - JSON serialized MetaData for snapshot "20131010" (includes only global metadata)
  *   |- snap-20131011 - JSON serialized Snapshot for snapshot "20131011"
@@ -181,6 +182,8 @@ public abstract class BlobStoreRepository extends AbstractLifecycleComponent imp
 
     private static final String INDEX_LATEST_BLOB = "index.latest";
 
+    private static final String INCOMPATIBLE_SNAPSHOTS_BLOB = "incompatible-snapshots";
+
     private static final String TESTS_FILE = "tests-";
 
     private static final String METADATA_NAME_FORMAT = "meta-%s.dat";
@@ -232,11 +235,11 @@ public abstract class BlobStoreRepository extends AbstractLifecycleComponent imp
         snapshotRateLimiter = getRateLimiter(metadata.settings(), "max_snapshot_bytes_per_sec", new ByteSizeValue(40, ByteSizeUnit.MB));
         restoreRateLimiter = getRateLimiter(metadata.settings(), "max_restore_bytes_per_sec", new ByteSizeValue(40, ByteSizeUnit.MB));
         readOnly = metadata.settings().getAsBoolean("readonly", false);
+
         indexShardSnapshotFormat = new ChecksumBlobStoreFormat<>(SNAPSHOT_CODEC, SNAPSHOT_NAME_FORMAT,
             BlobStoreIndexShardSnapshot::fromXContent, namedXContentRegistry, isCompress());
         indexShardSnapshotsFormat = new ChecksumBlobStoreFormat<>(SNAPSHOT_INDEX_CODEC, SNAPSHOT_INDEX_NAME_FORMAT,
             BlobStoreIndexShardSnapshots::fromXContent, namedXContentRegistry, isCompress());
-
     }
 
     @Override
@@ -305,7 +308,8 @@ public abstract class BlobStoreRepository extends AbstractLifecycleComponent imp
         try {
             final String snapshotName = snapshotId.getName();
             // check if the snapshot name already exists in the repository
-            if (getSnapshots().stream().anyMatch(s -> s.getName().equals(snapshotName))) {
+            final RepositoryData repositoryData = getRepositoryData();
+            if (repositoryData.getAllSnapshotIds().stream().anyMatch(s -> s.getName().equals(snapshotName))) {
                 throw new SnapshotCreationException(metadata.name(), snapshotId, "snapshot with the same name already exists");
             }
             if (snapshotFormat.exists(snapshotsBlobContainer, snapshotId.getUUID())) {
@@ -480,10 +484,6 @@ public abstract class BlobStoreRepository extends AbstractLifecycleComponent imp
         }
     }
 
-    public List getSnapshots() {
-        return getRepositoryData().getSnapshotIds();
-    }
-
     @Override
     public MetaData getSnapshotMetaData(SnapshotInfo snapshot, List indices) throws IOException {
         return readSnapshotMetaData(snapshot.snapshotId(), snapshot.version(), indices, false);
@@ -491,6 +491,15 @@ public abstract class BlobStoreRepository extends AbstractLifecycleComponent imp
 
     @Override
     public SnapshotInfo getSnapshotInfo(final SnapshotId snapshotId) {
+        if (getRepositoryData().getIncompatibleSnapshotIds().contains(snapshotId)) {
+            // an incompatible snapshot - cannot read its snapshot metadata file, just return
+            // a SnapshotInfo indicating its incompatible
+            return SnapshotInfo.incompatible(snapshotId);
+        }
+        return getSnapshotInfoInternal(snapshotId);
+    }
+
+    private SnapshotInfo getSnapshotInfoInternal(final SnapshotId snapshotId) {
         try {
             return snapshotFormat.read(snapshotsBlobContainer, snapshotId.getUUID());
         } catch (NoSuchFileException ex) {
@@ -633,9 +642,21 @@ public abstract class BlobStoreRepository extends AbstractLifecycleComponent imp
                 Streams.copy(blob, out);
                 // EMPTY is safe here because RepositoryData#fromXContent calls namedObject
                 try (XContentParser parser = XContentHelper.createParser(NamedXContentRegistry.EMPTY, out.bytes())) {
-                    repositoryData = RepositoryData.fromXContent(parser, indexGen);
+                    repositoryData = RepositoryData.snapshotsFromXContent(parser, indexGen);
                 }
             }
+
+            // now load the incompatible snapshot ids, if they exist
+            try (InputStream blob = snapshotsBlobContainer.readBlob(INCOMPATIBLE_SNAPSHOTS_BLOB)) {
+                BytesStreamOutput out = new BytesStreamOutput();
+                Streams.copy(blob, out);
+                try (XContentParser parser = XContentHelper.createParser(NamedXContentRegistry.EMPTY, out.bytes())) {
+                    repositoryData = repositoryData.incompatibleSnapshotsFromXContent(parser);
+                }
+            } catch (NoSuchFileException e) {
+                logger.debug("[{}] Incompatible snapshots blob [{}] does not exist, the likely reason is that " +
+                             "there are no incompatible snapshots in the repository", metadata.name(), INCOMPATIBLE_SNAPSHOTS_BLOB);
+            }
             return repositoryData;
         } catch (NoSuchFileException ex) {
             // repository doesn't have an index blob, its a new blank repo
@@ -674,7 +695,7 @@ public abstract class BlobStoreRepository extends AbstractLifecycleComponent imp
         try (BytesStreamOutput bStream = new BytesStreamOutput()) {
             try (StreamOutput stream = new OutputStreamStreamOutput(bStream)) {
                 XContentBuilder builder = XContentFactory.contentBuilder(XContentType.JSON, stream);
-                repositoryData.toXContent(builder, ToXContent.EMPTY_PARAMS);
+                repositoryData.snapshotsToXContent(builder, ToXContent.EMPTY_PARAMS);
                 builder.close();
             }
             snapshotsBytes = bStream.bytes();
@@ -687,10 +708,6 @@ public abstract class BlobStoreRepository extends AbstractLifecycleComponent imp
             if (snapshotsBlobContainer.blobExists(oldSnapshotIndexFile)) {
                 snapshotsBlobContainer.deleteBlob(oldSnapshotIndexFile);
             }
-            // delete the old index file (non-generational) if it exists
-            if (snapshotsBlobContainer.blobExists(SNAPSHOTS_FILE)) {
-                snapshotsBlobContainer.deleteBlob(SNAPSHOTS_FILE);
-            }
         }
 
         // write the current generation to the index-latest file
@@ -705,6 +722,26 @@ public abstract class BlobStoreRepository extends AbstractLifecycleComponent imp
         writeAtomic(INDEX_LATEST_BLOB, genBytes);
     }
 
+    /**
+     * Writes the incompatible snapshot ids list to the `incompatible-snapshots` blob in the repository.
+     *
+     * Package private for testing.
+     */
+    void writeIncompatibleSnapshots(RepositoryData repositoryData) throws IOException {
+        assert isReadOnly() == false; // can not write to a read only repository
+        final BytesReference bytes;
+        try (BytesStreamOutput bStream = new BytesStreamOutput()) {
+            try (StreamOutput stream = new OutputStreamStreamOutput(bStream)) {
+                XContentBuilder builder = XContentFactory.contentBuilder(XContentType.JSON, stream);
+                repositoryData.incompatibleSnapshotsToXContent(builder, ToXContent.EMPTY_PARAMS);
+                builder.close();
+            }
+            bytes = bStream.bytes();
+        }
+        // write the incompatible snapshots blob
+        writeAtomic(INCOMPATIBLE_SNAPSHOTS_BLOB, bytes);
+    }
+
     /**
      * Get the latest snapshot index blob id.  Snapshot index blobs are named index-N, where N is
      * the next version number from when the index blob was written.  Each individual index-N blob is
diff --git a/core/src/main/java/org/elasticsearch/snapshots/RestoreService.java b/core/src/main/java/org/elasticsearch/snapshots/RestoreService.java
index ac01bc6fc5d..1d7843e7826 100644
--- a/core/src/main/java/org/elasticsearch/snapshots/RestoreService.java
+++ b/core/src/main/java/org/elasticsearch/snapshots/RestoreService.java
@@ -176,6 +176,11 @@ public class RestoreService extends AbstractComponent implements ClusterStateApp
             // Read snapshot info and metadata from the repository
             Repository repository = repositoriesService.repository(request.repositoryName);
             final RepositoryData repositoryData = repository.getRepositoryData();
+            final Optional incompatibleSnapshotId =
+                repositoryData.getIncompatibleSnapshotIds().stream().filter(s -> request.snapshotName.equals(s.getName())).findFirst();
+            if (incompatibleSnapshotId.isPresent()) {
+                throw new SnapshotRestoreException(request.repositoryName, request.snapshotName, "cannot restore incompatible snapshot");
+            }
             final Optional matchingSnapshotId = repositoryData.getSnapshotIds().stream()
                 .filter(s -> request.snapshotName.equals(s.getName())).findFirst();
             if (matchingSnapshotId.isPresent() == false) {
diff --git a/core/src/main/java/org/elasticsearch/snapshots/SnapshotInfo.java b/core/src/main/java/org/elasticsearch/snapshots/SnapshotInfo.java
index bf65ad603a5..519393d49e1 100644
--- a/core/src/main/java/org/elasticsearch/snapshots/SnapshotInfo.java
+++ b/core/src/main/java/org/elasticsearch/snapshots/SnapshotInfo.java
@@ -21,6 +21,7 @@ package org.elasticsearch.snapshots;
 import org.elasticsearch.ElasticsearchParseException;
 import org.elasticsearch.Version;
 import org.elasticsearch.action.ShardOperationFailedException;
+import org.elasticsearch.common.Nullable;
 import org.elasticsearch.common.io.stream.StreamInput;
 import org.elasticsearch.common.io.stream.StreamOutput;
 import org.elasticsearch.common.io.stream.Writeable;
@@ -67,6 +68,8 @@ public final class SnapshotInfo implements Comparable, ToXContent,
     private static final String TOTAL_SHARDS = "total_shards";
     private static final String SUCCESSFUL_SHARDS = "successful_shards";
 
+    private static final Version VERSION_INCOMPATIBLE_INTRODUCED = Version.V_5_2_0_UNRELEASED;
+
     private final SnapshotId snapshotId;
 
     private final SnapshotState state;
@@ -83,6 +86,7 @@ public final class SnapshotInfo implements Comparable, ToXContent,
 
     private final int successfulShards;
 
+    @Nullable
     private final Version version;
 
     private final List shardFailures;
@@ -138,7 +142,21 @@ public final class SnapshotInfo implements Comparable, ToXContent,
         } else {
             shardFailures = Collections.emptyList();
         }
-        version = Version.readVersion(in);
+        if (in.getVersion().before(VERSION_INCOMPATIBLE_INTRODUCED)) {
+            version = Version.readVersion(in);
+        } else {
+            version = in.readBoolean() ? Version.readVersion(in) : null;
+        }
+    }
+
+    /**
+     * Gets a new {@link SnapshotInfo} instance for a snapshot that is incompatible with the
+     * current version of the cluster.
+     */
+    public static SnapshotInfo incompatible(SnapshotId snapshotId) {
+        return new SnapshotInfo(snapshotId, Collections.emptyList(), SnapshotState.INCOMPATIBLE,
+                                "the snapshot is incompatible with the current version of Elasticsearch and its exact version is unknown",
+                                null, 0L, 0L, 0, 0, Collections.emptyList());
     }
 
     /**
@@ -234,10 +252,12 @@ public final class SnapshotInfo implements Comparable, ToXContent,
     }
 
     /**
-     * Returns the version of elasticsearch that the snapshot was created with
+     * Returns the version of elasticsearch that the snapshot was created with.  Will only
+     * return {@code null} if {@link #state()} returns {@link SnapshotState#INCOMPATIBLE}.
      *
      * @return version of elasticsearch that the snapshot was created with
      */
+    @Nullable
     public Version version() {
         return version;
     }
@@ -305,8 +325,12 @@ public final class SnapshotInfo implements Comparable, ToXContent,
         builder.startObject();
         builder.field(SNAPSHOT, snapshotId.getName());
         builder.field(UUID, snapshotId.getUUID());
-        builder.field(VERSION_ID, version.id);
-        builder.field(VERSION, version.toString());
+        if (version != null) {
+            builder.field(VERSION_ID, version.id);
+            builder.field(VERSION, version.toString());
+        } else {
+            builder.field(VERSION, "unknown");
+        }
         builder.startArray(INDICES);
         for (String index : indices) {
             builder.value(index);
@@ -345,6 +369,7 @@ public final class SnapshotInfo implements Comparable, ToXContent,
         builder.startObject(SNAPSHOT);
         builder.field(NAME, snapshotId.getName());
         builder.field(UUID, snapshotId.getUUID());
+        assert version != null : "version must always be known when writing a snapshot metadata blob";
         builder.field(VERSION_ID, version.id);
         builder.startArray(INDICES);
         for (String index : indices) {
@@ -471,7 +496,11 @@ public final class SnapshotInfo implements Comparable, ToXContent,
         for (String index : indices) {
             out.writeString(index);
         }
-        out.writeByte(state.value());
+        if (out.getVersion().before(VERSION_INCOMPATIBLE_INTRODUCED) && state == SnapshotState.INCOMPATIBLE) {
+            out.writeByte(SnapshotState.FAILED.value());
+        } else {
+            out.writeByte(state.value());
+        }
         out.writeOptionalString(reason);
         out.writeVLong(startTime);
         out.writeVLong(endTime);
@@ -481,7 +510,20 @@ public final class SnapshotInfo implements Comparable, ToXContent,
         for (SnapshotShardFailure failure : shardFailures) {
             failure.writeTo(out);
         }
-        Version.writeVersion(version, out);
+        if (out.getVersion().before(VERSION_INCOMPATIBLE_INTRODUCED)) {
+            Version versionToWrite = version;
+            if (versionToWrite == null) {
+                versionToWrite = Version.CURRENT;
+            }
+            Version.writeVersion(versionToWrite, out);
+        } else {
+            if (version != null) {
+                out.writeBoolean(true);
+                Version.writeVersion(version, out);
+            } else {
+                out.writeBoolean(false);
+            }
+        }
     }
 
     private static SnapshotState snapshotState(final String reason, final List shardFailures) {
diff --git a/core/src/main/java/org/elasticsearch/snapshots/SnapshotState.java b/core/src/main/java/org/elasticsearch/snapshots/SnapshotState.java
index b893a372d13..3df5f8fff04 100644
--- a/core/src/main/java/org/elasticsearch/snapshots/SnapshotState.java
+++ b/core/src/main/java/org/elasticsearch/snapshots/SnapshotState.java
@@ -39,7 +39,11 @@ public enum SnapshotState {
     /**
      * Snapshot was partial successful
      */
-    PARTIAL((byte) 3, true, true);
+    PARTIAL((byte) 3, true, true),
+    /**
+     * Snapshot is incompatible with the current version of the cluster
+     */
+    INCOMPATIBLE((byte) 4, true, false);
 
     private byte value;
 
@@ -47,7 +51,7 @@ public enum SnapshotState {
 
     private boolean restorable;
 
-    private SnapshotState(byte value, boolean completed, boolean restorable) {
+    SnapshotState(byte value, boolean completed, boolean restorable) {
         this.value = value;
         this.completed = completed;
         this.restorable = restorable;
@@ -97,6 +101,8 @@ public enum SnapshotState {
                 return FAILED;
             case 3:
                 return PARTIAL;
+            case 4:
+                return INCOMPATIBLE;
             default:
                 throw new IllegalArgumentException("No snapshot state for value [" + value + "]");
         }
diff --git a/core/src/main/java/org/elasticsearch/snapshots/SnapshotsService.java b/core/src/main/java/org/elasticsearch/snapshots/SnapshotsService.java
index 1b5bfde167c..2f93c20c37a 100644
--- a/core/src/main/java/org/elasticsearch/snapshots/SnapshotsService.java
+++ b/core/src/main/java/org/elasticsearch/snapshots/SnapshotsService.java
@@ -131,15 +131,15 @@ public class SnapshotsService extends AbstractLifecycleComponent implements Clus
     }
 
     /**
-     * Retrieves list of snapshot ids that are present in a repository
+     * Gets the {@link RepositoryData} for the given repository.
      *
      * @param repositoryName repository name
-     * @return list of snapshot ids
+     * @return repository data
      */
-    public List snapshotIds(final String repositoryName) {
+    public RepositoryData getRepositoryData(final String repositoryName) {
         Repository repository = repositoriesService.repository(repositoryName);
         assert repository != null; // should only be called once we've validated the repository exists
-        return repository.getRepositoryData().getSnapshotIds();
+        return repository.getRepositoryData();
     }
 
     /**
@@ -1004,6 +1004,11 @@ public class SnapshotsService extends AbstractLifecycleComponent implements Clus
         // First, look for the snapshot in the repository
         final Repository repository = repositoriesService.repository(repositoryName);
         final RepositoryData repositoryData = repository.getRepositoryData();
+        final Optional incompatibleSnapshotId =
+            repositoryData.getIncompatibleSnapshotIds().stream().filter(s -> snapshotName.equals(s.getName())).findFirst();
+        if (incompatibleSnapshotId.isPresent()) {
+            throw new SnapshotException(repositoryName, snapshotName, "cannot delete incompatible snapshot");
+        }
         Optional matchedEntry = repositoryData.getSnapshotIds()
                                                 .stream()
                                                 .filter(s -> s.getName().equals(snapshotName))
diff --git a/core/src/test/java/org/elasticsearch/index/shard/IndexShardTests.java b/core/src/test/java/org/elasticsearch/index/shard/IndexShardTests.java
index d9d6015e422..ce3fb4c129c 100644
--- a/core/src/test/java/org/elasticsearch/index/shard/IndexShardTests.java
+++ b/core/src/test/java/org/elasticsearch/index/shard/IndexShardTests.java
@@ -126,6 +126,7 @@ import static org.elasticsearch.common.lucene.Lucene.cleanLuceneIndex;
 import static org.elasticsearch.common.xcontent.ToXContent.EMPTY_PARAMS;
 import static org.elasticsearch.common.xcontent.XContentFactory.jsonBuilder;
 import static org.elasticsearch.index.engine.Engine.Operation.Origin.PRIMARY;
+import static org.elasticsearch.repositories.RepositoryData.EMPTY_REPO_GEN;
 import static org.hamcrest.Matchers.containsString;
 import static org.hamcrest.Matchers.equalTo;
 import static org.hamcrest.Matchers.greaterThan;
@@ -1493,7 +1494,7 @@ public class IndexShardTests extends IndexShardTestCase {
         public RepositoryData getRepositoryData() {
             Map> map = new HashMap<>();
             map.put(new IndexId(indexName, "blah"), emptySet());
-            return RepositoryData.initRepositoryData(Collections.emptyList(), map);
+            return new RepositoryData(EMPTY_REPO_GEN, Collections.emptyList(), map, Collections.emptyList());
         }
 
         @Override
diff --git a/core/src/test/java/org/elasticsearch/repositories/RepositoryDataTests.java b/core/src/test/java/org/elasticsearch/repositories/RepositoryDataTests.java
index 97d415fe4f9..f9c620e3b9e 100644
--- a/core/src/test/java/org/elasticsearch/repositories/RepositoryDataTests.java
+++ b/core/src/test/java/org/elasticsearch/repositories/RepositoryDataTests.java
@@ -37,6 +37,7 @@ import java.util.List;
 import java.util.Map;
 import java.util.Set;
 
+import static org.elasticsearch.repositories.RepositoryData.EMPTY_REPO_GEN;
 import static org.hamcrest.Matchers.greaterThan;
 
 /**
@@ -54,10 +55,10 @@ public class RepositoryDataTests extends ESTestCase {
     public void testXContent() throws IOException {
         RepositoryData repositoryData = generateRandomRepoData();
         XContentBuilder builder = JsonXContent.contentBuilder();
-        repositoryData.toXContent(builder, ToXContent.EMPTY_PARAMS);
+        repositoryData.snapshotsToXContent(builder, ToXContent.EMPTY_PARAMS);
         XContentParser parser = createParser(JsonXContent.jsonXContent, builder.bytes());
         long gen = (long) randomIntBetween(0, 500);
-        RepositoryData fromXContent = RepositoryData.fromXContent(parser, gen);
+        RepositoryData fromXContent = RepositoryData.snapshotsFromXContent(parser, gen);
         assertEquals(repositoryData, fromXContent);
         assertEquals(gen, fromXContent.getGenId());
     }
@@ -65,7 +66,6 @@ public class RepositoryDataTests extends ESTestCase {
     public void testAddSnapshots() {
         RepositoryData repositoryData = generateRandomRepoData();
         // test that adding the same snapshot id to the repository data throws an exception
-        final SnapshotId snapshotId = repositoryData.getSnapshotIds().get(0);
         Map indexIdMap = repositoryData.getIndices();
         // test that adding a snapshot and its indices works
         SnapshotId newSnapshot = new SnapshotId(randomAsciiOfLength(7), UUIDs.randomBase64UUID());
@@ -95,6 +95,22 @@ public class RepositoryDataTests extends ESTestCase {
         assertEquals(repositoryData.getGenId(), newRepoData.getGenId());
     }
 
+    public void testInitIndices() {
+        final int numSnapshots = randomIntBetween(1, 30);
+        final List snapshotIds = new ArrayList<>(numSnapshots);
+        for (int i = 0; i < numSnapshots; i++) {
+            snapshotIds.add(new SnapshotId(randomAsciiOfLength(8), UUIDs.randomBase64UUID()));
+        }
+        RepositoryData repositoryData = new RepositoryData(EMPTY_REPO_GEN, snapshotIds, Collections.emptyMap(), Collections.emptyList());
+        // test that initializing indices works
+        Map> indices = randomIndices(snapshotIds);
+        RepositoryData newRepoData = repositoryData.initIndices(indices);
+        assertEquals(repositoryData.getSnapshotIds(), newRepoData.getSnapshotIds());
+        for (IndexId indexId : indices.keySet()) {
+            assertEquals(indices.get(indexId), newRepoData.getSnapshots(indexId));
+        }
+    }
+
     public void testRemoveSnapshot() {
         RepositoryData repositoryData = generateRandomRepoData();
         List snapshotIds = new ArrayList<>(repositoryData.getSnapshotIds());
@@ -121,8 +137,12 @@ public class RepositoryDataTests extends ESTestCase {
     }
 
     public static RepositoryData generateRandomRepoData() {
-        List snapshotIds = randomSnapshots(new ArrayList<>());
-        return RepositoryData.initRepositoryData(snapshotIds, randomIndices(snapshotIds));
+        return generateRandomRepoData(new ArrayList<>());
+    }
+
+    public static RepositoryData generateRandomRepoData(final List origSnapshotIds) {
+        List snapshotIds = randomSnapshots(origSnapshotIds);
+        return new RepositoryData(EMPTY_REPO_GEN, snapshotIds, randomIndices(snapshotIds), Collections.emptyList());
     }
 
     private static List randomSnapshots(final List origSnapshotIds) {
diff --git a/core/src/test/java/org/elasticsearch/repositories/blobstore/BlobStoreRepositoryTests.java b/core/src/test/java/org/elasticsearch/repositories/blobstore/BlobStoreRepositoryTests.java
index f5f036a2359..6e538e721a4 100644
--- a/core/src/test/java/org/elasticsearch/repositories/blobstore/BlobStoreRepositoryTests.java
+++ b/core/src/test/java/org/elasticsearch/repositories/blobstore/BlobStoreRepositoryTests.java
@@ -36,6 +36,7 @@ import java.io.IOException;
 import java.nio.file.Path;
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.Collections;
 import java.util.List;
 import java.util.stream.Collectors;
 
@@ -95,9 +96,8 @@ public class BlobStoreRepositoryTests extends ESSingleNodeTestCase {
             (BlobStoreRepository) repositoriesService.repository(repositoryName);
         final List originalSnapshots = Arrays.asList(snapshotId1, snapshotId2);
 
-        List snapshotIds = repository.getSnapshots().stream()
-                                                             .sorted((s1, s2) -> s1.getName().compareTo(s2.getName()))
-                                                             .collect(Collectors.toList());
+        List snapshotIds = repository.getRepositoryData().getSnapshotIds().stream()
+            .sorted((s1, s2) -> s1.getName().compareTo(s2.getName())).collect(Collectors.toList());
         assertThat(snapshotIds, equalTo(originalSnapshots));
     }
 
@@ -105,7 +105,7 @@ public class BlobStoreRepositoryTests extends ESSingleNodeTestCase {
         final BlobStoreRepository repository = setupRepo();
 
         // write to and read from a index file with no entries
-        assertThat(repository.getSnapshots().size(), equalTo(0));
+        assertThat(repository.getRepositoryData().getSnapshotIds().size(), equalTo(0));
         final RepositoryData emptyData = RepositoryData.EMPTY;
         repository.writeIndexGen(emptyData, emptyData.getGenId());
         RepositoryData repoData = repository.getRepositoryData();
@@ -162,6 +162,33 @@ public class BlobStoreRepositoryTests extends ESSingleNodeTestCase {
         expectThrows(RepositoryException.class, () -> repository.writeIndexGen(repositoryData, repositoryData.getGenId()));
     }
 
+    public void testReadAndWriteIncompatibleSnapshots() throws Exception {
+        final BlobStoreRepository repository = setupRepo();
+
+        // write to and read from incompatible snapshots file with no entries
+        assertEquals(0, repository.getRepositoryData().getIncompatibleSnapshotIds().size());
+        RepositoryData emptyData = RepositoryData.EMPTY;
+        repository.writeIndexGen(emptyData, emptyData.getGenId());
+        repository.writeIncompatibleSnapshots(emptyData);
+        RepositoryData readData = repository.getRepositoryData();
+        assertEquals(emptyData, readData);
+        assertEquals(0, readData.getIndices().size());
+        assertEquals(0, readData.getSnapshotIds().size());
+
+        // write to and read from incompatible snapshots with some number of entries
+        final int numSnapshots = randomIntBetween(1, 20);
+        final List snapshotIds = new ArrayList<>(numSnapshots);
+        for (int i = 0; i < numSnapshots; i++) {
+            snapshotIds.add(new SnapshotId(randomAsciiOfLength(8), UUIDs.randomBase64UUID()));
+        }
+        RepositoryData repositoryData = new RepositoryData(readData.getGenId(), Collections.emptyList(), Collections.emptyMap(),
+                                                              snapshotIds);
+        repository.blobContainer().deleteBlob("incompatible-snapshots");
+        repository.writeIncompatibleSnapshots(repositoryData);
+        readData = repository.getRepositoryData();
+        assertEquals(repositoryData.getIncompatibleSnapshotIds(), readData.getIncompatibleSnapshotIds());
+    }
+
     private BlobStoreRepository setupRepo() {
         final Client client = client();
         final Path location = ESIntegTestCase.randomRepoPath(node().settings());
diff --git a/core/src/test/resources/indices/bwc/compressed-repo-1.7.4.zip b/core/src/test/resources/indices/bwc/compressed-repo-1.7.4.zip
new file mode 100644
index 0000000000000000000000000000000000000000..9edf7d57527e502593779b3dd119e0dc45f816c4
GIT binary patch
literal 22789
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