Skip to content

Commit b5b877e

Browse files
ElenaStoevaJeremyDahlgren
authored andcommitted
Add state query param to Get snapshots API
1 parent e241efa commit b5b877e

File tree

8 files changed

+135
-6
lines changed

8 files changed

+135
-6
lines changed

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
}

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

Lines changed: 61 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@
5555
import java.util.ArrayList;
5656
import java.util.Collection;
5757
import java.util.Collections;
58+
import java.util.EnumSet;
5859
import java.util.HashSet;
5960
import java.util.List;
6061
import java.util.Map;
@@ -69,6 +70,7 @@
6970
import static org.hamcrest.Matchers.hasSize;
7071
import static org.hamcrest.Matchers.in;
7172
import static org.hamcrest.Matchers.is;
73+
import static org.hamcrest.core.StringContains.containsString;
7274

7375
public class GetSnapshotsIT extends AbstractSnapshotIntegTestCase {
7476

@@ -633,6 +635,55 @@ public void testRetrievingSnapshotsWhenRepositoryIsMissing() throws Exception {
633635
expectThrows(RepositoryMissingException.class, multiRepoFuture::actionGet);
634636
}
635637

638+
public void testFilterByState() throws Exception {
639+
final String repoName = "test-repo";
640+
final Path repoPath = randomRepoPath();
641+
createRepository(repoName, "mock", repoPath);
642+
643+
// Create a successful snapshot
644+
String successSnapshot = "snapshot-success";
645+
createFullSnapshot(repoName, successSnapshot);
646+
647+
// Fetch snapshots with state=SUCCESS
648+
GetSnapshotsResponse responseSuccess = clusterAdmin().prepareGetSnapshots(TEST_REQUEST_TIMEOUT, repoName)
649+
.setState(EnumSet.of(SnapshotState.SUCCESS))
650+
.get();
651+
assertThat(responseSuccess.getSnapshots(), hasSize(1));
652+
assertThat(responseSuccess.getSnapshots().get(0).state(), is(SnapshotState.SUCCESS));
653+
654+
// Create a snapshot in progress
655+
String inProgressSnapshot = "snapshot-in-progress";
656+
blockAllDataNodes(repoName);
657+
startFullSnapshot(repoName, inProgressSnapshot);
658+
awaitNumberOfSnapshotsInProgress(1);
659+
660+
// Fetch snapshots with state=IN_PROGRESS
661+
GetSnapshotsResponse responseInProgress = clusterAdmin().prepareGetSnapshots(TEST_REQUEST_TIMEOUT, repoName)
662+
.setState(EnumSet.of(SnapshotState.IN_PROGRESS))
663+
.get();
664+
assertThat(responseInProgress.getSnapshots(), hasSize(1));
665+
assertThat(responseInProgress.getSnapshots().get(0).state(), is(SnapshotState.IN_PROGRESS));
666+
667+
// Fetch snapshots with multiple states (SUCCESS, IN_PROGRESS)
668+
GetSnapshotsResponse responseMultipleStates = clusterAdmin().prepareGetSnapshots(TEST_REQUEST_TIMEOUT, repoName)
669+
.setState(EnumSet.of(SnapshotState.SUCCESS, SnapshotState.IN_PROGRESS))
670+
.get();
671+
assertThat(responseMultipleStates.getSnapshots(), hasSize(2));
672+
assertTrue(responseMultipleStates.getSnapshots().stream().map(SnapshotInfo::state).toList().contains(SnapshotState.SUCCESS));
673+
assertTrue(responseMultipleStates.getSnapshots().stream().map(SnapshotInfo::state).toList().contains(SnapshotState.IN_PROGRESS));
674+
675+
// Fetch all snapshots (without state)
676+
GetSnapshotsResponse responseAll = clusterAdmin().prepareGetSnapshots(TEST_REQUEST_TIMEOUT, repoName).get();
677+
assertThat(responseAll.getSnapshots(), hasSize(2));
678+
679+
// Fetch snapshots with an invalid state
680+
IllegalArgumentException e = expectThrows(
681+
IllegalArgumentException.class,
682+
() -> clusterAdmin().prepareGetSnapshots(TEST_REQUEST_TIMEOUT, repoName).setState(EnumSet.of(SnapshotState.of("FOO"))).get()
683+
);
684+
assertThat(e.getMessage(), containsString("Unknown state name [FOO]"));
685+
}
686+
636687
// Create a snapshot that is guaranteed to have a unique start time and duration for tests around ordering by either.
637688
// Don't use this with more than 3 snapshots on platforms with low-resolution clocks as the durations could always collide there
638689
// causing an infinite loop
@@ -912,9 +963,15 @@ public void testAllFeatures() {
912963
// INDICES and by SHARDS. The actual sorting behaviour for these cases is tested elsewhere, here we're just checking that sorting
913964
// interacts correctly with the other parameters to the API.
914965

966+
final EnumSet<SnapshotState> state = EnumSet.of(randomFrom(SnapshotState.values()));
967+
// Note: The selected state may not match any existing snapshots.
968+
// The actual filtering behaviour for such cases is tested in the dedicated test.
969+
// Here we're just checking that state interacts correctly with the other parameters to the API.
970+
915971
// compute the ordered sequence of snapshots which match the repository/snapshot name filters and SLM policy filter
916972
final var selectedSnapshots = snapshotInfos.stream()
917973
.filter(snapshotInfoPredicate)
974+
.filter(s -> state.contains(s.state()))
918975
.sorted(sortKey.getSnapshotInfoComparator(order))
919976
.toList();
920977

@@ -923,7 +980,8 @@ public void testAllFeatures() {
923980
)
924981
// apply sorting params
925982
.sort(sortKey)
926-
.order(order);
983+
.order(order)
984+
.state(state);
927985

928986
// sometimes use ?from_sort_value to skip some items; note that snapshots skipped in this way are subtracted from
929987
// GetSnapshotsResponse.totalCount whereas snapshots skipped by ?after and ?offset are not
@@ -1010,7 +1068,8 @@ public void testAllFeatures() {
10101068
.sort(sortKey)
10111069
.order(order)
10121070
.size(nextSize)
1013-
.after(SnapshotSortKey.decodeAfterQueryParam(nextRequestAfter));
1071+
.after(SnapshotSortKey.decodeAfterQueryParam(nextRequestAfter))
1072+
.state(state);
10141073
final GetSnapshotsResponse nextResponse = safeAwait(l -> client().execute(TransportGetSnapshotsAction.TYPE, nextRequest, l));
10151074

10161075
assertEquals(

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -269,7 +269,7 @@ static TransportVersion def(int id) {
269269
public static final TransportVersion SETTINGS_IN_DATA_STREAMS_DRY_RUN = def(9_081_0_00);
270270
public static final TransportVersion ML_INFERENCE_SAGEMAKER_CHAT_COMPLETION = def(9_082_0_00);
271271
public static final TransportVersion ML_INFERENCE_VERTEXAI_CHATCOMPLETION_ADDED = def(9_083_0_00);
272-
272+
public static final TransportVersion STATE_PARAM_GET_SNAPSHOT = def(9_084_0_00);
273273
/*
274274
* STOP! READ THIS FIRST! No, really,
275275
* ____ _____ ___ ____ _ ____ _____ _ ____ _____ _ _ ___ ____ _____ ___ ____ ____ _____ _

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

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,14 @@
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;
2931

3032
import static org.elasticsearch.action.ValidateActions.addValidationError;
@@ -39,6 +41,7 @@ public class GetSnapshotsRequest extends MasterNodeRequest<GetSnapshotsRequest>
3941
public static final boolean DEFAULT_VERBOSE_MODE = true;
4042

4143
private static final TransportVersion INDICES_FLAG_VERSION = TransportVersions.V_8_3_0;
44+
private static final TransportVersion STATE_FLAG_VERSION = TransportVersions.STATE_PARAM_GET_SNAPSHOT;
4245

4346
public static final int NO_LIMIT = -1;
4447

@@ -77,6 +80,8 @@ public class GetSnapshotsRequest extends MasterNodeRequest<GetSnapshotsRequest>
7780

7881
private boolean includeIndexNames = true;
7982

83+
private EnumSet<SnapshotState> state = EnumSet.noneOf(SnapshotState.class);
84+
8085
public GetSnapshotsRequest(TimeValue masterNodeTimeout) {
8186
super(masterNodeTimeout);
8287
}
@@ -118,6 +123,9 @@ public GetSnapshotsRequest(StreamInput in) throws IOException {
118123
if (in.getTransportVersion().onOrAfter(INDICES_FLAG_VERSION)) {
119124
includeIndexNames = in.readBoolean();
120125
}
126+
if (in.getTransportVersion().onOrAfter(STATE_FLAG_VERSION)) {
127+
state = in.readEnumSet(SnapshotState.class);
128+
}
121129
}
122130

123131
@Override
@@ -137,6 +145,9 @@ public void writeTo(StreamOutput out) throws IOException {
137145
if (out.getTransportVersion().onOrAfter(INDICES_FLAG_VERSION)) {
138146
out.writeBoolean(includeIndexNames);
139147
}
148+
if (out.getTransportVersion().onOrAfter(STATE_FLAG_VERSION)) {
149+
out.writeEnumSet(state);
150+
}
140151
}
141152

142153
@Override
@@ -342,6 +353,15 @@ public boolean verbose() {
342353
return verbose;
343354
}
344355

356+
public EnumSet<SnapshotState> state() {
357+
return state;
358+
}
359+
360+
public GetSnapshotsRequest state(EnumSet<SnapshotState> state) {
361+
this.state = state;
362+
return this;
363+
}
364+
345365
@Override
346366
public Task createTask(long id, String type, String action, TaskId parentTaskId, Map<String, String> headers) {
347367
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 setState(EnumSet<SnapshotState> state) {
157+
request.state(state);
158+
return this;
159+
}
153160
}

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

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@
4545
import org.elasticsearch.snapshots.SnapshotId;
4646
import org.elasticsearch.snapshots.SnapshotInfo;
4747
import org.elasticsearch.snapshots.SnapshotMissingException;
48+
import org.elasticsearch.snapshots.SnapshotState;
4849
import org.elasticsearch.snapshots.SnapshotsService;
4950
import org.elasticsearch.tasks.CancellableTask;
5051
import org.elasticsearch.tasks.Task;
@@ -54,6 +55,7 @@
5455

5556
import java.util.ArrayList;
5657
import java.util.Collections;
58+
import java.util.EnumSet;
5759
import java.util.HashMap;
5860
import java.util.HashSet;
5961
import java.util.Iterator;
@@ -160,7 +162,8 @@ protected void masterOperation(
160162
request.size(),
161163
SnapshotsInProgress.get(state),
162164
request.verbose(),
163-
request.includeIndexNames()
165+
request.includeIndexNames(),
166+
request.state()
164167
).runOperation(listener);
165168
}
166169

@@ -181,6 +184,7 @@ private class GetSnapshotsOperation {
181184
private final SnapshotNamePredicate snapshotNamePredicate;
182185
private final SnapshotPredicates fromSortValuePredicates;
183186
private final Predicate<String> slmPolicyPredicate;
187+
private final EnumSet<SnapshotState> state;
184188

185189
// snapshot ordering/pagination
186190
private final SnapshotSortKey sortBy;
@@ -224,7 +228,8 @@ private class GetSnapshotsOperation {
224228
int size,
225229
SnapshotsInProgress snapshotsInProgress,
226230
boolean verbose,
227-
boolean indices
231+
boolean indices,
232+
EnumSet<SnapshotState> state
228233
) {
229234
this.cancellableTask = cancellableTask;
230235
this.repositories = repositories;
@@ -237,6 +242,7 @@ private class GetSnapshotsOperation {
237242
this.snapshotsInProgress = snapshotsInProgress;
238243
this.verbose = verbose;
239244
this.indices = indices;
245+
this.state = state;
240246

241247
this.snapshotNamePredicate = SnapshotNamePredicate.forSnapshots(ignoreUnavailable, snapshots);
242248
this.fromSortValuePredicates = SnapshotPredicates.forFromSortValue(fromSortValue, sortBy, order);
@@ -558,11 +564,16 @@ private boolean matchesPredicates(SnapshotId snapshotId, RepositoryData reposito
558564
return false;
559565
}
560566

567+
final var details = repositoryData.getSnapshotDetails(snapshotId);
568+
569+
if (!state.isEmpty() && !state.contains(details.getSnapshotState())) {
570+
return false;
571+
}
572+
561573
if (slmPolicyPredicate == SlmPolicyPredicate.MATCH_ALL_POLICIES) {
562574
return true;
563575
}
564576

565-
final var details = repositoryData.getSnapshotDetails(snapshotId);
566577
return details == null || details.getSlmPolicy() == null || slmPolicyPredicate.test(details.getSlmPolicy());
567578
}
568579

server/src/main/java/org/elasticsearch/rest/action/admin/cluster/RestGetSnapshotsAction.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,11 @@
2020
import org.elasticsearch.rest.action.RestCancellableNodeClient;
2121
import org.elasticsearch.rest.action.RestRefCountedChunkedToXContentListener;
2222
import org.elasticsearch.search.sort.SortOrder;
23+
import org.elasticsearch.snapshots.SnapshotState;
2324

2425
import java.io.IOException;
26+
import java.util.Arrays;
27+
import java.util.EnumSet;
2528
import java.util.List;
2629
import java.util.Set;
2730

@@ -82,6 +85,14 @@ public RestChannelConsumer prepareRequest(final RestRequest request, final NodeC
8285
final SortOrder order = SortOrder.fromString(request.param("order", getSnapshotsRequest.order().toString()));
8386
getSnapshotsRequest.order(order);
8487
getSnapshotsRequest.includeIndexNames(request.paramAsBoolean(INDEX_NAMES_XCONTENT_PARAM, getSnapshotsRequest.includeIndexNames()));
88+
89+
final String stateString = request.param("state");
90+
if (stateString == null || stateString.isEmpty()) {
91+
getSnapshotsRequest.state(EnumSet.noneOf(SnapshotState.class));
92+
} else {
93+
getSnapshotsRequest.state(EnumSet.copyOf(Arrays.stream(stateString.split(",")).map(SnapshotState::of).toList()));
94+
}
95+
8596
return channel -> new RestCancellableNodeClient(client, request.getHttpChannel()).admin()
8697
.cluster()
8798
.getSnapshots(getSnapshotsRequest, new RestRefCountedChunkedToXContentListener<>(channel));

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

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,4 +89,21 @@ public static SnapshotState fromValue(byte value) {
8989
default -> throw new IllegalArgumentException("No snapshot state for value [" + value + "]");
9090
};
9191
}
92+
93+
/**
94+
* Generate snapshot state from a string (case-insensitive)
95+
*
96+
* @param name the state name
97+
* @return state
98+
*/
99+
public static SnapshotState of(String name) {
100+
return switch (name.toUpperCase()) {
101+
case "IN_PROGRESS" -> IN_PROGRESS;
102+
case "SUCCESS" -> SUCCESS;
103+
case "FAILED" -> FAILED;
104+
case "PARTIAL" -> PARTIAL;
105+
case "INCOMPATIBLE" -> INCOMPATIBLE;
106+
default -> throw new IllegalArgumentException("Unknown state name [" + name + "]");
107+
};
108+
}
92109
}

0 commit comments

Comments
 (0)