diff --git a/docs/reference/autoscaling/apis/get-autoscaling-decision.asciidoc b/docs/reference/autoscaling/apis/get-autoscaling-decision.asciidoc index aa66e3a0d034f..dfa14ac180636 100644 --- a/docs/reference/autoscaling/apis/get-autoscaling-decision.asciidoc +++ b/docs/reference/autoscaling/apis/get-autoscaling-decision.asciidoc @@ -47,6 +47,6 @@ The API returns the following result: [source,console-result] -------------------------------------------------- { - + decisions: [] } -------------------------------------------------- diff --git a/x-pack/plugin/autoscaling/src/main/java/org/elasticsearch/xpack/autoscaling/AutoscalingDecision.java b/x-pack/plugin/autoscaling/src/main/java/org/elasticsearch/xpack/autoscaling/AutoscalingDecision.java new file mode 100644 index 0000000000000..4f4a3ce70981b --- /dev/null +++ b/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); + } + +} diff --git a/x-pack/plugin/autoscaling/src/main/java/org/elasticsearch/xpack/autoscaling/AutoscalingDecisionType.java b/x-pack/plugin/autoscaling/src/main/java/org/elasticsearch/xpack/autoscaling/AutoscalingDecisionType.java new file mode 100644 index 0000000000000..8590e26426291 --- /dev/null +++ b/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; + } + +} diff --git a/x-pack/plugin/autoscaling/src/main/java/org/elasticsearch/xpack/autoscaling/AutoscalingDecisions.java b/x-pack/plugin/autoscaling/src/main/java/org/elasticsearch/xpack/autoscaling/AutoscalingDecisions.java new file mode 100644 index 0000000000000..5e7e23fcc35dd --- /dev/null +++ b/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 decisions; + + public AutoscalingDecisions(final Collection 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); + } + +} diff --git a/x-pack/plugin/autoscaling/src/main/java/org/elasticsearch/xpack/autoscaling/action/GetAutoscalingDecisionAction.java b/x-pack/plugin/autoscaling/src/main/java/org/elasticsearch/xpack/autoscaling/action/GetAutoscalingDecisionAction.java index 88ffc0fbc91c7..8e97eb0d3f64e 100644 --- a/x-pack/plugin/autoscaling/src/main/java/org/elasticsearch/xpack/autoscaling/action/GetAutoscalingDecisionAction.java +++ b/x-pack/plugin/autoscaling/src/main/java/org/elasticsearch/xpack/autoscaling/action/GetAutoscalingDecisionAction.java @@ -14,8 +14,13 @@ 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 { @@ -60,24 +65,37 @@ public XContentBuilder toXContent(final XContentBuilder builder, final Params pa public static class Response extends ActionResponse implements ToXContentObject { - public Response() { + private final SortedMap decisions; + public Response(final SortedMap 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 decision : decisions.entrySet()) { + builder.startObject(); + { + builder.field(decision.getKey(), decision.getValue()); + } + builder.endObject(); + } + } + builder.endArray(); } builder.endObject(); return builder; diff --git a/x-pack/plugin/autoscaling/src/main/java/org/elasticsearch/xpack/autoscaling/action/TransportGetAutoscalingDecisionAction.java b/x-pack/plugin/autoscaling/src/main/java/org/elasticsearch/xpack/autoscaling/action/TransportGetAutoscalingDecisionAction.java index 5f6b79358756d..ea1687239b74f 100644 --- a/x-pack/plugin/autoscaling/src/main/java/org/elasticsearch/xpack/autoscaling/action/TransportGetAutoscalingDecisionAction.java +++ b/x-pack/plugin/autoscaling/src/main/java/org/elasticsearch/xpack/autoscaling/action/TransportGetAutoscalingDecisionAction.java @@ -20,6 +20,8 @@ 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 @@ protected void masterOperation( final ClusterState state, final ActionListener listener ) { - listener.onResponse(new GetAutoscalingDecisionAction.Response()); + listener.onResponse(new GetAutoscalingDecisionAction.Response(Collections.unmodifiableSortedMap(new TreeMap<>()))); } @Override diff --git a/x-pack/plugin/autoscaling/src/test/java/org/elasticsearch/xpack/autoscaling/AutoscalingDecisionTests.java b/x-pack/plugin/autoscaling/src/test/java/org/elasticsearch/xpack/autoscaling/AutoscalingDecisionTests.java new file mode 100644 index 0000000000000..c6c8dfa94d114 --- /dev/null +++ b/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)); + } + +} diff --git a/x-pack/plugin/autoscaling/src/test/java/org/elasticsearch/xpack/autoscaling/AutoscalingDecisionTypeWireSerializingTests.java b/x-pack/plugin/autoscaling/src/test/java/org/elasticsearch/xpack/autoscaling/AutoscalingDecisionTypeWireSerializingTests.java new file mode 100644 index 0000000000000..caa5c6440bbb4 --- /dev/null +++ b/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 { + + @Override + protected Writeable.Reader 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 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")); + } + +} diff --git a/x-pack/plugin/autoscaling/src/test/java/org/elasticsearch/xpack/autoscaling/AutoscalingDecisionWireSerializingTests.java b/x-pack/plugin/autoscaling/src/test/java/org/elasticsearch/xpack/autoscaling/AutoscalingDecisionWireSerializingTests.java new file mode 100644 index 0000000000000..f24dbcabdd090 --- /dev/null +++ b/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 { + + @Override + protected Writeable.Reader instanceReader() { + return AutoscalingDecision::new; + } + + @Override + protected AutoscalingDecision createTestInstance() { + return AutoscalingTestCase.randomAutoscalingDecision(); + } + +} diff --git a/x-pack/plugin/autoscaling/src/test/java/org/elasticsearch/xpack/autoscaling/AutoscalingDecisionsTests.java b/x-pack/plugin/autoscaling/src/test/java/org/elasticsearch/xpack/autoscaling/AutoscalingDecisionsTests.java new file mode 100644 index 0000000000000..c397f8650f7b6 --- /dev/null +++ b/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)); + } + +} diff --git a/x-pack/plugin/autoscaling/src/test/java/org/elasticsearch/xpack/autoscaling/AutoscalingDecisionsWireSerializingTests.java b/x-pack/plugin/autoscaling/src/test/java/org/elasticsearch/xpack/autoscaling/AutoscalingDecisionsWireSerializingTests.java new file mode 100644 index 0000000000000..c058d7c81d5cf --- /dev/null +++ b/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 { + + @Override + protected Writeable.Reader instanceReader() { + return AutoscalingDecisions::new; + } + + @Override + protected AutoscalingDecisions createTestInstance() { + return AutoscalingTestCase.randomAutoscalingDecisions(); + } + +} diff --git a/x-pack/plugin/autoscaling/src/test/java/org/elasticsearch/xpack/autoscaling/AutoscalingTestCase.java b/x-pack/plugin/autoscaling/src/test/java/org/elasticsearch/xpack/autoscaling/AutoscalingTestCase.java new file mode 100644 index 0000000000000..29b9a3c41bda9 --- /dev/null +++ b/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 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 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); + } + +}