88 */
99
1010
11- import de.thetaphi.forbiddenapis.gradle.ForbiddenApisPlugin
1211import com.avast.gradle.dockercompose.tasks.ComposePull
1312import com.fasterxml.jackson.databind.JsonNode
1413import com.fasterxml.jackson.databind.ObjectMapper
15-
14+ import de.thetaphi.forbiddenapis.gradle.ForbiddenApisPlugin
1615import org.elasticsearch.gradle.DistributionDownloadPlugin
1716import org.elasticsearch.gradle.Version
1817import org.elasticsearch.gradle.VersionProperties
@@ -22,6 +21,9 @@ import org.elasticsearch.gradle.util.GradleUtils
2221import org.gradle.plugins.ide.eclipse.model.AccessRule
2322
2423import java.nio.file.Files
24+ import java.util.Map.Entry
25+ import java.util.regex.Matcher
26+ import java.util.stream.Collectors
2527
2628import static java.nio.file.StandardCopyOption.REPLACE_EXISTING
2729import static org.elasticsearch.gradle.util.GradleUtils.maybeConfigure
@@ -203,6 +205,89 @@ tasks.register("updateCIBwcVersions") {
203205 }
204206}
205207
208+ /**
209+ * This task forces all TransportVersion (TV) changes to first go through the main branch, to force synchronization of
210+ * TV changes. We never want to be in a situation where a transport version integer ID is duplicated in two places
211+ * (e.g. 8.x and main) for two independent constants. This situation has happened historically, and was a result of
212+ * forward-porting TV changes to main. This task is not applicable to PRs to main, but only for PRs to older versions.
213+ */
214+ tasks. register(" ensureTransportVersionsInMain" ) {
215+ doLast {
216+ if (gradle. startParameter. isOffline()) {
217+ throw new GradleException (" Must run in online mode to verify that transport versions already exist in main first." )
218+ }
219+ def mainTransportVersions = new URI (' https://raw.githubusercontent.com/elastic/elasticsearch/refs/heads/main/server/src/main/java/org/elasticsearch/TransportVersions.java' ). toURL(). text
220+ def currentTransportVersions = file(' server/src/main/java/org/elasticsearch/TransportVersions.java' ). text
221+
222+ def mainTVNameAndIds = extractTransportVersionNameAndID(mainTransportVersions)
223+ def currentNameAndIds = extractTransportVersionNameAndID(currentTransportVersions)
224+
225+ ensureAllCurrentTVsExistInMain(currentNameAndIds, mainTVNameAndIds)
226+ }
227+ }
228+
229+ static def ensureAllCurrentTVsExistInMain (Map<String , Integer > currentNameAndIds , Map<String , Integer > mainTVNameAndIds ) {
230+ currentNameAndIds. each { versionName , versionValue ->
231+ if (mainTVNameAndIds[versionName] != versionValue) {
232+ throw new GradleException (" Transport version ${ versionName} with value ${ versionValue} is not present in " +
233+ " the main branch. All TVs must be added to main first, prior to backporting." )
234+ }
235+ }
236+ }
237+
238+ static Map<String , Integer > extractTransportVersionNameAndID (String transportVersionFileAsText ) {
239+ def transportVersionRegex = / public static final TransportVersion (\w +) = def\( (\w +)\) ;/
240+ Matcher tVMatcher = transportVersionFileAsText =~ transportVersionRegex
241+ return tVMatcher. iterator(). collectEntries { [(it[1 ]): Integer . valueOf(it[2 ]. replace(" _" , " " ))] }
242+ }
243+
244+ /**
245+ * This task validates that the transport versions in the current branch are consistent with the main branch, and tests
246+ * for a number of race conditions that would lead to an invalid state if this branch was merged.
247+ */
248+ tasks. register(" validateTransportVersions" ) {
249+ doLast {
250+ if (gradle. startParameter. isOffline()) {
251+ throw new GradleException (" Must run in online mode to validate transport versions" )
252+ }
253+ def mainTransportVersions = new URI (' https://raw.githubusercontent.com/elastic/elasticsearch/refs/heads/main/server/src/main/java/org/elasticsearch/TransportVersions.java' ). toURL(). text
254+ def currentTransportVersions = file(' server/src/main/java/org/elasticsearch/TransportVersions.java' ). text
255+
256+ def mainTVNameAndIds = extractTransportVersionNameAndID(mainTransportVersions)
257+ def currentNameAndIds = extractTransportVersionNameAndID(currentTransportVersions)
258+
259+ // Ensure that this is the latest TV (e.g. there are no newer Ids merged to main). Applicable for all branches.
260+ def latestCurrentId = currentNameAndIds. values(). stream(). max(Integer ::compare). get();
261+ def latestMainId = mainTVNameAndIds. values(). stream(). max(Integer ::compare). get();
262+ if (latestCurrentId < latestMainId) {
263+ throw new GradleException (" The latest transport version with id ${ latestCurrentId} on the current branch is " +
264+ " out of date with main, who latest transport version has the id ${ latestMainId} " )
265+ }
266+
267+ // Ensure that there are no duplicate IDs. Applicable for main builds, preventing the race conditions author1
268+ // checks-out main, then author2 checks out main and commits a new TV with the next TV id, then author1 commits a
269+ // TV with the same TV id as author 2. This will break the main build in this situation, but catch the bug.
270+ def idSet = new HashSet<Integer > ()
271+ currentNameAndIds. each { versionName , versionValue ->
272+ if (idSet. contains(versionValue)) {
273+ throw new GradleException (" Duplicate transport version id ${ versionValue} found for version ${ versionName} . " +
274+ " Transport versions must have unique IDs." )
275+ }
276+ idSet. add(versionValue)
277+ }
278+
279+ // Ensure there isn't a TV with another name but same ID in main. An additional check to try to catch the race
280+ // condition described above, but hopefully earlier on the PR prior to breaking the main build.
281+ def mainTVIdToName = mainTVNameAndIds. entrySet(). stream(). collect(Collectors . toMap(Entry ::getValue, Entry ::getKey))
282+ currentNameAndIds. each { versionName , versionValue ->
283+ if (mainTVIdToName. containsKey(versionValue) && mainTVIdToName. get(versionValue) != versionName) {
284+ throw new GradleException (" Transport version id ${ versionValue} is already used by ${ mainTVIdToName.get(versionValue)} in main, " +
285+ " but is being used by ${ versionName} in the current branch. Transport versions must have unique IDs." )
286+ }
287+ }
288+ }
289+ }
290+
206291tasks. register(" verifyVersions" ) {
207292 def verifyCiYaml = { File file , List<Version > versions ->
208293 String ciYml = file. text
0 commit comments