瀏覽代碼

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 <jaymode@users.noreply.github.com>
William Brafford 4 年之前
父節點
當前提交
fc7c06d8a1

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

@@ -8,13 +8,22 @@
 
 package org.elasticsearch.client.feature;
 
+import org.elasticsearch.ElasticsearchException;
+import org.elasticsearch.common.Nullable;
 import org.elasticsearch.common.ParseField;
 import org.elasticsearch.common.xcontent.ConstructingObjectParser;
 import org.elasticsearch.common.xcontent.ObjectParser;
 import org.elasticsearch.common.xcontent.XContentParser;
 
 import java.util.List;
+import java.util.Objects;
 
+/**
+ * This class represents the response of the Feature State Reset API. It is a
+ * list containing the response of every feature whose state can be reset. The
+ * response from each feature will indicate success or failure. In the case of a
+ * failure, the cause will be returned as well.
+ */
 public class ResetFeaturesResponse {
     private final List<ResetFeatureStateStatus> features;
 
@@ -22,7 +31,7 @@ public class ResetFeaturesResponse {
 
     @SuppressWarnings("unchecked")
     private static final ConstructingObjectParser<ResetFeaturesResponse, Void> PARSER = new ConstructingObjectParser<>(
-        "snapshottable_features_response", true,
+        "features_reset_status_response", true,
         (a, ctx) -> new ResetFeaturesResponse((List<ResetFeatureStateStatus>) a[0])
     );
 
@@ -32,11 +41,18 @@ public class ResetFeaturesResponse {
             ResetFeaturesResponse.ResetFeatureStateStatus::parse, FEATURES);
     }
 
+    /**
+     * Create a new ResetFeaturesResponse
+     * @param features A full list of status responses from individual feature reset operations.
+     */
     public ResetFeaturesResponse(List<ResetFeatureStateStatus> features) {
         this.features = features;
     }
 
-    public List<ResetFeatureStateStatus> getFeatures() {
+    /**
+     * @return List containing a reset status for each feature that we have tried to reset.
+     */
+    public List<ResetFeatureStateStatus> getFeatureResetStatuses() {
         return features;
     }
 
@@ -44,15 +60,24 @@ public class ResetFeaturesResponse {
         return PARSER.apply(parser, null);
     }
 
+    /**
+     * A class representing the status of an attempt to reset a feature's state.
+     * The attempt to reset either succeeds and we return the name of the
+     * feature and a success flag; or it fails and we return the name of the feature,
+     * a status flag, and the exception thrown during the attempt to reset the feature.
+     */
     public static class ResetFeatureStateStatus {
         private final String featureName;
         private final String status;
+        private final Exception exception;
 
         private static final ParseField FEATURE_NAME = new ParseField("feature_name");
         private static final ParseField STATUS = new ParseField("status");
+        private static final ParseField EXCEPTION = new ParseField("exception");
 
-        private static final ConstructingObjectParser<ResetFeatureStateStatus, Void> PARSER =  new ConstructingObjectParser<>(
-            "features", true, (a, ctx) -> new ResetFeatureStateStatus((String) a[0], (String) a[1])
+        private static final ConstructingObjectParser<ResetFeatureStateStatus, Void> PARSER = new ConstructingObjectParser<>(
+            "feature_state_reset_stats", true,
+            (a, ctx) -> new ResetFeatureStateStatus((String) a[0], (String) a[1], (ElasticsearchException) a[2])
         );
 
         static {
@@ -60,23 +85,49 @@ public class ResetFeaturesResponse {
                 (p, c) -> p.text(), FEATURE_NAME, ObjectParser.ValueType.STRING);
             PARSER.declareField(ConstructingObjectParser.constructorArg(),
                 (p, c) -> p.text(), STATUS, ObjectParser.ValueType.STRING);
+            PARSER.declareObject(ConstructingObjectParser.optionalConstructorArg(),
+                (p, c) -> ElasticsearchException.fromXContent(p), EXCEPTION);
         }
 
-        ResetFeatureStateStatus(String featureName, String status) {
+        /**
+         * Create a ResetFeatureStateStatus.
+         * @param featureName Name of the feature whose status has been reset.
+         * @param status Whether the reset attempt succeeded or failed.
+         * @param exception If the reset attempt failed, the exception that caused the
+         *                  failure. Must be null when status is "SUCCESS".
+         */
+        ResetFeatureStateStatus(String featureName, String status, @Nullable Exception exception) {
             this.featureName = featureName;
+            assert "SUCCESS".equals(status) || "FAILURE".equals(status);
             this.status = status;
+            assert "FAILURE".equals(status) ? Objects.nonNull(exception) : Objects.isNull(exception);
+            this.exception = exception;
         }
 
         public static ResetFeatureStateStatus parse(XContentParser parser, Void ctx) {
             return PARSER.apply(parser, ctx);
         }
 
+        /**
+         * @return Name of the feature that we tried to reset
+         */
         public String getFeatureName() {
             return featureName;
         }
 
+        /**
+         * @return "SUCCESS" if the reset attempt succeeded, "FAILURE" otherwise.
+         */
         public String getStatus() {
             return status;
         }
+
+        /**
+         * @return The exception that caused the reset attempt to fail.
+         */
+        @Nullable
+        public Exception getException() {
+            return exception;
+        }
     }
 }

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

@@ -12,9 +12,15 @@ import org.elasticsearch.client.feature.GetFeaturesRequest;
 import org.elasticsearch.client.feature.GetFeaturesResponse;
 import org.elasticsearch.client.feature.ResetFeaturesRequest;
 import org.elasticsearch.client.feature.ResetFeaturesResponse;
+import org.elasticsearch.common.settings.Settings;
+import org.elasticsearch.search.SearchModule;
 
 import java.io.IOException;
+import java.util.Collections;
+import java.util.Set;
+import java.util.stream.Collectors;
 
+import static org.hamcrest.Matchers.contains;
 import static org.hamcrest.Matchers.greaterThan;
 import static org.hamcrest.Matchers.notNullValue;
 
@@ -34,13 +40,25 @@ public class FeaturesIT extends ESRestHighLevelClientTestCase {
     public void testResetFeatures() throws IOException {
         ResetFeaturesRequest request = new ResetFeaturesRequest();
 
+        // need superuser privileges to execute the reset
+        RestHighLevelClient adminHighLevelClient = new RestHighLevelClient(
+            adminClient(),
+            (client) -> {},
+            new SearchModule(Settings.EMPTY, Collections.emptyList()).getNamedXContents());
         ResetFeaturesResponse response = execute(request,
-            highLevelClient().features()::resetFeatures, highLevelClient().features()::resetFeaturesAsync);
+            adminHighLevelClient.features()::resetFeatures,
+            adminHighLevelClient.features()::resetFeaturesAsync);
 
         assertThat(response, notNullValue());
-        assertThat(response.getFeatures(), notNullValue());
-        assertThat(response.getFeatures().size(), greaterThan(1));
-        assertTrue(response.getFeatures().stream().anyMatch(
+        assertThat(response.getFeatureResetStatuses(), notNullValue());
+        assertThat(response.getFeatureResetStatuses().size(), greaterThan(1));
+        assertTrue(response.getFeatureResetStatuses().stream().anyMatch(
             feature -> "tasks".equals(feature.getFeatureName()) && "SUCCESS".equals(feature.getStatus())));
+
+        Set<String> statuses = response.getFeatureResetStatuses().stream()
+            .map(ResetFeaturesResponse.ResetFeatureStateStatus::getStatus)
+            .collect(Collectors.toSet());
+
+        assertThat(statuses, contains("SUCCESS"));
     }
 }

+ 65 - 0
client/rest-high-level/src/test/java/org/elasticsearch/client/snapshots/ResetFeaturesResponseTests.java

@@ -0,0 +1,65 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+package org.elasticsearch.client.snapshots;
+
+import org.elasticsearch.ElasticsearchException;
+import org.elasticsearch.action.admin.cluster.snapshots.features.ResetFeatureStateResponse;
+import org.elasticsearch.client.AbstractResponseTestCase;
+import org.elasticsearch.client.feature.ResetFeaturesResponse;
+import org.elasticsearch.common.xcontent.XContentParser;
+import org.elasticsearch.common.xcontent.XContentType;
+
+import java.io.IOException;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+import static org.hamcrest.Matchers.everyItem;
+import static org.hamcrest.Matchers.hasSize;
+import static org.hamcrest.Matchers.in;
+import static org.hamcrest.Matchers.is;
+
+public class ResetFeaturesResponseTests extends AbstractResponseTestCase<ResetFeatureStateResponse, ResetFeaturesResponse> {
+
+    @Override
+    protected ResetFeatureStateResponse createServerTestInstance(
+        XContentType xContentType) {
+        return new org.elasticsearch.action.admin.cluster.snapshots.features.ResetFeatureStateResponse(
+            randomList(
+                10,
+                () -> randomBoolean()
+                    ? ResetFeatureStateResponse.ResetFeatureStateStatus.success(randomAlphaOfLengthBetween(6, 10))
+                    : ResetFeatureStateResponse.ResetFeatureStateStatus.failure(
+                        randomAlphaOfLengthBetween(6, 10), new ElasticsearchException("something went wrong"))
+            )
+        );
+    }
+
+    @Override
+    protected ResetFeaturesResponse doParseToClientInstance(XContentParser parser) throws IOException {
+        return ResetFeaturesResponse.parse(parser);
+    }
+
+    @Override
+    protected void assertInstances(ResetFeatureStateResponse serverTestInstance, ResetFeaturesResponse clientInstance) {
+
+        assertNotNull(serverTestInstance.getFeatureStateResetStatuses());
+        assertNotNull(clientInstance.getFeatureResetStatuses());
+
+        assertThat(clientInstance.getFeatureResetStatuses(), hasSize(serverTestInstance.getFeatureStateResetStatuses().size()));
+
+        Map<String, String> clientFeatures = clientInstance.getFeatureResetStatuses()
+            .stream()
+            .collect(Collectors.toMap(f -> f.getFeatureName(), f -> f.getStatus()));
+        Map<String, String> serverFeatures = serverTestInstance.getFeatureStateResetStatuses()
+            .stream()
+            .collect(Collectors.toMap(f -> f.getFeatureName(), f -> f.getStatus().toString()));
+
+        assertThat(clientFeatures.entrySet(), everyItem(is(in(serverFeatures.entrySet()))));
+    }
+}

+ 5 - 3
docs/reference/features/apis/reset-features-api.asciidoc

@@ -8,7 +8,7 @@ experimental::[]
 
 Clears all of the the state information stored in system indices by {es} features, including the security and machine learning indices.
 
-WARNING: Intended for development and testing use only. Do not reset features on a production cluster.  
+WARNING: Intended for development and testing use only. Do not reset features on a production cluster.
 
 [source,console]
 -----------------------------------
@@ -26,9 +26,11 @@ POST /_features/_reset
 
 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.
 
-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. 
+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.
 
-To list the features that will be affected, use the <<get-features-api,get features API>>. 
+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.
+
+To list the features that will be affected, use the <<get-features-api,get features API>>.
 
 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.
 

+ 77 - 4
server/src/internalClusterTest/java/org/elasticsearch/snapshots/FeatureStateResetApiIT.java

@@ -8,10 +8,14 @@
 
 package org.elasticsearch.snapshots;
 
+import org.elasticsearch.ElasticsearchException;
+import org.elasticsearch.action.ActionListener;
 import org.elasticsearch.action.admin.cluster.snapshots.features.ResetFeatureStateAction;
 import org.elasticsearch.action.admin.cluster.snapshots.features.ResetFeatureStateRequest;
 import org.elasticsearch.action.admin.cluster.snapshots.features.ResetFeatureStateResponse;
 import org.elasticsearch.action.admin.indices.get.GetIndexResponse;
+import org.elasticsearch.client.Client;
+import org.elasticsearch.cluster.service.ClusterService;
 import org.elasticsearch.common.settings.Settings;
 import org.elasticsearch.index.IndexNotFoundException;
 import org.elasticsearch.indices.SystemIndexDescriptor;
@@ -23,10 +27,13 @@ import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.List;
+import java.util.stream.Collectors;
 
 import static org.hamcrest.Matchers.arrayContaining;
+import static org.hamcrest.Matchers.contains;
 import static org.hamcrest.Matchers.containsInAnyOrder;
 import static org.hamcrest.Matchers.containsString;
+import static org.hamcrest.Matchers.notNullValue;
 
 public class FeatureStateResetApiIT extends ESIntegTestCase {
 
@@ -35,6 +42,7 @@ public class FeatureStateResetApiIT extends ESIntegTestCase {
         List<Class<? extends Plugin>> plugins = new ArrayList<>(super.nodePlugins());
         plugins.add(SystemIndexTestPlugin.class);
         plugins.add(SecondSystemIndexTestPlugin.class);
+        plugins.add(EvilSystemIndexTestPlugin.class);
         return plugins;
     }
 
@@ -62,10 +70,11 @@ public class FeatureStateResetApiIT extends ESIntegTestCase {
 
         // call the reset API
         ResetFeatureStateResponse apiResponse = client().execute(ResetFeatureStateAction.INSTANCE, new ResetFeatureStateRequest()).get();
-        assertThat(apiResponse.getItemList(), containsInAnyOrder(
-            new ResetFeatureStateResponse.ResetFeatureStateStatus("SystemIndexTestPlugin", "SUCCESS"),
-            new ResetFeatureStateResponse.ResetFeatureStateStatus("SecondSystemIndexTestPlugin", "SUCCESS"),
-            new ResetFeatureStateResponse.ResetFeatureStateStatus("tasks", "SUCCESS")
+        assertThat(apiResponse.getFeatureStateResetStatuses(), containsInAnyOrder(
+            ResetFeatureStateResponse.ResetFeatureStateStatus.success("SystemIndexTestPlugin"),
+            ResetFeatureStateResponse.ResetFeatureStateStatus.success("SecondSystemIndexTestPlugin"),
+            ResetFeatureStateResponse.ResetFeatureStateStatus.success("EvilSystemIndexTestPlugin"),
+            ResetFeatureStateResponse.ResetFeatureStateStatus.success("tasks")
         ));
 
         // verify that both indices are gone
@@ -94,6 +103,31 @@ public class FeatureStateResetApiIT extends ESIntegTestCase {
         assertThat(response.getIndices(), arrayContaining("my_index"));
     }
 
+    /**
+     * Evil test - test that when a feature fails to reset, we get a response object
+     * indicating the failure
+     */
+    public void testFeatureResetFailure() throws Exception {
+        try {
+            EvilSystemIndexTestPlugin.setBeEvil(true);
+            ResetFeatureStateResponse resetFeatureStateResponse = client().execute(ResetFeatureStateAction.INSTANCE,
+                new ResetFeatureStateRequest()).get();
+
+            List<String> failedFeatures = resetFeatureStateResponse.getFeatureStateResetStatuses().stream()
+                .filter(status -> status.getStatus() == ResetFeatureStateResponse.ResetFeatureStateStatus.Status.FAILURE)
+                .peek(status -> assertThat(status.getException(), notNullValue()))
+                .map(status -> {
+                    // all failed statuses should have exceptions
+                    assertThat(status.getException(), notNullValue());
+                    return status.getFeatureName();
+                })
+                .collect(Collectors.toList());
+            assertThat(failedFeatures, contains("EvilSystemIndexTestPlugin"));
+        } finally {
+            EvilSystemIndexTestPlugin.setBeEvil(false);
+        }
+    }
+
     /**
      * A test plugin with patterns for system indices and associated indices.
      */
@@ -145,4 +179,43 @@ public class FeatureStateResetApiIT extends ESIntegTestCase {
             return "A second test plugin";
         }
     }
+
+    /**
+     * An evil test plugin to test failure cases.
+     */
+    public static class EvilSystemIndexTestPlugin extends Plugin implements SystemIndexPlugin {
+
+        private static boolean beEvil = false;
+
+        @Override
+        public String getFeatureName() {
+            return "EvilSystemIndexTestPlugin";
+        }
+
+        @Override
+        public String getFeatureDescription() {
+            return "a plugin that can be very bad";
+        }
+
+        public static synchronized void setBeEvil(boolean evil) {
+            beEvil = evil;
+        }
+
+        public static synchronized boolean isEvil() {
+            return beEvil;
+        }
+
+        @Override
+        public void cleanUpFeature(
+            ClusterService clusterService,
+            Client client,
+            ActionListener<ResetFeatureStateResponse.ResetFeatureStateStatus> listener) {
+            if (isEvil()) {
+                listener.onResponse(ResetFeatureStateResponse.ResetFeatureStateStatus.failure(getFeatureName(),
+                    new ElasticsearchException("problem!")));
+            } else {
+                listener.onResponse(ResetFeatureStateResponse.ResetFeatureStateStatus.success(getFeatureName()));
+            }
+        }
+    }
 }

+ 94 - 17
server/src/main/java/org/elasticsearch/action/admin/cluster/snapshots/features/ResetFeatureStateResponse.java

@@ -8,7 +8,9 @@
 
 package org.elasticsearch.action.admin.cluster.snapshots.features;
 
+import org.elasticsearch.ElasticsearchException;
 import org.elasticsearch.action.ActionResponse;
+import org.elasticsearch.common.Nullable;
 import org.elasticsearch.common.io.stream.StreamInput;
 import org.elasticsearch.common.io.stream.StreamOutput;
 import org.elasticsearch.common.io.stream.Writeable;
@@ -17,6 +19,7 @@ import org.elasticsearch.common.xcontent.XContentBuilder;
 
 import java.io.IOException;
 import java.util.ArrayList;
+import java.util.Collections;
 import java.util.Comparator;
 import java.util.List;
 import java.util.Objects;
@@ -30,11 +33,11 @@ public class ResetFeatureStateResponse extends ActionResponse implements ToXCont
      * Create a response showing which features have had state reset and success
      * or failure status.
      *
-     * @param statusList A list of status responses
+     * @param resetFeatureStateStatuses A list of status responses
      */
-    public ResetFeatureStateResponse(List<ResetFeatureStateStatus> statusList) {
+    public ResetFeatureStateResponse(List<ResetFeatureStateStatus> resetFeatureStateStatuses) {
         resetFeatureStateStatusList = new ArrayList<>();
-        resetFeatureStateStatusList.addAll(statusList);
+        resetFeatureStateStatusList.addAll(resetFeatureStateStatuses);
         resetFeatureStateStatusList.sort(Comparator.comparing(ResetFeatureStateStatus::getFeatureName));
     }
 
@@ -43,8 +46,11 @@ public class ResetFeatureStateResponse extends ActionResponse implements ToXCont
         this.resetFeatureStateStatusList = in.readList(ResetFeatureStateStatus::new);
     }
 
-    public List<ResetFeatureStateStatus> getItemList() {
-        return this.resetFeatureStateStatusList;
+    /**
+     * @return List of statuses for individual reset operations, one per feature that we tried to reset
+     */
+    public List<ResetFeatureStateStatus> getFeatureStateResetStatuses() {
+        return Collections.unmodifiableList(this.resetFeatureStateStatusList);
     }
 
     @Override
@@ -92,59 +98,130 @@ public class ResetFeatureStateResponse extends ActionResponse implements ToXCont
      */
     public static class ResetFeatureStateStatus implements Writeable, ToXContentObject {
         private final String featureName;
-        private final String status;
+        private final Status status;
+        private final Exception exception;
+
+        /**
+         * Success or failure enum. Not a boolean so that we can easily display
+         * "SUCCESS" or "FAILURE" when this object is serialized.
+         */
+        public enum Status {
+            SUCCESS,
+            FAILURE
+        }
 
-        public ResetFeatureStateStatus(String featureName, String status) {
+        /**
+         * Create a feature status for a successful reset operation
+         * @param featureName Name of the feature whose state was successfully reset
+         * @return Success status for a feature
+         */
+        public static ResetFeatureStateStatus success(String featureName) {
+            return new ResetFeatureStateStatus(featureName, Status.SUCCESS, null);
+        }
+
+        /**
+         * Create a feature status for a failed reset operation
+         * @param featureName Name of the feature that failed
+         * @param exception The exception that caused or described the failure
+         * @return Failure status for a feature
+         */
+        public static ResetFeatureStateStatus failure(String featureName, Exception exception) {
+            return new ResetFeatureStateStatus(
+                featureName,
+                Status.FAILURE,
+                exception);
+        }
+
+        private ResetFeatureStateStatus(String featureName, Status status, @Nullable Exception exception) {
             this.featureName = featureName;
             this.status = status;
+            assert Status.FAILURE.equals(status) ? Objects.nonNull(exception) : Objects.isNull(exception);
+            this.exception = exception;
         }
 
         ResetFeatureStateStatus(StreamInput in) throws IOException {
             this.featureName = in.readString();
-            this.status = in.readString();
+            this.status = Status.valueOf(in.readString());
+            this.exception = in.readBoolean() ? in.readException() : null;
         }
 
+        /**
+         * @return Name of the feature we tried to reset
+         */
         public String getFeatureName() {
             return this.featureName;
         }
 
-        public String getStatus() {
+        /**
+         * @return Success or failure for the reset operation
+         */
+        public Status getStatus() {
             return this.status;
         }
 
+        /**
+         * @return For a failed reset operation, the exception that caused or describes the failure.
+         */
+        @Nullable
+        public Exception getException() {
+            return this.exception;
+        }
+
         @Override
         public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
             builder.startObject();
             builder.field("feature_name", this.featureName);
             builder.field("status", this.status);
+            if (Objects.nonNull(this.exception)) {
+                builder.field("exception");
+                builder.startObject();
+                new ElasticsearchException(exception).toXContent(builder, params);
+                builder.endObject();
+            }
             builder.endObject();
             return builder;
         }
 
-        @Override
-        public void writeTo(StreamOutput out) throws IOException {
-            out.writeString(this.featureName);
-            out.writeString(this.status);
-        }
-
+        /**
+         * Without a convenient way to compare Exception equality, we consider
+         * only feature name and success or failure for equality.
+         * @param o An object to compare for equality
+         * @return True if the feature name and status are equal, false otherwise
+         */
         @Override
         public boolean equals(Object o) {
             if (this == o) return true;
             if (o == null || getClass() != o.getClass()) return false;
             ResetFeatureStateStatus that = (ResetFeatureStateStatus) o;
-            return Objects.equals(featureName, that.featureName) && Objects.equals(status, that.status);
+            return Objects.equals(featureName, that.featureName) && status == that.status;
         }
 
+        /**
+         * @return Hash code based only on feature name and status.
+         */
         @Override
         public int hashCode() {
             return Objects.hash(featureName, status);
         }
 
+        @Override
+        public void writeTo(StreamOutput out) throws IOException {
+            out.writeString(this.featureName);
+            out.writeString(this.status.toString());
+            if (exception != null) {
+                out.writeBoolean(true);
+                out.writeException(exception);
+            } else {
+                out.writeBoolean(false);
+            }
+        }
+
         @Override
         public String toString() {
             return "ResetFeatureStateStatus{" +
                 "featureName='" + featureName + '\'' +
-                ", status='" + status + '\'' +
+                ", status=" + status +
+                ", exception='" + exception + '\'' +
                 '}';
         }
     }

+ 3 - 3
server/src/main/java/org/elasticsearch/indices/SystemIndices.java

@@ -596,7 +596,7 @@ public class SystemIndices {
 
             if (allIndices.isEmpty()) {
                 // if no actual indices match the pattern, we can stop here
-                listener.onResponse(new ResetFeatureStateStatus(name, "SUCCESS"));
+                listener.onResponse(ResetFeatureStateStatus.success(name));
                 return;
             }
 
@@ -605,12 +605,12 @@ public class SystemIndices {
             client.execute(DeleteIndexAction.INSTANCE, deleteIndexRequest, new ActionListener<>() {
                 @Override
                 public void onResponse(AcknowledgedResponse acknowledgedResponse) {
-                    listener.onResponse(new ResetFeatureStateStatus(name, "SUCCESS"));
+                    listener.onResponse(ResetFeatureStateStatus.success(name));
                 }
 
                 @Override
                 public void onFailure(Exception e) {
-                    listener.onResponse(new ResetFeatureStateStatus(name, "FAILURE: " + e.getMessage()));
+                    listener.onResponse(ResetFeatureStateStatus.failure(name, e));
                 }
             });
         }

+ 19 - 1
server/src/main/java/org/elasticsearch/rest/action/admin/cluster/RestResetFeatureStateAction.java

@@ -10,9 +10,11 @@ package org.elasticsearch.rest.action.admin.cluster;
 
 import org.elasticsearch.action.admin.cluster.snapshots.features.ResetFeatureStateAction;
 import org.elasticsearch.action.admin.cluster.snapshots.features.ResetFeatureStateRequest;
+import org.elasticsearch.action.admin.cluster.snapshots.features.ResetFeatureStateResponse;
 import org.elasticsearch.client.node.NodeClient;
 import org.elasticsearch.rest.BaseRestHandler;
 import org.elasticsearch.rest.RestRequest;
+import org.elasticsearch.rest.RestStatus;
 import org.elasticsearch.rest.action.RestToXContentListener;
 
 import java.io.IOException;
@@ -39,6 +41,22 @@ public class RestResetFeatureStateAction extends BaseRestHandler {
     protected RestChannelConsumer prepareRequest(RestRequest request, NodeClient client) throws IOException {
         final ResetFeatureStateRequest req = new ResetFeatureStateRequest();
 
-        return restChannel -> client.execute(ResetFeatureStateAction.INSTANCE, req, new RestToXContentListener<>(restChannel));
+        return restChannel -> client.execute(
+            ResetFeatureStateAction.INSTANCE,
+            req,
+            new RestToXContentListener<>(restChannel) {
+                @Override
+                protected RestStatus getStatus(ResetFeatureStateResponse response) {
+                    long failures = response.getFeatureStateResetStatuses().stream()
+                        .filter(status -> status.getStatus() == ResetFeatureStateResponse.ResetFeatureStateStatus.Status.FAILURE)
+                        .count();
+                    if (failures == 0) {
+                        return RestStatus.OK;
+                    } else if (failures == response.getFeatureStateResetStatuses().size()) {
+                        return RestStatus.INTERNAL_SERVER_ERROR;
+                    }
+                    return RestStatus.MULTI_STATUS;
+                }
+            });
     }
 }

+ 11 - 9
server/src/test/java/org/elasticsearch/action/admin/cluster/snapshots/features/ResetFeatureStateResponseTests.java

@@ -8,6 +8,7 @@
 
 package org.elasticsearch.action.admin.cluster.snapshots.features;
 
+import org.elasticsearch.ElasticsearchException;
 import org.elasticsearch.common.io.stream.Writeable;
 import org.elasticsearch.test.AbstractWireSerializingTestCase;
 
@@ -29,25 +30,26 @@ public class ResetFeatureStateResponseTests extends AbstractWireSerializingTestC
         List<ResetFeatureStateResponse.ResetFeatureStateStatus> resetStatuses = new ArrayList<>();
         String feature1 = randomAlphaOfLengthBetween(4, 10);
         String feature2 = randomValueOtherThan(feature1, () -> randomAlphaOfLengthBetween(4, 10));
-        resetStatuses.add(new ResetFeatureStateResponse.ResetFeatureStateStatus(
-            feature1, randomFrom("SUCCESS", "FAILURE")));
-        resetStatuses.add(new ResetFeatureStateResponse.ResetFeatureStateStatus(
-            feature2, randomFrom("SUCCESS", "FAILURE")));
+        resetStatuses.add(randomFrom(
+            ResetFeatureStateResponse.ResetFeatureStateStatus.success(feature1),
+            ResetFeatureStateResponse.ResetFeatureStateStatus.failure(feature1, new ElasticsearchException("bad"))));
+        resetStatuses.add(randomFrom(
+            ResetFeatureStateResponse.ResetFeatureStateStatus.success(feature2),
+            ResetFeatureStateResponse.ResetFeatureStateStatus.failure(feature2, new ElasticsearchException("bad"))));
         return new ResetFeatureStateResponse(resetStatuses);
     }
 
     @Override
     protected ResetFeatureStateResponse mutateInstance(ResetFeatureStateResponse instance) throws IOException {
         int minSize = 0;
-        if (instance.getItemList().size() == 0) {
+        if (instance.getFeatureStateResetStatuses().size() == 0) {
             minSize = 1;
         }
-        Set<String> existingFeatureNames = instance.getItemList().stream()
+        Set<String> existingFeatureNames = instance.getFeatureStateResetStatuses().stream()
             .map(ResetFeatureStateResponse.ResetFeatureStateStatus::getFeatureName)
             .collect(Collectors.toSet());
         return new ResetFeatureStateResponse(randomList(minSize, 10,
-            () -> new ResetFeatureStateResponse.ResetFeatureStateStatus(
-                randomValueOtherThanMany(existingFeatureNames::contains, () -> randomAlphaOfLengthBetween(4, 10)),
-                randomAlphaOfLengthBetween(5, 10))));
+            () -> ResetFeatureStateResponse.ResetFeatureStateStatus.success(
+                randomValueOtherThanMany(existingFeatureNames::contains, () -> randomAlphaOfLengthBetween(4, 10)))));
     }
 }

+ 2 - 1
x-pack/plugin/transform/src/main/java/org/elasticsearch/xpack/transform/Transform.java

@@ -414,7 +414,8 @@ public class Transform extends Plugin implements SystemIndexPlugin, PersistentTa
                     + (stopTransformsResponse.getTaskFailures().isEmpty()
                         ? ""
                         : "task failures: " + stopTransformsResponse.getTaskFailures());
-                unsetResetModeListener.onResponse(new ResetFeatureStateResponse.ResetFeatureStateStatus(this.getFeatureName(), errMsg));
+                unsetResetModeListener.onResponse(ResetFeatureStateResponse.ResetFeatureStateStatus.failure(this.getFeatureName(),
+                    new ElasticsearchException(errMsg)));
             }
         }, unsetResetModeListener::onFailure);