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.
This commit is contained in:
Jason Tedor 2020-03-23 22:53:51 -04:00
parent c1c9f7a735
commit e3ca124537
No known key found for this signature in database
GPG Key ID: FA89F05560F16BC5
12 changed files with 488 additions and 6 deletions

View File

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

View File

@ -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);
}
}

View File

@ -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;
}
}

View File

@ -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);
}
}

View File

@ -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;

View File

@ -19,6 +19,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,
@ -59,7 +61,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

View File

@ -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));
}
}

View File

@ -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"));
}
}

View File

@ -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();
}
}

View File

@ -0,0 +1,38 @@
/*
* 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.Collections;
import static org.hamcrest.Matchers.equalTo;
public class AutoscalingDecisionsTests extends AutoscalingTestCase {
public void testAutoscalingDecisionsRejectsEmptyDecisions() {
final IllegalArgumentException e = expectThrows(
IllegalArgumentException.class,
() -> new AutoscalingDecisions(Collections.emptyList())
);
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));
}
}

View File

@ -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();
}
}

View File

@ -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);
}
}