Skip to content

Add state query param to GET snapshots API #128635

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 25 commits into from
Jun 16, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
b5b877e
Add state query param to Get snapshots API
ElenaStoeva Feb 27, 2025
442b5a6
Address code review comments from PR 123618
JeremyDahlgren May 29, 2025
04fcc55
Update docs/changelog/128635.yaml
JeremyDahlgren May 29, 2025
44f5992
Merge branch 'main' into fix/es-11525
JeremyDahlgren May 30, 2025
a05e099
Merge branch 'main' into fix/es-11525
JeremyDahlgren May 30, 2025
6a560fa
Use test_runner_features: capabilities in new yaml test
JeremyDahlgren May 31, 2025
b78ed72
Merge branch 'main' into fix/es-11525
JeremyDahlgren May 31, 2025
c2d8c72
Merge branch 'main' into fix/es-11525
JeremyDahlgren Jun 2, 2025
8bb5b22
Add cluster_features clause to new yaml test to check for new NodeFea…
JeremyDahlgren Jun 2, 2025
95b0fc5
Merge branch 'main' into fix/es-11525
JeremyDahlgren Jun 3, 2025
6883acb
Merge branch 'main' into fix/es-11525
JeremyDahlgren Jun 3, 2025
e9d6f25
Merge branch 'main' into fix/es-11525
JeremyDahlgren Jun 3, 2025
8450318
Merge branch 'main' into fix/es-11525
JeremyDahlgren Jun 3, 2025
419f234
Merge branch 'main' into fix/es-11525
JeremyDahlgren Jun 10, 2025
a3039f0
Merge branch 'main' into fix/es-11525
JeremyDahlgren Jun 11, 2025
25da78e
Merge branch 'main' into fix/es-11525
JeremyDahlgren Jun 12, 2025
64d3729
Add handling for null (missing) and present but empty state param values
JeremyDahlgren Jun 12, 2025
548fe0e
Use IllegalArgumentException when state is used in mixed cluster
JeremyDahlgren Jun 12, 2025
3ed9350
Remove unnecessary check for states.isEmpty() in predicates, enforce …
JeremyDahlgren Jun 12, 2025
627c9cc
Add exclusions for unconsumed response parameters
JeremyDahlgren Jun 12, 2025
0656d9e
Add response params to consumed before assertion check, adjust comment
JeremyDahlgren Jun 13, 2025
56f4f44
Merge branch 'main' into fix/es-11525
JeremyDahlgren Jun 13, 2025
757726b
Merge branch 'main' into fix/es-11525
JeremyDahlgren Jun 16, 2025
f8627ba
Change the NodeFeature to snapshots.get.state_parameter
JeremyDahlgren Jun 16, 2025
644bbc5
Merge branch 'main' into fix/es-11525
JeremyDahlgren Jun 16, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions docs/changelog/128635.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
pr: 128635
summary: Add `state` query param to Get snapshots API
area: Snapshot/Restore
type: enhancement
issues:
- 97446
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,10 @@
"verbose":{
"type":"boolean",
"description":"Whether to show verbose snapshot info or only show the basic info found in the repository index blob"
},
"state": {
"type": "list",
"description": "Filter snapshots by a comma-separated list of states. Valid state values are 'SUCCESS', 'IN_PROGRESS', 'FAILED', 'PARTIAL', or 'INCOMPATIBLE'."
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -303,3 +303,72 @@ setup:
snapshot.delete:
repository: test_repo_get_1
snapshot: test_snapshot_no_repo_name

---
"Get snapshot using state parameter":
- requires:
cluster_features: "snapshots.get.state_parameter"
test_runner_features: capabilities
capabilities:
- method: GET
path: /_snapshot/{repository}/{snapshot}
parameters: [ state ]
reason: "state parameter was introduced in 9.1"

- do:
indices.create:
index: test_index
body:
settings:
number_of_shards: 1
number_of_replicas: 0

- do:
snapshot.create:
repository: test_repo_get_1
snapshot: test_snapshot_with_state_param
wait_for_completion: true

- do:
snapshot.get:
repository: test_repo_get_1
snapshot: test_snapshot_with_state_param
state: SUCCESS

- is_true: snapshots
- match: { snapshots.0.snapshot: test_snapshot_with_state_param }
- match: { snapshots.0.state: SUCCESS }

- do:
snapshot.get:
repository: test_repo_get_1
snapshot: test_snapshot_with_state_param
state: SUCCESS,PARTIAL

- is_true: snapshots
- match: { snapshots.0.snapshot: test_snapshot_with_state_param }
- match: { snapshots.0.state: SUCCESS }

- do:
snapshot.get:
repository: test_repo_get_1
snapshot: test_snapshot_with_state_param
state: FAILED

- is_true: snapshots
- length: { snapshots: 0 }

- do:
catch: bad_request
snapshot.get:
repository: test_repo_get_1
snapshot: test_snapshot_with_state_param
state: FOO

- match: { error.type: "illegal_argument_exception" }
- match: { error.reason: "No enum constant org.elasticsearch.snapshots.SnapshotState.FOO" }

- do:
snapshot.delete:
repository: test_repo_get_1
snapshot: test_snapshot_with_state_param
Original file line number Diff line number Diff line change
Expand Up @@ -55,13 +55,16 @@
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.EnumSet;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.Collectors;

Expand Down Expand Up @@ -635,6 +638,63 @@ public void testRetrievingSnapshotsWhenRepositoryIsMissing() throws Exception {
expectThrows(RepositoryMissingException.class, multiRepoFuture::actionGet);
}

public void testFilterByState() throws Exception {
final String repoName = "test-repo";
final Path repoPath = randomRepoPath();
createRepository(repoName, "mock", repoPath);

// Create a successful snapshot
createFullSnapshot(repoName, "snapshot-success");

final Function<EnumSet<SnapshotState>, List<SnapshotInfo>> getSnapshotsForStates = (states) -> {
return clusterAdmin().prepareGetSnapshots(TEST_REQUEST_TIMEOUT, repoName).setStates(states).get().getSnapshots();
};

// Fetch snapshots with state=SUCCESS
var snapshots = getSnapshotsForStates.apply(EnumSet.of(SnapshotState.SUCCESS));
assertThat(snapshots, hasSize(1));
assertThat(snapshots.getFirst().state(), is(SnapshotState.SUCCESS));

// Create a snapshot in progress
blockAllDataNodes(repoName);
startFullSnapshot(repoName, "snapshot-in-progress");
awaitNumberOfSnapshotsInProgress(1);

// Fetch snapshots with state=IN_PROGRESS
snapshots = getSnapshotsForStates.apply(EnumSet.of(SnapshotState.IN_PROGRESS));
assertThat(snapshots, hasSize(1));
assertThat(snapshots.getFirst().state(), is(SnapshotState.IN_PROGRESS));

// Fetch snapshots with multiple states (SUCCESS, IN_PROGRESS)
snapshots = getSnapshotsForStates.apply(EnumSet.of(SnapshotState.SUCCESS, SnapshotState.IN_PROGRESS));
assertThat(snapshots, hasSize(2));
var states = snapshots.stream().map(SnapshotInfo::state).collect(Collectors.toSet());
assertTrue(states.contains(SnapshotState.SUCCESS));
assertTrue(states.contains(SnapshotState.IN_PROGRESS));

// Fetch all snapshots (without state)
snapshots = clusterAdmin().prepareGetSnapshots(TEST_REQUEST_TIMEOUT, repoName).get().getSnapshots();
assertThat(snapshots, hasSize(2));

// Fetch snapshots with an invalid state
IllegalArgumentException e = expectThrows(
IllegalArgumentException.class,
() -> getSnapshotsForStates.apply(EnumSet.of(SnapshotState.valueOf("FOO")))
);
assertThat(e.getMessage(), is("No enum constant org.elasticsearch.snapshots.SnapshotState.FOO"));

// Allow the IN_PROGRESS snapshot to finish, then verify GET using SUCCESS has results and IN_PROGRESS does not.
unblockAllDataNodes(repoName);
awaitNumberOfSnapshotsInProgress(0);
snapshots = clusterAdmin().prepareGetSnapshots(TEST_REQUEST_TIMEOUT, repoName).get().getSnapshots();
assertThat(snapshots, hasSize(2));
states = snapshots.stream().map(SnapshotInfo::state).collect(Collectors.toSet());
assertThat(states, hasSize(1));
assertTrue(states.contains(SnapshotState.SUCCESS));
snapshots = getSnapshotsForStates.apply(EnumSet.of(SnapshotState.IN_PROGRESS));
assertThat(snapshots, hasSize(0));
}

public void testRetrievingSnapshotsWhenRepositoryIsUnreadable() throws Exception {
final String repoName = randomIdentifier();
final Path repoPath = randomRepoPath();
Expand Down Expand Up @@ -956,6 +1016,12 @@ public void testAllFeatures() {
// INDICES and by SHARDS. The actual sorting behaviour for these cases is tested elsewhere, here we're just checking that sorting
// interacts correctly with the other parameters to the API.

final EnumSet<SnapshotState> states = EnumSet.copyOf(randomNonEmptySubsetOf(Arrays.asList(SnapshotState.values())));
// Note: The selected state(s) may not match any existing snapshots.
// The actual filtering behaviour for such cases is tested in the dedicated test.
// Here we're just checking that states interacts correctly with the other parameters to the API.
snapshotInfoPredicate = snapshotInfoPredicate.and(si -> states.contains(si.state()));

// compute the ordered sequence of snapshots which match the repository/snapshot name filters and SLM policy filter
final var selectedSnapshots = snapshotInfos.stream()
.filter(snapshotInfoPredicate)
Expand All @@ -967,7 +1033,8 @@ public void testAllFeatures() {
)
// apply sorting params
.sort(sortKey)
.order(order);
.order(order)
.states(states);

// sometimes use ?from_sort_value to skip some items; note that snapshots skipped in this way are subtracted from
// GetSnapshotsResponse.totalCount whereas snapshots skipped by ?after and ?offset are not
Expand Down Expand Up @@ -1054,7 +1121,8 @@ public void testAllFeatures() {
.sort(sortKey)
.order(order)
.size(nextSize)
.after(SnapshotSortKey.decodeAfterQueryParam(nextRequestAfter));
.after(SnapshotSortKey.decodeAfterQueryParam(nextRequestAfter))
.states(states);
final GetSnapshotsResponse nextResponse = safeAwait(l -> client().execute(TransportGetSnapshotsAction.TYPE, nextRequest, l));

assertEquals(
Expand Down
1 change: 1 addition & 0 deletions server/src/main/java/module-info.java
Original file line number Diff line number Diff line change
Expand Up @@ -425,6 +425,7 @@
org.elasticsearch.action.bulk.BulkFeatures,
org.elasticsearch.features.InfrastructureFeatures,
org.elasticsearch.rest.action.admin.cluster.ClusterRerouteFeatures,
org.elasticsearch.rest.action.admin.cluster.GetSnapshotsFeatures,
org.elasticsearch.index.mapper.MapperFeatures,
org.elasticsearch.index.IndexFeatures,
org.elasticsearch.search.SearchFeatures,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -299,6 +299,7 @@ static TransportVersion def(int id) {
public static final TransportVersion NONE_CHUNKING_STRATEGY = def(9_097_0_00);
public static final TransportVersion PROJECT_DELETION_GLOBAL_BLOCK = def(9_098_0_00);
public static final TransportVersion SECURITY_CLOUD_API_KEY_REALM_AND_TYPE = def(9_099_0_00);
public static final TransportVersion STATE_PARAM_GET_SNAPSHOT = def(9_100_0_00);

/*
* STOP! READ THIS FIRST! No, really,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -864,7 +864,7 @@ public void initRestHandlers(Supplier<DiscoveryNodes> nodesInCluster, Predicate<
registerHandler.accept(new RestDeleteRepositoryAction());
registerHandler.accept(new RestVerifyRepositoryAction());
registerHandler.accept(new RestCleanupRepositoryAction());
registerHandler.accept(new RestGetSnapshotsAction());
registerHandler.accept(new RestGetSnapshotsAction(clusterSupportsFeature));
registerHandler.accept(new RestCreateSnapshotAction());
registerHandler.accept(new RestCloneSnapshotAction());
registerHandler.accept(new RestRestoreSnapshotAction());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,16 @@
import org.elasticsearch.core.Nullable;
import org.elasticsearch.core.TimeValue;
import org.elasticsearch.search.sort.SortOrder;
import org.elasticsearch.snapshots.SnapshotState;
import org.elasticsearch.tasks.CancellableTask;
import org.elasticsearch.tasks.Task;
import org.elasticsearch.tasks.TaskId;

import java.io.IOException;
import java.util.Arrays;
import java.util.EnumSet;
import java.util.Map;
import java.util.Objects;

import static org.elasticsearch.action.ValidateActions.addValidationError;

Expand All @@ -39,6 +42,7 @@ public class GetSnapshotsRequest extends MasterNodeRequest<GetSnapshotsRequest>
public static final boolean DEFAULT_VERBOSE_MODE = true;

private static final TransportVersion INDICES_FLAG_VERSION = TransportVersions.V_8_3_0;
private static final TransportVersion STATE_FLAG_VERSION = TransportVersions.STATE_PARAM_GET_SNAPSHOT;

public static final int NO_LIMIT = -1;

Expand Down Expand Up @@ -77,6 +81,8 @@ public class GetSnapshotsRequest extends MasterNodeRequest<GetSnapshotsRequest>

private boolean includeIndexNames = true;

private EnumSet<SnapshotState> states = EnumSet.allOf(SnapshotState.class);

public GetSnapshotsRequest(TimeValue masterNodeTimeout) {
super(masterNodeTimeout);
}
Expand Down Expand Up @@ -118,6 +124,11 @@ public GetSnapshotsRequest(StreamInput in) throws IOException {
if (in.getTransportVersion().onOrAfter(INDICES_FLAG_VERSION)) {
includeIndexNames = in.readBoolean();
}
if (in.getTransportVersion().onOrAfter(STATE_FLAG_VERSION)) {
states = in.readEnumSet(SnapshotState.class);
} else {
states = EnumSet.allOf(SnapshotState.class);
}
}

@Override
Expand All @@ -137,6 +148,13 @@ public void writeTo(StreamOutput out) throws IOException {
if (out.getTransportVersion().onOrAfter(INDICES_FLAG_VERSION)) {
out.writeBoolean(includeIndexNames);
}
if (out.getTransportVersion().onOrAfter(STATE_FLAG_VERSION)) {
out.writeEnumSet(states);
} else if (states.equals(EnumSet.allOf(SnapshotState.class)) == false) {
final var errorString = "GetSnapshotsRequest [states] field is not supported on all nodes in the cluster";
assert false : errorString;
throw new IllegalStateException(errorString);
}
}

@Override
Expand Down Expand Up @@ -177,6 +195,9 @@ public ActionRequestValidationException validate() {
} else if (after != null && fromSortValue != null) {
validationException = addValidationError("can't use after and from_sort_value simultaneously", validationException);
}
if (states.isEmpty()) {
validationException = addValidationError("states is empty", validationException);
}
return validationException;
}

Expand Down Expand Up @@ -342,6 +363,15 @@ public boolean verbose() {
return verbose;
}

public EnumSet<SnapshotState> states() {
return states;
}

public GetSnapshotsRequest states(EnumSet<SnapshotState> states) {
this.states = Objects.requireNonNull(states);
return this;
}

@Override
public Task createTask(long id, String type, String action, TaskId parentTaskId, Map<String, String> headers) {
return new CancellableTask(id, type, action, getDescription(), parentTaskId, headers);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@
import org.elasticsearch.core.Nullable;
import org.elasticsearch.core.TimeValue;
import org.elasticsearch.search.sort.SortOrder;
import org.elasticsearch.snapshots.SnapshotState;

import java.util.EnumSet;

/**
* Get snapshots request builder
Expand Down Expand Up @@ -150,4 +153,8 @@ public GetSnapshotsRequestBuilder setIncludeIndexNames(boolean indices) {

}

public GetSnapshotsRequestBuilder setStates(EnumSet<SnapshotState> states) {
request.states(states);
return this;
}
}
Loading
Loading