This is an automated email from the ASF dual-hosted git repository.
jason810496 pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/airflow.git
The following commit(s) were added to refs/heads/main by this push:
new 7a4c0599ff6 Speed up the java_sdk e2e Gradle builds with concurrency
and caching (#69239)
7a4c0599ff6 is described below
commit 7a4c0599ff6048f74ec203338f63e1aff1086ca0
Author: Jason(Zhe-You) Liu <[email protected]>
AuthorDate: Fri Jul 10 16:20:46 2026 +0900
Speed up the java_sdk e2e Gradle builds with concurrency and caching
(#69239)
* Build the Java SDK and Scala Spark e2e bundles concurrently
The example and scala_spark_example bundles are independent Gradle builds
that both consume the SDK artifact published to the local Maven repo, so the
Scala Spark dependency resolution no longer has to wait for the Java example
build to finish before it starts.
The bundle containers run with --network=host because Gradle's cross-process
lock handover pings the lock owner over loopback, which isolated container
network namespaces would break, and each build's output is captured and
printed under its own header so concurrent logs do not interleave.
* Cache the Java SDK Gradle dependencies in the e2e job
The java_sdk e2e job builds the SDK and its example bundles with Gradle on
every canary and java-sdk-touching run, starting from an empty
GRADLE_USER_HOME each time. That forces a full re-download of the example
bundles' dependency tree (Spark alone is around 1GB) on every run. Reusing
the resolved dependencies across runs removes that repeated download.
The restore/save split saves the warm-up even when the e2e tests fail, the
key hashes the Kotlin-DSL build files and version catalog that pin the SDK
dependencies, and runner.arch keeps the arch-specific auto-provisioned JDKs
in .gradle/jdks from crossing between amd64 and arm64 runners.
* Address java_sdk e2e review feedback on caching and build output
Excludes churny *.lock/journal files from the Gradle cache per GitHub's
own caching guidance, makes bundle build output print stdout/stderr
symmetrically through Rich with markup disabled so raw Gradle text
can't be misparsed as markup, ties the build pool size to the bundle
list instead of a hardcoded count, and cites the Gradle lock-handover
mechanism the --network=host workaround depends on.
---
.github/workflows/airflow-e2e-tests.yml | 32 +++++
.../tests/airflow_e2e_tests/conftest.py | 140 ++++++++++-----------
2 files changed, 100 insertions(+), 72 deletions(-)
diff --git a/.github/workflows/airflow-e2e-tests.yml
b/.github/workflows/airflow-e2e-tests.yml
index 3a87ea525f7..4fb1fcf8d84 100644
--- a/.github/workflows/airflow-e2e-tests.yml
+++ b/.github/workflows/airflow-e2e-tests.yml
@@ -113,11 +113,43 @@ jobs:
use-uv: ${{ inputs.use-uv }}
make-mnt-writeable-and-cleanup: true
id: breeze
+ - name: "Restore Java SDK Gradle dependency cache"
+ # Only the java_sdk mode runs the Gradle builds; every other e2e mode
skips
+ # this. Without it each java_sdk run re-downloads the example bundles'
full
+ # dependency tree (Spark alone is ~1GB) into a fresh GRADLE_USER_HOME.
+ # runner.arch keys the cache per architecture: .gradle/jdks holds
+ # arch-specific auto-provisioned JDKs and this job runs on amd64 and
arm64.
+ if: inputs.e2e_test_mode == 'java_sdk'
+ id: gradle-cache
+ uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9
# v6.1.0
+ with:
+ path: |
+ java-sdk/.gradle/caches
+ java-sdk/.gradle/wrapper
+ java-sdk/.gradle/jdks
+ !java-sdk/.gradle/caches/*.lock
+ !java-sdk/.gradle/caches/journal-1
+ key: java-sdk-gradle-${{ runner.os }}-${{ runner.arch }}-${{
hashFiles('java-sdk/**/*.gradle*', 'java-sdk/**/gradle.properties',
'java-sdk/gradle/wrapper/gradle-wrapper.properties',
'java-sdk/gradle/libs.versions.toml') }} # yamllint disable-line
rule:line-length
+ restore-keys: |
+ java-sdk-gradle-${{ runner.os }}-${{ runner.arch }}-
- name: "Test e2e integration tests"
run: breeze testing airflow-e2e-tests
env:
DOCKER_IMAGE: "${{ inputs.docker-image-tag }}"
E2E_TEST_MODE: "${{ inputs.e2e_test_mode }}"
+ - name: "Save Java SDK Gradle dependency cache"
+ # Saved even when the e2e tests fail: the Gradle warm-up is
independent of
+ # test outcome, and actions/cache's post step would drop it on every
red run.
+ if: always() && inputs.e2e_test_mode == 'java_sdk' &&
steps.gradle-cache.outputs.cache-hit != 'true'
+ uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 #
v6.1.0
+ with:
+ path: |
+ java-sdk/.gradle/caches
+ java-sdk/.gradle/wrapper
+ java-sdk/.gradle/jdks
+ !java-sdk/.gradle/caches/*.lock
+ !java-sdk/.gradle/caches/journal-1
+ key: ${{ steps.gradle-cache.outputs.cache-primary-key }}
- name: Zip logs
run: |
cd ./airflow-e2e-tests && zip -r logs.zip logs
diff --git a/airflow-e2e-tests/tests/airflow_e2e_tests/conftest.py
b/airflow-e2e-tests/tests/airflow_e2e_tests/conftest.py
index 639d378bc4d..eb6ed32e9a6 100644
--- a/airflow-e2e-tests/tests/airflow_e2e_tests/conftest.py
+++ b/airflow-e2e-tests/tests/airflow_e2e_tests/conftest.py
@@ -19,6 +19,7 @@ from __future__ import annotations
import json
import os
import subprocess
+from concurrent.futures import ThreadPoolExecutor
from datetime import datetime
from pathlib import Path
from shutil import copyfile, copytree, rmtree
@@ -278,29 +279,31 @@ _SPARK_JAVA_MODULE_OPTIONS = [
]
-def _setup_java_sdk_integration(dot_env_file, tmp_dir):
- """Set up the java_sdk E2E test mode.
-
- Builds the Java example bundle via the Gradle wrapper, then builds a
- Java-capable Airflow worker image, copies the JARs into the temp directory,
- and writes the coordinator configuration.
+def _run_java_sdk_gradle_container(workdir, *gradle_argv,
capture_output=False):
+ """Run the Java SDK Gradle wrapper inside the pinned JDK container.
+
+ * --user keeps build outputs owned by the current user (not root).
+ * --network=host shares one loopback across concurrent builds so Gradle's
+ cross-process lock handover works: a UDP ping to the lock owner's port
+ (org.gradle.cache.internal.locklistener.FileLockCommunicator.pingOwner,
+ see
https://github.com/gradle/gradle/blob/v8.14.4/platforms/core-execution/persistent-cache/src/main/java/org/gradle/cache/internal/locklistener/FileLockCommunicator.java)
+ would fail across isolated container network namespaces, as reported in
+ https://github.com/gradle/gradle/issues/851.
+ * --no-daemon avoids a background JVM that would outlive the container.
+ * GRADLE_USER_HOME persists the Gradle distribution and dependency cache
+ in java-sdk/.gradle/ so subsequent runs skip straight to compilation.
+ * HOME is set explicitly because --user runs as the host UID which has no
+ entry in the container's /etc/passwd; Docker would otherwise inherit the
+ image's HOME (/root) which the non-root process cannot write to.
+ * files/m2 is mounted directly as ~/.m2 so publishToMavenLocal writes
+ there without nesting, and its contents are visible on the host.
"""
- # * --user keeps build outputs owned by the current user (not root).
- # * --no-daemon avoids a background JVM that would outlive the container.
- # * GRADLE_USER_HOME persists the Gradle distribution and dependency cache
- # in java-sdk/.gradle/ so subsequent runs skip straight to compilation.
- # * HOME is set explicitly because --user runs as the host UID which has no
- # entry in the container's /etc/passwd; Docker would otherwise inherit
the
- # image's HOME (/root) which the non-root process cannot write to.
- # * files/m2 is mounted directly as ~/.m2 so publishToMavenLocal writes
- # there without nesting, and its contents are visible on the host.
- console.print("[yellow]Publishing Java SDK artifacts to local Maven
repository...")
- JAVA_SDK_MAVEN_CACHE_PATH.mkdir(parents=True, exist_ok=True)
- subprocess.run(
+ return subprocess.run(
[
"docker",
"run",
"--rm",
+ "--network=host",
"--user",
f"{os.getuid()}:{os.getgid()}",
"-e",
@@ -312,72 +315,65 @@ def _setup_java_sdk_integration(dot_env_file, tmp_dir):
"-v",
f"{AIRFLOW_ROOT_PATH}:/repo",
"-w",
- "/repo/java-sdk",
+ workdir,
"eclipse-temurin:17-jdk",
- "./gradlew",
- "publishToMavenLocal",
- "-PskipSigning=true",
+ "/repo/java-sdk/gradlew",
"--no-daemon",
+ *gradle_argv,
],
check=True,
+ capture_output=capture_output,
+ text=True,
)
- # TODO: Make the following build steps parallel
+
+
+def _build_example_bundle(workdir):
+ """Build one example bundle, capturing output so concurrent builds don't
interleave."""
+ try:
+ completed = _run_java_sdk_gradle_container(workdir, "bundle",
capture_output=True)
+ except subprocess.CalledProcessError as e:
+ console.print(f"[red]Bundle build failed in {workdir}:")
+ console.print(e.stdout, e.stderr, sep="\n", markup=False,
soft_wrap=True)
+ raise
+ console.print(f"[yellow]Bundle build finished in {workdir}:")
+ console.print(completed.stdout, completed.stderr, sep="\n", markup=False,
soft_wrap=True)
+
+
+def _setup_java_sdk_integration(dot_env_file, tmp_dir):
+ """Set up the java_sdk E2E test mode.
+
+ Builds the Java SDK and Scala Spark example bundles via the Gradle wrapper,
+ then builds a Java-capable Airflow worker image, copies the JARs into the
+ temp directory, and writes the coordinator configuration.
+ """
+ console.print("[yellow]Publishing Java SDK artifacts to local Maven
repository...")
+ JAVA_SDK_MAVEN_CACHE_PATH.mkdir(parents=True, exist_ok=True)
+ _run_java_sdk_gradle_container("/repo/java-sdk", "publishToMavenLocal",
"-PskipSigning=true")
+
+ # The example and scala_spark_example are independent Gradle builds that
both
+ # consume the SDK artifact published above, so build them concurrently.
Sharing
+ # a writable Gradle user home between concurrent builds is safe only
because
+ # --network=host lets each build ping the other's lock-owner port (see the
+ # helper's docstring); publishToMavenLocal has already unpacked the shared
+ # wrapper distribution, so neither build races to fetch it.
+ #
# The Gradle `bundle` task is a Copy that never prunes its destination, so
# JARs from an earlier build linger. A stale dependency JAR with its own
# Main-Class would make JavaCoordinator's Main-Class discovery ambiguous,
so
# start each bundle from an empty directory.
rmtree(JAVA_SDK_EXAMPLE_LIBS_PATH, ignore_errors=True)
- console.print("[yellow]Building Java SDK example bundle
(eclipse-temurin:17-jdk)...")
- subprocess.run(
- [
- "docker",
- "run",
- "--rm",
- "--user",
- f"{os.getuid()}:{os.getgid()}",
- "-e",
- "GRADLE_USER_HOME=/repo/java-sdk/.gradle",
- "-e",
- "HOME=/workspace-home",
- "-v",
- f"{JAVA_SDK_MAVEN_CACHE_PATH}:/workspace-home/.m2",
- "-v",
- f"{AIRFLOW_ROOT_PATH}:/repo",
- "-w",
- "/repo/java-sdk/example",
- "eclipse-temurin:17-jdk",
- "../gradlew",
- "bundle",
- "--no-daemon",
- ],
- check=True,
- )
rmtree(SCALA_SPARK_EXAMPLE_LIBS_PATH, ignore_errors=True)
- console.print("[yellow]Building Scala Spark example bundle
(eclipse-temurin:17-jdk)...")
- subprocess.run(
- [
- "docker",
- "run",
- "--rm",
- "--user",
- f"{os.getuid()}:{os.getgid()}",
- "-e",
- "GRADLE_USER_HOME=/repo/java-sdk/.gradle",
- "-e",
- "HOME=/workspace-home",
- "-v",
- f"{JAVA_SDK_MAVEN_CACHE_PATH}:/workspace-home/.m2",
- "-v",
- f"{AIRFLOW_ROOT_PATH}:/repo",
- "-w",
- "/repo/java-sdk/scala_spark_example",
- "eclipse-temurin:17-jdk",
- "../gradlew",
- "bundle",
- "--no-daemon",
- ],
- check=True,
+ console.print(
+ "[yellow]Building Java SDK and Scala Spark example bundles
concurrently (eclipse-temurin:17-jdk)..."
)
+ example_bundle_workdirs = [
+ "/repo/java-sdk/example",
+ "/repo/java-sdk/scala_spark_example",
+ ]
+ with ThreadPoolExecutor(max_workers=len(example_bundle_workdirs)) as pool:
+ bundle_builds = [pool.submit(_build_example_bundle, workdir) for
workdir in example_bundle_workdirs]
+ for build in bundle_builds:
+ build.result()
# Copy compose override and Dockerfile into the temp directory.
copyfile(JAVA_COMPOSE_PATH, tmp_dir / "java.yml")