Skip to content

Commit b6bdc2e

Browse files
committed
Fix NullPointerException when creating or deleting a snapshot
A NullPointerException is thrown when trying to create or delete a snapshot in a repository that has been used by a pre-5.5 cluster and a post-5.5 cluster. The way snapshots are formatted in the repository snapshots index file changed in elastic#24477 and it can cause a NPE if a very specific set of operations is written in the repository.
1 parent 707ba28 commit b6bdc2e

File tree

6 files changed

+79
-25
lines changed

6 files changed

+79
-25
lines changed

docs/CHANGELOG.asciidoc

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@
2424

2525
=== Bug Fixes
2626

27+
Fix NullPointerException when creating or deleting a snapshot ({pull}TODO[#TODO])
28+
2729
=== Regressions
2830

2931
=== Known Issues

docs/reference/modules/snapshots.asciidoc

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,9 +44,9 @@ 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
47+
IMPORTANT: The snapshot format can change across major versions, so if you have
4848
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
49+
snapshots written by one version may not be visible to the other. While
5050
setting the repository to `readonly` on all but one of the clusters should work
5151
with multiple clusters differing by one major version, it is not a supported
5252
configuration.

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

Lines changed: 3 additions & 3 deletions
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

Lines changed: 23 additions & 17 deletions
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,23 @@ 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+
if (uuid != null) {
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("Unknown snapshot uuid [" + uuid
435+
+ "] for index " + indexId);
436+
}
437+
}
432438
}
433439
}
434440
}
435441
assert indexId != null;
436-
indexSnapshots.put(new IndexId(indexName, indexId), snapshotIds);
442+
indexSnapshots.put(indexId, snapshotIds);
437443
}
438444
} else {
439445
throw new ElasticsearchParseException("unknown field name [" + field + "]");

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

Lines changed: 1 addition & 2 deletions
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

Lines changed: 48 additions & 1 deletion
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;
@@ -40,6 +43,7 @@
4043
import java.util.Set;
4144

4245
import static org.elasticsearch.repositories.RepositoryData.EMPTY_REPO_GEN;
46+
import static org.hamcrest.Matchers.equalTo;
4347
import static org.hamcrest.Matchers.greaterThan;
4448

4549
/**
@@ -101,15 +105,18 @@ public void testAddSnapshots() {
101105
public void testInitIndices() {
102106
final int numSnapshots = randomIntBetween(1, 30);
103107
final Map<String, SnapshotId> snapshotIds = new HashMap<>(numSnapshots);
108+
final Map<String, SnapshotState> snapshotStates = new HashMap<>(numSnapshots);
104109
for (int i = 0; i < numSnapshots; i++) {
105110
final SnapshotId snapshotId = new SnapshotId(randomAlphaOfLength(8), UUIDs.randomBase64UUID());
106111
snapshotIds.put(snapshotId.getUUID(), snapshotId);
112+
snapshotStates.put(snapshotId.getUUID(), randomFrom(SnapshotState.values()));
107113
}
108114
RepositoryData repositoryData = new RepositoryData(EMPTY_REPO_GEN, snapshotIds,
109115
Collections.emptyMap(), Collections.emptyMap(), Collections.emptyList());
110116
// test that initializing indices works
111117
Map<IndexId, Set<SnapshotId>> indices = randomIndices(snapshotIds);
112-
RepositoryData newRepoData = repositoryData.initIndices(indices);
118+
RepositoryData newRepoData = new RepositoryData(repositoryData.getGenId(), snapshotIds, snapshotStates, indices,
119+
new ArrayList<>(repositoryData.getIncompatibleSnapshotIds()));
113120
List<SnapshotId> expected = new ArrayList<>(repositoryData.getSnapshotIds());
114121
Collections.sort(expected);
115122
List<SnapshotId> actual = new ArrayList<>(newRepoData.getSnapshotIds());
@@ -153,6 +160,46 @@ public void testGetSnapshotState() {
153160
assertNull(repositoryData.getSnapshotState(new SnapshotId(randomAlphaOfLength(8), UUIDs.randomBase64UUID())));
154161
}
155162

163+
public void testUnknownSnapshot() throws IOException {
164+
final XContent xContent = randomFrom(XContentType.values()).xContent();
165+
final RepositoryData repositoryData = generateRandomRepoData();
166+
167+
XContentBuilder builder = XContentBuilder.builder(xContent);
168+
repositoryData.snapshotsToXContent(builder, ToXContent.EMPTY_PARAMS);
169+
RepositoryData parsedRepositoryData = RepositoryData.snapshotsFromXContent(createParser(builder), repositoryData.getGenId());
170+
assertEquals(repositoryData, parsedRepositoryData);
171+
172+
Map<String, SnapshotId> snapshotIds = new HashMap<>();
173+
Map<String, SnapshotState> snapshotStates = new HashMap<>();
174+
for (SnapshotId snapshotId : parsedRepositoryData.getSnapshotIds()) {
175+
snapshotIds.put(snapshotId.getUUID(), snapshotId);
176+
snapshotStates.put(snapshotId.getUUID(), parsedRepositoryData.getSnapshotState(snapshotId));
177+
}
178+
179+
final IndexId corruptedIndexId = randomFrom(parsedRepositoryData.getIndices().values());
180+
181+
Map<IndexId, Set<SnapshotId>> indexSnapshots = new HashMap<>();
182+
for (Map.Entry<String, IndexId> snapshottedIndex : parsedRepositoryData.getIndices().entrySet()) {
183+
IndexId indexId = snapshottedIndex.getValue();
184+
Set<SnapshotId> snapshotsIds = new LinkedHashSet<>(parsedRepositoryData.getSnapshots(indexId));
185+
if (corruptedIndexId.equals(indexId)) {
186+
snapshotsIds.add(new SnapshotId("_uuid", "_does_not_exist"));
187+
}
188+
indexSnapshots.put(indexId, snapshotsIds);
189+
}
190+
assertNotNull(corruptedIndexId);
191+
192+
RepositoryData corruptedRepositoryData = new RepositoryData(parsedRepositoryData.getGenId(), snapshotIds, snapshotStates,
193+
indexSnapshots, new ArrayList<>(parsedRepositoryData.getIncompatibleSnapshotIds()));
194+
195+
final XContentBuilder corruptedBuilder = XContentBuilder.builder(xContent);
196+
corruptedRepositoryData.snapshotsToXContent(corruptedBuilder, ToXContent.EMPTY_PARAMS);
197+
198+
ElasticsearchParseException e = expectThrows(ElasticsearchParseException.class, () ->
199+
RepositoryData.snapshotsFromXContent(createParser(corruptedBuilder), corruptedRepositoryData.getGenId()));
200+
assertThat(e.getMessage(), equalTo("Unknown snapshot uuid [_does_not_exist] for index " + corruptedIndexId));
201+
}
202+
156203
public static RepositoryData generateRandomRepoData() {
157204
final int numIndices = randomIntBetween(1, 30);
158205
final List<IndexId> indices = new ArrayList<>(numIndices);

0 commit comments

Comments
 (0)