Browse Source

Introduce autoscaling decisions (#53934)

This is the first in a series of commits that will introduce the
autoscaling deciders framework. This commit introduces the basic
framework for representing autoscaling decisions.
Jason Tedor 5 years ago
parent
commit
94e81b0aa0

+ 1 - 1
docs/reference/autoscaling/apis/get-autoscaling-decision.asciidoc

@@ -47,6 +47,6 @@ The API returns the following result:
 [source,console-result]
 --------------------------------------------------
 {
-
+  decisions: []
 }
 --------------------------------------------------

+ 85 - 0
x-pack/plugin/autoscaling/src/main/java/org/elasticsearch/xpack/autoscaling/AutoscalingDecision.java

@@ -0,0 +1,85 @@
+/*
+ * 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.common.io.stream.StreamInput;
+import org.elasticsearch.common.io.stream.StreamOutput;
+import org.elasticsearch.common.io.stream.Writeable;
+import org.elasticsearch.common.xcontent.ToXContent;
+import org.elasticsearch.common.xcontent.XContentBuilder;
+
+import java.io.IOException;
+import java.util.Objects;
+
+/**
+ * Represents an autoscaling decision.
+ */
+public class AutoscalingDecision implements ToXContent, Writeable {
+
+    private final String name;
+
+    public String name() {
+        return name;
+    }
+
+    private final AutoscalingDecisionType type;
+
+    public AutoscalingDecisionType type() {
+        return type;
+    }
+
+    private final String reason;
+
+    public String reason() {
+        return reason;
+    }
+
+    public AutoscalingDecision(final String name, final AutoscalingDecisionType type, final String reason) {
+        this.name = Objects.requireNonNull(name);
+        this.type = Objects.requireNonNull(type);
+        this.reason = Objects.requireNonNull(reason);
+    }
+
+    public AutoscalingDecision(final StreamInput in) throws IOException {
+        this.name = in.readString();
+        this.type = AutoscalingDecisionType.readFrom(in);
+        this.reason = in.readString();
+    }
+
+    @Override
+    public void writeTo(final StreamOutput out) throws IOException {
+        out.writeString(name);
+        type.writeTo(out);
+        out.writeString(reason);
+    }
+
+    @Override
+    public XContentBuilder toXContent(final XContentBuilder builder, final ToXContent.Params params) throws IOException {
+        builder.startObject();
+        {
+            builder.field("name", name);
+            builder.field("type", type);
+            builder.field("reason", reason);
+        }
+        builder.endObject();
+        return builder;
+    }
+
+    @Override
+    public boolean equals(final Object o) {
+        if (this == o) return true;
+        if (o == null || getClass() != o.getClass()) return false;
+        final AutoscalingDecision that = (AutoscalingDecision) o;
+        return name.equals(that.name) && type == that.type && reason.equals(that.reason);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(name, type, reason);
+    }
+
+}

+ 73 - 0
x-pack/plugin/autoscaling/src/main/java/org/elasticsearch/xpack/autoscaling/AutoscalingDecisionType.java

@@ -0,0 +1,73 @@
+/*
+ * 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.common.io.stream.StreamInput;
+import org.elasticsearch.common.io.stream.StreamOutput;
+import org.elasticsearch.common.io.stream.Writeable;
+import org.elasticsearch.common.xcontent.ToXContentFragment;
+import org.elasticsearch.common.xcontent.XContentBuilder;
+
+import java.io.IOException;
+import java.util.Locale;
+
+/**
+ * Represents the type of an autoscaling decision: to indicating if a scale down, no scaling event, or a scale up is needed.
+ */
+public enum AutoscalingDecisionType implements Writeable, ToXContentFragment {
+
+    /**
+     * Indicates that a scale down event is needed.
+     */
+    SCALE_DOWN((byte) 0),
+
+    /**
+     * Indicates that no scaling event is needed.
+     */
+    NO_SCALE((byte) 1),
+
+    /**
+     * Indicates that a scale up event is needed.
+     */
+    SCALE_UP((byte) 2);
+
+    private final byte id;
+
+    byte id() {
+        return id;
+    }
+
+    AutoscalingDecisionType(final byte id) {
+        this.id = id;
+    }
+
+    public static AutoscalingDecisionType readFrom(final StreamInput in) throws IOException {
+        final byte id = in.readByte();
+        switch (id) {
+            case 0:
+                return SCALE_DOWN;
+            case 1:
+                return NO_SCALE;
+            case 2:
+                return SCALE_UP;
+            default:
+                throw new IllegalArgumentException("unexpected value [" + id + "] for autoscaling decision type");
+        }
+    }
+
+    @Override
+    public void writeTo(final StreamOutput out) throws IOException {
+        out.writeByte(id);
+    }
+
+    @Override
+    public XContentBuilder toXContent(final XContentBuilder builder, final Params params) throws IOException {
+        builder.value(name().toLowerCase(Locale.ROOT));
+        return builder;
+    }
+
+}

+ 77 - 0
x-pack/plugin/autoscaling/src/main/java/org/elasticsearch/xpack/autoscaling/AutoscalingDecisions.java

@@ -0,0 +1,77 @@
+/*
+ * 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.common.io.stream.StreamInput;
+import org.elasticsearch.common.io.stream.StreamOutput;
+import org.elasticsearch.common.io.stream.Writeable;
+import org.elasticsearch.common.xcontent.ToXContent;
+import org.elasticsearch.common.xcontent.XContentBuilder;
+
+import java.io.IOException;
+import java.util.Collection;
+import java.util.Objects;
+
+/**
+ * Represents a collection of individual autoscaling decisions that can be aggregated into a single autoscaling decision.
+ */
+public class AutoscalingDecisions implements ToXContent, Writeable {
+
+    private final Collection<AutoscalingDecision> decisions;
+
+    public AutoscalingDecisions(final Collection<AutoscalingDecision> decisions) {
+        Objects.requireNonNull(decisions);
+        if (decisions.isEmpty()) {
+            throw new IllegalArgumentException("decisions can not be empty");
+        }
+        this.decisions = decisions;
+    }
+
+    public AutoscalingDecisions(final StreamInput in) throws IOException {
+        this.decisions = in.readList(AutoscalingDecision::new);
+    }
+
+    @Override
+    public void writeTo(final StreamOutput out) throws IOException {
+        out.writeCollection(decisions);
+    }
+
+    @Override
+    public XContentBuilder toXContent(final XContentBuilder builder, final Params params) throws IOException {
+        for (final AutoscalingDecision decision : decisions) {
+            decision.toXContent(builder, params);
+        }
+        return builder;
+    }
+
+    public AutoscalingDecisionType type() {
+        if (decisions.stream().anyMatch(p -> p.type() == AutoscalingDecisionType.SCALE_UP)) {
+            // if any deciders say to scale up
+            return AutoscalingDecisionType.SCALE_UP;
+        } else if (decisions.stream().allMatch(p -> p.type() == AutoscalingDecisionType.SCALE_DOWN)) {
+            // if all deciders say to scale down
+            return AutoscalingDecisionType.SCALE_DOWN;
+        } else {
+            // otherwise, do not scale
+            return AutoscalingDecisionType.NO_SCALE;
+        }
+    }
+
+    @Override
+    public boolean equals(final Object o) {
+        if (this == o) return true;
+        if (o == null || getClass() != o.getClass()) return false;
+        final org.elasticsearch.xpack.autoscaling.AutoscalingDecisions that = (org.elasticsearch.xpack.autoscaling.AutoscalingDecisions) o;
+        return decisions.equals(that.decisions);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(decisions);
+    }
+
+}

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

@@ -14,8 +14,13 @@ import org.elasticsearch.common.io.stream.StreamInput;
 import org.elasticsearch.common.io.stream.StreamOutput;
 import org.elasticsearch.common.xcontent.ToXContentObject;
 import org.elasticsearch.common.xcontent.XContentBuilder;
+import org.elasticsearch.xpack.autoscaling.AutoscalingDecisions;
 
 import java.io.IOException;
+import java.util.Map;
+import java.util.Objects;
+import java.util.SortedMap;
+import java.util.TreeMap;
 
 public class GetAutoscalingDecisionAction extends ActionType<GetAutoscalingDecisionAction.Response> {
 
@@ -60,24 +65,37 @@ public class GetAutoscalingDecisionAction extends ActionType<GetAutoscalingDecis
 
     public static class Response extends ActionResponse implements ToXContentObject {
 
-        public Response() {
+        private final SortedMap<String, AutoscalingDecisions> decisions;
 
+        public Response(final SortedMap<String, AutoscalingDecisions> decisions) {
+            this.decisions = Objects.requireNonNull(decisions);
         }
 
         public Response(final StreamInput in) throws IOException {
             super(in);
+            decisions = new TreeMap<>(in.readMap(StreamInput::readString, AutoscalingDecisions::new));
         }
 
         @Override
-        public void writeTo(final StreamOutput out) {
-
+        public void writeTo(final StreamOutput out) throws IOException {
+            out.writeMap(decisions, StreamOutput::writeString, (o, decision) -> decision.writeTo(o));
         }
 
         @Override
         public XContentBuilder toXContent(final XContentBuilder builder, final Params params) throws IOException {
             builder.startObject();
             {
-
+                builder.startArray("decisions");
+                {
+                    for (final Map.Entry<String, AutoscalingDecisions> decision : decisions.entrySet()) {
+                        builder.startObject();
+                        {
+                            builder.field(decision.getKey(), decision.getValue());
+                        }
+                        builder.endObject();
+                    }
+                }
+                builder.endArray();
             }
             builder.endObject();
             return builder;

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

@@ -20,6 +20,8 @@ import org.elasticsearch.threadpool.ThreadPool;
 import org.elasticsearch.transport.TransportService;
 
 import java.io.IOException;
+import java.util.Collections;
+import java.util.TreeMap;
 
 public class TransportGetAutoscalingDecisionAction extends TransportMasterNodeAction<
     GetAutoscalingDecisionAction.Request,
@@ -61,7 +63,7 @@ public class TransportGetAutoscalingDecisionAction extends TransportMasterNodeAc
         final ClusterState state,
         final ActionListener<GetAutoscalingDecisionAction.Response> listener
     ) {
-        listener.onResponse(new GetAutoscalingDecisionAction.Response());
+        listener.onResponse(new GetAutoscalingDecisionAction.Response(Collections.unmodifiableSortedMap(new TreeMap<>())));
     }
 
     @Override

+ 31 - 0
x-pack/plugin/autoscaling/src/test/java/org/elasticsearch/xpack/autoscaling/AutoscalingDecisionTests.java

@@ -0,0 +1,31 @@
+/*
+ * 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.common.io.stream.BytesStreamOutput;
+
+import java.io.IOException;
+
+import static org.hamcrest.Matchers.equalTo;
+
+public class AutoscalingDecisionTests extends AutoscalingTestCase {
+
+    public void testAutoscalingDecisionType() {
+        final AutoscalingDecisionType type = randomFrom(AutoscalingDecisionType.values());
+        final AutoscalingDecision decision = randomAutoscalingDecisionOfType(type);
+        assertThat(decision.type(), equalTo(type));
+    }
+
+    public void testAutoscalingDecisionTypeSerialization() throws IOException {
+        final AutoscalingDecisionType before = randomFrom(AutoscalingDecisionType.values());
+        final BytesStreamOutput out = new BytesStreamOutput();
+        before.writeTo(out);
+        final AutoscalingDecisionType after = AutoscalingDecisionType.readFrom(out.bytes().streamInput());
+        assertThat(after, equalTo(before));
+    }
+
+}

+ 54 - 0
x-pack/plugin/autoscaling/src/test/java/org/elasticsearch/xpack/autoscaling/AutoscalingDecisionTypeWireSerializingTests.java

@@ -0,0 +1,54 @@
+/*
+ * 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.common.io.stream.BytesStreamOutput;
+import org.elasticsearch.common.io.stream.Writeable;
+import org.elasticsearch.test.AbstractWireSerializingTestCase;
+import org.elasticsearch.test.ESTestCase;
+
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+import static org.hamcrest.Matchers.equalTo;
+
+public class AutoscalingDecisionTypeWireSerializingTests extends AbstractWireSerializingTestCase<AutoscalingDecisionType> {
+
+    @Override
+    protected Writeable.Reader<AutoscalingDecisionType> instanceReader() {
+        return AutoscalingDecisionType::readFrom;
+    }
+
+    @Override
+    protected AutoscalingDecisionType createTestInstance() {
+        return randomFrom(AutoscalingDecisionType.values());
+    }
+
+    @Override
+    protected void assertEqualInstances(final AutoscalingDecisionType expectedInstance, final AutoscalingDecisionType newInstance) {
+        assertSame(expectedInstance, newInstance);
+        assertEquals(expectedInstance, newInstance);
+        assertEquals(expectedInstance.hashCode(), newInstance.hashCode());
+    }
+
+    public void testInvalidAutoscalingDecisionTypeSerialization() throws IOException {
+        final BytesStreamOutput out = new BytesStreamOutput();
+        final Set<Byte> values = Arrays.stream(AutoscalingDecisionType.values())
+            .map(AutoscalingDecisionType::id)
+            .collect(Collectors.toSet());
+        final byte value = randomValueOtherThanMany(values::contains, ESTestCase::randomByte);
+        out.writeByte(value);
+        final IllegalArgumentException e = expectThrows(
+            IllegalArgumentException.class,
+            () -> AutoscalingDecisionType.readFrom(out.bytes().streamInput())
+        );
+        assertThat(e.getMessage(), equalTo("unexpected value [" + value + "] for autoscaling decision type"));
+    }
+
+}

+ 24 - 0
x-pack/plugin/autoscaling/src/test/java/org/elasticsearch/xpack/autoscaling/AutoscalingDecisionWireSerializingTests.java

@@ -0,0 +1,24 @@
+/*
+ * 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.common.io.stream.Writeable;
+import org.elasticsearch.test.AbstractWireSerializingTestCase;
+
+public class AutoscalingDecisionWireSerializingTests extends AbstractWireSerializingTestCase<AutoscalingDecision> {
+
+    @Override
+    protected Writeable.Reader<AutoscalingDecision> instanceReader() {
+        return AutoscalingDecision::new;
+    }
+
+    @Override
+    protected AutoscalingDecision createTestInstance() {
+        return AutoscalingTestCase.randomAutoscalingDecision();
+    }
+
+}

+ 35 - 0
x-pack/plugin/autoscaling/src/test/java/org/elasticsearch/xpack/autoscaling/AutoscalingDecisionsTests.java

@@ -0,0 +1,35 @@
+/*
+ * 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 java.util.List;
+
+import static org.hamcrest.Matchers.equalTo;
+
+public class AutoscalingDecisionsTests extends AutoscalingTestCase {
+
+    public void testAutoscalingDecisionsRejectsEmptyDecisions() {
+        final IllegalArgumentException e = expectThrows(IllegalArgumentException.class, () -> new AutoscalingDecisions(List.of()));
+        assertThat(e.getMessage(), equalTo("decisions can not be empty"));
+    }
+
+    public void testAutoscalingDecisionsTypeDown() {
+        final AutoscalingDecisions decisions = randomAutoscalingDecisions(randomIntBetween(1, 8), 0, 0);
+        assertThat(decisions.type(), equalTo(AutoscalingDecisionType.SCALE_DOWN));
+    }
+
+    public void testAutoscalingDecisionsTypeNo() {
+        final AutoscalingDecisions decision = randomAutoscalingDecisions(randomIntBetween(0, 8), randomIntBetween(1, 8), 0);
+        assertThat(decision.type(), equalTo(AutoscalingDecisionType.NO_SCALE));
+    }
+
+    public void testAutoscalingDecisionsTypeUp() {
+        final AutoscalingDecisions decision = randomAutoscalingDecisions(0, 0, randomIntBetween(1, 8));
+        assertThat(decision.type(), equalTo(AutoscalingDecisionType.SCALE_UP));
+    }
+
+}

+ 24 - 0
x-pack/plugin/autoscaling/src/test/java/org/elasticsearch/xpack/autoscaling/AutoscalingDecisionsWireSerializingTests.java

@@ -0,0 +1,24 @@
+/*
+ * 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.common.io.stream.Writeable;
+import org.elasticsearch.test.AbstractWireSerializingTestCase;
+
+public class AutoscalingDecisionsWireSerializingTests extends AbstractWireSerializingTestCase<AutoscalingDecisions> {
+
+    @Override
+    protected Writeable.Reader<AutoscalingDecisions> instanceReader() {
+        return AutoscalingDecisions::new;
+    }
+
+    @Override
+    protected AutoscalingDecisions createTestInstance() {
+        return AutoscalingTestCase.randomAutoscalingDecisions();
+    }
+
+}

+ 56 - 0
x-pack/plugin/autoscaling/src/test/java/org/elasticsearch/xpack/autoscaling/AutoscalingTestCase.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;
+
+import org.elasticsearch.common.Randomness;
+import org.elasticsearch.test.ESTestCase;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public abstract class AutoscalingTestCase extends ESTestCase {
+
+    static AutoscalingDecision randomAutoscalingDecision() {
+        return randomAutoscalingDecisionOfType(randomFrom(AutoscalingDecisionType.values()));
+    }
+
+    static AutoscalingDecision randomAutoscalingDecisionOfType(final AutoscalingDecisionType type) {
+        return new AutoscalingDecision(randomAlphaOfLength(8), type, randomAlphaOfLength(8));
+    }
+
+    static AutoscalingDecisions randomAutoscalingDecisions() {
+        final int numberOfDecisions = 1 + randomIntBetween(1, 8);
+        final List<AutoscalingDecision> decisions = new ArrayList<>(numberOfDecisions);
+        for (int i = 0; i < numberOfDecisions; i++) {
+            decisions.add(randomAutoscalingDecisionOfType(AutoscalingDecisionType.SCALE_DOWN));
+        }
+        final int numberOfDownDecisions = randomIntBetween(0, 8);
+        final int numberOfNoDecisions = randomIntBetween(0, 8);
+        final int numberOfUpDecisions = randomIntBetween(numberOfDownDecisions + numberOfNoDecisions == 0 ? 1 : 0, 8);
+        return randomAutoscalingDecisions(numberOfDownDecisions, numberOfNoDecisions, numberOfUpDecisions);
+    }
+
+    static AutoscalingDecisions randomAutoscalingDecisions(
+        final int numberOfDownDecisions,
+        final int numberOfNoDecisions,
+        final int numberOfUpDecisions
+    ) {
+        final List<AutoscalingDecision> decisions = new ArrayList<>(numberOfDownDecisions + numberOfNoDecisions + numberOfUpDecisions);
+        for (int i = 0; i < numberOfDownDecisions; i++) {
+            decisions.add(randomAutoscalingDecisionOfType(AutoscalingDecisionType.SCALE_DOWN));
+        }
+        for (int i = 0; i < numberOfNoDecisions; i++) {
+            decisions.add(randomAutoscalingDecisionOfType(AutoscalingDecisionType.NO_SCALE));
+        }
+        for (int i = 0; i < numberOfUpDecisions; i++) {
+            decisions.add(randomAutoscalingDecisionOfType(AutoscalingDecisionType.SCALE_UP));
+        }
+        Randomness.shuffle(decisions);
+        return new AutoscalingDecisions(decisions);
+    }
+
+}