Skip to content

Commit 21f5b83

Browse files
committed
Be lenient when parsing build flavor and type on the wire (#40734)
Today we are strict when parsing build flavor and types off the wire. This means that if a later version introduces a new build flavor or type, an older version would not be able to parse what that new version is sending. For a practical example of this, we recently added the build type "docker", and this means that in a rolling upgrade scenario older nodes would not be able to understand the build type that the newer node is sending. This breaks clusters and is bad. We do not normally think of adding a new enumeration value as being a serialization breaking change, it is just not a lesson that we have learned before. We should be lenient here though, so that we can add future changes without running the risk of breaking ourselves horribly. It is either that, or we have super-strict testing infrastructure here yet still I fear the possibility of mistakes. This commit changes the parsing of build flavor and build type so that we are still strict at startup, yet we are lenient with values coming across the wire. This will help avoid us breaking rolling upgrades, or clients that are on an older version.
1 parent 5f822e9 commit 21f5b83

File tree

3 files changed

+69
-10
lines changed

3 files changed

+69
-10
lines changed

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

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ public String displayName() {
5757
return displayName;
5858
}
5959

60-
public static Flavor fromDisplayName(final String displayName) {
60+
public static Flavor fromDisplayName(final String displayName, final boolean strict) {
6161
switch (displayName) {
6262
case "default":
6363
return Flavor.DEFAULT;
@@ -66,7 +66,12 @@ public static Flavor fromDisplayName(final String displayName) {
6666
case "unknown":
6767
return Flavor.UNKNOWN;
6868
default:
69-
throw new IllegalStateException("unexpected distribution flavor [" + displayName + "]; your distribution is broken");
69+
if (strict) {
70+
final String message = "unexpected distribution flavor [" + displayName + "]; your distribution is broken";
71+
throw new IllegalStateException(message);
72+
} else {
73+
return Flavor.UNKNOWN;
74+
}
7075
}
7176
}
7277

@@ -91,7 +96,7 @@ public String displayName() {
9196
this.displayName = displayName;
9297
}
9398

94-
public static Type fromDisplayName(final String displayName) {
99+
public static Type fromDisplayName(final String displayName, final boolean strict) {
95100
switch (displayName) {
96101
case "deb":
97102
return Type.DEB;
@@ -106,9 +111,14 @@ public static Type fromDisplayName(final String displayName) {
106111
case "unknown":
107112
return Type.UNKNOWN;
108113
default:
109-
throw new IllegalStateException("unexpected distribution type [" + displayName + "]; your distribution is broken");
114+
if (strict) {
115+
throw new IllegalStateException("unexpected distribution type [" + displayName + "]; your distribution is broken");
116+
} else {
117+
return Type.UNKNOWN;
118+
}
110119
}
111120
}
121+
112122
}
113123

114124
static {
@@ -119,8 +129,9 @@ public static Type fromDisplayName(final String displayName) {
119129
final boolean isSnapshot;
120130
final String version;
121131

122-
flavor = Flavor.fromDisplayName(System.getProperty("es.distribution.flavor", "unknown"));
123-
type = Type.fromDisplayName(System.getProperty("es.distribution.type", "unknown"));
132+
// these are parsed at startup, and we require that we are able to recognize the values passed in by the startup scripts
133+
flavor = Flavor.fromDisplayName(System.getProperty("es.distribution.flavor", "unknown"), true);
134+
type = Type.fromDisplayName(System.getProperty("es.distribution.type", "unknown"), true);
124135

125136
final String esPrefix = "elasticsearch-" + Version.CURRENT;
126137
final URL url = getElasticsearchCodeSourceLocation();
@@ -214,12 +225,14 @@ public static Build readBuild(StreamInput in) throws IOException {
214225
final Flavor flavor;
215226
final Type type;
216227
if (in.getVersion().onOrAfter(Version.V_6_3_0)) {
217-
flavor = Flavor.fromDisplayName(in.readString());
228+
// be lenient when reading on the wire, the enumeration values from other versions might be different than what we know
229+
flavor = Flavor.fromDisplayName(in.readString(), false);
218230
} else {
219231
flavor = Flavor.OSS;
220232
}
221233
if (in.getVersion().onOrAfter(Version.V_6_3_0)) {
222-
type = Type.fromDisplayName(in.readString());
234+
// be lenient when reading on the wire, the enumeration values from other versions might be different than what we know
235+
type = Type.fromDisplayName(in.readString(), false);
223236
} else {
224237
type = Type.UNKNOWN;
225238
}

server/src/main/java/org/elasticsearch/action/main/MainResponse.java

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -135,8 +135,12 @@ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws
135135
final String buildType = (String) value.get("build_type");
136136
response.build =
137137
new Build(
138-
buildFlavor == null ? Build.Flavor.UNKNOWN : Build.Flavor.fromDisplayName(buildFlavor),
139-
buildType == null ? Build.Type.UNKNOWN : Build.Type.fromDisplayName(buildType),
138+
/*
139+
* Be lenient when reading on the wire, the enumeration values from other versions might be different than what
140+
* we know.
141+
*/
142+
buildFlavor == null ? Build.Flavor.UNKNOWN : Build.Flavor.fromDisplayName(buildFlavor, false),
143+
buildType == null ? Build.Type.UNKNOWN : Build.Type.fromDisplayName(buildType, false),
140144
(String) value.get("build_hash"),
141145
(String) value.get("build_date"),
142146
(boolean) value.get("build_snapshot"),

server/src/test/java/org/elasticsearch/BuildTests.java

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,10 @@
3535
import java.util.Set;
3636
import java.util.stream.Collectors;
3737

38+
import static org.hamcrest.Matchers.containsString;
3839
import static org.hamcrest.Matchers.equalTo;
40+
import static org.hamcrest.Matchers.hasToString;
41+
import static org.hamcrest.Matchers.sameInstance;
3942

4043
public class BuildTests extends ESTestCase {
4144

@@ -223,4 +226,43 @@ public void testSerializationBWC() throws IOException {
223226
assertThat(post67pre70.build.getQualifiedVersion(), equalTo(post67Pre70Version.toString()));
224227
assertThat(post70.build.getQualifiedVersion(), equalTo(dockerBuild.build.getQualifiedVersion()));
225228
}
229+
230+
public void testFlavorParsing() {
231+
for (final Build.Flavor flavor : Build.Flavor.values()) {
232+
// strict or not should not impact parsing at all here
233+
assertThat(Build.Flavor.fromDisplayName(flavor.displayName(), randomBoolean()), sameInstance(flavor));
234+
}
235+
}
236+
237+
public void testTypeParsing() {
238+
for (final Build.Type type : Build.Type.values()) {
239+
// strict or not should not impact parsing at all here
240+
assertThat(Build.Type.fromDisplayName(type.displayName(), randomBoolean()), sameInstance(type));
241+
}
242+
}
243+
244+
public void testLenientFlavorParsing() {
245+
final String displayName = randomAlphaOfLength(8);
246+
assertThat(Build.Flavor.fromDisplayName(displayName, false), equalTo(Build.Flavor.UNKNOWN));
247+
}
248+
249+
public void testStrictFlavorParsing() {
250+
final String displayName = randomAlphaOfLength(8);
251+
@SuppressWarnings("ResultOfMethodCallIgnored") final IllegalStateException e =
252+
expectThrows(IllegalStateException.class, () -> Build.Flavor.fromDisplayName(displayName, true));
253+
assertThat(e, hasToString(containsString("unexpected distribution flavor [" + displayName + "]; your distribution is broken")));
254+
}
255+
256+
public void testLenientTypeParsing() {
257+
final String displayName = randomAlphaOfLength(8);
258+
assertThat(Build.Type.fromDisplayName(displayName, false), equalTo(Build.Type.UNKNOWN));
259+
}
260+
261+
public void testStrictTypeParsing() {
262+
final String displayName = randomAlphaOfLength(8);
263+
@SuppressWarnings("ResultOfMethodCallIgnored") final IllegalStateException e =
264+
expectThrows(IllegalStateException.class, () -> Build.Type.fromDisplayName(displayName, true));
265+
assertThat(e, hasToString(containsString("unexpected distribution type [" + displayName + "]; your distribution is broken")));
266+
}
267+
226268
}

0 commit comments

Comments
 (0)