Browse Source

Update `TransportVersion` to support a new model (#131488) (#131889)

This change updates `TransportVersion` to support our new model while still allowing the old model to
work as well giving us time to migrate.
Jack Conradson 2 months ago
parent
commit
c057ec13a7

+ 263 - 18
server/src/main/java/org/elasticsearch/TransportVersion.java

@@ -12,15 +12,35 @@ package org.elasticsearch;
 import org.elasticsearch.common.VersionId;
 import org.elasticsearch.common.io.stream.StreamInput;
 import org.elasticsearch.common.io.stream.StreamOutput;
-import org.elasticsearch.internal.VersionExtension;
-import org.elasticsearch.plugins.ExtensionLoader;
 
+import java.io.BufferedReader;
 import java.io.IOException;
-import java.util.ServiceLoader;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.UncheckedIOException;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.function.Function;
+import java.util.function.IntFunction;
+import java.util.stream.Collectors;
 
 /**
  * Represents the version of the wire protocol used to communicate between a pair of ES nodes.
  * <p>
+ * Note: We are currently transitioning to a file-based system to load and maintain transport versions. These file-based transport
+ * versions are named and are referred to as named transport versions. Named transport versions also maintain a linked list of their
+ * own patch versions to simplify transport version compatibility checks. Transport versions that continue to be loaded through
+ * {@link TransportVersions} are referred to as unnamed transport versions. Unnamed transport versions will continue being used
+ * over the wire as we only need the id for compatibility checks even against named transport versions. There are changes
+ * throughout {@link TransportVersion} that are for this transition. For now, continue to use the existing system of adding unnamed
+ * transport versions to {@link TransportVersions}.
+ * <p>
  * Prior to 8.8.0, the release {@link Version} was used everywhere. This class separates the wire protocol version from the release version.
  * <p>
  * Each transport version constant has an id number, which for versions prior to 8.9.0 is the same as the release version for backwards
@@ -50,14 +70,62 @@ import java.util.ServiceLoader;
  * different version value. If you need to know whether the cluster as a whole speaks a new enough {@link TransportVersion} to understand a
  * newly-added feature, use {@link org.elasticsearch.cluster.ClusterState#getMinTransportVersion}.
  */
-public record TransportVersion(int id) implements VersionId<TransportVersion> {
+public record TransportVersion(String name, int id, TransportVersion nextPatchVersion) implements VersionId<TransportVersion> {
+
+    /**
+     * Constructs an unnamed transport version.
+     */
+    public TransportVersion(int id) {
+        this(null, id, null);
+    }
+
+    /**
+     * Constructs a named transport version along with its set of compatible patch versions from x-content.
+     * This method takes in the parameter {@code latest} which is the highest valid transport version id
+     * supported by this node. Versions newer than the current transport version id for this node are discarded.
+     */
+    public static TransportVersion fromInputStream(String path, boolean nameInFile, InputStream stream, Integer latest) {
+        try (BufferedReader reader = new BufferedReader(new InputStreamReader(stream, StandardCharsets.UTF_8))) {
+            String line = reader.readLine();
+            String[] parts = line.replaceAll("\\s+", "").split(",");
+            String check;
+            while ((check = reader.readLine()) != null) {
+                if (check.replaceAll("\\s+", "").isEmpty() == false) {
+                    throw new IllegalArgumentException("invalid transport version file format [" + path + "]");
+                }
+            }
+            if (parts.length < (nameInFile ? 2 : 1)) {
+                throw new IllegalStateException("invalid transport version file format [" + path + "]");
+            }
+            String name = nameInFile ? parts[0] : path.substring(path.lastIndexOf('/') + 1, path.length() - 4);
+            List<Integer> ids = new ArrayList<>();
+            for (int i = nameInFile ? 1 : 0; i < parts.length; ++i) {
+                try {
+                    ids.add(Integer.parseInt(parts[i]));
+                } catch (NumberFormatException nfe) {
+                    throw new IllegalStateException("invalid transport version file format [" + path + "]", nfe);
+                }
+            }
+            ids.sort(Integer::compareTo);
+            TransportVersion transportVersion = null;
+            for (int idIndex = 0; idIndex < ids.size(); ++idIndex) {
+                if (ids.get(idIndex) > latest) {
+                    break;
+                }
+                transportVersion = new TransportVersion(name, ids.get(idIndex), transportVersion);
+            }
+            return transportVersion;
+        } catch (IOException ioe) {
+            throw new UncheckedIOException("cannot parse transport version [" + path + "]", ioe);
+        }
+    }
 
     public static TransportVersion readVersion(StreamInput in) throws IOException {
         return fromId(in.readVInt());
     }
 
     public static TransportVersion fromId(int id) {
-        TransportVersion known = TransportVersions.VERSION_IDS.get(id);
+        TransportVersion known = VersionsHolder.ALL_VERSIONS_BY_ID.get(id);
         if (known != null) {
             return known;
         }
@@ -65,6 +133,23 @@ public record TransportVersion(int id) implements VersionId<TransportVersion> {
         return new TransportVersion(id);
     }
 
+    /**
+     * Finds a {@link TransportVersion} by its name. The parameter {@code name} must be a {@link String}
+     * direct value or validation checks will fail. {@code TransportVersion.fromName("direct_value")}.
+     * <p>
+     * This will only return the latest known named transport version for a given name and not its
+     * patch versions. Patch versions are constructed as a linked list internally and may be found by
+     * cycling through them in a loop using {@link TransportVersion#nextPatchVersion()}.
+     *
+     */
+    public static TransportVersion fromName(String name) {
+        TransportVersion known = VersionsHolder.ALL_VERSIONS_BY_NAME.get(name);
+        if (known == null) {
+            throw new IllegalStateException("unknown transport version [" + name + "]");
+        }
+        return known;
+    }
+
     public static void writeVersion(TransportVersion version, StreamOutput out) throws IOException {
         out.writeVInt(version.id);
     }
@@ -95,7 +180,14 @@ public record TransportVersion(int id) implements VersionId<TransportVersion> {
      * This should be the transport version with the highest id.
      */
     public static TransportVersion current() {
-        return CurrentHolder.CURRENT;
+        return VersionsHolder.CURRENT;
+    }
+
+    /**
+     * Sorted list of all defined transport versions
+     */
+    public static List<TransportVersion> getAllVersions() {
+        return VersionsHolder.ALL_VERSIONS;
     }
 
     /**
@@ -104,7 +196,7 @@ public record TransportVersion(int id) implements VersionId<TransportVersion> {
      *         in the wild (they're sent over the wire by numeric ID) but we don't know how to communicate using such versions.
      */
     public boolean isKnown() {
-        return before(TransportVersions.V_8_9_X) || TransportVersions.VERSION_IDS.containsKey(id);
+        return before(TransportVersions.V_8_9_X) || VersionsHolder.ALL_VERSIONS_BY_ID.containsKey(id);
     }
 
     /**
@@ -116,7 +208,7 @@ public record TransportVersion(int id) implements VersionId<TransportVersion> {
             return this;
         }
         TransportVersion bestSoFar = TransportVersions.ZERO;
-        for (final var knownVersion : TransportVersions.VERSION_IDS.values()) {
+        for (final var knownVersion : VersionsHolder.ALL_VERSIONS_BY_ID.values()) {
             if (knownVersion.after(bestSoFar) && knownVersion.before(this)) {
                 bestSoFar = knownVersion;
             }
@@ -152,12 +244,75 @@ public record TransportVersion(int id) implements VersionId<TransportVersion> {
         return onOrAfter(version) && id < version.id + 100 - (version.id % 100);
     }
 
+    /**
+     * Supports is used to determine if a named transport version is supported
+     * by a caller transport version. This will check both the latest id
+     * and all of its patch ids for compatibility. This replaces the pattern
+     * of {@code wireTV.onOrAfter(TV_FEATURE) || wireTV.isPatchFrom(TV_FEATURE_BACKPORT) || ...}
+     * for unnamed transport versions with {@code wireTV.supports(TV_FEATURE)} for named
+     * transport versions (since named versions know about their own patch versions).
+     * <p>
+     * The recommended use of this method is to declare a static final {@link TransportVersion}
+     * as part of the file that it's used in. This constant is then used in conjunction with
+     * this method to check transport version compatability.
+     * <p>
+     * An example:
+     * {@code
+     * public class ExampleClass {
+     * ...
+     *     TransportVersion TV_FEATURE = TransportVersion.fromName("tv_feature");
+     *     ...
+     *     public static ExampleClass readFrom(InputStream in) {
+     *         ...
+     *         if (in.getTransportVersion().supports(TV_FEATURE) {
+     *             // read newer values
+     *         }
+     *         ...
+     *     }
+     *     ...
+     *     public void writeTo(OutputStream out) {
+     *         ...
+     *         if (out.getTransportVersion().supports(TV_FEATURE) {
+     *             // write newer values
+     *         }
+     *         ...
+     *     }
+     *     ...
+     * }
+     * }
+     */
+    public boolean supports(TransportVersion version) {
+        if (onOrAfter(version)) {
+            return true;
+        }
+        TransportVersion nextPatchVersion = version.nextPatchVersion;
+        while (nextPatchVersion != null) {
+            if (isPatchFrom(nextPatchVersion)) {
+                return true;
+            }
+            nextPatchVersion = nextPatchVersion.nextPatchVersion;
+        }
+        return false;
+    }
+
     /**
      * Returns a string representing the Elasticsearch release version of this transport version,
      * if applicable for this deployment, otherwise the raw version number.
      */
     public String toReleaseVersion() {
-        return TransportVersions.VERSION_LOOKUP.apply(id);
+        return VersionsHolder.VERSION_LOOKUP_BY_RELEASE.apply(id);
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (o == null || getClass() != o.getClass()) return false;
+        TransportVersion that = (TransportVersion) o;
+        return id == that.id;
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hashCode(id);
     }
 
     @Override
@@ -165,16 +320,106 @@ public record TransportVersion(int id) implements VersionId<TransportVersion> {
         return Integer.toString(id);
     }
 
-    private static class CurrentHolder {
-        private static final TransportVersion CURRENT = findCurrent();
+    /**
+     * This class holds various data structures for looking up known transport versions both
+     * named and unnamed. While we transition to named transport versions, this class will
+     * load and merge unnamed transport versions from {@link TransportVersions} along with
+     * named transport versions specified in a manifest file in resources.
+     */
+    private static class VersionsHolder {
+
+        private static final List<TransportVersion> ALL_VERSIONS;
+        private static final Map<Integer, TransportVersion> ALL_VERSIONS_BY_ID;
+        private static final Map<String, TransportVersion> ALL_VERSIONS_BY_NAME;
+        private static final IntFunction<String> VERSION_LOOKUP_BY_RELEASE;
+        private static final TransportVersion CURRENT;
 
-        // finds the pluggable current version
-        private static TransportVersion findCurrent() {
-            var version = ExtensionLoader.loadSingleton(ServiceLoader.load(VersionExtension.class))
-                .map(e -> e.getCurrentTransportVersion(TransportVersions.LATEST_DEFINED))
-                .orElse(TransportVersions.LATEST_DEFINED);
-            assert version.onOrAfter(TransportVersions.LATEST_DEFINED);
-            return version;
+        static {
+            // collect all the transport versions from server and es modules/plugins (defined in server)
+            List<TransportVersion> allVersions = new ArrayList<>(TransportVersions.DEFINED_VERSIONS);
+            Map<String, TransportVersion> allVersionsByName = loadTransportVersionsByName();
+            addTransportVersions(allVersionsByName.values(), allVersions).sort(TransportVersion::compareTo);
+
+            // set the transport version lookups
+            ALL_VERSIONS = Collections.unmodifiableList(allVersions);
+            ALL_VERSIONS_BY_ID = ALL_VERSIONS.stream().collect(Collectors.toUnmodifiableMap(TransportVersion::id, Function.identity()));
+            ALL_VERSIONS_BY_NAME = Collections.unmodifiableMap(allVersionsByName);
+            VERSION_LOOKUP_BY_RELEASE = ReleaseVersions.generateVersionsLookup(
+                TransportVersions.class,
+                allVersions.get(allVersions.size() - 1).id()
+            );
+            CURRENT = ALL_VERSIONS.get(ALL_VERSIONS.size() - 1);
+        }
+
+        private static Map<String, TransportVersion> loadTransportVersionsByName() {
+            Map<String, TransportVersion> transportVersions = new HashMap<>();
+
+            String latestLocation = "/transport/latest/" + Version.CURRENT.major + "." + Version.CURRENT.minor + ".csv";
+            int latestId = -1;
+            try (InputStream inputStream = TransportVersion.class.getResourceAsStream(latestLocation)) {
+                // this check is required until bootstrapping for the new transport versions format is completed;
+                // when load is false, we will only use the transport versions in the legacy format;
+                // load becomes false if we don't find the latest or manifest files required for the new format
+                if (inputStream != null) {
+                    TransportVersion latest = fromInputStream(latestLocation, true, inputStream, Integer.MAX_VALUE);
+                    if (latest == null) {
+                        throw new IllegalStateException(
+                            "invalid latest transport version for minor version ["
+                                + Version.CURRENT.major
+                                + "."
+                                + Version.CURRENT.minor
+                                + "]"
+                        );
+                    }
+                    latestId = latest.id();
+                }
+            } catch (IOException ioe) {
+                throw new UncheckedIOException("latest transport version file not found at [" + latestLocation + "]", ioe);
+            }
+
+            String manifestLocation = "/transport/constant/manifest.txt";
+            List<String> versionFileNames = null;
+            if (latestId > -1) {
+                try (InputStream inputStream = TransportVersion.class.getResourceAsStream(manifestLocation)) {
+                    if (inputStream != null) {
+                        BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8));
+                        versionFileNames = reader.lines().filter(line -> line.isBlank() == false).toList();
+                    }
+                } catch (IOException ioe) {
+                    throw new UncheckedIOException("transport version manifest file not found at [" + manifestLocation + "]", ioe);
+                }
+            }
+
+            if (versionFileNames != null) {
+                for (String name : versionFileNames) {
+                    String versionLocation = "/transport/constant/" + name;
+                    try (InputStream inputStream = TransportVersion.class.getResourceAsStream(versionLocation)) {
+                        if (inputStream == null) {
+                            throw new IllegalStateException("transport version file not found at [" + versionLocation + "]");
+                        }
+                        TransportVersion transportVersion = TransportVersion.fromInputStream(versionLocation, false, inputStream, latestId);
+                        if (transportVersion != null) {
+                            transportVersions.put(transportVersion.name(), transportVersion);
+                        }
+                    } catch (IOException ioe) {
+                        throw new UncheckedIOException("transport version file not found at [ " + versionLocation + "]", ioe);
+                    }
+                }
+            }
+
+            return transportVersions;
+        }
+
+        private static List<TransportVersion> addTransportVersions(Collection<TransportVersion> addFrom, List<TransportVersion> addTo) {
+            for (TransportVersion transportVersion : addFrom) {
+                addTo.add(transportVersion);
+                TransportVersion patchVersion = transportVersion.nextPatchVersion();
+                while (patchVersion != null) {
+                    addTo.add(patchVersion);
+                    patchVersion = patchVersion.nextPatchVersion();
+                }
+            }
+            return addTo;
         }
     }
 }

+ 11 - 24
server/src/main/java/org/elasticsearch/TransportVersions.java

@@ -12,15 +12,13 @@ package org.elasticsearch;
 import org.elasticsearch.core.Assertions;
 
 import java.lang.reflect.Field;
-import java.util.Collection;
+import java.util.ArrayList;
 import java.util.Collections;
 import java.util.HashMap;
+import java.util.List;
 import java.util.Map;
-import java.util.NavigableMap;
 import java.util.Set;
-import java.util.TreeMap;
 import java.util.TreeSet;
-import java.util.function.IntFunction;
 
 /**
  * <p>Transport version is used to coordinate compatible wire protocol communication between nodes, at a fine-grained level.  This replaces
@@ -325,21 +323,14 @@ public class TransportVersions {
      */
     public static final TransportVersion MINIMUM_CCS_VERSION = RETRY_ILM_ASYNC_ACTION_REQUIRE_ERROR_8_18;
 
-    static final NavigableMap<Integer, TransportVersion> VERSION_IDS = getAllVersionIds(TransportVersions.class);
-
-    // the highest transport version constant defined in this file, used as a fallback for TransportVersion.current()
-    static final TransportVersion LATEST_DEFINED;
-    static {
-        LATEST_DEFINED = VERSION_IDS.lastEntry().getValue();
-
-        // see comment on IDS field
-        // now we're registered all the transport versions, we can clear the map
-        IDS = null;
-    }
+    /**
+     * Sorted list of all versions defined in this class
+     */
+    static final List<TransportVersion> DEFINED_VERSIONS = collectAllVersionIdsDefinedInClass(TransportVersions.class);
 
-    public static NavigableMap<Integer, TransportVersion> getAllVersionIds(Class<?> cls) {
+    public static List<TransportVersion> collectAllVersionIdsDefinedInClass(Class<?> cls) {
         Map<Integer, String> versionIdFields = new HashMap<>();
-        NavigableMap<Integer, TransportVersion> builder = new TreeMap<>();
+        List<TransportVersion> definedTransportVersions = new ArrayList<>();
 
         Set<String> ignore = Set.of("ZERO", "CURRENT", "MINIMUM_COMPATIBLE", "MINIMUM_CCS_VERSION");
 
@@ -356,7 +347,7 @@ public class TransportVersions {
                 } catch (IllegalAccessException e) {
                     throw new AssertionError(e);
                 }
-                builder.put(version.id(), version);
+                definedTransportVersions.add(version);
 
                 if (Assertions.ENABLED) {
                     // check the version number is unique
@@ -373,15 +364,11 @@ public class TransportVersions {
             }
         }
 
-        return Collections.unmodifiableNavigableMap(builder);
-    }
+        Collections.sort(definedTransportVersions);
 
-    static Collection<TransportVersion> getAllVersions() {
-        return VERSION_IDS.values();
+        return List.copyOf(definedTransportVersions);
     }
 
-    static final IntFunction<String> VERSION_LOOKUP = ReleaseVersions.generateVersionsLookup(TransportVersions.class, LATEST_DEFINED.id());
-
     // no instance
     private TransportVersions() {}
 }

+ 1 - 0
server/src/main/resources/transport/latest/8.18.csv

@@ -0,0 +1 @@
+placeholder,8840007

+ 1 - 0
server/src/main/resources/transport/latest/8.19.csv

@@ -0,0 +1 @@
+placeholder,8841064

+ 1 - 0
server/src/main/resources/transport/latest/9.0.csv

@@ -0,0 +1 @@
+placeholder,9000014

+ 1 - 0
server/src/main/resources/transport/latest/9.1.csv

@@ -0,0 +1 @@
+placeholder,9112003

+ 1 - 0
server/src/main/resources/transport/latest/9.2.csv

@@ -0,0 +1 @@
+placeholder,9130000

+ 123 - 19
server/src/test/java/org/elasticsearch/TransportVersionTests.java

@@ -15,15 +15,14 @@ import org.elasticsearch.test.TransportVersionUtils;
 import java.lang.reflect.Modifier;
 import java.util.Collections;
 import java.util.List;
-import java.util.Map;
 import java.util.Set;
 import java.util.TreeSet;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 
+import static org.hamcrest.Matchers.contains;
 import static org.hamcrest.Matchers.containsString;
 import static org.hamcrest.Matchers.endsWith;
-import static org.hamcrest.Matchers.equalTo;
 import static org.hamcrest.Matchers.greaterThan;
 import static org.hamcrest.Matchers.is;
 import static org.hamcrest.Matchers.lessThan;
@@ -37,7 +36,7 @@ public class TransportVersionTests extends ESTestCase {
      * If the test fails, there is something wrong with your backport PR.
      */
     public void testMaximumAllowedTransportVersion() {
-        assertThat(TransportVersions.LATEST_DEFINED.isPatchFrom(TransportVersions.INITIAL_ELASTICSEARCH_8_19), is(true));
+        assertThat(TransportVersion.current().isPatchFrom(TransportVersions.INITIAL_ELASTICSEARCH_8_19), is(true));
     }
 
     public void testVersionComparison() {
@@ -79,21 +78,18 @@ public class TransportVersionTests extends ESTestCase {
 
     public void testStaticTransportVersionChecks() {
         assertThat(
-            TransportVersions.getAllVersionIds(CorrectFakeVersion.class),
-            equalTo(
-                Map.of(
-                    199,
-                    CorrectFakeVersion.V_0_00_01,
-                    2,
-                    CorrectFakeVersion.V_0_000_002,
-                    3,
-                    CorrectFakeVersion.V_0_000_003,
-                    4,
-                    CorrectFakeVersion.V_0_000_004
-                )
+            TransportVersions.collectAllVersionIdsDefinedInClass(CorrectFakeVersion.class),
+            contains(
+                CorrectFakeVersion.V_0_000_002,
+                CorrectFakeVersion.V_0_000_003,
+                CorrectFakeVersion.V_0_000_004,
+                CorrectFakeVersion.V_0_00_01
             )
         );
-        AssertionError e = expectThrows(AssertionError.class, () -> TransportVersions.getAllVersionIds(DuplicatedIdFakeVersion.class));
+        AssertionError e = expectThrows(
+            AssertionError.class,
+            () -> TransportVersions.collectAllVersionIdsDefinedInClass(DuplicatedIdFakeVersion.class)
+        );
         assertThat(e.getMessage(), containsString("have the same version number"));
     }
 
@@ -196,7 +192,7 @@ public class TransportVersionTests extends ESTestCase {
     }
 
     public void testCURRENTIsLatest() {
-        assertThat(Collections.max(TransportVersions.getAllVersions()), is(TransportVersion.current()));
+        assertThat(Collections.max(TransportVersion.getAllVersions()), is(TransportVersion.current()));
     }
 
     public void testPatchVersionsStillAvailable() {
@@ -233,7 +229,7 @@ public class TransportVersionTests extends ESTestCase {
     public void testDenseTransportVersions() {
         Set<Integer> missingVersions = new TreeSet<>();
         TransportVersion previous = null;
-        for (var tv : TransportVersions.getAllVersions()) {
+        for (var tv : TransportVersion.getAllVersions()) {
             if (tv.before(TransportVersions.V_8_16_0)) {
                 continue;
             }
@@ -261,7 +257,7 @@ public class TransportVersionTests extends ESTestCase {
     }
 
     public void testDuplicateConstants() {
-        List<TransportVersion> tvs = TransportVersions.getAllVersions().stream().sorted().toList();
+        List<TransportVersion> tvs = TransportVersion.getAllVersions().stream().sorted().toList();
         TransportVersion previous = tvs.get(0);
         for (int i = 1; i < tvs.size(); i++) {
             TransportVersion next = tvs.get(i);
@@ -271,4 +267,112 @@ public class TransportVersionTests extends ESTestCase {
             previous = next;
         }
     }
+
+    public void testFromName() {
+        assertThat(TransportVersion.fromName("test_0"), is(new TransportVersion("test_0", 3001000, null)));
+        assertThat(TransportVersion.fromName("test_1"), is(new TransportVersion("test_1", 3002000, null)));
+        assertThat(
+            TransportVersion.fromName("test_2"),
+            is(
+                new TransportVersion(
+                    "test_2",
+                    3003000,
+                    new TransportVersion("test_2", 2001001, new TransportVersion("test_2", 1001001, null))
+                )
+            )
+        );
+        assertThat(
+            TransportVersion.fromName("test_3"),
+            is(new TransportVersion("test_3", 3003001, new TransportVersion("test_3", 2001002, null)))
+        );
+        assertThat(
+            TransportVersion.fromName("test_4"),
+            is(
+                new TransportVersion(
+                    "test_4",
+                    3003002,
+                    new TransportVersion("test_4", 2001003, new TransportVersion("test_4", 1001002, null))
+                )
+            )
+        );
+    }
+
+    public void testSupports() {
+        TransportVersion test0 = TransportVersion.fromName("test_0");
+        assertThat(new TransportVersion(null, 2003000, null).supports(test0), is(false));
+        assertThat(new TransportVersion(null, 3001000, null).supports(test0), is(true));
+        assertThat(new TransportVersion(null, 100001001, null).supports(test0), is(true));
+
+        TransportVersion test1 = TransportVersion.fromName("test_1");
+        assertThat(new TransportVersion(null, 2003000, null).supports(test1), is(false));
+        assertThat(new TransportVersion(null, 3001000, null).supports(test1), is(false));
+        assertThat(new TransportVersion(null, 3001001, null).supports(test1), is(false));
+        assertThat(new TransportVersion(null, 3002000, null).supports(test1), is(true));
+        assertThat(new TransportVersion(null, 100001000, null).supports(test1), is(true));
+        assertThat(new TransportVersion(null, 100001001, null).supports(test1), is(true));
+
+        TransportVersion test2 = TransportVersion.fromName("test_2");
+        assertThat(new TransportVersion(null, 1001000, null).supports(test2), is(false));
+        assertThat(new TransportVersion(null, 1001001, null).supports(test2), is(true));
+        assertThat(new TransportVersion(null, 1001002, null).supports(test2), is(true));
+        assertThat(new TransportVersion(null, 1002000, null).supports(test2), is(false));
+        assertThat(new TransportVersion(null, 1002001, null).supports(test2), is(false));
+        assertThat(new TransportVersion(null, 2001000, null).supports(test2), is(false));
+        assertThat(new TransportVersion(null, 2001001, null).supports(test2), is(true));
+        assertThat(new TransportVersion(null, 2001002, null).supports(test2), is(true));
+        assertThat(new TransportVersion(null, 2003000, null).supports(test2), is(false));
+        assertThat(new TransportVersion(null, 2003001, null).supports(test2), is(false));
+        assertThat(new TransportVersion(null, 3001000, null).supports(test2), is(false));
+        assertThat(new TransportVersion(null, 3001001, null).supports(test2), is(false));
+        assertThat(new TransportVersion(null, 3003000, null).supports(test2), is(true));
+        assertThat(new TransportVersion(null, 3003001, null).supports(test2), is(true));
+        assertThat(new TransportVersion(null, 3003002, null).supports(test2), is(true));
+        assertThat(new TransportVersion(null, 3003003, null).supports(test2), is(true));
+        assertThat(new TransportVersion(null, 100001000, null).supports(test2), is(true));
+        assertThat(new TransportVersion(null, 100001001, null).supports(test2), is(true));
+
+        TransportVersion test3 = TransportVersion.fromName("test_3");
+        assertThat(new TransportVersion(null, 1001001, null).supports(test3), is(false));
+        assertThat(new TransportVersion(null, 1001002, null).supports(test3), is(false));
+        assertThat(new TransportVersion(null, 1001003, null).supports(test3), is(false));
+        assertThat(new TransportVersion(null, 1002001, null).supports(test3), is(false));
+        assertThat(new TransportVersion(null, 1002002, null).supports(test3), is(false));
+        assertThat(new TransportVersion(null, 2001001, null).supports(test3), is(false));
+        assertThat(new TransportVersion(null, 2001002, null).supports(test3), is(true));
+        assertThat(new TransportVersion(null, 2001003, null).supports(test3), is(true));
+        assertThat(new TransportVersion(null, 2003000, null).supports(test3), is(false));
+        assertThat(new TransportVersion(null, 2003001, null).supports(test3), is(false));
+        assertThat(new TransportVersion(null, 3001000, null).supports(test3), is(false));
+        assertThat(new TransportVersion(null, 3001001, null).supports(test3), is(false));
+        assertThat(new TransportVersion(null, 3003000, null).supports(test3), is(false));
+        assertThat(new TransportVersion(null, 3003001, null).supports(test3), is(true));
+        assertThat(new TransportVersion(null, 3003002, null).supports(test3), is(true));
+        assertThat(new TransportVersion(null, 3003003, null).supports(test3), is(true));
+        assertThat(new TransportVersion(null, 3004000, null).supports(test3), is(true));
+        assertThat(new TransportVersion(null, 100001000, null).supports(test3), is(true));
+        assertThat(new TransportVersion(null, 100001001, null).supports(test3), is(true));
+
+        TransportVersion test4 = TransportVersion.fromName("test_4");
+        assertThat(new TransportVersion(null, 1001001, null).supports(test4), is(false));
+        assertThat(new TransportVersion(null, 1001002, null).supports(test4), is(true));
+        assertThat(new TransportVersion(null, 1001003, null).supports(test4), is(true));
+        assertThat(new TransportVersion(null, 1002001, null).supports(test4), is(false));
+        assertThat(new TransportVersion(null, 1002002, null).supports(test4), is(false));
+        assertThat(new TransportVersion(null, 1002003, null).supports(test3), is(false));
+        assertThat(new TransportVersion(null, 2001002, null).supports(test4), is(false));
+        assertThat(new TransportVersion(null, 2001003, null).supports(test4), is(true));
+        assertThat(new TransportVersion(null, 2001004, null).supports(test4), is(true));
+        assertThat(new TransportVersion(null, 2003000, null).supports(test4), is(false));
+        assertThat(new TransportVersion(null, 2003001, null).supports(test4), is(false));
+        assertThat(new TransportVersion(null, 3001000, null).supports(test4), is(false));
+        assertThat(new TransportVersion(null, 3001001, null).supports(test4), is(false));
+        assertThat(new TransportVersion(null, 3003000, null).supports(test4), is(false));
+        assertThat(new TransportVersion(null, 3003001, null).supports(test4), is(false));
+        assertThat(new TransportVersion(null, 3003002, null).supports(test4), is(true));
+        assertThat(new TransportVersion(null, 3003003, null).supports(test4), is(true));
+        assertThat(new TransportVersion(null, 3003004, null).supports(test4), is(true));
+        assertThat(new TransportVersion(null, 3004000, null).supports(test4), is(true));
+        assertThat(new TransportVersion(null, 100001000, null).supports(test4), is(true));
+        assertThat(new TransportVersion(null, 100001001, null).supports(test4), is(true));
+    }
 }

+ 5 - 0
server/src/test/resources/transport/constant/manifest.txt

@@ -0,0 +1,5 @@
+test_0.csv
+test_1.csv
+test_2.csv
+test_3.csv
+test_4.csv

+ 1 - 0
server/src/test/resources/transport/constant/test_0.csv

@@ -0,0 +1 @@
+100001000,3001000

+ 2 - 0
server/src/test/resources/transport/constant/test_1.csv

@@ -0,0 +1,2 @@
+3002000
+

+ 1 - 0
server/src/test/resources/transport/constant/test_2.csv

@@ -0,0 +1 @@
+3003000,2001001,1001001

+ 1 - 0
server/src/test/resources/transport/constant/test_3.csv

@@ -0,0 +1 @@
+100002000,3003001,2001002

+ 1 - 0
server/src/test/resources/transport/constant/test_4.csv

@@ -0,0 +1 @@
+100002000,3003002,2001003,1001002

+ 1 - 1
test/framework/src/main/java/org/elasticsearch/KnownTransportVersions.java

@@ -18,5 +18,5 @@ public class KnownTransportVersions {
     /**
      * A sorted list of all known transport versions
      */
-    public static final List<TransportVersion> ALL_VERSIONS = List.copyOf(TransportVersions.getAllVersions());
+    public static final List<TransportVersion> ALL_VERSIONS = List.copyOf(TransportVersion.getAllVersions());
 }