diff --git a/.github/workflows/README.md b/.github/workflows/README.md index 3b8f9c56a65..b47ed127844 100644 --- a/.github/workflows/README.md +++ b/.github/workflows/README.md @@ -43,6 +43,12 @@ required one, so a red 3.4 there changes nothing. It is the next push with the label still applied that runs 3.4 under `Required Checks`, and with the queue run gone that push is the only thing that makes a 3.4 failure blocking. +`spark_4_2` is outside the tiers for the opposite reason: Spark 4.2 support +is new and experimental rather than deprecated. It runs only when a pull +request carries `run-spark-4.2-tests`, or from a `workflow_dispatch`, and the +same label-run caveat above applies to it. Moving it into the nightly sweep +is a follow-up once the version has settled. + Heavy jobs have no `push` tier. The queue already tested the exact tree that lands, so re-running them on push to main would double the cost of every merge. Two jobs are still on `push`: `docs`, because it deploys to `asf-site` @@ -107,6 +113,7 @@ tiers partition the list and that the `pr` tier is exactly the default profile. label or dispatch only spark_4_1 sql_hive run-spark-4.1-hive-tests ---------------------- iceberg_1_11 run-iceberg-tests spark_3_4 run-spark-3.4-tests + spark_4_2 run-spark-4.2-tests nightly tier, or PR with label ------------------------------ pr_build_linux run-all-spark-profiles @@ -134,6 +141,9 @@ tiers partition the list and that the `pr` tier is exactly the default profile. | issue | +-----------------------+ + spark_4_2 + (workflow_dispatch only) + reusable workflows invoked via `uses:`: pr_build_linux.yml spark_sql_test_reusable.yml pr_build_macos.yml iceberg_spark_test_reusable.yml @@ -157,6 +167,7 @@ tiers partition the list and that the `pr` tier is exactly the default profile. | `spark_4_1` | merge group, **or** PR with `run-spark-4.1-tests`; the `sql_hive` shards alone with `run-spark-4.1-hive-tests` | Spark 4.1 sources | | `spark_3_4` | PR with `run-spark-3.4-tests`, or dispatch | Spark 3.4 sources | | `spark_4_0` | nightly, **or** PR with `run-spark-4.0-tests` | Spark 4.0 sources | +| `spark_4_2` | PR with `run-spark-4.2-tests`, or dispatch | Spark 4.2 sources | | `iceberg_1_11` | merge group, **or** PR with `run-iceberg-tests` | Iceberg sources | | `iceberg_1_8` | nightly, **or** PR with `run-iceberg-tests` | Iceberg sources | | `iceberg_1_9` | nightly, **or** PR with `run-iceberg-tests` | Iceberg sources | @@ -261,16 +272,16 @@ umbrella doesn't watch, or operate independently of the rest of CI: ## Reusable workflows (called by `ci.yml`) -| File | Called from `ci.yml` job(s) | -| --------------------------------- | ------------------------------------------------------------ | -| `pr_build_linux.yml` | `pr_build_linux` | -| `pr_build_macos.yml` | `pr_build_macos` | -| `pr_benchmark_check.yml` | `pr_benchmark_check` | -| `delta_build_gate.yml` | `delta_build_gate` | -| `pyarrow_udf_test.yml` | `pyarrow_udf_test` | -| `docs.yaml` | `docs` | -| `spark_sql_test_reusable.yml` | `spark_3_4`, `spark_3_5`, `spark_4_0`, `spark_4_1` | -| `iceberg_spark_test_reusable.yml` | `iceberg_1_8`, `iceberg_1_9`, `iceberg_1_10`, `iceberg_1_11` | +| File | Called from `ci.yml` job(s) | +| --------------------------------- | --------------------------------------------------------------- | +| `pr_build_linux.yml` | `pr_build_linux` | +| `pr_build_macos.yml` | `pr_build_macos` | +| `pr_benchmark_check.yml` | `pr_benchmark_check` | +| `delta_build_gate.yml` | `delta_build_gate` | +| `pyarrow_udf_test.yml` | `pyarrow_udf_test` | +| `docs.yaml` | `docs` | +| `spark_sql_test_reusable.yml` | `spark_3_4`, `spark_3_5`, `spark_4_0`, `spark_4_1`, `spark_4_2` | +| `iceberg_spark_test_reusable.yml` | `iceberg_1_8`, `iceberg_1_9`, `iceberg_1_10`, `iceberg_1_11` | ## Changing what runs when diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d5bdde5aa68..2ac0db7bf57 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -77,6 +77,14 @@ on: - cron: '0 6 * * *' workflow_dispatch: +# Least-privilege default for the whole pipeline: every job here builds and tests, +# and nothing needs to write back to the repository. Jobs that call reusable +# workflows inherit this, and a callee can only narrow it further -- so the one +# job that does need to write (docs, which pushes the built site to asf-site) +# raises it to `contents: write` on itself. +permissions: + contents: read + jobs: # --------------------------------------------------------------------------- # preflight: cheap checks that gate everything else. Failure short-circuits @@ -191,6 +199,7 @@ jobs: spark_4_0: ${{ steps.compute.outputs.spark_4_0 }} spark_4_1: ${{ steps.compute.outputs.spark_4_1 }} spark_4_1_hive: ${{ steps.compute.outputs.spark_4_1_hive }} + spark_4_2: ${{ steps.compute.outputs.spark_4_2 }} iceberg_1_8: ${{ steps.compute.outputs.iceberg_1_8 }} iceberg_1_9: ${{ steps.compute.outputs.iceberg_1_9 }} iceberg_1_10: ${{ steps.compute.outputs.iceberg_1_10 }} @@ -347,6 +356,10 @@ jobs: needs: changes # docs deploys to asf-site, so only run on push-to-main (or a manual dispatch). if: needs.changes.outputs.docs == 'true' + # docs.yaml commits and pushes the generated site to the asf-site branch, so + # this job needs write access on top of the read-only default above. + permissions: + contents: write uses: ./.github/workflows/docs.yaml spark_3_4: @@ -415,6 +428,22 @@ jobs: || needs.changes.outputs.spark_4_1_hive != 'true' && 'core' || 'all' }} + spark_4_2: + name: Spark SQL Tests (Spark 4.2) + needs: changes + # On demand only for now: Spark 4.2 support is brand new, so it sits + # outside the pull request, queue and nightly tiers, exactly like the + # deprecated 3.4 suite. It runs from `run-spark-4.2-tests` on a pull + # request, or from a workflow_dispatch; POLICY in compute-changes.py is + # where that is expressed, not an event check here. Folding it into the + # nightly is a follow-up once the version has settled. + if: needs.changes.outputs.spark_4_2 == 'true' + uses: ./.github/workflows/spark_sql_test_reusable.yml + with: + spark-short: '4.2' + spark-full: '4.2.0' + java: 17 + iceberg_1_8: name: Iceberg Spark SQL Tests (Iceberg 1.8) needs: changes @@ -517,6 +546,7 @@ jobs: - spark_3_5 - spark_4_0 - spark_4_1 + - spark_4_2 - iceberg_1_8 - iceberg_1_9 - iceberg_1_10 diff --git a/dev/ci/check-ci-config.py b/dev/ci/check-ci-config.py index abb6b1f2d4b..bb2676f63e9 100644 --- a/dev/ci/check-ci-config.py +++ b/dev/ci/check-ci-config.py @@ -103,6 +103,7 @@ "spark_4_0", "spark_4_1", "spark_4_1_hive", + "spark_4_2", "iceberg_1_8", "iceberg_1_9", "iceberg_1_10", @@ -185,10 +186,16 @@ # `schedule` cases below assert its absence rather than quietly accept it # coming back. SPARK_DEPRECATED = {"spark_3_4"} +# Spark 4.2 is experimental rather than deprecated, but it is routed the same +# way: outside the queue and nightly tiers entirely, reachable through +# `run-spark-4.2-tests` on a pull request or a workflow_dispatch. Keeping it +# in its own set is what makes the `merge_group` and `schedule` cases below +# assert its absence rather than quietly accept it arriving. +SPARK_EXPERIMENTAL = {"spark_4_2"} # One label opts a pull request into every Iceberg version, whichever tier # each sits in. ICEBERG_OPT_IN = {"iceberg_1_8", "iceberg_1_9", "iceberg_1_10", "iceberg_1_11"} -ALL_JOBS = QUEUE_TIER | NIGHTLY_TIER | SPARK_DEPRECATED | {"docs"} +ALL_JOBS = QUEUE_TIER | NIGHTLY_TIER | SPARK_DEPRECATED | SPARK_EXPERIMENTAL | {"docs"} assert not QUEUE_TIER & NIGHTLY_TIER, "a job is queue or nightly, never both" POLICY_CASES = [ @@ -333,6 +340,21 @@ }, {"spark_3_4"}, ), + # Spark 4.2 is experimental and routed like 3.4: the label is the only way + # it reaches a pull request, and it pulls in no other Spark version. + ( + {"name": "pull_request", "action": "synchronize", "labels": ["run-spark-4.2-tests"]}, + PR_TIER | {"spark_4_2"}, + ), + ( + { + "name": "pull_request", + "action": "labeled", + "label": "run-spark-4.2-tests", + "labels": ["run-spark-4.2-tests"], + }, + {"spark_4_2"}, + ), ( {"name": "pull_request", "action": "synchronize", "labels": ["run-iceberg-tests"]}, PR_TIER | ICEBERG_OPT_IN, diff --git a/dev/ci/compute-changes.py b/dev/ci/compute-changes.py index 799b791624a..d1e8d3b44fc 100644 --- a/dev/ci/compute-changes.py +++ b/dev/ci/compute-changes.py @@ -286,6 +286,33 @@ ".mvn/**", "mvnw", ], + "spark_4_2": [ + "native/**/src/**", + "native/**/Cargo.toml", + "native/Cargo.lock", + "common/src/main/**", + "common/pom.xml", + "spark/src/main/**", + "!spark/src/main/spark-3.4/**", + "!spark/src/main/spark-3.5/**", + "!spark/src/main/spark-3.x/**", + "!spark/src/main/spark-4.0/**", + "!spark/src/main/spark-4.1/**", + "!spark/src/main/scala/org/apache/comet/GenerateDocs.scala", + "spark/pom.xml", + "dev/diffs/4.2.0.diff", + "pom.xml", + "rust-toolchain.toml", + ".github/workflows/ci.yml", + ".github/workflows/spark_sql_test_reusable.yml", + "dev/ci/spark-sql-modules.py", + ".github/actions/setup-builder/**", + ".github/actions/setup-spark-builder/**", + ".github/actions/upload-artifact-retry/**", + ".github/actions/download-artifact-retry/**", + ".mvn/**", + "mvnw", + ], # Same inputs as spark_4_1: this is not a separate job but a second # POLICY decision for the same call, selecting the sql_hive matrix rows. # ci.yml folds the two outputs into the reusable workflow's `modules` @@ -493,6 +520,11 @@ "label:run-spark-4.1-tests", "label:run-spark-4.1-hive-tests", ], + # Spark 4.2 support is new and experimental, so like the deprecated 3.4 + # suite it sits outside all three tiers: it runs from `run-spark-4.2-tests` + # on a pull request, or a workflow_dispatch, and gates no merge. Folding it + # into the nightly is the follow-up once the version has settled. + "spark_4_2": ["label:run-spark-4.2-tests"], # Same shape for Iceberg: 1.11 is the only Spark 4.1 coverage, so it is # the one Iceberg version the queue runs; the three older versions run # nightly. One label opts a pull request into all four. diff --git a/dev/diffs/4.2.0.diff b/dev/diffs/4.2.0.diff new file mode 100644 index 00000000000..c8ca219075c --- /dev/null +++ b/dev/diffs/4.2.0.diff @@ -0,0 +1,4305 @@ +diff --git a/core/src/test/scala/org/apache/spark/storage/FallbackStorageSuite.scala b/core/src/test/scala/org/apache/spark/storage/FallbackStorageSuite.scala +index 6df8bc85b51..dabb75e2b75 100644 +--- a/core/src/test/scala/org/apache/spark/storage/FallbackStorageSuite.scala ++++ b/core/src/test/scala/org/apache/spark/storage/FallbackStorageSuite.scala +@@ -268,6 +268,11 @@ class FallbackStorageSuite extends SparkFunSuite with LocalSparkContext { + } + + test("Upload from all decommissioned executors") { ++ // Comet replaces Spark's shuffle with its own native shuffle, which is incompatible with ++ // the fallback storage migration path used by BlockManagerDecommissioner. ++ val cometEnv = System.getenv("ENABLE_COMET") ++ assume(cometEnv == null || cometEnv == "0" || cometEnv == "false", ++ "Skipped when Comet is enabled: incompatible with Comet native shuffle storage") + sc = new SparkContext(getSparkConf(2, 2)) + withSpark(sc) { sc => + TestUtils.waitUntilExecutorsUp(sc, 2, 60000) +@@ -298,6 +303,11 @@ class FallbackStorageSuite extends SparkFunSuite with LocalSparkContext { + } + + test("Upload multi stages") { ++ // Comet replaces Spark's shuffle with its own native shuffle, which is incompatible with ++ // the fallback storage migration path used by BlockManagerDecommissioner. ++ val cometEnv = System.getenv("ENABLE_COMET") ++ assume(cometEnv == null || cometEnv == "0" || cometEnv == "false", ++ "Skipped when Comet is enabled: incompatible with Comet native shuffle storage") + sc = new SparkContext(getSparkConf()) + withSpark(sc) { sc => + TestUtils.waitUntilExecutorsUp(sc, 1, 60000) +@@ -332,6 +342,11 @@ class FallbackStorageSuite extends SparkFunSuite with LocalSparkContext { + + CompressionCodec.shortCompressionCodecNames.keys.foreach { codec => + test(s"$codec - Newly added executors should access old data from remote storage") { ++ // Comet replaces Spark's shuffle with its own native shuffle, which is incompatible with ++ // the fallback storage migration path used by BlockManagerDecommissioner. ++ val cometEnv = System.getenv("ENABLE_COMET") ++ assume(cometEnv == null || cometEnv == "0" || cometEnv == "false", ++ "Skipped when Comet is enabled: incompatible with Comet native shuffle storage") + sc = new SparkContext(getSparkConf(2, 0).set(IO_COMPRESSION_CODEC, codec)) + withSpark(sc) { sc => + TestUtils.waitUntilExecutorsUp(sc, 2, 60000) +diff --git a/pom.xml b/pom.xml +index 46558134f41..862c9a6eb9e 100644 +--- a/pom.xml ++++ b/pom.xml +@@ -154,6 +154,8 @@ + 4.0.3 + 2.5.3 + 2.0.8 ++ 4.2 ++ 1.1.0-SNAPSHOT + + + org.apache.datasketches +diff --git a/sql/core/pom.xml b/sql/core/pom.xml +index 82810f181ac..21a83831188 100644 +--- a/sql/core/pom.xml ++++ b/sql/core/pom.xml +@@ -97,6 +97,10 @@ + org.apache.spark + spark-tags_${scala.binary.version} + ++ ++ org.apache.datafusion ++ comet-spark-spark${spark.version.short}_${scala.binary.version} ++ + +