Skip to content

Update shardGenerations for all indices on snapshot finalization #128650

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 20 commits into from
Jun 2, 2025

Conversation

ywangd
Copy link
Member

@ywangd ywangd commented May 30, 2025

If an index is deleted after a snapshot has written its shardGenerations file but before the snapshot is finalized, we exclude this index from the snapshot because its indexMetadata is no longer available. However, the shardGenerations file is still valid in that it is the latest copy with all necessary information despite it containing an extra snapshot entry. This is OK. Instead of dropping this shardGenerations file, this PR changes to carry it forward by updating RepositoryData and relevant in-progress snapshots so that the next finalization builds on top of this one.

Resolves: #108907

Co-authored-by: DaveCTurner [email protected]

If an index is deleted after a snapshot has written its shardGenerations
file but before the snapshot is finalized, we exclude this index from
the snapshot because its indexMetadata is no longer available. However,
the shardGenerations file is still valid in that it is the latest copy
with all necessary information despite it containing an extra snapshot
entry. This is OK. Instead of dropping this shardGenerations file, this
PR changes to carry it forward so that the next snapshot to finalize,
either earlier or later in the list of entries, builts the next
shardGenerations based on this one.

Resolves: elastic#108907
@ywangd ywangd requested a review from DaveCTurner May 30, 2025 05:47
@ywangd ywangd added >enhancement :Distributed Coordination/Snapshot/Restore Anything directly related to the `_snapshot/*` APIs v9.1.0 labels May 30, 2025
@elasticsearchmachine elasticsearchmachine added the Team:Distributed Coordination Meta label for Distributed Coordination team label May 30, 2025
@elasticsearchmachine
Copy link
Collaborator

Pinging @elastic/es-distributed-coordination (Team:Distributed Coordination)

@elasticsearchmachine
Copy link
Collaborator

Hi @ywangd, I've created a changelog YAML for you.

Comment on lines 1579 to 1593
.andThen(l -> {
client.admin()
.cluster()
.prepareGetSnapshots(TEST_REQUEST_TIMEOUT, repoName)
.setSnapshots(IntStream.range(0, snapshotCount).mapToObj(i -> "snapshot-" + i).toArray(String[]::new))
.execute(ActionTestUtils.assertNoFailureListener(getSnapshotsResponse -> {
for (final var snapshot : getSnapshotsResponse.getSnapshots()) {
assertThat(snapshot.state(), is(SnapshotState.SUCCESS));
final String snapshotName = snapshot.snapshot().getSnapshotId().getName();
// Does not contain the deleted index in the snapshot
assertThat(snapshot.indices(), contains("index-" + snapshotName.charAt(snapshotName.length() - 1)));
}
l.onResponse(null);
}));
});
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@DaveCTurner The test is copied from your comment. I only added this part of checking the GetSnapshots response.

I am still looking to add a randomization so that the earlier entry is a clone.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Clone is added in efdeaed

@ywangd
Copy link
Member Author

ywangd commented May 30, 2025

@DaveCTurner
ConcurrentSnapshotsIT#testDeleteIndexWithOutOfOrderFinalization is failing on this PR.

The change makes a finalizing snapshot to update shard generation for earlier entry even when the index is deleted. As a result, it removes the old shard generation. But since the index is deleted, the new generation is not recorded in RepositoryData, i.e. it still refers to the old shard generation which is now deleted. This causes subsequent snapshot failure while trying to load the latest shard generation based on RepositoryData. To fix this, I tried to include the new shard generation into RepositoryData#shardGenerations. Unfortuately, this leads to more failures due to this assertion for consistent indices and shard generations in RepositoryData.

I don't think we want to remove the assertion. So the solution seems to be bringing in the concept of "shard generations for deleted indices" (e.g. something like UpdatedShardGenerations in this PR) into RepositoryData. It's doable but it is quite a lot changes including serialization. So I'd like to check with you before proceeding further. Thanks!

@DaveCTurner
Copy link
Contributor

Ugh yeah this is tricky, but IMO it doesn't make sense to track shard generations for deleted indices in RepositoryData. If an index appears in no snapshots then it has no business being mentioned in RepositoryData. AIUI this can only be a transient situation related to snapshots happening concurrently with index deletes, so we should track it somewhere more ephemeral like SnapshotsInProgress.

@ywangd
Copy link
Member Author

ywangd commented May 30, 2025

OK I think there might be a simple solution, see b478b95

The old generation got deleted not because of RepositoryData but due to SnapshotsInProgress#obsoleteGenerations. As you suggested, RepositoryData has no business with a deleted index that is not snapshotted. Since this PR updates old generation to new one for earlier entires, it must also preserve the old generation in the process since that is what recorded in RepositoryData. The new generation may end up not being used at all. That's ok, it leaves a redundant shard generation file in the repository which will be removed when snapshot deletion runs.

* In this case, its shard generation is tracked in {@link #deletedIndices}. Otherwise, it is tracked in
* {@link #liveIndices}.
*/
public record UpdatedShardGenerations(ShardGenerations liveIndices, ShardGenerations deletedIndices) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes I think this will work (but I will add some other comments elsewhere)

.entrySet()
.stream()
// We want to keep both old and new generations for deleted indices, so we filter them out here to avoid deletion.
// We need the old generations because they are what get recorded in the RepositoryData.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm this seems suspicious. We should be updating the shard generations for existing shards in RepositoryData and discarding the previous values, even if the index isn't included in the snapshot. AIUI the tripping assertion you mentioned in an earlier comment related to generations for shards that were totally absent from RepositoryData. We should drop those shards entirely from RepositoryData, but update any ones that do exist.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah yes you are absolutely right. We should conditionally update the shard generations in RepositoryData for the deleted indices. The issue with my previous attempt is that the update is "unconditional" which triggered the assertion. It's now updated as suggested. Thanks a lot!

@ywangd ywangd changed the title Always update shardGenerations for previous in-progress snapshots Update shardGenerations for all indices on snapshot finalization May 31, 2025
@ywangd ywangd requested a review from DaveCTurner May 31, 2025 01:35
Copy link
Contributor

@DaveCTurner DaveCTurner left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep I like it, just a few tiny suggestions

@@ -1749,7 +1749,7 @@ int sizeInBytes() {
public void finalizeSnapshot(final FinalizeSnapshotContext finalizeSnapshotContext) {
assert ThreadPool.assertCurrentThreadPool(ThreadPool.Names.SNAPSHOT);
final long repositoryStateId = finalizeSnapshotContext.repositoryStateId();
final ShardGenerations shardGenerations = finalizeSnapshotContext.updatedShardGenerations();
final ShardGenerations shardGenerations = finalizeSnapshotContext.updatedShardGenerations().liveIndices();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: maybe inline this, it's only used in one place, and we now need to care about which ShardGenerations we're talking about so the variable name is ambiguous.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point. See 1071d95

return;
}
builder.put(key.index(), key.shardId(), value);
});
}
return builder.build();
return new UpdatedShardGenerations(builder.build(), deletedBuilder.build());
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

deletedBuilder.build() is mostly going to be ShardGenerations.EMPTY, can we special-case the empty collection in ShardGenerations.Builder to skip a bunch of unnecessary allocations?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense. Special cased in both Builder#build and initialization of the variable here 81f88ba

public record UpdatedShardGenerations(ShardGenerations liveIndices, ShardGenerations deletedIndices) {
public static final UpdatedShardGenerations EMPTY = new UpdatedShardGenerations(ShardGenerations.EMPTY, ShardGenerations.EMPTY);

public UpdatedShardGenerations(ShardGenerations updated) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This constructor is only used in tests; IMO it'd be better to just be explicit and pass in ShardGenerations.EMPTY as the second parameter rather than take the risk that some future caller uses this constructor when they should be accounting for deleted indices too.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep see 1852486

@@ -424,6 +425,7 @@ public RepositoryData addSnapshot(
// the new master, so we make the operation idempotent
return this;
}
final var shardGenerations = updatedShardGenerations.liveIndices();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Likewise this name is ambiguous; here we actually only care about the live index IDs now, maybe it'd avoid some confusion to extract that variable instead:

Suggested change
final var shardGenerations = updatedShardGenerations.liveIndices();
final var liveIndexIds = updatedShardGenerations.liveIndices().indices();

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep fair call 6d148e5

}
}
});
return this;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IntelliJ indicates the return value is unused

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right. It is a leftover from the initial version where this method is public. It's indeed no longer needed. See a257d39

@@ -244,6 +254,20 @@ public Builder put(IndexId indexId, int shardId, ShardGeneration generation) {
return this;
}

private Builder updateIfPresent(ShardGenerations shardGenerations) {
shardGenerations.shardGenerations.forEach((indexId, gens) -> {
if (generations.containsKey(indexId)) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we do this with a generations.computeIfPresent() rather than a .containsKey() check followed by several .get() calls? Or even just .get() up front and then a null check?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point. I replaced it with a .get() and null check. I personally like it slightly better for readability than computeIfPresent. See a257d39

@ywangd ywangd requested a review from DaveCTurner June 2, 2025 00:13
@ywangd ywangd added the auto-backport Automatically create backport pull requests when merged label Jun 2, 2025
Copy link
Contributor

@DaveCTurner DaveCTurner left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM great stuff 🚀

@ywangd
Copy link
Member Author

ywangd commented Jun 2, 2025

@elasticmachine update branch

@elasticmachine
Copy link
Collaborator

There are no new commits on the base branch.

@ywangd ywangd merged commit aa0397f into elastic:main Jun 2, 2025
18 checks passed
@elasticsearchmachine
Copy link
Collaborator

💔 Backport failed

Status Branch Result
8.19
9.0 Commit could not be cherrypicked due to conflicts

You can use sqren/backport to manually backport by running backport --upstream elastic/elasticsearch --pr 128650

@ywangd
Copy link
Member Author

ywangd commented Jun 2, 2025

💚 All backports created successfully

Status Branch Result
9.0

Questions ?

Please refer to the Backport tool documentation

ywangd added a commit to ywangd/elasticsearch that referenced this pull request Jun 2, 2025
…stic#128650)

If an index is deleted after a snapshot has written its shardGenerations
file but before the snapshot is finalized, we exclude this index from the
snapshot because its indexMetadata is no longer available. However,
the shardGenerations file is still valid in that it is the latest copy with all
necessary information despite it containing an extra snapshot entry.
This is OK. Instead of dropping this shardGenerations file, this PR
changes to carry it forward by updating RepositoryData and relevant
in-progress snapshots so that the next finalization builds on top of this one.

Co-authored-by: David Turner <[email protected]>
(cherry picked from commit aa0397f)

# Conflicts:
#	server/src/main/java/org/elasticsearch/snapshots/SnapshotsService.java
elasticsearchmachine pushed a commit that referenced this pull request Jun 2, 2025
…8650) (#128724)

If an index is deleted after a snapshot has written its shardGenerations 
file but before the snapshot is finalized, we exclude this index from the 
snapshot because its indexMetadata is no longer available. However, 
the shardGenerations file is still valid in that it is the latest copy with all 
necessary information despite it containing an extra snapshot entry. 
This is OK. Instead of dropping this shardGenerations file, this PR 
changes to carry it forward by updating RepositoryData and relevant 
in-progress snapshots so that the next finalization builds on top of this one.

Co-authored-by: David Turner <[email protected]>
elasticsearchmachine pushed a commit that referenced this pull request Jun 2, 2025
…8650) (#128725)

If an index is deleted after a snapshot has written its shardGenerations
file but before the snapshot is finalized, we exclude this index from the
snapshot because its indexMetadata is no longer available. However,
the shardGenerations file is still valid in that it is the latest copy with all
necessary information despite it containing an extra snapshot entry.
This is OK. Instead of dropping this shardGenerations file, this PR
changes to carry it forward by updating RepositoryData and relevant
in-progress snapshots so that the next finalization builds on top of this one.

Co-authored-by: David Turner <[email protected]>
(cherry picked from commit aa0397f)

# Conflicts:
#	server/src/main/java/org/elasticsearch/snapshots/SnapshotsService.java
mridula-s109 pushed a commit that referenced this pull request Jun 2, 2025
…8650)

If an index is deleted after a snapshot has written its shardGenerations 
file but before the snapshot is finalized, we exclude this index from the 
snapshot because its indexMetadata is no longer available. However, 
the shardGenerations file is still valid in that it is the latest copy with all 
necessary information despite it containing an extra snapshot entry. 
This is OK. Instead of dropping this shardGenerations file, this PR 
changes to carry it forward by updating RepositoryData and relevant 
in-progress snapshots so that the next finalization builds on top of this one.

Co-authored-by: David Turner <[email protected]>
mridula-s109 pushed a commit to mridula-s109/elasticsearch that referenced this pull request Jun 3, 2025
…stic#128650)

If an index is deleted after a snapshot has written its shardGenerations 
file but before the snapshot is finalized, we exclude this index from the 
snapshot because its indexMetadata is no longer available. However, 
the shardGenerations file is still valid in that it is the latest copy with all 
necessary information despite it containing an extra snapshot entry. 
This is OK. Instead of dropping this shardGenerations file, this PR 
changes to carry it forward by updating RepositoryData and relevant 
in-progress snapshots so that the next finalization builds on top of this one.

Co-authored-by: David Turner <[email protected]>
joshua-adams-1 pushed a commit to joshua-adams-1/elasticsearch that referenced this pull request Jun 3, 2025
…stic#128650)

If an index is deleted after a snapshot has written its shardGenerations 
file but before the snapshot is finalized, we exclude this index from the 
snapshot because its indexMetadata is no longer available. However, 
the shardGenerations file is still valid in that it is the latest copy with all 
necessary information despite it containing an extra snapshot entry. 
This is OK. Instead of dropping this shardGenerations file, this PR 
changes to carry it forward by updating RepositoryData and relevant 
in-progress snapshots so that the next finalization builds on top of this one.

Co-authored-by: David Turner <[email protected]>
Samiul-TheSoccerFan pushed a commit to Samiul-TheSoccerFan/elasticsearch that referenced this pull request Jun 5, 2025
…stic#128650)

If an index is deleted after a snapshot has written its shardGenerations 
file but before the snapshot is finalized, we exclude this index from the 
snapshot because its indexMetadata is no longer available. However, 
the shardGenerations file is still valid in that it is the latest copy with all 
necessary information despite it containing an extra snapshot entry. 
This is OK. Instead of dropping this shardGenerations file, this PR 
changes to carry it forward by updating RepositoryData and relevant 
in-progress snapshots so that the next finalization builds on top of this one.

Co-authored-by: David Turner <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
auto-backport Automatically create backport pull requests when merged backport pending :Distributed Coordination/Snapshot/Restore Anything directly related to the `_snapshot/*` APIs >enhancement Team:Distributed Coordination Meta label for Distributed Coordination team v8.19.0 v9.0.3 v9.1.0
Projects
None yet
Development

Successfully merging this pull request may close these issues.

[CI] SnapshotStressTestsIT testRandomActivities failing
4 participants