Skip to content

Commit d43198e

Browse files
Add 'state' query param to GET snapshots API (#128635)
This change introduces a new optional 'state' query parameter for the Get Snapshots API, allowing users to filter snapshots by state. The parameter accepts comma-separated values for states: SUCCESS, IN_PROGRESS, FAILED, PARTIAL, INCOMPATIBLE (case-insensitive). A new 'snapshots.get.state_parameter' NodeFeature has been added with this change. The new state query parameter will only be supported in clusters where all nodes support this feature. --------- Co-authored-by: Elena Stoeva <elenastoeva99@gmail.com>
1 parent 217275c commit d43198e

File tree

14 files changed

+293
-8
lines changed

14 files changed

+293
-8
lines changed

docs/changelog/128635.yaml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
pr: 128635
2+
summary: Add `state` query param to Get snapshots API
3+
area: Snapshot/Restore
4+
type: enhancement
5+
issues:
6+
- 97446

rest-api-spec/src/main/resources/rest-api-spec/api/snapshot.get.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,10 @@
8585
"verbose":{
8686
"type":"boolean",
8787
"description":"Whether to show verbose snapshot info or only show the basic info found in the repository index blob"
88+
},
89+
"state": {
90+
"type": "list",
91+
"description": "Filter snapshots by a comma-separated list of states. Valid state values are 'SUCCESS', 'IN_PROGRESS', 'FAILED', 'PARTIAL', or 'INCOMPATIBLE'."
8892
}
8993
}
9094
}

rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/snapshot.get/10_basic.yml

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -303,3 +303,72 @@ setup:
303303
snapshot.delete:
304304
repository: test_repo_get_1
305305
snapshot: test_snapshot_no_repo_name
306+
307+
---
308+
"Get snapshot using state parameter":
309+
- requires:
310+
cluster_features: "snapshots.get.state_parameter"
311+
test_runner_features: capabilities
312+
capabilities:
313+
- method: GET
314+
path: /_snapshot/{repository}/{snapshot}
315+
parameters: [ state ]
316+
reason: "state parameter was introduced in 9.1"
317+
318+
- do:
319+
indices.create:
320+
index: test_index
321+
body:
322+
settings:
323+
number_of_shards: 1
324+
number_of_replicas: 0
325+
326+
- do:
327+
snapshot.create:
328+
repository: test_repo_get_1
329+
snapshot: test_snapshot_with_state_param
330+
wait_for_completion: true
331+
332+
- do:
333+
snapshot.get:
334+
repository: test_repo_get_1
335+
snapshot: test_snapshot_with_state_param
336+
state: SUCCESS
337+
338+
- is_true: snapshots
339+
- match: { snapshots.0.snapshot: test_snapshot_with_state_param }
340+
- match: { snapshots.0.state: SUCCESS }
341+
342+
- do:
343+
snapshot.get:
344+
repository: test_repo_get_1
345+
snapshot: test_snapshot_with_state_param
346+
state: SUCCESS,PARTIAL
347+
348+
- is_true: snapshots
349+
- match: { snapshots.0.snapshot: test_snapshot_with_state_param }
350+
- match: { snapshots.0.state: SUCCESS }
351+
352+
- do:
353+
snapshot.get:
354+
repository: test_repo_get_1
355+
snapshot: test_snapshot_with_state_param
356+
state: FAILED
357+
358+
- is_true: snapshots
359+
- length: { snapshots: 0 }
360+
361+
- do:
362+
catch: bad_request
363+
snapshot.get:
364+
repository: test_repo_get_1
365+
snapshot: test_snapshot_with_state_param
366+
state: FOO
367+
368+
- match: { error.type: "illegal_argument_exception" }
369+
- match: { error.reason: "No enum constant org.elasticsearch.snapshots.SnapshotState.FOO" }
370+
371+
- do:
372+
snapshot.delete:
373+
repository: test_repo_get_1
374+
snapshot: test_snapshot_with_state_param

server/src/internalClusterTest/java/org/elasticsearch/snapshots/GetSnapshotsIT.java

Lines changed: 70 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,13 +55,16 @@
5555
import java.nio.file.Files;
5656
import java.nio.file.Path;
5757
import java.util.ArrayList;
58+
import java.util.Arrays;
5859
import java.util.Collection;
5960
import java.util.Collections;
61+
import java.util.EnumSet;
6062
import java.util.HashSet;
6163
import java.util.List;
6264
import java.util.Map;
6365
import java.util.Objects;
6466
import java.util.Set;
67+
import java.util.function.Function;
6568
import java.util.function.Predicate;
6669
import java.util.stream.Collectors;
6770

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

641+
public void testFilterByState() throws Exception {
642+
final String repoName = "test-repo";
643+
final Path repoPath = randomRepoPath();
644+
createRepository(repoName, "mock", repoPath);
645+
646+
// Create a successful snapshot
647+
createFullSnapshot(repoName, "snapshot-success");
648+
649+
final Function<EnumSet<SnapshotState>, List<SnapshotInfo>> getSnapshotsForStates = (states) -> {
650+
return clusterAdmin().prepareGetSnapshots(TEST_REQUEST_TIMEOUT, repoName).setStates(states).get().getSnapshots();
651+
};
652+
653+
// Fetch snapshots with state=SUCCESS
654+
var snapshots = getSnapshotsForStates.apply(EnumSet.of(SnapshotState.SUCCESS));
655+
assertThat(snapshots, hasSize(1));
656+
assertThat(snapshots.getFirst().state(), is(SnapshotState.SUCCESS));
657+
658+
// Create a snapshot in progress
659+
blockAllDataNodes(repoName);
660+
startFullSnapshot(repoName, "snapshot-in-progress");
661+
awaitNumberOfSnapshotsInProgress(1);
662+
663+
// Fetch snapshots with state=IN_PROGRESS
664+
snapshots = getSnapshotsForStates.apply(EnumSet.of(SnapshotState.IN_PROGRESS));
665+
assertThat(snapshots, hasSize(1));
666+
assertThat(snapshots.getFirst().state(), is(SnapshotState.IN_PROGRESS));
667+
668+
// Fetch snapshots with multiple states (SUCCESS, IN_PROGRESS)
669+
snapshots = getSnapshotsForStates.apply(EnumSet.of(SnapshotState.SUCCESS, SnapshotState.IN_PROGRESS));
670+
assertThat(snapshots, hasSize(2));
671+
var states = snapshots.stream().map(SnapshotInfo::state).collect(Collectors.toSet());
672+
assertTrue(states.contains(SnapshotState.SUCCESS));
673+
assertTrue(states.contains(SnapshotState.IN_PROGRESS));
674+
675+
// Fetch all snapshots (without state)
676+
snapshots = clusterAdmin().prepareGetSnapshots(TEST_REQUEST_TIMEOUT, repoName).get().getSnapshots();
677+
assertThat(snapshots, hasSize(2));
678+
679+
// Fetch snapshots with an invalid state
680+
IllegalArgumentException e = expectThrows(
681+
IllegalArgumentException.class,
682+
() -> getSnapshotsForStates.apply(EnumSet.of(SnapshotState.valueOf("FOO")))
683+
);
684+
assertThat(e.getMessage(), is("No enum constant org.elasticsearch.snapshots.SnapshotState.FOO"));
685+
686+
// Allow the IN_PROGRESS snapshot to finish, then verify GET using SUCCESS has results and IN_PROGRESS does not.
687+
unblockAllDataNodes(repoName);
688+
awaitNumberOfSnapshotsInProgress(0);
689+
snapshots = clusterAdmin().prepareGetSnapshots(TEST_REQUEST_TIMEOUT, repoName).get().getSnapshots();
690+
assertThat(snapshots, hasSize(2));
691+
states = snapshots.stream().map(SnapshotInfo::state).collect(Collectors.toSet());
692+
assertThat(states, hasSize(1));
693+
assertTrue(states.contains(SnapshotState.SUCCESS));
694+
snapshots = getSnapshotsForStates.apply(EnumSet.of(SnapshotState.IN_PROGRESS));
695+
assertThat(snapshots, hasSize(0));
696+
}
697+
638698
public void testRetrievingSnapshotsWhenRepositoryIsUnreadable() throws Exception {
639699
final String repoName = randomIdentifier();
640700
final Path repoPath = randomRepoPath();
@@ -956,6 +1016,12 @@ public void testAllFeatures() {
9561016
// INDICES and by SHARDS. The actual sorting behaviour for these cases is tested elsewhere, here we're just checking that sorting
9571017
// interacts correctly with the other parameters to the API.
9581018

1019+
final EnumSet<SnapshotState> states = EnumSet.copyOf(randomNonEmptySubsetOf(Arrays.asList(SnapshotState.values())));
1020+
// Note: The selected state(s) may not match any existing snapshots.
1021+
// The actual filtering behaviour for such cases is tested in the dedicated test.
1022+
// Here we're just checking that states interacts correctly with the other parameters to the API.
1023+
snapshotInfoPredicate = snapshotInfoPredicate.and(si -> states.contains(si.state()));
1024+
9591025
// compute the ordered sequence of snapshots which match the repository/snapshot name filters and SLM policy filter
9601026
final var selectedSnapshots = snapshotInfos.stream()
9611027
.filter(snapshotInfoPredicate)
@@ -967,7 +1033,8 @@ public void testAllFeatures() {
9671033
)
9681034
// apply sorting params
9691035
.sort(sortKey)
970-
.order(order);
1036+
.order(order)
1037+
.states(states);
9711038

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

10601128
assertEquals(

server/src/main/java/module-info.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -425,6 +425,7 @@
425425
org.elasticsearch.action.bulk.BulkFeatures,
426426
org.elasticsearch.features.InfrastructureFeatures,
427427
org.elasticsearch.rest.action.admin.cluster.ClusterRerouteFeatures,
428+
org.elasticsearch.rest.action.admin.cluster.GetSnapshotsFeatures,
428429
org.elasticsearch.index.mapper.MapperFeatures,
429430
org.elasticsearch.index.IndexFeatures,
430431
org.elasticsearch.search.SearchFeatures,

server/src/main/java/org/elasticsearch/TransportVersions.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -299,6 +299,7 @@ static TransportVersion def(int id) {
299299
public static final TransportVersion NONE_CHUNKING_STRATEGY = def(9_097_0_00);
300300
public static final TransportVersion PROJECT_DELETION_GLOBAL_BLOCK = def(9_098_0_00);
301301
public static final TransportVersion SECURITY_CLOUD_API_KEY_REALM_AND_TYPE = def(9_099_0_00);
302+
public static final TransportVersion STATE_PARAM_GET_SNAPSHOT = def(9_100_0_00);
302303

303304
/*
304305
* STOP! READ THIS FIRST! No, really,

server/src/main/java/org/elasticsearch/action/ActionModule.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -864,7 +864,7 @@ public void initRestHandlers(Supplier<DiscoveryNodes> nodesInCluster, Predicate<
864864
registerHandler.accept(new RestDeleteRepositoryAction());
865865
registerHandler.accept(new RestVerifyRepositoryAction());
866866
registerHandler.accept(new RestCleanupRepositoryAction());
867-
registerHandler.accept(new RestGetSnapshotsAction());
867+
registerHandler.accept(new RestGetSnapshotsAction(clusterSupportsFeature));
868868
registerHandler.accept(new RestCreateSnapshotAction());
869869
registerHandler.accept(new RestCloneSnapshotAction());
870870
registerHandler.accept(new RestRestoreSnapshotAction());

server/src/main/java/org/elasticsearch/action/admin/cluster/snapshots/get/GetSnapshotsRequest.java

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,16 @@
1919
import org.elasticsearch.core.Nullable;
2020
import org.elasticsearch.core.TimeValue;
2121
import org.elasticsearch.search.sort.SortOrder;
22+
import org.elasticsearch.snapshots.SnapshotState;
2223
import org.elasticsearch.tasks.CancellableTask;
2324
import org.elasticsearch.tasks.Task;
2425
import org.elasticsearch.tasks.TaskId;
2526

2627
import java.io.IOException;
2728
import java.util.Arrays;
29+
import java.util.EnumSet;
2830
import java.util.Map;
31+
import java.util.Objects;
2932

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

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

4144
private static final TransportVersion INDICES_FLAG_VERSION = TransportVersions.V_8_3_0;
45+
private static final TransportVersion STATE_FLAG_VERSION = TransportVersions.STATE_PARAM_GET_SNAPSHOT;
4246

4347
public static final int NO_LIMIT = -1;
4448

@@ -77,6 +81,8 @@ public class GetSnapshotsRequest extends MasterNodeRequest<GetSnapshotsRequest>
7781

7882
private boolean includeIndexNames = true;
7983

84+
private EnumSet<SnapshotState> states = EnumSet.allOf(SnapshotState.class);
85+
8086
public GetSnapshotsRequest(TimeValue masterNodeTimeout) {
8187
super(masterNodeTimeout);
8288
}
@@ -118,6 +124,11 @@ public GetSnapshotsRequest(StreamInput in) throws IOException {
118124
if (in.getTransportVersion().onOrAfter(INDICES_FLAG_VERSION)) {
119125
includeIndexNames = in.readBoolean();
120126
}
127+
if (in.getTransportVersion().onOrAfter(STATE_FLAG_VERSION)) {
128+
states = in.readEnumSet(SnapshotState.class);
129+
} else {
130+
states = EnumSet.allOf(SnapshotState.class);
131+
}
121132
}
122133

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

142160
@Override
@@ -177,6 +195,9 @@ public ActionRequestValidationException validate() {
177195
} else if (after != null && fromSortValue != null) {
178196
validationException = addValidationError("can't use after and from_sort_value simultaneously", validationException);
179197
}
198+
if (states.isEmpty()) {
199+
validationException = addValidationError("states is empty", validationException);
200+
}
180201
return validationException;
181202
}
182203

@@ -342,6 +363,15 @@ public boolean verbose() {
342363
return verbose;
343364
}
344365

366+
public EnumSet<SnapshotState> states() {
367+
return states;
368+
}
369+
370+
public GetSnapshotsRequest states(EnumSet<SnapshotState> states) {
371+
this.states = Objects.requireNonNull(states);
372+
return this;
373+
}
374+
345375
@Override
346376
public Task createTask(long id, String type, String action, TaskId parentTaskId, Map<String, String> headers) {
347377
return new CancellableTask(id, type, action, getDescription(), parentTaskId, headers);

server/src/main/java/org/elasticsearch/action/admin/cluster/snapshots/get/GetSnapshotsRequestBuilder.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@
1515
import org.elasticsearch.core.Nullable;
1616
import org.elasticsearch.core.TimeValue;
1717
import org.elasticsearch.search.sort.SortOrder;
18+
import org.elasticsearch.snapshots.SnapshotState;
19+
20+
import java.util.EnumSet;
1821

1922
/**
2023
* Get snapshots request builder
@@ -150,4 +153,8 @@ public GetSnapshotsRequestBuilder setIncludeIndexNames(boolean indices) {
150153

151154
}
152155

156+
public GetSnapshotsRequestBuilder setStates(EnumSet<SnapshotState> states) {
157+
request.states(states);
158+
return this;
159+
}
153160
}

0 commit comments

Comments
 (0)