diff --git a/.github/workflows/delta-conversion-ci.yml b/.github/workflows/delta-conversion-ci.yml index 630f1678cff4..7ff3aab82c5b 100644 --- a/.github/workflows/delta-conversion-ci.yml +++ b/.github/workflows/delta-conversion-ci.yml @@ -78,12 +78,14 @@ jobs: strategy: max-parallel: 15 matrix: - jvm: [17, 21] + jvm: [17, 21, 25] event_name: ['${{ github.event_name }}'] exclude: # Pull requests run the baseline JDK only. - event_name: pull_request jvm: 21 + - event_name: pull_request + jvm: 25 env: SPARK_LOCAL_IP: localhost steps: @@ -112,12 +114,14 @@ jobs: strategy: max-parallel: 15 matrix: - jvm: [17, 21] + jvm: [17, 21, 25] event_name: ['${{ github.event_name }}'] exclude: # Pull requests run the baseline JDK only. - event_name: pull_request jvm: 21 + - event_name: pull_request + jvm: 25 env: SPARK_LOCAL_IP: localhost steps: diff --git a/.github/workflows/flink-ci.yml b/.github/workflows/flink-ci.yml index f525c11cb659..440e70ca96fe 100644 --- a/.github/workflows/flink-ci.yml +++ b/.github/workflows/flink-ci.yml @@ -81,12 +81,14 @@ jobs: strategy: max-parallel: 15 matrix: - jvm: [17, 21] + jvm: [17, 21, 25] event_name: ['${{ github.event_name }}'] exclude: # Pull requests run the baseline JDK only. - event_name: pull_request jvm: 21 + - event_name: pull_request + jvm: 25 flink: ['1.20', '2.0', '2.1'] env: SPARK_LOCAL_IP: localhost diff --git a/.github/workflows/hive-ci.yml b/.github/workflows/hive-ci.yml index d45860a3011d..21ead639b642 100644 --- a/.github/workflows/hive-ci.yml +++ b/.github/workflows/hive-ci.yml @@ -79,12 +79,14 @@ jobs: strategy: max-parallel: 15 matrix: - jvm: [17, 21] + jvm: [17, 21, 25] event_name: ['${{ github.event_name }}'] exclude: # Pull requests run the baseline JDK only. - event_name: pull_request jvm: 21 + - event_name: pull_request + jvm: 25 env: SPARK_LOCAL_IP: localhost steps: diff --git a/.github/workflows/java-ci.yml b/.github/workflows/java-ci.yml index 30a4db917693..72f8fe1c9e2a 100644 --- a/.github/workflows/java-ci.yml +++ b/.github/workflows/java-ci.yml @@ -74,12 +74,14 @@ jobs: strategy: max-parallel: 15 matrix: - jvm: [17, 21] + jvm: [17, 21, 25] event_name: ['${{ github.event_name }}'] exclude: # Pull requests run the baseline JDK only. - event_name: pull_request jvm: 21 + - event_name: pull_request + jvm: 25 env: SPARK_LOCAL_IP: localhost steps: @@ -108,12 +110,14 @@ jobs: strategy: max-parallel: 15 matrix: - jvm: [17, 21] + jvm: [17, 21, 25] event_name: ['${{ github.event_name }}'] exclude: # Pull requests run the baseline JDK only. - event_name: pull_request jvm: 21 + - event_name: pull_request + jvm: 25 steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: @@ -133,12 +137,14 @@ jobs: strategy: max-parallel: 15 matrix: - jvm: [17, 21] + jvm: [17, 21, 25] event_name: ['${{ github.event_name }}'] exclude: # Pull requests run the baseline JDK only. - event_name: pull_request jvm: 21 + - event_name: pull_request + jvm: 25 steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: diff --git a/.github/workflows/kafka-connect-ci.yml b/.github/workflows/kafka-connect-ci.yml index 66234ba364e6..0b38e604e236 100644 --- a/.github/workflows/kafka-connect-ci.yml +++ b/.github/workflows/kafka-connect-ci.yml @@ -79,12 +79,14 @@ jobs: strategy: max-parallel: 15 matrix: - jvm: [17, 21] + jvm: [17, 21, 25] event_name: ['${{ github.event_name }}'] exclude: # Pull requests run the baseline JDK only. - event_name: pull_request jvm: 21 + - event_name: pull_request + jvm: 25 env: SPARK_LOCAL_IP: localhost steps: diff --git a/.github/workflows/spark-ci.yml b/.github/workflows/spark-ci.yml index 9eae0731f4d6..6f521713a4b7 100644 --- a/.github/workflows/spark-ci.yml +++ b/.github/workflows/spark-ci.yml @@ -81,7 +81,7 @@ jobs: # Keep matrix <= 20 jobs; exceeding it queues a second wave and slows CI. max-parallel: 20 matrix: - jvm: [17, 21] + jvm: [17, 21, 25] event_name: ['${{ github.event_name }}'] spark: ['3.5', '4.0', '4.1'] scala: ['2.12', '2.13'] @@ -95,9 +95,18 @@ jobs: scala: '2.12' - spark: '4.1' scala: '2.12' + # Java 25 is only validated against Spark 4.1. Spark 3.5 supports Java 8/11/17 and + # Spark 4.0 supports Java 17/21, so neither runs on Java 25. Restricting Java 25 to + # Spark 4.1 also keeps the matrix within the 20-job Apache infra ceiling above. + - jvm: 25 + spark: '3.5' + - jvm: 25 + spark: '4.0' # Pull requests run the baseline JDK only. - event_name: pull_request jvm: 21 + - event_name: pull_request + jvm: 25 env: SPARK_LOCAL_IP: localhost steps: diff --git a/build.gradle b/build.gradle index dd4f145303b0..5be4ea02200c 100644 --- a/build.gradle +++ b/build.gradle @@ -51,7 +51,7 @@ try { project.logger.error(e.getMessage()) } -if (JavaVersion.current() == JavaVersion.VERSION_17 || JavaVersion.current() == JavaVersion.VERSION_21) { +if (JavaVersion.current() == JavaVersion.VERSION_17 || JavaVersion.current() == JavaVersion.VERSION_21 || JavaVersion.current() == JavaVersion.VERSION_25) { project.ext.jdkVersion = JavaVersion.current().getMajorVersion().toString() project.ext.extraJvmArgs = ["--add-opens", "java.base/java.io=ALL-UNNAMED", "--add-opens", "java.base/java.lang.invoke=ALL-UNNAMED", @@ -74,8 +74,18 @@ if (JavaVersion.current() == JavaVersion.VERSION_17 || JavaVersion.current() == "--add-opens", "java.base/sun.nio.cs=ALL-UNNAMED", "--add-opens", "java.base/sun.security.action=ALL-UNNAMED", "--add-opens", "java.base/sun.util.calendar=ALL-UNNAMED"] + + if (JavaVersion.current() > JavaVersion.VERSION_21) { + // Netty disables sun.misc.Unsafe by default on Java 24+, but arrow-memory-netty's + // PooledByteBufAllocatorL casts allocations to PooledUnsafeDirectByteBuf, so the + // non-Unsafe fallback fails with a ClassCastException. Keep the Unsafe path enabled + // and allow the memory-access methods it relies on. + project.ext.extraJvmArgs += ["--sun-misc-unsafe-memory-access=allow", + "-Dio.netty.noUnsafe=false", + "-Dio.netty.tryReflectionSetAccessible=true"] + } } else { - throw new GradleException("This build must be run with JDK 17 or 21 but was executed with JDK " + JavaVersion.current()) + throw new GradleException("This build must be run with JDK 17, 21, or 25 but was executed with JDK " + JavaVersion.current()) } tasks.withType(AbstractArchiveTask).configureEach { @@ -188,7 +198,12 @@ subprojects { compileClasspath { // do not exclude Guava so the bundle project can reference classes. - if (project.name != 'iceberg-bundled-guava') { + // iceberg-gcp needs the real Guava on the compile classpath only so that javac can + // resolve the Guava types referenced by annotated google-cloud-storage signatures; + // on JDK 22+ javac attaches type annotations across compilation boundaries + // (JDK-8225377) and errors when the annotated type is absent. Iceberg code still + // uses the shaded copy at runtime. + if (project.name != 'iceberg-bundled-guava' && project.name != 'iceberg-gcp') { exclude group: 'com.google.guava', module: 'guava' } // contains a copy of Guava @@ -207,6 +222,7 @@ subprojects { substitute module("org.lz4:lz4-java") using module(libs.lz4Java.get().toString()) because("Enforce lz4-java that contains CVE-2025-12183 and CVE-2025-66566 fixes") substitute module("io.airlift:aircompressor") using module(libs.aircompressor.get().toString()) because("Enforce aircompressor that contains CVE-2025-67721 fix") substitute module("org.bouncycastle:bcprov-jdk18on") using module(libs.bouncycastle.bcprov.get().toString()) because("Enforce BouncyCastle that contains CVE-2026-5598 fix") + substitute module("org.apache.datasketches:datasketches-memory") using module(libs.datasketchesMemory.get().toString()) because("datasketches-memory rejects any JDK outside 1.8/8/11/17/21; the java17_25 artifact targets Java 17 bytecode without a version gate, so it runs on 17, 21 and 25") } eachDependency { details -> if (details.requested.group == 'io.netty' @@ -767,6 +783,9 @@ project(':iceberg-gcp') { compileOnly "com.google.cloud:google-cloud-storage" compileOnly "com.google.cloud:google-cloud-kms" compileOnly(libs.gcs.analytics.core) + // Required so javac can resolve the Guava types in annotated google-cloud-storage + // signatures; see the iceberg-gcp note on the compileClasspath Guava exclusion. + compileOnly libs.guava.guava testImplementation "com.google.cloud:google-cloud-nio" diff --git a/common/src/test/java/org/apache/iceberg/common/TestDynConstructors.java b/common/src/test/java/org/apache/iceberg/common/TestDynConstructors.java index 1edf70a7fbba..7db9f448a2b6 100644 --- a/common/src/test/java/org/apache/iceberg/common/TestDynConstructors.java +++ b/common/src/test/java/org/apache/iceberg/common/TestDynConstructors.java @@ -47,7 +47,7 @@ public void testInterfaceWrongImplString() throws Exception { // TODO this should throw, since the MyUnrelatedClass does not implement MyInterface .impl("org.apache.iceberg.common.TestDynConstructors$MyUnrelatedClass") .buildChecked(); - assertThatThrownBy(ctor::newInstance) + assertThatThrownBy(() -> consume(ctor.newInstance())) .isInstanceOf(ClassCastException.class) .hasMessageStartingWith( "class org.apache.iceberg.common.TestDynConstructors$MyUnrelatedClass cannot be cast to class org.apache.iceberg.common.TestDynConstructors$MyInterface"); @@ -61,7 +61,7 @@ public void testInterfaceWrongImplClass() throws Exception { // implement MyInterface .impl(MyUnrelatedClass.class) .buildChecked(); - assertThatThrownBy(ctor::newInstance) + assertThatThrownBy(() -> consume(ctor.newInstance())) .isInstanceOf(ClassCastException.class) .hasMessageStartingWith( "class org.apache.iceberg.common.TestDynConstructors$MyUnrelatedClass cannot be cast to class org.apache.iceberg.common.TestDynConstructors$MyInterface"); @@ -82,4 +82,11 @@ public interface MyInterface {} public static class MyClass implements MyInterface {} public static class MyUnrelatedClass {} + + /** + * Receives the constructed value as {@link MyInterface} so the narrowing cast is always present + * in the generated bytecode. javac drops the cast when the result of the call is discarded, which + * would otherwise make the surrounding assertions depend on the compiler version. + */ + private static void consume(MyInterface instance) {} } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index cce82a3ba1fa..0c05c0d59a61 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -41,6 +41,7 @@ bson-ver = "4.11.5" caffeine = "2.9.3" calcite = "1.42.0" datasketches = "6.2.0" +datasketchesMemory = "7.0.0" delta-standalone = "3.3.2" delta-spark = "3.3.2" derby = "10.15.2.0" @@ -118,6 +119,7 @@ caffeine = { module = "com.github.ben-manes.caffeine:caffeine", version.ref = "c calcite-core = { module = "org.apache.calcite:calcite-core", version.ref = "calcite" } calcite-druid = { module = "org.apache.calcite:calcite-druid", version.ref = "calcite" } datasketches = { module = "org.apache.datasketches:datasketches-java", version.ref = "datasketches" } +datasketchesMemory = { module = "org.apache.datasketches:datasketches-memory-java17_25", version.ref = "datasketchesMemory" } delta-standalone = { module = "io.delta:delta-standalone_2.12", version.ref = "delta-standalone" } errorprone-annotations = { module = "com.google.errorprone:error_prone_annotations", version.ref = "errorprone-annotations" } failsafe = { module = "dev.failsafe:failsafe", version.ref = "failsafe"} diff --git a/jmh.gradle b/jmh.gradle index a4d794f1e41a..74a86ff0fe40 100644 --- a/jmh.gradle +++ b/jmh.gradle @@ -17,8 +17,8 @@ * under the License. */ -if (jdkVersion != '17' && jdkVersion != '21') { - throw new GradleException("The JMH benchmarks must be run with JDK 17 or JDK 21") +if (jdkVersion != '17' && jdkVersion != '21' && jdkVersion != '25') { + throw new GradleException("The JMH benchmarks must be run with JDK 17, JDK 21, or JDK 25") } def flinkVersions = (System.getProperty("flinkVersions") != null ? System.getProperty("flinkVersions") : System.getProperty("defaultFlinkVersions")).split(",")