Skip to content

Commit 63148dd

Browse files
authored
Fail snapshot operations early on repository corruption (#30140)
A NullPointerException is thrown when trying to create or delete a snapshot in a repository that has been written to by an older Elasticsearch after writing to it with a newer Elasticsearch version. This is because the way snapshots are formatted in the repository snapshots index file changed in #24477. This commit changes the parsing of the repository index file so that it now detects a corrupted index file and fails early the snapshot operation. closes #29052
1 parent a7e69b0 commit 63148dd

File tree

6 files changed

+121
-29
lines changed

6 files changed

+121
-29
lines changed

docs/CHANGELOG.asciidoc

+3
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,9 @@
2424

2525
=== Bug Fixes
2626

27+
Fail snapshot operations early when creating or deleting a snapshot on a repository that has been
28+
written to by an older Elasticsearch after writing to it with a newer Elasticsearch version. ({pull}30140[#30140])
29+
2730
=== Regressions
2831

2932
=== Known Issues

docs/reference/modules/snapshots.asciidoc

+6-6
Original file line numberDiff line numberDiff line change
@@ -44,12 +44,12 @@ If you register same snapshot repository with multiple clusters, only
4444
one cluster should have write access to the repository. All other clusters
4545
connected to that repository should set the repository to `readonly` mode.
4646

47-
NOTE: The snapshot format can change across major versions, so if you have
48-
clusters on different major versions trying to write the same repository,
49-
new snapshots written by one version will not be visible to the other. While
50-
setting the repository to `readonly` on all but one of the clusters should work
51-
with multiple clusters differing by one major version, it is not a supported
52-
configuration.
47+
IMPORTANT: The snapshot format can change across major versions, so if you have
48+
clusters on different versions trying to write the same repository, snapshots
49+
written by one version may not be visible to the other and the repository could
50+
be corrupted. While setting the repository to `readonly` on all but one of the
51+
clusters should work with multiple clusters differing by one major version, it
52+
is not a supported configuration.
5353

5454
[source,js]
5555
-----------------------------------

server/src/main/java/org/elasticsearch/action/admin/cluster/snapshots/status/TransportSnapshotsStatusAction.java

+3-3
Original file line numberDiff line numberDiff line change
@@ -230,9 +230,9 @@ private SnapshotsStatusResponse buildResponse(SnapshotsStatusRequest request, Li
230230
SnapshotInfo snapshotInfo = snapshotsService.snapshot(repositoryName, snapshotId);
231231
List<SnapshotIndexShardStatus> shardStatusBuilder = new ArrayList<>();
232232
if (snapshotInfo.state().completed()) {
233-
Map<ShardId, IndexShardSnapshotStatus> shardStatues =
234-
snapshotsService.snapshotShards(request.repository(), snapshotInfo);
235-
for (Map.Entry<ShardId, IndexShardSnapshotStatus> shardStatus : shardStatues.entrySet()) {
233+
Map<ShardId, IndexShardSnapshotStatus> shardStatuses =
234+
snapshotsService.snapshotShards(repositoryName, repositoryData, snapshotInfo);
235+
for (Map.Entry<ShardId, IndexShardSnapshotStatus> shardStatus : shardStatuses.entrySet()) {
236236
IndexShardSnapshotStatus.Copy lastSnapshotStatus = shardStatus.getValue().asCopy();
237237
shardStatusBuilder.add(new SnapshotIndexShardStatus(shardStatus.getKey(), lastSnapshotStatus));
238238
}

server/src/main/java/org/elasticsearch/repositories/RepositoryData.java

+22-17
Original file line numberDiff line numberDiff line change
@@ -230,13 +230,6 @@ public Set<SnapshotId> getSnapshots(final IndexId indexId) {
230230
return snapshotIds;
231231
}
232232

233-
/**
234-
* Initializes the indices in the repository metadata; returns a new instance.
235-
*/
236-
public RepositoryData initIndices(final Map<IndexId, Set<SnapshotId>> indexSnapshots) {
237-
return new RepositoryData(genId, snapshotIds, snapshotStates, indexSnapshots, incompatibleSnapshotIds);
238-
}
239-
240233
@Override
241234
public boolean equals(Object obj) {
242235
if (this == obj) {
@@ -352,9 +345,10 @@ public XContentBuilder snapshotsToXContent(final XContentBuilder builder, final
352345
* Reads an instance of {@link RepositoryData} from x-content, loading the snapshots and indices metadata.
353346
*/
354347
public static RepositoryData snapshotsFromXContent(final XContentParser parser, long genId) throws IOException {
355-
Map<String, SnapshotId> snapshots = new HashMap<>();
356-
Map<String, SnapshotState> snapshotStates = new HashMap<>();
357-
Map<IndexId, Set<SnapshotId>> indexSnapshots = new HashMap<>();
348+
final Map<String, SnapshotId> snapshots = new HashMap<>();
349+
final Map<String, SnapshotState> snapshotStates = new HashMap<>();
350+
final Map<IndexId, Set<SnapshotId>> indexSnapshots = new HashMap<>();
351+
358352
if (parser.nextToken() == XContentParser.Token.START_OBJECT) {
359353
while (parser.nextToken() == XContentParser.Token.FIELD_NAME) {
360354
String field = parser.currentName();
@@ -397,17 +391,18 @@ public static RepositoryData snapshotsFromXContent(final XContentParser parser,
397391
throw new ElasticsearchParseException("start object expected [indices]");
398392
}
399393
while (parser.nextToken() != XContentParser.Token.END_OBJECT) {
400-
String indexName = parser.currentName();
401-
String indexId = null;
402-
Set<SnapshotId> snapshotIds = new LinkedHashSet<>();
394+
final String indexName = parser.currentName();
395+
final Set<SnapshotId> snapshotIds = new LinkedHashSet<>();
396+
397+
IndexId indexId = null;
403398
if (parser.nextToken() != XContentParser.Token.START_OBJECT) {
404399
throw new ElasticsearchParseException("start object expected index[" + indexName + "]");
405400
}
406401
while (parser.nextToken() != XContentParser.Token.END_OBJECT) {
407-
String indexMetaFieldName = parser.currentName();
402+
final String indexMetaFieldName = parser.currentName();
408403
parser.nextToken();
409404
if (INDEX_ID.equals(indexMetaFieldName)) {
410-
indexId = parser.text();
405+
indexId = new IndexId(indexName, parser.text());
411406
} else if (SNAPSHOTS.equals(indexMetaFieldName)) {
412407
if (parser.currentToken() != XContentParser.Token.START_ARRAY) {
413408
throw new ElasticsearchParseException("start array expected [snapshots]");
@@ -428,12 +423,22 @@ public static RepositoryData snapshotsFromXContent(final XContentParser parser,
428423
// since we already have the name/uuid combo in the snapshots array
429424
uuid = parser.text();
430425
}
431-
snapshotIds.add(snapshots.get(uuid));
426+
427+
SnapshotId snapshotId = snapshots.get(uuid);
428+
if (snapshotId != null) {
429+
snapshotIds.add(snapshotId);
430+
} else {
431+
// A snapshotted index references a snapshot which does not exist in
432+
// the list of snapshots. This can happen when multiple clusters in
433+
// different versions create or delete snapshot in the same repository.
434+
throw new ElasticsearchParseException("Detected a corrupted repository, index " + indexId
435+
+ " references an unknown snapshot uuid [" + uuid + "]");
436+
}
432437
}
433438
}
434439
}
435440
assert indexId != null;
436-
indexSnapshots.put(new IndexId(indexName, indexId), snapshotIds);
441+
indexSnapshots.put(indexId, snapshotIds);
437442
}
438443
} else {
439444
throw new ElasticsearchParseException("unknown field name [" + field + "]");

server/src/main/java/org/elasticsearch/snapshots/SnapshotsService.java

+1-2
Original file line numberDiff line numberDiff line change
@@ -592,10 +592,9 @@ public List<SnapshotsInProgress.Entry> currentSnapshots(final String repository,
592592
* @return map of shard id to snapshot status
593593
*/
594594
public Map<ShardId, IndexShardSnapshotStatus> snapshotShards(final String repositoryName,
595+
final RepositoryData repositoryData,
595596
final SnapshotInfo snapshotInfo) throws IOException {
596597
final Repository repository = repositoriesService.repository(repositoryName);
597-
final RepositoryData repositoryData = repository.getRepositoryData();
598-
599598
final Map<ShardId, IndexShardSnapshotStatus> shardStatus = new HashMap<>();
600599
for (String index : snapshotInfo.indices()) {
601600
IndexId indexId = repositoryData.resolveIndexId(index);

server/src/test/java/org/elasticsearch/repositories/RepositoryDataTests.java

+86-1
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,14 @@
1919

2020
package org.elasticsearch.repositories;
2121

22+
import org.elasticsearch.ElasticsearchParseException;
2223
import org.elasticsearch.common.UUIDs;
2324
import org.elasticsearch.common.bytes.BytesReference;
2425
import org.elasticsearch.common.xcontent.ToXContent;
26+
import org.elasticsearch.common.xcontent.XContent;
2527
import org.elasticsearch.common.xcontent.XContentBuilder;
2628
import org.elasticsearch.common.xcontent.XContentParser;
29+
import org.elasticsearch.common.xcontent.XContentType;
2730
import org.elasticsearch.common.xcontent.json.JsonXContent;
2831
import org.elasticsearch.snapshots.SnapshotId;
2932
import org.elasticsearch.snapshots.SnapshotState;
@@ -39,7 +42,11 @@
3942
import java.util.Map;
4043
import java.util.Set;
4144

45+
import static java.util.Collections.emptySet;
46+
import static java.util.Collections.singleton;
4247
import static org.elasticsearch.repositories.RepositoryData.EMPTY_REPO_GEN;
48+
import static org.hamcrest.Matchers.empty;
49+
import static org.hamcrest.Matchers.equalTo;
4350
import static org.hamcrest.Matchers.greaterThan;
4451

4552
/**
@@ -101,15 +108,18 @@ public void testAddSnapshots() {
101108
public void testInitIndices() {
102109
final int numSnapshots = randomIntBetween(1, 30);
103110
final Map<String, SnapshotId> snapshotIds = new HashMap<>(numSnapshots);
111+
final Map<String, SnapshotState> snapshotStates = new HashMap<>(numSnapshots);
104112
for (int i = 0; i < numSnapshots; i++) {
105113
final SnapshotId snapshotId = new SnapshotId(randomAlphaOfLength(8), UUIDs.randomBase64UUID());
106114
snapshotIds.put(snapshotId.getUUID(), snapshotId);
115+
snapshotStates.put(snapshotId.getUUID(), randomFrom(SnapshotState.values()));
107116
}
108117
RepositoryData repositoryData = new RepositoryData(EMPTY_REPO_GEN, snapshotIds,
109118
Collections.emptyMap(), Collections.emptyMap(), Collections.emptyList());
110119
// test that initializing indices works
111120
Map<IndexId, Set<SnapshotId>> indices = randomIndices(snapshotIds);
112-
RepositoryData newRepoData = repositoryData.initIndices(indices);
121+
RepositoryData newRepoData = new RepositoryData(repositoryData.getGenId(), snapshotIds, snapshotStates, indices,
122+
new ArrayList<>(repositoryData.getIncompatibleSnapshotIds()));
113123
List<SnapshotId> expected = new ArrayList<>(repositoryData.getSnapshotIds());
114124
Collections.sort(expected);
115125
List<SnapshotId> actual = new ArrayList<>(newRepoData.getSnapshotIds());
@@ -153,6 +163,81 @@ public void testGetSnapshotState() {
153163
assertNull(repositoryData.getSnapshotState(new SnapshotId(randomAlphaOfLength(8), UUIDs.randomBase64UUID())));
154164
}
155165

166+
public void testIndexThatReferencesAnUnknownSnapshot() throws IOException {
167+
final XContent xContent = randomFrom(XContentType.values()).xContent();
168+
final RepositoryData repositoryData = generateRandomRepoData();
169+
170+
XContentBuilder builder = XContentBuilder.builder(xContent);
171+
repositoryData.snapshotsToXContent(builder, ToXContent.EMPTY_PARAMS);
172+
RepositoryData parsedRepositoryData = RepositoryData.snapshotsFromXContent(createParser(builder), repositoryData.getGenId());
173+
assertEquals(repositoryData, parsedRepositoryData);
174+
175+
Map<String, SnapshotId> snapshotIds = new HashMap<>();
176+
Map<String, SnapshotState> snapshotStates = new HashMap<>();
177+
for (SnapshotId snapshotId : parsedRepositoryData.getSnapshotIds()) {
178+
snapshotIds.put(snapshotId.getUUID(), snapshotId);
179+
snapshotStates.put(snapshotId.getUUID(), parsedRepositoryData.getSnapshotState(snapshotId));
180+
}
181+
182+
final IndexId corruptedIndexId = randomFrom(parsedRepositoryData.getIndices().values());
183+
184+
Map<IndexId, Set<SnapshotId>> indexSnapshots = new HashMap<>();
185+
for (Map.Entry<String, IndexId> snapshottedIndex : parsedRepositoryData.getIndices().entrySet()) {
186+
IndexId indexId = snapshottedIndex.getValue();
187+
Set<SnapshotId> snapshotsIds = new LinkedHashSet<>(parsedRepositoryData.getSnapshots(indexId));
188+
if (corruptedIndexId.equals(indexId)) {
189+
snapshotsIds.add(new SnapshotId("_uuid", "_does_not_exist"));
190+
}
191+
indexSnapshots.put(indexId, snapshotsIds);
192+
}
193+
assertNotNull(corruptedIndexId);
194+
195+
RepositoryData corruptedRepositoryData = new RepositoryData(parsedRepositoryData.getGenId(), snapshotIds, snapshotStates,
196+
indexSnapshots, new ArrayList<>(parsedRepositoryData.getIncompatibleSnapshotIds()));
197+
198+
final XContentBuilder corruptedBuilder = XContentBuilder.builder(xContent);
199+
corruptedRepositoryData.snapshotsToXContent(corruptedBuilder, ToXContent.EMPTY_PARAMS);
200+
201+
ElasticsearchParseException e = expectThrows(ElasticsearchParseException.class, () ->
202+
RepositoryData.snapshotsFromXContent(createParser(corruptedBuilder), corruptedRepositoryData.getGenId()));
203+
assertThat(e.getMessage(), equalTo("Detected a corrupted repository, index " + corruptedIndexId + " references an unknown " +
204+
"snapshot uuid [_does_not_exist]"));
205+
}
206+
207+
public void testIndexThatReferenceANullSnapshot() throws IOException {
208+
final XContentBuilder builder = XContentBuilder.builder(randomFrom(XContentType.JSON).xContent());
209+
builder.startObject();
210+
{
211+
builder.startArray("snapshots");
212+
builder.value(new SnapshotId("_name", "_uuid"));
213+
builder.endArray();
214+
215+
builder.startObject("indices");
216+
{
217+
builder.startObject("docs");
218+
{
219+
builder.field("id", "_id");
220+
builder.startArray("snapshots");
221+
{
222+
builder.startObject();
223+
if (randomBoolean()) {
224+
builder.field("name", "_name");
225+
}
226+
builder.endObject();
227+
}
228+
builder.endArray();
229+
}
230+
builder.endObject();
231+
}
232+
builder.endObject();
233+
}
234+
builder.endObject();
235+
236+
ElasticsearchParseException e = expectThrows(ElasticsearchParseException.class, () ->
237+
RepositoryData.snapshotsFromXContent(createParser(builder), randomNonNegativeLong()));
238+
assertThat(e.getMessage(), equalTo("Detected a corrupted repository, index [docs/_id] references an unknown snapshot uuid [null]"));
239+
}
240+
156241
public static RepositoryData generateRandomRepoData() {
157242
final int numIndices = randomIntBetween(1, 30);
158243
final List<IndexId> indices = new ArrayList<>(numIndices);

0 commit comments

Comments
 (0)