Skip to content

Commit b40821a

Browse files
committed
add yes-not-preferred allocation decision
1 parent a8d6582 commit b40821a

File tree

8 files changed

+248
-61
lines changed

8 files changed

+248
-61
lines changed

server/src/main/java/org/elasticsearch/TransportVersions.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -366,6 +366,7 @@ static TransportVersion def(int id) {
366366
public static final TransportVersion SIMULATE_INGEST_EFFECTIVE_MAPPING = def(9_140_0_00);
367367
public static final TransportVersion RESOLVE_INDEX_MODE_ADDED = def(9_141_0_00);
368368
public static final TransportVersion DATA_STREAM_WRITE_INDEX_ONLY_SETTINGS = def(9_142_0_00);
369+
public static final TransportVersion ALLOCATION_DECISION_YES_NOT_PREFERRED = def(9_143_0_00);
369370

370371
/*
371372
* STOP! READ THIS FIRST! No, really,

server/src/main/java/org/elasticsearch/cluster/routing/allocation/decider/AllocationDeciders.java

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -205,27 +205,37 @@ private Decision withDeciders(
205205
BiFunction<String, Decision, String> logMessageCreator
206206
) {
207207
if (debugMode == RoutingAllocation.DebugMode.OFF) {
208-
Decision result = Decision.YES;
208+
Decision finalDecision = Decision.YES;
209+
Decision.Preferred preferred = Decision.Preferred.YES;
209210
for (AllocationDecider decider : deciders) {
210211
var decision = deciderAction.apply(decider);
211212
if (decision.type() == Decision.Type.NO) {
212213
if (logger.isTraceEnabled()) {
213214
logger.trace(() -> logMessageCreator.apply(decider.getClass().getSimpleName(), decision));
214215
}
215216
return decision;
216-
} else if (result.type() == Decision.Type.YES && decision.type() == Decision.Type.THROTTLE) {
217-
result = decision;
217+
} else if (finalDecision.type() == Decision.Type.YES && decision.type() == Decision.Type.THROTTLE) {
218+
finalDecision = decision;
219+
} else if (decision.preferred() == Decision.Preferred.NO) {
220+
preferred = Decision.Preferred.NO;
218221
}
219222
}
220-
return result;
223+
if (finalDecision.type() == Decision.Type.YES && preferred == Decision.Preferred.NO) {
224+
return Decision.YES_NOT_PREFERRED;
225+
} else {
226+
return finalDecision;
227+
}
221228
} else {
222229
var result = new Decision.Multi();
223230
for (AllocationDecider decider : deciders) {
224231
var decision = deciderAction.apply(decider);
225232
if (logger.isTraceEnabled() && decision.type() == Decision.Type.NO) {
226233
logger.trace(() -> logMessageCreator.apply(decider.getClass().getSimpleName(), decision));
227234
}
228-
if (decision != Decision.ALWAYS && (debugMode == RoutingAllocation.DebugMode.ON || decision.type() != Decision.Type.YES)) {
235+
if (decision != Decision.ALWAYS
236+
&& (debugMode == RoutingAllocation.DebugMode.ON
237+
|| decision.type() != Decision.Type.YES
238+
|| decision.preferred() != Decision.Preferred.YES)) {
229239
result.add(decision);
230240
}
231241
}

server/src/main/java/org/elasticsearch/cluster/routing/allocation/decider/Decision.java

Lines changed: 87 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99

1010
package org.elasticsearch.cluster.routing.allocation.decider;
1111

12+
import org.elasticsearch.TransportVersions;
1213
import org.elasticsearch.common.io.stream.StreamInput;
1314
import org.elasticsearch.common.io.stream.StreamOutput;
1415
import org.elasticsearch.common.io.stream.Writeable;
@@ -33,6 +34,7 @@ public sealed interface Decision extends ToXContent, Writeable permits Decision.
3334

3435
Single ALWAYS = new Single(Type.YES);
3536
Single YES = new Single(Type.YES);
37+
Single YES_NOT_PREFERRED = new Single(Type.YES, Preferred.NO);
3638
Single NO = new Single(Type.NO);
3739
Single THROTTLE = new Single(Type.THROTTLE);
3840

@@ -48,6 +50,28 @@ static Decision single(Type type, @Nullable String label, @Nullable String expla
4850
return new Single(type, label, explanation, explanationParams);
4951
}
5052

53+
/**
54+
* Creates YES/PREFERRED decision
55+
* @param label label for the Decider that produced this decision
56+
* @param explanation explanation of the decision
57+
* @param explanationParams additional parameters for the decision
58+
* @return new {@link Decision} instance
59+
*/
60+
static Decision preferred(@Nullable String label, @Nullable String explanation, @Nullable Object... explanationParams) {
61+
return new Single(Type.YES, Preferred.YES, label, explanation, explanationParams);
62+
}
63+
64+
/**
65+
* Creates YES/NOT-PREFERRED decision
66+
* @param label label for the Decider that produced this decision
67+
* @param explanation explanation of the decision
68+
* @param explanationParams additional parameters for the decision
69+
* @return new {@link Decision} instance
70+
*/
71+
static Decision notPreferred(@Nullable String label, @Nullable String explanation, @Nullable Object... explanationParams) {
72+
return new Single(Type.YES, Preferred.NO, label, explanation, explanationParams);
73+
}
74+
5175
static Decision readFrom(StreamInput in) throws IOException {
5276
// Determine whether to read a Single or Multi Decision
5377
if (in.readBoolean()) {
@@ -67,16 +91,20 @@ static Decision readFrom(StreamInput in) throws IOException {
6791

6892
private static Single readSingleFrom(StreamInput in) throws IOException {
6993
final Type type = Type.readFrom(in);
94+
Preferred preferred = type == Type.YES ? Preferred.YES : Preferred.NO;
95+
if (in.getTransportVersion().onOrAfter(TransportVersions.ALLOCATION_DECISION_YES_NOT_PREFERRED)) {
96+
preferred = in.readEnum(Preferred.class);
97+
}
7098
final String label = in.readOptionalString();
7199
final String explanation = in.readOptionalString();
72100
if (label == null && explanation == null) {
73101
return switch (type) {
74-
case YES -> YES;
102+
case YES -> preferred == Preferred.YES ? YES : YES_NOT_PREFERRED;
75103
case THROTTLE -> THROTTLE;
76104
case NO -> NO;
77105
};
78106
}
79-
return new Single(type, label, explanation);
107+
return new Single(type, preferred, label, explanation);
80108
}
81109

82110
/**
@@ -85,6 +113,11 @@ private static Single readSingleFrom(StreamInput in) throws IOException {
85113
*/
86114
Type type();
87115

116+
/**
117+
* @return {@link Preferred}, a soft yes/no
118+
*/
119+
Preferred preferred();
120+
88121
/**
89122
* Get the description label for this decision.
90123
*/
@@ -149,10 +182,22 @@ public static Type min(Type a, Type b) {
149182

150183
}
151184

185+
/**
186+
* Preferred indicates if allocation is favorable. It's a soft YES/NO, means allocation is still possible when not-preferred.
187+
*/
188+
enum Preferred {
189+
NO,
190+
YES
191+
}
192+
152193
/**
153194
* Simple class representing a single decision
154195
*/
155-
record Single(Type type, String label, String explanationString) implements Decision, ToXContentObject {
196+
record Single(Type type, Preferred preferred, String label, String explanationString) implements Decision, ToXContentObject {
197+
public Single {
198+
assert type == Type.YES || preferred == Preferred.NO : "only YES type can have preference";
199+
}
200+
156201
/**
157202
* Creates a new {@link Single} decision of a given type
158203
* @param type {@link Type} of the decision
@@ -161,6 +206,10 @@ private Single(Type type) {
161206
this(type, null, null, (Object[]) null);
162207
}
163208

209+
private Single(Type type, Preferred preferred) {
210+
this(type, preferred, null, null, (Object[]) null);
211+
}
212+
164213
/**
165214
* Creates a new {@link Single} decision of a given type
166215
*
@@ -169,8 +218,27 @@ private Single(Type type) {
169218
* @param explanationParams A set of additional parameters
170219
*/
171220
public Single(Type type, @Nullable String label, @Nullable String explanation, @Nullable Object... explanationParams) {
221+
this(type, type == Type.YES ? Preferred.YES : Preferred.NO, label, explanation, explanationParams);
222+
}
223+
224+
/**
225+
* Creates a new {@link Single} decision of a given type
226+
*
227+
* @param type {@link Type} of the decision
228+
* @param preferred {@link Preferred} soft yes/no
229+
* @param explanation An explanation of this {@link Decision}
230+
* @param explanationParams A set of additional parameters
231+
*/
232+
public Single(
233+
Type type,
234+
Preferred preferred,
235+
@Nullable String label,
236+
@Nullable String explanation,
237+
@Nullable Object... explanationParams
238+
) {
172239
this(
173240
type,
241+
preferred,
174242
label,
175243
explanationParams != null && explanationParams.length > 0
176244
? String.format(Locale.ROOT, explanation, explanationParams)
@@ -194,10 +262,11 @@ public String getExplanation() {
194262

195263
@Override
196264
public String toString() {
265+
var preference = type == Type.YES && preferred == Preferred.NO ? "_NOT_PREFERRED" : "";
197266
if (explanationString != null) {
198-
return type + "(" + explanationString + ")";
267+
return type + preference + "(" + explanationString + ")";
199268
}
200-
return type + "()";
269+
return type + preference + "()";
201270
}
202271

203272
@Override
@@ -214,6 +283,9 @@ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws
214283
public void writeTo(StreamOutput out) throws IOException {
215284
out.writeBoolean(false); // flag specifying its a single decision
216285
type.writeTo(out);
286+
if (out.getTransportVersion().onOrAfter(TransportVersions.ALLOCATION_DECISION_YES_NOT_PREFERRED)) {
287+
out.writeEnum(preferred);
288+
}
217289
out.writeOptionalString(label);
218290
// Flatten explanation on serialization, so that explanationParams
219291
// do not need to be serialized
@@ -255,6 +327,16 @@ public Type type() {
255327
return ret;
256328
}
257329

330+
@Override
331+
public Preferred preferred() {
332+
for (var decision : decisions) {
333+
if (decision.type() != Type.YES || decision.preferred() == Preferred.NO) {
334+
return Preferred.NO;
335+
}
336+
}
337+
return Preferred.YES;
338+
}
339+
258340
@Override
259341
@Nullable
260342
public String label() {

server/src/main/java/org/elasticsearch/cluster/routing/allocation/decider/WriteLoadConstraintDecider.java

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ public Decision canAllocate(ShardRouting shardRouting, RoutingNode node, Routing
7373
shardRouting.shardId()
7474
);
7575
logger.debug(explain);
76-
return Decision.single(Decision.Type.NO, NAME, explain);
76+
return Decision.notPreferred(NAME, explain);
7777
}
7878

7979
if (calculateShardMovementChange(nodeWriteThreadPoolStats, shardWriteLoad) >= nodeWriteThreadPoolLoadThreshold) {
@@ -92,7 +92,7 @@ public Decision canAllocate(ShardRouting shardRouting, RoutingNode node, Routing
9292
nodeWriteThreadPoolStats.totalThreadPoolThreads()
9393
);
9494
logger.debug(explain);
95-
return Decision.single(Decision.Type.NO, NAME, explain);
95+
return Decision.notPreferred(NAME, explain);
9696
}
9797

9898
return Decision.YES;
@@ -101,12 +101,12 @@ public Decision canAllocate(ShardRouting shardRouting, RoutingNode node, Routing
101101
@Override
102102
public Decision canRemain(IndexMetadata indexMetadata, ShardRouting shardRouting, RoutingNode node, RoutingAllocation allocation) {
103103
if (writeLoadConstraintSettings.getWriteLoadConstraintEnabled().notFullyEnabled()) {
104-
return Decision.single(Decision.Type.YES, NAME, "canRemain() is not enabled");
104+
return Decision.preferred(NAME, "canRemain() is not enabled");
105105
}
106106

107107
// TODO: implement
108108

109-
return Decision.single(Decision.Type.YES, NAME, "canRemain() is not yet implemented");
109+
return Decision.preferred(NAME, "canRemain() is not yet implemented");
110110
}
111111

112112
/**

server/src/test/java/org/elasticsearch/cluster/routing/allocation/decider/AllocationDecidersTests.java

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,17 @@ public void testCheckAllDecidersBeforeReturningYes() {
5555
verifyDecidersCall(debugMode, allDecisions, allDecisions.size(), expectedDecision);
5656
}
5757

58+
public void testCheckAllDecidersBeforeReturningYesNotPreferred() {
59+
var debugMode = randomFrom(RoutingAllocation.DebugMode.values());
60+
var allDecisions = generateDecisions(Decision.YES_NOT_PREFERRED, () -> Decision.YES);
61+
var expectedDecision = switch (debugMode) {
62+
case OFF -> Decision.YES_NOT_PREFERRED;
63+
case EXCLUDE_YES_DECISIONS -> collectToMultiDecision(allDecisions, decision -> decision.preferred() != Decision.Preferred.YES);
64+
case ON -> collectToMultiDecision(allDecisions);
65+
};
66+
verifyDecidersCall(debugMode, allDecisions, allDecisions.size(), expectedDecision);
67+
}
68+
5869
public void testCheckAllDecidersBeforeReturningThrottle() {
5970
var allDecisions = generateDecisions(Decision.THROTTLE, () -> Decision.YES);
6071
var debugMode = randomFrom(RoutingAllocation.DebugMode.values());
@@ -69,7 +80,10 @@ public void testCheckAllDecidersBeforeReturningThrottle() {
6980

7081
public void testExitsAfterFirstNoDecision() {
7182
var expectedDecision = randomFrom(Decision.NO, Decision.single(Decision.Type.NO, "no with label", "explanation"));
72-
var allDecisions = generateDecisions(expectedDecision, () -> randomFrom(Decision.YES, Decision.THROTTLE));
83+
var allDecisions = generateDecisions(
84+
expectedDecision,
85+
() -> randomFrom(Decision.YES, Decision.YES_NOT_PREFERRED, Decision.THROTTLE)
86+
);
7387
var expectedCalls = allDecisions.indexOf(expectedDecision) + 1;
7488

7589
verifyDecidersCall(RoutingAllocation.DebugMode.OFF, allDecisions, expectedCalls, expectedDecision);
@@ -79,6 +93,7 @@ public void testCollectsAllDecisionsForDebugModeOn() {
7993
var allDecisions = generateDecisions(
8094
() -> randomFrom(
8195
Decision.YES,
96+
Decision.YES_NOT_PREFERRED,
8297
Decision.THROTTLE,
8398
Decision.single(Decision.Type.THROTTLE, "throttle with label", "explanation"),
8499
Decision.NO,
@@ -94,13 +109,17 @@ public void testCollectsNoAndThrottleDecisionsForDebugModeExcludeYesDecisions()
94109
var allDecisions = generateDecisions(
95110
() -> randomFrom(
96111
Decision.YES,
112+
Decision.YES_NOT_PREFERRED,
97113
Decision.THROTTLE,
98114
Decision.single(Decision.Type.THROTTLE, "throttle with label", "explanation"),
99115
Decision.NO,
100116
Decision.single(Decision.Type.NO, "no with label", "explanation")
101117
)
102118
);
103-
var expectedDecision = collectToMultiDecision(allDecisions, decision -> decision.type() != Decision.Type.YES);
119+
var expectedDecision = collectToMultiDecision(
120+
allDecisions,
121+
decision -> decision.type() != Decision.Type.YES || decision.preferred() != Decision.Preferred.YES
122+
);
104123

105124
verifyDecidersCall(RoutingAllocation.DebugMode.EXCLUDE_YES_DECISIONS, allDecisions, allDecisions.size(), expectedDecision);
106125
}

server/src/test/java/org/elasticsearch/cluster/routing/allocation/decider/DecisionTests.java

Lines changed: 44 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,13 @@
99

1010
package org.elasticsearch.cluster.routing.allocation.decider;
1111

12+
import org.elasticsearch.TransportVersions;
1213
import org.elasticsearch.cluster.routing.allocation.decider.Decision.Type;
13-
import org.elasticsearch.test.ESTestCase;
14+
import org.elasticsearch.common.io.stream.Writeable;
15+
import org.elasticsearch.test.AbstractWireSerializingTestCase;
16+
import org.elasticsearch.test.TransportVersionUtils;
17+
18+
import java.io.IOException;
1419

1520
import static org.elasticsearch.cluster.routing.allocation.decider.Decision.Type.NO;
1621
import static org.elasticsearch.cluster.routing.allocation.decider.Decision.Type.THROTTLE;
@@ -19,7 +24,7 @@
1924
/**
2025
* A class for unit testing the {@link Decision} class.
2126
*/
22-
public class DecisionTests extends ESTestCase {
27+
public class DecisionTests extends AbstractWireSerializingTestCase<Decision> {
2328

2429
/**
2530
* Tests {@link Type#higherThan(Type)}
@@ -40,4 +45,41 @@ public void testHigherThan() {
4045
assertFalse(NO.higherThan(THROTTLE));
4146
assertFalse(NO.higherThan(YES));
4247
}
48+
49+
public void testNotPreferredSerializationBeforeVersion() throws IOException {
50+
var inDecision = Decision.YES_NOT_PREFERRED;
51+
var outDecision = copyInstance(
52+
inDecision,
53+
TransportVersionUtils.getPreviousVersion(TransportVersions.ALLOCATION_DECISION_YES_NOT_PREFERRED)
54+
);
55+
assertEquals("should fallback to always-preferred in prior versions", Decision.YES, outDecision);
56+
57+
inDecision = Decision.NO;
58+
outDecision = copyInstance(
59+
inDecision,
60+
TransportVersionUtils.getPreviousVersion(TransportVersions.ALLOCATION_DECISION_YES_NOT_PREFERRED)
61+
);
62+
assertEquals(Decision.NO, outDecision);
63+
}
64+
65+
public void testNotPreferredSerialization() throws IOException {
66+
var inDecision = Decision.YES_NOT_PREFERRED;
67+
var outDecision = copyInstance(inDecision, TransportVersions.ALLOCATION_DECISION_YES_NOT_PREFERRED);
68+
assertEquals(inDecision, outDecision);
69+
}
70+
71+
@Override
72+
protected Writeable.Reader<Decision> instanceReader() {
73+
return Decision::readFrom;
74+
}
75+
76+
@Override
77+
protected Decision createTestInstance() {
78+
return Decision.single(YES, "label", "explanation", "param1", 2);
79+
}
80+
81+
@Override
82+
protected Decision mutateInstance(Decision instance) throws IOException {
83+
return instance.type() == YES ? Decision.NO : Decision.YES;
84+
}
4385
}

0 commit comments

Comments
 (0)