Skip to content

Commit 4f24739

Browse files
Fix Index Deletion During Partial Snapshot Create (#50234) (#50266)
We can simply filter out shard generation updates for indices that were removed from the cluster state concurrently to fix index deletes during partial snapshots as that completely removes any reference to those shards from the snapshot. Follow up to #50202 Closes #50200
1 parent 2e7b1ab commit 4f24739

File tree

3 files changed

+67
-11
lines changed

3 files changed

+67
-11
lines changed

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

+7
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,13 @@ private ShardGenerations(Map<IndexId, List<String>> shardGenerations) {
5454
this.shardGenerations = shardGenerations;
5555
}
5656

57+
/**
58+
* Returns the total number of shards tracked by this instance.
59+
*/
60+
public int totalShards() {
61+
return shardGenerations.values().stream().mapToInt(List::size).sum();
62+
}
63+
5764
/**
5865
* Returns all indices for which shard generations are tracked.
5966
*

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

+18-6
Original file line numberDiff line numberDiff line change
@@ -582,16 +582,17 @@ public void onNoLongerMaster() {
582582
private void cleanupAfterError(Exception exception) {
583583
threadPool.generic().execute(() -> {
584584
if (snapshotCreated) {
585+
final MetaData metaData = clusterService.state().metaData();
585586
repositoriesService.repository(snapshot.snapshot().getRepository())
586587
.finalizeSnapshot(snapshot.snapshot().getSnapshotId(),
587-
buildGenerations(snapshot),
588+
buildGenerations(snapshot, metaData),
588589
snapshot.startTime(),
589590
ExceptionsHelper.stackTrace(exception),
590591
0,
591592
Collections.emptyList(),
592593
snapshot.repositoryStateId(),
593594
snapshot.includeGlobalState(),
594-
metaDataForSnapshot(snapshot, clusterService.state().metaData()),
595+
metaDataForSnapshot(snapshot, metaData),
595596
snapshot.userMetadata(),
596597
snapshot.useShardGenerations(),
597598
ActionListener.runAfter(ActionListener.wrap(ignored -> {
@@ -607,11 +608,21 @@ private void cleanupAfterError(Exception exception) {
607608
}
608609
}
609610

610-
private static ShardGenerations buildGenerations(SnapshotsInProgress.Entry snapshot) {
611+
private static ShardGenerations buildGenerations(SnapshotsInProgress.Entry snapshot, MetaData metaData) {
611612
ShardGenerations.Builder builder = ShardGenerations.builder();
612613
final Map<String, IndexId> indexLookup = new HashMap<>();
613614
snapshot.indices().forEach(idx -> indexLookup.put(idx.getName(), idx));
614-
snapshot.shards().forEach(c -> builder.put(indexLookup.get(c.key.getIndexName()), c.key.id(), c.value.generation()));
615+
snapshot.shards().forEach(c -> {
616+
if (metaData.index(c.key.getIndex()) == null) {
617+
assert snapshot.partial() :
618+
"Index [" + c.key.getIndex() + "] was deleted during a snapshot but snapshot was not partial.";
619+
return;
620+
}
621+
final IndexId indexId = indexLookup.get(c.key.getIndexName());
622+
if (indexId != null) {
623+
builder.put(indexId, c.key.id(), c.value.generation());
624+
}
625+
});
615626
return builder.build();
616627
}
617628

@@ -1046,12 +1057,13 @@ protected void doRun() {
10461057
shardFailures.add(new SnapshotShardFailure(status.nodeId(), shardId, status.reason()));
10471058
}
10481059
}
1060+
final ShardGenerations shardGenerations = buildGenerations(entry, metaData);
10491061
repository.finalizeSnapshot(
10501062
snapshot.getSnapshotId(),
1051-
buildGenerations(entry),
1063+
shardGenerations,
10521064
entry.startTime(),
10531065
failure,
1054-
entry.shards().size(),
1066+
entry.partial() ? shardGenerations.totalShards() : entry.shards().size(),
10551067
unmodifiableList(shardFailures),
10561068
entry.repositoryStateId(),
10571069
entry.includeGlobalState(),

server/src/test/java/org/elasticsearch/snapshots/SnapshotResiliencyTests.java

+42-5
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121

2222
import org.apache.logging.log4j.LogManager;
2323
import org.apache.logging.log4j.Logger;
24+
import org.apache.lucene.util.SetOnce;
2425
import org.elasticsearch.ExceptionsHelper;
2526
import org.elasticsearch.Version;
2627
import org.elasticsearch.action.ActionListener;
@@ -143,6 +144,7 @@
143144
import org.elasticsearch.env.TestEnvironment;
144145
import org.elasticsearch.gateway.MetaStateService;
145146
import org.elasticsearch.gateway.TransportNodesListGatewayStartedShards;
147+
import org.elasticsearch.index.Index;
146148
import org.elasticsearch.index.analysis.AnalysisRegistry;
147149
import org.elasticsearch.index.seqno.GlobalCheckpointSyncAction;
148150
import org.elasticsearch.index.seqno.RetentionLeaseSyncer;
@@ -213,6 +215,7 @@
213215
import static org.hamcrest.Matchers.empty;
214216
import static org.hamcrest.Matchers.hasSize;
215217
import static org.hamcrest.Matchers.instanceOf;
218+
import static org.hamcrest.Matchers.is;
216219
import static org.hamcrest.Matchers.lessThanOrEqualTo;
217220
import static org.mockito.Mockito.mock;
218221

@@ -505,7 +508,7 @@ public void testConcurrentSnapshotCreateAndDeleteOther() {
505508
}
506509
}
507510

508-
public void testConcurrentSnapshotDeleteAndDeleteIndex() {
511+
public void testConcurrentSnapshotDeleteAndDeleteIndex() throws IOException {
509512
setupTestCluster(randomFrom(1, 3, 5), randomIntBetween(2, 10));
510513

511514
String repoName = "repo";
@@ -516,11 +519,13 @@ public void testConcurrentSnapshotDeleteAndDeleteIndex() {
516519
testClusterNodes.currentMaster(testClusterNodes.nodes.values().iterator().next().clusterService.state());
517520

518521
final StepListener<Collection<CreateIndexResponse>> createIndicesListener = new StepListener<>();
522+
final int indices = randomIntBetween(5, 20);
519523

524+
final SetOnce<Index> firstIndex = new SetOnce<>();
520525
continueOrDie(createRepoAndIndex(repoName, index, 1), createIndexResponse -> {
526+
firstIndex.set(masterNode.clusterService.state().metaData().index(index).getIndex());
521527
// create a few more indices to make it more likely that the subsequent index delete operation happens before snapshot
522528
// finalization
523-
final int indices = randomIntBetween(5, 20);
524529
final GroupedActionListener<CreateIndexResponse> listener = new GroupedActionListener<>(createIndicesListener, indices);
525530
for (int i = 0; i < indices; ++i) {
526531
client().admin().indices().create(new CreateIndexRequest("index-" + i), listener);
@@ -529,23 +534,55 @@ public void testConcurrentSnapshotDeleteAndDeleteIndex() {
529534

530535
final StepListener<CreateSnapshotResponse> createSnapshotResponseStepListener = new StepListener<>();
531536

537+
final boolean partialSnapshot = randomBoolean();
538+
532539
continueOrDie(createIndicesListener, createIndexResponses ->
533540
client().admin().cluster().prepareCreateSnapshot(repoName, snapshotName).setWaitForCompletion(false)
534-
.execute(createSnapshotResponseStepListener));
541+
.setPartial(partialSnapshot).execute(createSnapshotResponseStepListener));
535542

536543
continueOrDie(createSnapshotResponseStepListener,
537-
createSnapshotResponse -> client().admin().indices().delete(new DeleteIndexRequest(index), noopListener()));
544+
createSnapshotResponse -> client().admin().indices().delete(new DeleteIndexRequest(index),
545+
new ActionListener<AcknowledgedResponse>() {
546+
@Override
547+
public void onResponse(AcknowledgedResponse acknowledgedResponse) {
548+
if (partialSnapshot) {
549+
// Recreate index by the same name to test that we don't snapshot conflicting metadata in this scenario
550+
client().admin().indices().create(new CreateIndexRequest(index), noopListener());
551+
}
552+
}
553+
554+
@Override
555+
public void onFailure(Exception e) {
556+
if (partialSnapshot) {
557+
throw new AssertionError("Delete index should always work during partial snapshots", e);
558+
}
559+
}
560+
}));
538561

539562
deterministicTaskQueue.runAllRunnableTasks();
540563

541564
SnapshotsInProgress finalSnapshotsInProgress = masterNode.clusterService.state().custom(SnapshotsInProgress.TYPE);
542565
assertFalse(finalSnapshotsInProgress.entries().stream().anyMatch(entry -> entry.state().completed() == false));
543566
final Repository repository = masterNode.repositoriesService.repository(repoName);
544-
Collection<SnapshotId> snapshotIds = getRepositoryData(repository).getSnapshotIds();
567+
final RepositoryData repositoryData = getRepositoryData(repository);
568+
Collection<SnapshotId> snapshotIds = repositoryData.getSnapshotIds();
545569
assertThat(snapshotIds, hasSize(1));
546570

547571
final SnapshotInfo snapshotInfo = repository.getSnapshotInfo(snapshotIds.iterator().next());
548572
assertEquals(SnapshotState.SUCCESS, snapshotInfo.state());
573+
if (partialSnapshot) {
574+
// Single shard for each index so we either get all indices or all except for the deleted index
575+
assertThat(snapshotInfo.successfulShards(), either(is(indices + 1)).or(is(indices)));
576+
if (snapshotInfo.successfulShards() == indices + 1) {
577+
final IndexMetaData indexMetaData =
578+
repository.getSnapshotIndexMetaData(snapshotInfo.snapshotId(), repositoryData.resolveIndexId(index));
579+
// Make sure we snapshotted the metadata of this index and not the recreated version
580+
assertEquals(indexMetaData.getIndex(), firstIndex.get());
581+
}
582+
} else {
583+
// Index delete must be blocked for non-partial snapshots and we get a snapshot for every index
584+
assertEquals(snapshotInfo.successfulShards(), indices + 1);
585+
}
549586
assertEquals(0, snapshotInfo.failedShards());
550587
}
551588

0 commit comments

Comments
 (0)