Skip to content

Commit fc7c06d

Browse files
Make feature reset API response more informative (#71240)
Previously, the ResetFeatureStateStatus object captured its status in a String, which meant that if we wanted to know if something succeeded or failed, we'd have to parse information out of the string. This isn't a good way of doing things. I've introduced a SUCCESS/FAILURE enum for status constants, and added a check for failures in the transport action. We return a 207 if some but not all reset actions fail, and for every failure, we also return information about the exception or error that caused it. Co-authored-by: Jay Modi <[email protected]>
1 parent 377fe5d commit fc7c06d

File tree

10 files changed

+354
-47
lines changed

10 files changed

+354
-47
lines changed

client/rest-high-level/src/main/java/org/elasticsearch/client/feature/ResetFeaturesResponse.java

Lines changed: 56 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,21 +8,30 @@
88

99
package org.elasticsearch.client.feature;
1010

11+
import org.elasticsearch.ElasticsearchException;
12+
import org.elasticsearch.common.Nullable;
1113
import org.elasticsearch.common.ParseField;
1214
import org.elasticsearch.common.xcontent.ConstructingObjectParser;
1315
import org.elasticsearch.common.xcontent.ObjectParser;
1416
import org.elasticsearch.common.xcontent.XContentParser;
1517

1618
import java.util.List;
19+
import java.util.Objects;
1720

21+
/**
22+
* This class represents the response of the Feature State Reset API. It is a
23+
* list containing the response of every feature whose state can be reset. The
24+
* response from each feature will indicate success or failure. In the case of a
25+
* failure, the cause will be returned as well.
26+
*/
1827
public class ResetFeaturesResponse {
1928
private final List<ResetFeatureStateStatus> features;
2029

2130
private static final ParseField FEATURES = new ParseField("features");
2231

2332
@SuppressWarnings("unchecked")
2433
private static final ConstructingObjectParser<ResetFeaturesResponse, Void> PARSER = new ConstructingObjectParser<>(
25-
"snapshottable_features_response", true,
34+
"features_reset_status_response", true,
2635
(a, ctx) -> new ResetFeaturesResponse((List<ResetFeatureStateStatus>) a[0])
2736
);
2837

@@ -32,51 +41,93 @@ public class ResetFeaturesResponse {
3241
ResetFeaturesResponse.ResetFeatureStateStatus::parse, FEATURES);
3342
}
3443

44+
/**
45+
* Create a new ResetFeaturesResponse
46+
* @param features A full list of status responses from individual feature reset operations.
47+
*/
3548
public ResetFeaturesResponse(List<ResetFeatureStateStatus> features) {
3649
this.features = features;
3750
}
3851

39-
public List<ResetFeatureStateStatus> getFeatures() {
52+
/**
53+
* @return List containing a reset status for each feature that we have tried to reset.
54+
*/
55+
public List<ResetFeatureStateStatus> getFeatureResetStatuses() {
4056
return features;
4157
}
4258

4359
public static ResetFeaturesResponse parse(XContentParser parser) {
4460
return PARSER.apply(parser, null);
4561
}
4662

63+
/**
64+
* A class representing the status of an attempt to reset a feature's state.
65+
* The attempt to reset either succeeds and we return the name of the
66+
* feature and a success flag; or it fails and we return the name of the feature,
67+
* a status flag, and the exception thrown during the attempt to reset the feature.
68+
*/
4769
public static class ResetFeatureStateStatus {
4870
private final String featureName;
4971
private final String status;
72+
private final Exception exception;
5073

5174
private static final ParseField FEATURE_NAME = new ParseField("feature_name");
5275
private static final ParseField STATUS = new ParseField("status");
76+
private static final ParseField EXCEPTION = new ParseField("exception");
5377

54-
private static final ConstructingObjectParser<ResetFeatureStateStatus, Void> PARSER = new ConstructingObjectParser<>(
55-
"features", true, (a, ctx) -> new ResetFeatureStateStatus((String) a[0], (String) a[1])
78+
private static final ConstructingObjectParser<ResetFeatureStateStatus, Void> PARSER = new ConstructingObjectParser<>(
79+
"feature_state_reset_stats", true,
80+
(a, ctx) -> new ResetFeatureStateStatus((String) a[0], (String) a[1], (ElasticsearchException) a[2])
5681
);
5782

5883
static {
5984
PARSER.declareField(ConstructingObjectParser.constructorArg(),
6085
(p, c) -> p.text(), FEATURE_NAME, ObjectParser.ValueType.STRING);
6186
PARSER.declareField(ConstructingObjectParser.constructorArg(),
6287
(p, c) -> p.text(), STATUS, ObjectParser.ValueType.STRING);
88+
PARSER.declareObject(ConstructingObjectParser.optionalConstructorArg(),
89+
(p, c) -> ElasticsearchException.fromXContent(p), EXCEPTION);
6390
}
6491

65-
ResetFeatureStateStatus(String featureName, String status) {
92+
/**
93+
* Create a ResetFeatureStateStatus.
94+
* @param featureName Name of the feature whose status has been reset.
95+
* @param status Whether the reset attempt succeeded or failed.
96+
* @param exception If the reset attempt failed, the exception that caused the
97+
* failure. Must be null when status is "SUCCESS".
98+
*/
99+
ResetFeatureStateStatus(String featureName, String status, @Nullable Exception exception) {
66100
this.featureName = featureName;
101+
assert "SUCCESS".equals(status) || "FAILURE".equals(status);
67102
this.status = status;
103+
assert "FAILURE".equals(status) ? Objects.nonNull(exception) : Objects.isNull(exception);
104+
this.exception = exception;
68105
}
69106

70107
public static ResetFeatureStateStatus parse(XContentParser parser, Void ctx) {
71108
return PARSER.apply(parser, ctx);
72109
}
73110

111+
/**
112+
* @return Name of the feature that we tried to reset
113+
*/
74114
public String getFeatureName() {
75115
return featureName;
76116
}
77117

118+
/**
119+
* @return "SUCCESS" if the reset attempt succeeded, "FAILURE" otherwise.
120+
*/
78121
public String getStatus() {
79122
return status;
80123
}
124+
125+
/**
126+
* @return The exception that caused the reset attempt to fail.
127+
*/
128+
@Nullable
129+
public Exception getException() {
130+
return exception;
131+
}
81132
}
82133
}

client/rest-high-level/src/test/java/org/elasticsearch/client/FeaturesIT.java

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,15 @@
1212
import org.elasticsearch.client.feature.GetFeaturesResponse;
1313
import org.elasticsearch.client.feature.ResetFeaturesRequest;
1414
import org.elasticsearch.client.feature.ResetFeaturesResponse;
15+
import org.elasticsearch.common.settings.Settings;
16+
import org.elasticsearch.search.SearchModule;
1517

1618
import java.io.IOException;
19+
import java.util.Collections;
20+
import java.util.Set;
21+
import java.util.stream.Collectors;
1722

23+
import static org.hamcrest.Matchers.contains;
1824
import static org.hamcrest.Matchers.greaterThan;
1925
import static org.hamcrest.Matchers.notNullValue;
2026

@@ -34,13 +40,25 @@ public void testGetFeatures() throws IOException {
3440
public void testResetFeatures() throws IOException {
3541
ResetFeaturesRequest request = new ResetFeaturesRequest();
3642

43+
// need superuser privileges to execute the reset
44+
RestHighLevelClient adminHighLevelClient = new RestHighLevelClient(
45+
adminClient(),
46+
(client) -> {},
47+
new SearchModule(Settings.EMPTY, Collections.emptyList()).getNamedXContents());
3748
ResetFeaturesResponse response = execute(request,
38-
highLevelClient().features()::resetFeatures, highLevelClient().features()::resetFeaturesAsync);
49+
adminHighLevelClient.features()::resetFeatures,
50+
adminHighLevelClient.features()::resetFeaturesAsync);
3951

4052
assertThat(response, notNullValue());
41-
assertThat(response.getFeatures(), notNullValue());
42-
assertThat(response.getFeatures().size(), greaterThan(1));
43-
assertTrue(response.getFeatures().stream().anyMatch(
53+
assertThat(response.getFeatureResetStatuses(), notNullValue());
54+
assertThat(response.getFeatureResetStatuses().size(), greaterThan(1));
55+
assertTrue(response.getFeatureResetStatuses().stream().anyMatch(
4456
feature -> "tasks".equals(feature.getFeatureName()) && "SUCCESS".equals(feature.getStatus())));
57+
58+
Set<String> statuses = response.getFeatureResetStatuses().stream()
59+
.map(ResetFeaturesResponse.ResetFeatureStateStatus::getStatus)
60+
.collect(Collectors.toSet());
61+
62+
assertThat(statuses, contains("SUCCESS"));
4563
}
4664
}
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License
4+
* 2.0 and the Server Side Public License, v 1; you may not use this file except
5+
* in compliance with, at your election, the Elastic License 2.0 or the Server
6+
* Side Public License, v 1.
7+
*/
8+
9+
package org.elasticsearch.client.snapshots;
10+
11+
import org.elasticsearch.ElasticsearchException;
12+
import org.elasticsearch.action.admin.cluster.snapshots.features.ResetFeatureStateResponse;
13+
import org.elasticsearch.client.AbstractResponseTestCase;
14+
import org.elasticsearch.client.feature.ResetFeaturesResponse;
15+
import org.elasticsearch.common.xcontent.XContentParser;
16+
import org.elasticsearch.common.xcontent.XContentType;
17+
18+
import java.io.IOException;
19+
import java.util.Map;
20+
import java.util.stream.Collectors;
21+
22+
import static org.hamcrest.Matchers.everyItem;
23+
import static org.hamcrest.Matchers.hasSize;
24+
import static org.hamcrest.Matchers.in;
25+
import static org.hamcrest.Matchers.is;
26+
27+
public class ResetFeaturesResponseTests extends AbstractResponseTestCase<ResetFeatureStateResponse, ResetFeaturesResponse> {
28+
29+
@Override
30+
protected ResetFeatureStateResponse createServerTestInstance(
31+
XContentType xContentType) {
32+
return new org.elasticsearch.action.admin.cluster.snapshots.features.ResetFeatureStateResponse(
33+
randomList(
34+
10,
35+
() -> randomBoolean()
36+
? ResetFeatureStateResponse.ResetFeatureStateStatus.success(randomAlphaOfLengthBetween(6, 10))
37+
: ResetFeatureStateResponse.ResetFeatureStateStatus.failure(
38+
randomAlphaOfLengthBetween(6, 10), new ElasticsearchException("something went wrong"))
39+
)
40+
);
41+
}
42+
43+
@Override
44+
protected ResetFeaturesResponse doParseToClientInstance(XContentParser parser) throws IOException {
45+
return ResetFeaturesResponse.parse(parser);
46+
}
47+
48+
@Override
49+
protected void assertInstances(ResetFeatureStateResponse serverTestInstance, ResetFeaturesResponse clientInstance) {
50+
51+
assertNotNull(serverTestInstance.getFeatureStateResetStatuses());
52+
assertNotNull(clientInstance.getFeatureResetStatuses());
53+
54+
assertThat(clientInstance.getFeatureResetStatuses(), hasSize(serverTestInstance.getFeatureStateResetStatuses().size()));
55+
56+
Map<String, String> clientFeatures = clientInstance.getFeatureResetStatuses()
57+
.stream()
58+
.collect(Collectors.toMap(f -> f.getFeatureName(), f -> f.getStatus()));
59+
Map<String, String> serverFeatures = serverTestInstance.getFeatureStateResetStatuses()
60+
.stream()
61+
.collect(Collectors.toMap(f -> f.getFeatureName(), f -> f.getStatus().toString()));
62+
63+
assertThat(clientFeatures.entrySet(), everyItem(is(in(serverFeatures.entrySet()))));
64+
}
65+
}

docs/reference/features/apis/reset-features-api.asciidoc

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ experimental::[]
88

99
Clears all of the the state information stored in system indices by {es} features, including the security and machine learning indices.
1010

11-
WARNING: Intended for development and testing use only. Do not reset features on a production cluster.
11+
WARNING: Intended for development and testing use only. Do not reset features on a production cluster.
1212

1313
[source,console]
1414
-----------------------------------
@@ -26,9 +26,11 @@ POST /_features/_reset
2626

2727
Return a cluster to the same state as a new installation by resetting the feature state for all {es} features. This deletes all state information stored in system indices.
2828

29-
Note that select features might provide a way to reset particular system indices. Using this API resets _all_ features, both those that are built-in and implemented as plugins.
29+
The response code is `HTTP 200` if state is successfully reset for all features, `HTTP 207` if there is a mixture of successes and failures, and `HTTP 500` if the reset operation fails for all features.
3030

31-
To list the features that will be affected, use the <<get-features-api,get features API>>.
31+
Note that select features might provide a way to reset particular system indices. Using this API resets _all_ features, both those that are built-in and implemented as plugins.
32+
33+
To list the features that will be affected, use the <<get-features-api,get features API>>.
3234

3335
IMPORTANT: The features installed on the node you submit this request to are the features that will be reset. Run on the master node if you have any doubts about which plugins are installed on individual nodes.
3436

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

Lines changed: 77 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,14 @@
88

99
package org.elasticsearch.snapshots;
1010

11+
import org.elasticsearch.ElasticsearchException;
12+
import org.elasticsearch.action.ActionListener;
1113
import org.elasticsearch.action.admin.cluster.snapshots.features.ResetFeatureStateAction;
1214
import org.elasticsearch.action.admin.cluster.snapshots.features.ResetFeatureStateRequest;
1315
import org.elasticsearch.action.admin.cluster.snapshots.features.ResetFeatureStateResponse;
1416
import org.elasticsearch.action.admin.indices.get.GetIndexResponse;
17+
import org.elasticsearch.client.Client;
18+
import org.elasticsearch.cluster.service.ClusterService;
1519
import org.elasticsearch.common.settings.Settings;
1620
import org.elasticsearch.index.IndexNotFoundException;
1721
import org.elasticsearch.indices.SystemIndexDescriptor;
@@ -23,10 +27,13 @@
2327
import java.util.Collection;
2428
import java.util.Collections;
2529
import java.util.List;
30+
import java.util.stream.Collectors;
2631

2732
import static org.hamcrest.Matchers.arrayContaining;
33+
import static org.hamcrest.Matchers.contains;
2834
import static org.hamcrest.Matchers.containsInAnyOrder;
2935
import static org.hamcrest.Matchers.containsString;
36+
import static org.hamcrest.Matchers.notNullValue;
3037

3138
public class FeatureStateResetApiIT extends ESIntegTestCase {
3239

@@ -35,6 +42,7 @@ protected Collection<Class<? extends Plugin>> nodePlugins() {
3542
List<Class<? extends Plugin>> plugins = new ArrayList<>(super.nodePlugins());
3643
plugins.add(SystemIndexTestPlugin.class);
3744
plugins.add(SecondSystemIndexTestPlugin.class);
45+
plugins.add(EvilSystemIndexTestPlugin.class);
3846
return plugins;
3947
}
4048

@@ -62,10 +70,11 @@ public void testResetSystemIndices() throws Exception {
6270

6371
// call the reset API
6472
ResetFeatureStateResponse apiResponse = client().execute(ResetFeatureStateAction.INSTANCE, new ResetFeatureStateRequest()).get();
65-
assertThat(apiResponse.getItemList(), containsInAnyOrder(
66-
new ResetFeatureStateResponse.ResetFeatureStateStatus("SystemIndexTestPlugin", "SUCCESS"),
67-
new ResetFeatureStateResponse.ResetFeatureStateStatus("SecondSystemIndexTestPlugin", "SUCCESS"),
68-
new ResetFeatureStateResponse.ResetFeatureStateStatus("tasks", "SUCCESS")
73+
assertThat(apiResponse.getFeatureStateResetStatuses(), containsInAnyOrder(
74+
ResetFeatureStateResponse.ResetFeatureStateStatus.success("SystemIndexTestPlugin"),
75+
ResetFeatureStateResponse.ResetFeatureStateStatus.success("SecondSystemIndexTestPlugin"),
76+
ResetFeatureStateResponse.ResetFeatureStateStatus.success("EvilSystemIndexTestPlugin"),
77+
ResetFeatureStateResponse.ResetFeatureStateStatus.success("tasks")
6978
));
7079

7180
// verify that both indices are gone
@@ -94,6 +103,31 @@ public void testResetSystemIndices() throws Exception {
94103
assertThat(response.getIndices(), arrayContaining("my_index"));
95104
}
96105

106+
/**
107+
* Evil test - test that when a feature fails to reset, we get a response object
108+
* indicating the failure
109+
*/
110+
public void testFeatureResetFailure() throws Exception {
111+
try {
112+
EvilSystemIndexTestPlugin.setBeEvil(true);
113+
ResetFeatureStateResponse resetFeatureStateResponse = client().execute(ResetFeatureStateAction.INSTANCE,
114+
new ResetFeatureStateRequest()).get();
115+
116+
List<String> failedFeatures = resetFeatureStateResponse.getFeatureStateResetStatuses().stream()
117+
.filter(status -> status.getStatus() == ResetFeatureStateResponse.ResetFeatureStateStatus.Status.FAILURE)
118+
.peek(status -> assertThat(status.getException(), notNullValue()))
119+
.map(status -> {
120+
// all failed statuses should have exceptions
121+
assertThat(status.getException(), notNullValue());
122+
return status.getFeatureName();
123+
})
124+
.collect(Collectors.toList());
125+
assertThat(failedFeatures, contains("EvilSystemIndexTestPlugin"));
126+
} finally {
127+
EvilSystemIndexTestPlugin.setBeEvil(false);
128+
}
129+
}
130+
97131
/**
98132
* A test plugin with patterns for system indices and associated indices.
99133
*/
@@ -145,4 +179,43 @@ public String getFeatureDescription() {
145179
return "A second test plugin";
146180
}
147181
}
182+
183+
/**
184+
* An evil test plugin to test failure cases.
185+
*/
186+
public static class EvilSystemIndexTestPlugin extends Plugin implements SystemIndexPlugin {
187+
188+
private static boolean beEvil = false;
189+
190+
@Override
191+
public String getFeatureName() {
192+
return "EvilSystemIndexTestPlugin";
193+
}
194+
195+
@Override
196+
public String getFeatureDescription() {
197+
return "a plugin that can be very bad";
198+
}
199+
200+
public static synchronized void setBeEvil(boolean evil) {
201+
beEvil = evil;
202+
}
203+
204+
public static synchronized boolean isEvil() {
205+
return beEvil;
206+
}
207+
208+
@Override
209+
public void cleanUpFeature(
210+
ClusterService clusterService,
211+
Client client,
212+
ActionListener<ResetFeatureStateResponse.ResetFeatureStateStatus> listener) {
213+
if (isEvil()) {
214+
listener.onResponse(ResetFeatureStateResponse.ResetFeatureStateStatus.failure(getFeatureName(),
215+
new ElasticsearchException("problem!")));
216+
} else {
217+
listener.onResponse(ResetFeatureStateResponse.ResetFeatureStateStatus.success(getFeatureName()));
218+
}
219+
}
220+
}
148221
}

0 commit comments

Comments
 (0)