Skip to content

Commit 3db1394

Browse files
williamrandolphdebadairjaymode
authored
Make feature reset API response more informative (#71240) (#72341)
* Add docs for feature reset API (#71759) * 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. * Fix 7.x backport compilation issues * Feature reset integration test should tolerate failed resets (#72326) Co-authored-by: debadair <[email protected]> Co-authored-by: Jay Modi <[email protected]>
1 parent 80036f6 commit 3db1394

File tree

11 files changed

+402
-44
lines changed

11 files changed

+402
-44
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: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,11 @@
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;
1720

1821
import static org.hamcrest.Matchers.greaterThan;
1922
import static org.hamcrest.Matchers.notNullValue;
@@ -31,16 +34,28 @@ public void testGetFeatures() throws IOException {
3134
assertTrue(response.getFeatures().stream().anyMatch(feature -> "tasks".equals(feature.getFeatureName())));
3235
}
3336

37+
/**
38+
* This test assumes that at least one of our defined features should reset successfully.
39+
* Since plugins should be testing their own reset operations if they use something
40+
* other than the default, this test tolerates failures in the response from the
41+
* feature reset API. We just need to check that we can reset the "tasks" system index.
42+
*/
3443
public void testResetFeatures() throws IOException {
3544
ResetFeaturesRequest request = new ResetFeaturesRequest();
3645

46+
// need superuser privileges to execute the reset
47+
RestHighLevelClient adminHighLevelClient = new RestHighLevelClient(
48+
adminClient(),
49+
(client) -> {},
50+
new SearchModule(Settings.EMPTY, true, Collections.emptyList()).getNamedXContents());
3751
ResetFeaturesResponse response = execute(request,
38-
highLevelClient().features()::resetFeatures, highLevelClient().features()::resetFeaturesAsync);
52+
adminHighLevelClient.features()::resetFeatures,
53+
adminHighLevelClient.features()::resetFeaturesAsync);
3954

4055
assertThat(response, notNullValue());
41-
assertThat(response.getFeatures(), notNullValue());
42-
assertThat(response.getFeatures().size(), greaterThan(1));
43-
assertTrue(response.getFeatures().stream().anyMatch(
56+
assertThat(response.getFeatureResetStatuses(), notNullValue());
57+
assertThat(response.getFeatureResetStatuses().size(), greaterThan(1));
58+
assertTrue(response.getFeatureResetStatuses().stream().anyMatch(
4459
feature -> "tasks".equals(feature.getFeatureName()) && "SUCCESS".equals(feature.getStatus())));
4560
}
4661
}
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/features-apis.asciidoc

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,5 +7,7 @@ by Elasticsearch and Elasticsearch plugins.
77
[discrete]
88
=== Features APIs
99
* <<get-features-api,Get Features API>>
10+
* <<reset-features-api,Rest Features API>>
1011

1112
include::get-features-api.asciidoc[]
13+
include::reset-features-api.asciidoc[]
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
[[reset-features-api]]
2+
=== Reset features API
3+
++++
4+
<titleabbrev>Reset features</titleabbrev>
5+
++++
6+
7+
experimental::[]
8+
9+
Clears all of the the state information stored in system indices by {es} features, including the security and machine learning indices.
10+
11+
WARNING: Intended for development and testing use only. Do not reset features on a production cluster.
12+
13+
[source,console]
14+
-----------------------------------
15+
POST /_features/_reset
16+
-----------------------------------
17+
18+
[[reset-features-api-request]]
19+
==== {api-request-title}
20+
21+
`POST /_features/_reset`
22+
23+
24+
[[reset-features-api-desc]]
25+
==== {api-description-title}
26+
27+
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.
28+
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.
30+
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>>.
34+
35+
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.
36+
37+
==== {api-examples-title}
38+
Example response:
39+
[source,console-result]
40+
----
41+
{
42+
"features" : [
43+
{
44+
"feature_name" : "security",
45+
"status" : "SUCCESS"
46+
},
47+
{
48+
"feature_name" : "tasks",
49+
"status" : "SUCCESS"
50+
}
51+
]
52+
}
53+
----
54+
// TESTRESPONSE[s/"features" : \[[^\]]*\]/"features": $body.$_path/]

0 commit comments

Comments
 (0)