Browse Source

Autoscaling decider and decision service (#59005)

Split the autoscaling decider into a service and configuration
in order to enable having additional context information available
in the service. Added AutoscalingDeciderContext holding generic
information all deciders are expected to need. Implemented GET
_autoscaling/decision
Henning Andersen 5 years ago
parent
commit
92d70534ef
17 changed files with 452 additions and 50 deletions
  1. 11 0
      docs/reference/autoscaling/apis/get-autoscaling-policy.asciidoc
  2. 20 0
      docs/reference/autoscaling/apis/put-autoscaling-policy.asciidoc
  3. 26 1
      x-pack/plugin/autoscaling/qa/rest/src/test/resources/rest-api-spec/test/autoscaling/get_autoscaling_decision.yml
  4. 63 8
      x-pack/plugin/autoscaling/src/main/java/org/elasticsearch/xpack/autoscaling/Autoscaling.java
  5. 20 0
      x-pack/plugin/autoscaling/src/main/java/org/elasticsearch/xpack/autoscaling/AutoscalingExtension.java
  6. 8 4
      x-pack/plugin/autoscaling/src/main/java/org/elasticsearch/xpack/autoscaling/action/TransportGetAutoscalingDecisionAction.java
  7. 8 10
      x-pack/plugin/autoscaling/src/main/java/org/elasticsearch/xpack/autoscaling/decision/AlwaysAutoscalingDeciderConfiguration.java
  8. 25 0
      x-pack/plugin/autoscaling/src/main/java/org/elasticsearch/xpack/autoscaling/decision/AlwaysAutoscalingDeciderService.java
  9. 1 9
      x-pack/plugin/autoscaling/src/main/java/org/elasticsearch/xpack/autoscaling/decision/AutoscalingDeciderConfiguration.java
  10. 13 0
      x-pack/plugin/autoscaling/src/main/java/org/elasticsearch/xpack/autoscaling/decision/AutoscalingDeciderContext.java
  11. 29 0
      x-pack/plugin/autoscaling/src/main/java/org/elasticsearch/xpack/autoscaling/decision/AutoscalingDeciderService.java
  12. 84 0
      x-pack/plugin/autoscaling/src/main/java/org/elasticsearch/xpack/autoscaling/decision/AutoscalingDecisionService.java
  13. 4 3
      x-pack/plugin/autoscaling/src/main/java/org/elasticsearch/xpack/autoscaling/decision/AutoscalingDecisions.java
  14. 9 9
      x-pack/plugin/autoscaling/src/main/java/org/elasticsearch/xpack/autoscaling/policy/AutoscalingPolicy.java
  15. 10 6
      x-pack/plugin/autoscaling/src/test/java/org/elasticsearch/xpack/autoscaling/AutoscalingTestCase.java
  16. 56 0
      x-pack/plugin/autoscaling/src/test/java/org/elasticsearch/xpack/autoscaling/action/GetAutoscalingDecisionActionResponseTests.java
  17. 65 0
      x-pack/plugin/autoscaling/src/test/java/org/elasticsearch/xpack/autoscaling/decision/AutoscalingDecisionServiceTests.java

+ 11 - 0
docs/reference/autoscaling/apis/get-autoscaling-policy.asciidoc

@@ -25,6 +25,17 @@ PUT /_autoscaling/policy/my_autoscaling_policy
 --------------------------------------------------
 // TESTSETUP
 
+//////////////////////////
+
+[source,console]
+--------------------------------------------------
+DELETE /_autoscaling/policy/my_autoscaling_policy
+--------------------------------------------------
+// TEST
+// TEARDOWN
+
+//////////////////////////
+
 [source,console]
 --------------------------------------------------
 GET /_autoscaling/policy/<name>

+ 20 - 0
docs/reference/autoscaling/apis/put-autoscaling-policy.asciidoc

@@ -25,6 +25,16 @@ PUT /_autoscaling/policy/<name>
 --------------------------------------------------
 // TEST[s/<name>/name/]
 
+//////////////////////////
+
+[source,console]
+--------------------------------------------------
+DELETE /_autoscaling/policy/name
+--------------------------------------------------
+// TEST[continued]
+
+//////////////////////////
+
 [[autoscaling-put-autoscaling-policy-prereqs]]
 ==== {api-prereq-title}
 
@@ -65,3 +75,13 @@ The API returns the following result:
   "acknowledged": true
 }
 --------------------------------------------------
+
+//////////////////////////
+
+[source,console]
+--------------------------------------------------
+DELETE /_autoscaling/policy/my_autoscaling_policy
+--------------------------------------------------
+// TEST[continued]
+
+//////////////////////////

+ 26 - 1
x-pack/plugin/autoscaling/qa/rest/src/test/resources/rest-api-spec/test/autoscaling/get_autoscaling_decision.yml

@@ -1,6 +1,31 @@
 ---
-"Test get autoscaling decision":
+"Test get empty autoscaling decision":
   - do:
       autoscaling.get_autoscaling_decision: {}
 
   - match: { "decisions": [] }
+
+---
+"Test get always autoscaling decision":
+  - do:
+      autoscaling.put_autoscaling_policy:
+        name: my_autoscaling_policy
+        body:
+          policy:
+            deciders:
+              always: {}
+
+  - match: { "acknowledged": true }
+
+  - do:
+      autoscaling.get_autoscaling_decision: {}
+
+  - match: { decisions.0.my_autoscaling_policy.decision: scale_up }
+  - match: { decisions.0.my_autoscaling_policy.decisions.0.name: always }
+  - match: { decisions.0.my_autoscaling_policy.decisions.0.type: scale_up }
+  - match: { decisions.0.my_autoscaling_policy.decisions.0.reason: always }
+
+  # test cleanup
+  - do:
+      autoscaling.delete_autoscaling_policy:
+        name: my_autoscaling_policy

+ 63 - 8
x-pack/plugin/autoscaling/src/main/java/org/elasticsearch/xpack/autoscaling/Autoscaling.java

@@ -6,13 +6,17 @@
 
 package org.elasticsearch.xpack.autoscaling;
 
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
 import org.elasticsearch.Build;
 import org.elasticsearch.action.ActionRequest;
 import org.elasticsearch.action.ActionResponse;
+import org.elasticsearch.client.Client;
 import org.elasticsearch.cluster.NamedDiff;
 import org.elasticsearch.cluster.metadata.IndexNameExpressionResolver;
 import org.elasticsearch.cluster.metadata.Metadata;
 import org.elasticsearch.cluster.node.DiscoveryNodes;
+import org.elasticsearch.cluster.service.ClusterService;
 import org.elasticsearch.common.ParseField;
 import org.elasticsearch.common.io.stream.NamedWriteableRegistry;
 import org.elasticsearch.common.settings.ClusterSettings;
@@ -21,11 +25,18 @@ import org.elasticsearch.common.settings.Setting;
 import org.elasticsearch.common.settings.Settings;
 import org.elasticsearch.common.settings.SettingsFilter;
 import org.elasticsearch.common.xcontent.NamedXContentRegistry;
+import org.elasticsearch.env.Environment;
+import org.elasticsearch.env.NodeEnvironment;
 import org.elasticsearch.license.XPackLicenseState;
 import org.elasticsearch.plugins.ActionPlugin;
+import org.elasticsearch.plugins.ExtensiblePlugin;
 import org.elasticsearch.plugins.Plugin;
+import org.elasticsearch.repositories.RepositoriesService;
 import org.elasticsearch.rest.RestController;
 import org.elasticsearch.rest.RestHandler;
+import org.elasticsearch.script.ScriptService;
+import org.elasticsearch.threadpool.ThreadPool;
+import org.elasticsearch.watcher.ResourceWatcherService;
 import org.elasticsearch.xpack.autoscaling.action.DeleteAutoscalingPolicyAction;
 import org.elasticsearch.xpack.autoscaling.action.GetAutoscalingDecisionAction;
 import org.elasticsearch.xpack.autoscaling.action.GetAutoscalingPolicyAction;
@@ -34,22 +45,29 @@ import org.elasticsearch.xpack.autoscaling.action.TransportDeleteAutoscalingPoli
 import org.elasticsearch.xpack.autoscaling.action.TransportGetAutoscalingDecisionAction;
 import org.elasticsearch.xpack.autoscaling.action.TransportGetAutoscalingPolicyAction;
 import org.elasticsearch.xpack.autoscaling.action.TransportPutAutoscalingPolicyAction;
-import org.elasticsearch.xpack.autoscaling.decision.AlwaysAutoscalingDecider;
-import org.elasticsearch.xpack.autoscaling.decision.AutoscalingDecider;
+import org.elasticsearch.xpack.autoscaling.decision.AlwaysAutoscalingDeciderConfiguration;
+import org.elasticsearch.xpack.autoscaling.decision.AlwaysAutoscalingDeciderService;
+import org.elasticsearch.xpack.autoscaling.decision.AutoscalingDeciderConfiguration;
+import org.elasticsearch.xpack.autoscaling.decision.AutoscalingDeciderService;
+import org.elasticsearch.xpack.autoscaling.decision.AutoscalingDecisionService;
 import org.elasticsearch.xpack.autoscaling.rest.RestDeleteAutoscalingPolicyHandler;
 import org.elasticsearch.xpack.autoscaling.rest.RestGetAutoscalingDecisionHandler;
 import org.elasticsearch.xpack.autoscaling.rest.RestGetAutoscalingPolicyHandler;
 import org.elasticsearch.xpack.autoscaling.rest.RestPutAutoscalingPolicyHandler;
 import org.elasticsearch.xpack.core.XPackPlugin;
 
+import java.util.ArrayList;
+import java.util.Collection;
 import java.util.List;
+import java.util.Set;
 import java.util.function.Supplier;
+import java.util.stream.Collectors;
 
 /**
  * Container class for autoscaling functionality.
  */
-public class Autoscaling extends Plugin implements ActionPlugin {
-
+public class Autoscaling extends Plugin implements ActionPlugin, ExtensiblePlugin, AutoscalingExtension {
+    private static final Logger logger = LogManager.getLogger(AutoscalingExtension.class);
     private static final Boolean AUTOSCALING_FEATURE_FLAG_REGISTERED;
 
     static {
@@ -78,8 +96,11 @@ public class Autoscaling extends Plugin implements ActionPlugin {
 
     private final boolean enabled;
 
+    private final List<AutoscalingExtension> autoscalingExtensions;
+
     public Autoscaling(final Settings settings) {
         this.enabled = AUTOSCALING_ENABLED_SETTING.get(settings);
+        this.autoscalingExtensions = new ArrayList<>(List.of(this));
     }
 
     /**
@@ -100,6 +121,23 @@ public class Autoscaling extends Plugin implements ActionPlugin {
         return Build.CURRENT.isSnapshot();
     }
 
+    @Override
+    public Collection<Object> createComponents(
+        Client client,
+        ClusterService clusterService,
+        ThreadPool threadPool,
+        ResourceWatcherService resourceWatcherService,
+        ScriptService scriptService,
+        NamedXContentRegistry xContentRegistry,
+        Environment environment,
+        NodeEnvironment nodeEnvironment,
+        NamedWriteableRegistry namedWriteableRegistry,
+        IndexNameExpressionResolver indexNameExpressionResolver,
+        Supplier<RepositoriesService> repositoriesServiceSupplier
+    ) {
+        return List.of(new AutoscalingDecisionService.Holder(this));
+    }
+
     @Override
     public List<ActionHandler<? extends ActionRequest, ? extends ActionResponse>> getActions() {
         if (enabled) {
@@ -141,7 +179,11 @@ public class Autoscaling extends Plugin implements ActionPlugin {
         return List.of(
             new NamedWriteableRegistry.Entry(Metadata.Custom.class, AutoscalingMetadata.NAME, AutoscalingMetadata::new),
             new NamedWriteableRegistry.Entry(NamedDiff.class, AutoscalingMetadata.NAME, AutoscalingMetadata.AutoscalingMetadataDiff::new),
-            new NamedWriteableRegistry.Entry(AutoscalingDecider.class, AlwaysAutoscalingDecider.NAME, AlwaysAutoscalingDecider::new)
+            new NamedWriteableRegistry.Entry(
+                AutoscalingDeciderConfiguration.class,
+                AlwaysAutoscalingDeciderConfiguration.NAME,
+                AlwaysAutoscalingDeciderConfiguration::new
+            )
         );
     }
 
@@ -150,9 +192,9 @@ public class Autoscaling extends Plugin implements ActionPlugin {
         return List.of(
             new NamedXContentRegistry.Entry(Metadata.Custom.class, new ParseField(AutoscalingMetadata.NAME), AutoscalingMetadata::parse),
             new NamedXContentRegistry.Entry(
-                AutoscalingDecider.class,
-                new ParseField(AlwaysAutoscalingDecider.NAME),
-                AlwaysAutoscalingDecider::parse
+                AutoscalingDeciderConfiguration.class,
+                new ParseField(AlwaysAutoscalingDeciderConfiguration.NAME),
+                AlwaysAutoscalingDeciderConfiguration::parse
             )
         );
     }
@@ -161,4 +203,17 @@ public class Autoscaling extends Plugin implements ActionPlugin {
         return XPackPlugin.getSharedLicenseState();
     }
 
+    @Override
+    public void loadExtensions(ExtensionLoader loader) {
+        loader.loadExtensions(AutoscalingExtension.class).forEach(autoscalingExtensions::add);
+    }
+
+    @Override
+    public Collection<AutoscalingDeciderService<? extends AutoscalingDeciderConfiguration>> deciders() {
+        return List.of(new AlwaysAutoscalingDeciderService());
+    }
+
+    public Set<AutoscalingDeciderService<? extends AutoscalingDeciderConfiguration>> createDeciderServices() {
+        return autoscalingExtensions.stream().flatMap(p -> p.deciders().stream()).collect(Collectors.toSet());
+    }
 }

+ 20 - 0
x-pack/plugin/autoscaling/src/main/java/org/elasticsearch/xpack/autoscaling/AutoscalingExtension.java

@@ -0,0 +1,20 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+package org.elasticsearch.xpack.autoscaling;
+
+import org.elasticsearch.xpack.autoscaling.decision.AutoscalingDeciderConfiguration;
+import org.elasticsearch.xpack.autoscaling.decision.AutoscalingDeciderService;
+
+import java.util.Collection;
+
+public interface AutoscalingExtension {
+    /**
+     * Get the list of decider services for this plugin. This is called after createComponents has been called.
+     * @return list of decider services
+     */
+    Collection<AutoscalingDeciderService<? extends AutoscalingDeciderConfiguration>> deciders();
+}

+ 8 - 4
x-pack/plugin/autoscaling/src/main/java/org/elasticsearch/xpack/autoscaling/action/TransportGetAutoscalingDecisionAction.java

@@ -18,22 +18,24 @@ import org.elasticsearch.common.io.stream.StreamInput;
 import org.elasticsearch.tasks.Task;
 import org.elasticsearch.threadpool.ThreadPool;
 import org.elasticsearch.transport.TransportService;
+import org.elasticsearch.xpack.autoscaling.decision.AutoscalingDecisionService;
 
 import java.io.IOException;
-import java.util.Collections;
-import java.util.TreeMap;
 
 public class TransportGetAutoscalingDecisionAction extends TransportMasterNodeAction<
     GetAutoscalingDecisionAction.Request,
     GetAutoscalingDecisionAction.Response> {
 
+    private final AutoscalingDecisionService decisionService;
+
     @Inject
     public TransportGetAutoscalingDecisionAction(
         final TransportService transportService,
         final ClusterService clusterService,
         final ThreadPool threadPool,
         final ActionFilters actionFilters,
-        final IndexNameExpressionResolver indexNameExpressionResolver
+        final IndexNameExpressionResolver indexNameExpressionResolver,
+        final AutoscalingDecisionService.Holder decisionServiceHolder
     ) {
         super(
             GetAutoscalingDecisionAction.NAME,
@@ -44,6 +46,8 @@ public class TransportGetAutoscalingDecisionAction extends TransportMasterNodeAc
             GetAutoscalingDecisionAction.Request::new,
             indexNameExpressionResolver
         );
+        this.decisionService = decisionServiceHolder.get();
+        assert this.decisionService != null;
     }
 
     @Override
@@ -63,7 +67,7 @@ public class TransportGetAutoscalingDecisionAction extends TransportMasterNodeAc
         final ClusterState state,
         final ActionListener<GetAutoscalingDecisionAction.Response> listener
     ) {
-        listener.onResponse(new GetAutoscalingDecisionAction.Response(Collections.unmodifiableSortedMap(new TreeMap<>())));
+        listener.onResponse(new GetAutoscalingDecisionAction.Response(decisionService.decide(state)));
     }
 
     @Override

+ 8 - 10
x-pack/plugin/autoscaling/src/main/java/org/elasticsearch/xpack/autoscaling/decision/AlwaysAutoscalingDecider.java → x-pack/plugin/autoscaling/src/main/java/org/elasticsearch/xpack/autoscaling/decision/AlwaysAutoscalingDeciderConfiguration.java

@@ -14,20 +14,23 @@ import org.elasticsearch.common.xcontent.XContentParser;
 
 import java.io.IOException;
 
-public class AlwaysAutoscalingDecider implements AutoscalingDecider {
+public class AlwaysAutoscalingDeciderConfiguration implements AutoscalingDeciderConfiguration {
 
     public static final String NAME = "always";
 
-    private static final ObjectParser<AlwaysAutoscalingDecider, Void> PARSER = new ObjectParser<>(NAME, AlwaysAutoscalingDecider::new);
+    private static final ObjectParser<AlwaysAutoscalingDeciderConfiguration, Void> PARSER = new ObjectParser<>(
+        NAME,
+        AlwaysAutoscalingDeciderConfiguration::new
+    );
 
-    public static AlwaysAutoscalingDecider parse(final XContentParser parser) {
+    public static AlwaysAutoscalingDeciderConfiguration parse(final XContentParser parser) {
         return PARSER.apply(parser, null);
     }
 
-    public AlwaysAutoscalingDecider() {}
+    public AlwaysAutoscalingDeciderConfiguration() {}
 
     @SuppressWarnings("unused")
-    public AlwaysAutoscalingDecider(final StreamInput in) {
+    public AlwaysAutoscalingDeciderConfiguration(final StreamInput in) {
 
     }
 
@@ -36,11 +39,6 @@ public class AlwaysAutoscalingDecider implements AutoscalingDecider {
         return NAME;
     }
 
-    @Override
-    public AutoscalingDecision scale() {
-        return new AutoscalingDecision(NAME, AutoscalingDecisionType.SCALE_UP, "always");
-    }
-
     @Override
     public String getWriteableName() {
         return NAME;

+ 25 - 0
x-pack/plugin/autoscaling/src/main/java/org/elasticsearch/xpack/autoscaling/decision/AlwaysAutoscalingDeciderService.java

@@ -0,0 +1,25 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+package org.elasticsearch.xpack.autoscaling.decision;
+
+import org.elasticsearch.common.inject.Inject;
+
+public class AlwaysAutoscalingDeciderService implements AutoscalingDeciderService<AlwaysAutoscalingDeciderConfiguration> {
+
+    @Inject
+    public AlwaysAutoscalingDeciderService() {}
+
+    @Override
+    public String name() {
+        return AlwaysAutoscalingDeciderConfiguration.NAME;
+    }
+
+    @Override
+    public AutoscalingDecision scale(AlwaysAutoscalingDeciderConfiguration decider, AutoscalingDeciderContext context) {
+        return new AutoscalingDecision(AlwaysAutoscalingDeciderConfiguration.NAME, AutoscalingDecisionType.SCALE_UP, "always");
+    }
+}

+ 1 - 9
x-pack/plugin/autoscaling/src/main/java/org/elasticsearch/xpack/autoscaling/decision/AutoscalingDecider.java → x-pack/plugin/autoscaling/src/main/java/org/elasticsearch/xpack/autoscaling/decision/AutoscalingDeciderConfiguration.java

@@ -12,7 +12,7 @@ import org.elasticsearch.common.xcontent.ToXContentObject;
 /**
  * Represents an autoscaling decider, a component that determines whether or not to scale.
  */
-public interface AutoscalingDecider extends ToXContentObject, NamedWriteable {
+public interface AutoscalingDeciderConfiguration extends ToXContentObject, NamedWriteable {
 
     /**
      * The name of the autoscaling decider.
@@ -20,12 +20,4 @@ public interface AutoscalingDecider extends ToXContentObject, NamedWriteable {
      * @return the name
      */
     String name();
-
-    /**
-     * Whether or not to scale based on the current state.
-     *
-     * @return the autoscaling decision
-     */
-    AutoscalingDecision scale();
-
 }

+ 13 - 0
x-pack/plugin/autoscaling/src/main/java/org/elasticsearch/xpack/autoscaling/decision/AutoscalingDeciderContext.java

@@ -0,0 +1,13 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+package org.elasticsearch.xpack.autoscaling.decision;
+
+import org.elasticsearch.cluster.ClusterState;
+
+public interface AutoscalingDeciderContext {
+    ClusterState state();
+}

+ 29 - 0
x-pack/plugin/autoscaling/src/main/java/org/elasticsearch/xpack/autoscaling/decision/AutoscalingDeciderService.java

@@ -0,0 +1,29 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+package org.elasticsearch.xpack.autoscaling.decision;
+
+/**
+ * A service to decide for a specific decider.
+ */
+public interface AutoscalingDeciderService<D extends AutoscalingDeciderConfiguration> {
+
+    /**
+     * The name of the autoscaling decider.
+     *
+     * @return the name
+     */
+    String name();
+
+    /**
+     * Whether or not to scale based on the current state.
+     *
+     * @param context provides access to information about current state
+     * @return the autoscaling decision
+     */
+    AutoscalingDecision scale(D decider, AutoscalingDeciderContext context);
+
+}

+ 84 - 0
x-pack/plugin/autoscaling/src/main/java/org/elasticsearch/xpack/autoscaling/decision/AutoscalingDecisionService.java

@@ -0,0 +1,84 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+package org.elasticsearch.xpack.autoscaling.decision;
+
+import org.apache.lucene.util.SetOnce;
+import org.elasticsearch.cluster.ClusterState;
+import org.elasticsearch.common.collect.Tuple;
+import org.elasticsearch.xpack.autoscaling.Autoscaling;
+import org.elasticsearch.xpack.autoscaling.AutoscalingMetadata;
+import org.elasticsearch.xpack.autoscaling.policy.AutoscalingPolicy;
+
+import java.util.Collection;
+import java.util.Map;
+import java.util.Set;
+import java.util.SortedMap;
+import java.util.TreeMap;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+
+public class AutoscalingDecisionService {
+    private Map<String, AutoscalingDeciderService<? extends AutoscalingDeciderConfiguration>> deciderByName;
+
+    public AutoscalingDecisionService(Set<AutoscalingDeciderService<? extends AutoscalingDeciderConfiguration>> deciders) {
+        assert deciders.size() >= 1; // always have always
+        this.deciderByName = deciders.stream().collect(Collectors.toMap(AutoscalingDeciderService::name, Function.identity()));
+    }
+
+    public static class Holder {
+        private final Autoscaling autoscaling;
+        private final SetOnce<AutoscalingDecisionService> servicesSetOnce = new SetOnce<>();
+
+        public Holder(Autoscaling autoscaling) {
+            this.autoscaling = autoscaling;
+        }
+
+        public AutoscalingDecisionService get() {
+            // defer constructing services until transport action creation time.
+            AutoscalingDecisionService autoscalingDecisionService = servicesSetOnce.get();
+            if (autoscalingDecisionService == null) {
+                autoscalingDecisionService = new AutoscalingDecisionService(autoscaling.createDeciderServices());
+                servicesSetOnce.set(autoscalingDecisionService);
+            }
+
+            return autoscalingDecisionService;
+        }
+    }
+
+    public SortedMap<String, AutoscalingDecisions> decide(ClusterState state) {
+        AutoscalingDeciderContext context = () -> state;
+
+        AutoscalingMetadata autoscalingMetadata = state.metadata().custom(AutoscalingMetadata.NAME);
+        if (autoscalingMetadata != null) {
+            return new TreeMap<>(
+                autoscalingMetadata.policies()
+                    .entrySet()
+                    .stream()
+                    .map(e -> Tuple.tuple(e.getKey(), getDecision(e.getValue().policy(), context)))
+                    .collect(Collectors.toMap(Tuple::v1, Tuple::v2))
+            );
+        } else {
+            return new TreeMap<>();
+        }
+    }
+
+    private AutoscalingDecisions getDecision(AutoscalingPolicy policy, AutoscalingDeciderContext context) {
+        Collection<AutoscalingDecision> decisions = policy.deciders()
+            .values()
+            .stream()
+            .map(decider -> getDecision(decider, context))
+            .collect(Collectors.toList());
+        return new AutoscalingDecisions(decisions);
+    }
+
+    private <T extends AutoscalingDeciderConfiguration> AutoscalingDecision getDecision(T decider, AutoscalingDeciderContext context) {
+        assert deciderByName.containsKey(decider.name());
+        @SuppressWarnings("unchecked")
+        AutoscalingDeciderService<T> service = (AutoscalingDeciderService<T>) deciderByName.get(decider.name());
+        return service.scale(decider, context);
+    }
+}

+ 4 - 3
x-pack/plugin/autoscaling/src/main/java/org/elasticsearch/xpack/autoscaling/decision/AutoscalingDecisions.java

@@ -46,9 +46,10 @@ public class AutoscalingDecisions implements ToXContent, Writeable {
 
     @Override
     public XContentBuilder toXContent(final XContentBuilder builder, final Params params) throws IOException {
-        for (final AutoscalingDecision decision : decisions) {
-            decision.toXContent(builder, params);
-        }
+        builder.startObject();
+        builder.field("decision", type());
+        builder.array("decisions", decisions.toArray());
+        builder.endObject();
         return builder;
     }
 

+ 9 - 9
x-pack/plugin/autoscaling/src/main/java/org/elasticsearch/xpack/autoscaling/policy/AutoscalingPolicy.java

@@ -15,7 +15,7 @@ import org.elasticsearch.common.xcontent.ConstructingObjectParser;
 import org.elasticsearch.common.xcontent.ToXContentObject;
 import org.elasticsearch.common.xcontent.XContentBuilder;
 import org.elasticsearch.common.xcontent.XContentParser;
-import org.elasticsearch.xpack.autoscaling.decision.AutoscalingDecider;
+import org.elasticsearch.xpack.autoscaling.decision.AutoscalingDeciderConfiguration;
 
 import java.io.IOException;
 import java.util.AbstractMap;
@@ -38,7 +38,7 @@ public class AutoscalingPolicy extends AbstractDiffable<AutoscalingPolicy> imple
     static {
         PARSER = new ConstructingObjectParser<>(NAME, false, (c, name) -> {
             @SuppressWarnings("unchecked")
-            final List<Map.Entry<String, AutoscalingDecider>> deciders = (List<Map.Entry<String, AutoscalingDecider>>) c[0];
+            final var deciders = (List<Map.Entry<String, AutoscalingDeciderConfiguration>>) c[0];
             return new AutoscalingPolicy(
                 name,
                 new TreeMap<>(deciders.stream().collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)))
@@ -46,7 +46,7 @@ public class AutoscalingPolicy extends AbstractDiffable<AutoscalingPolicy> imple
         });
         PARSER.declareNamedObjects(
             ConstructingObjectParser.constructorArg(),
-            (p, c, n) -> new AbstractMap.SimpleEntry<>(n, p.namedObject(AutoscalingDecider.class, n, null)),
+            (p, c, n) -> new AbstractMap.SimpleEntry<>(n, p.namedObject(AutoscalingDeciderConfiguration.class, n, null)),
             DECIDERS_FIELD
         );
     }
@@ -61,13 +61,13 @@ public class AutoscalingPolicy extends AbstractDiffable<AutoscalingPolicy> imple
         return name;
     }
 
-    private final SortedMap<String, AutoscalingDecider> deciders;
+    private final SortedMap<String, AutoscalingDeciderConfiguration> deciders;
 
-    public SortedMap<String, AutoscalingDecider> deciders() {
+    public SortedMap<String, AutoscalingDeciderConfiguration> deciders() {
         return deciders;
     }
 
-    public AutoscalingPolicy(final String name, final SortedMap<String, AutoscalingDecider> deciders) {
+    public AutoscalingPolicy(final String name, final SortedMap<String, AutoscalingDeciderConfiguration> deciders) {
         this.name = Objects.requireNonNull(name);
         // TODO: validate that the policy deciders are non-empty
         this.deciders = Objects.requireNonNull(deciders);
@@ -76,9 +76,9 @@ public class AutoscalingPolicy extends AbstractDiffable<AutoscalingPolicy> imple
     public AutoscalingPolicy(final StreamInput in) throws IOException {
         name = in.readString();
         deciders = new TreeMap<>(
-            in.readNamedWriteableList(AutoscalingDecider.class)
+            in.readNamedWriteableList(AutoscalingDeciderConfiguration.class)
                 .stream()
-                .collect(Collectors.toMap(AutoscalingDecider::name, Function.identity()))
+                .collect(Collectors.toMap(AutoscalingDeciderConfiguration::name, Function.identity()))
         );
     }
 
@@ -94,7 +94,7 @@ public class AutoscalingPolicy extends AbstractDiffable<AutoscalingPolicy> imple
         {
             builder.startObject(DECIDERS_FIELD.getPreferredName());
             {
-                for (final Map.Entry<String, AutoscalingDecider> entry : deciders.entrySet()) {
+                for (final Map.Entry<String, AutoscalingDeciderConfiguration> entry : deciders.entrySet()) {
                     builder.field(entry.getKey(), entry.getValue());
                 }
             }

+ 10 - 6
x-pack/plugin/autoscaling/src/test/java/org/elasticsearch/xpack/autoscaling/AutoscalingTestCase.java

@@ -11,8 +11,8 @@ import org.elasticsearch.common.io.stream.NamedWriteableRegistry;
 import org.elasticsearch.common.settings.Settings;
 import org.elasticsearch.common.xcontent.NamedXContentRegistry;
 import org.elasticsearch.test.ESTestCase;
-import org.elasticsearch.xpack.autoscaling.decision.AlwaysAutoscalingDecider;
-import org.elasticsearch.xpack.autoscaling.decision.AutoscalingDecider;
+import org.elasticsearch.xpack.autoscaling.decision.AlwaysAutoscalingDeciderConfiguration;
+import org.elasticsearch.xpack.autoscaling.decision.AutoscalingDeciderConfiguration;
 import org.elasticsearch.xpack.autoscaling.decision.AutoscalingDecision;
 import org.elasticsearch.xpack.autoscaling.decision.AutoscalingDecisionType;
 import org.elasticsearch.xpack.autoscaling.decision.AutoscalingDecisions;
@@ -68,9 +68,11 @@ public abstract class AutoscalingTestCase extends ESTestCase {
         return new AutoscalingDecisions(decisions);
     }
 
-    public static SortedMap<String, AutoscalingDecider> randomAutoscalingDeciders() {
+    public static SortedMap<String, AutoscalingDeciderConfiguration> randomAutoscalingDeciders() {
         return new TreeMap<>(
-            List.of(new AlwaysAutoscalingDecider()).stream().collect(Collectors.toMap(AutoscalingDecider::name, Function.identity()))
+            List.of(new AlwaysAutoscalingDeciderConfiguration())
+                .stream()
+                .collect(Collectors.toMap(AutoscalingDeciderConfiguration::name, Function.identity()))
         );
     }
 
@@ -83,7 +85,7 @@ public abstract class AutoscalingTestCase extends ESTestCase {
     }
 
     public static AutoscalingPolicy mutateAutoscalingPolicy(final AutoscalingPolicy instance) {
-        final SortedMap<String, AutoscalingDecider> deciders;
+        final SortedMap<String, AutoscalingDeciderConfiguration> deciders;
         if (randomBoolean()) {
             // if the policy name did not change, or randomly, use a mutated set of deciders
             deciders = mutateAutoscalingDeciders(instance.deciders());
@@ -93,7 +95,9 @@ public abstract class AutoscalingTestCase extends ESTestCase {
         return new AutoscalingPolicy(randomValueOtherThan(instance.name(), () -> randomAlphaOfLength(8)), deciders);
     }
 
-    public static SortedMap<String, AutoscalingDecider> mutateAutoscalingDeciders(final SortedMap<String, AutoscalingDecider> deciders) {
+    public static SortedMap<String, AutoscalingDeciderConfiguration> mutateAutoscalingDeciders(
+        final SortedMap<String, AutoscalingDeciderConfiguration> deciders
+    ) {
         if (deciders.size() == 0) {
             return randomAutoscalingDeciders();
         } else {

+ 56 - 0
x-pack/plugin/autoscaling/src/test/java/org/elasticsearch/xpack/autoscaling/action/GetAutoscalingDecisionActionResponseTests.java

@@ -0,0 +1,56 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+package org.elasticsearch.xpack.autoscaling.action;
+
+import org.elasticsearch.common.bytes.BytesReference;
+import org.elasticsearch.common.collect.Tuple;
+import org.elasticsearch.common.xcontent.XContentBuilder;
+import org.elasticsearch.common.xcontent.XContentType;
+import org.elasticsearch.xpack.autoscaling.AutoscalingTestCase;
+import org.elasticsearch.xpack.autoscaling.decision.AutoscalingDecisions;
+import org.hamcrest.Matchers;
+
+import java.io.IOException;
+import java.util.Map;
+import java.util.Set;
+import java.util.SortedMap;
+import java.util.TreeMap;
+import java.util.stream.Collectors;
+import java.util.stream.IntStream;
+
+public class GetAutoscalingDecisionActionResponseTests extends AutoscalingTestCase {
+
+    public void testToXContent() throws IOException {
+        Set<String> policyNames = IntStream.range(0, randomIntBetween(1, 10))
+            .mapToObj(i -> randomAlphaOfLength(10))
+            .collect(Collectors.toSet());
+
+        SortedMap<String, AutoscalingDecisions> decisions = new TreeMap<>(
+            policyNames.stream().map(s -> Tuple.tuple(s, randomAutoscalingDecisions())).collect(Collectors.toMap(Tuple::v1, Tuple::v2))
+        );
+
+        GetAutoscalingDecisionAction.Response response = new GetAutoscalingDecisionAction.Response(decisions);
+        XContentType xContentType = randomFrom(XContentType.values());
+
+        XContentBuilder builder = XContentBuilder.builder(xContentType.xContent());
+        response.toXContent(builder, null);
+        BytesReference responseBytes = BytesReference.bytes(builder);
+
+        XContentBuilder expected = XContentBuilder.builder(xContentType.xContent());
+        expected.startObject();
+        expected.startArray("decisions");
+        for (Map.Entry<String, AutoscalingDecisions> entry : decisions.entrySet()) {
+            expected.startObject();
+            expected.field(entry.getKey(), entry.getValue());
+            expected.endObject();
+        }
+        expected.endArray();
+        expected.endObject();
+        BytesReference expectedBytes = BytesReference.bytes(expected);
+        assertThat(responseBytes, Matchers.equalTo(expectedBytes));
+    }
+}

+ 65 - 0
x-pack/plugin/autoscaling/src/test/java/org/elasticsearch/xpack/autoscaling/decision/AutoscalingDecisionServiceTests.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;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+package org.elasticsearch.xpack.autoscaling.decision;
+
+import org.elasticsearch.cluster.ClusterName;
+import org.elasticsearch.cluster.ClusterState;
+import org.elasticsearch.cluster.metadata.Metadata;
+import org.elasticsearch.common.collect.Tuple;
+import org.elasticsearch.xpack.autoscaling.AutoscalingMetadata;
+import org.elasticsearch.xpack.autoscaling.AutoscalingTestCase;
+import org.elasticsearch.xpack.autoscaling.policy.AutoscalingPolicy;
+import org.elasticsearch.xpack.autoscaling.policy.AutoscalingPolicyMetadata;
+import org.hamcrest.Matchers;
+
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.SortedMap;
+import java.util.TreeMap;
+import java.util.stream.Collectors;
+import java.util.stream.IntStream;
+
+public class AutoscalingDecisionServiceTests extends AutoscalingTestCase {
+    public void testAlwaysDecision() {
+        AutoscalingDecisionService service = new AutoscalingDecisionService(Set.of(new AlwaysAutoscalingDeciderService()));
+        Set<String> policyNames = IntStream.range(0, randomIntBetween(1, 10))
+            .mapToObj(i -> randomAlphaOfLength(10))
+            .collect(Collectors.toSet());
+        SortedMap<String, AutoscalingDeciderConfiguration> deciders = new TreeMap<>(
+            Map.of(AlwaysAutoscalingDeciderConfiguration.NAME, new AlwaysAutoscalingDeciderConfiguration())
+        );
+        SortedMap<String, AutoscalingPolicyMetadata> policies = new TreeMap<>(
+            policyNames.stream()
+                .map(s -> Tuple.tuple(s, new AutoscalingPolicyMetadata(new AutoscalingPolicy(s, deciders))))
+                .collect(Collectors.toMap(Tuple::v1, Tuple::v2))
+        );
+        ClusterState state = ClusterState.builder(ClusterName.DEFAULT)
+            .metadata(Metadata.builder().putCustom(AutoscalingMetadata.NAME, new AutoscalingMetadata(policies)))
+            .build();
+        SortedMap<String, AutoscalingDecisions> decisions = service.decide(state);
+        SortedMap<String, AutoscalingDecisions> expected = new TreeMap<>(
+            policyNames.stream()
+                .map(
+                    s -> Tuple.tuple(
+                        s,
+                        new AutoscalingDecisions(
+                            List.of(
+                                new AutoscalingDecision(
+                                    AlwaysAutoscalingDeciderConfiguration.NAME,
+                                    AutoscalingDecisionType.SCALE_UP,
+                                    "always"
+                                )
+                            )
+                        )
+                    )
+                )
+                .collect(Collectors.toMap(Tuple::v1, Tuple::v2))
+        );
+        assertThat(decisions, Matchers.equalTo(expected));
+    }
+}