Skip to content

Commit 1d008ba

Browse files
tkaymakclaudegemini-code-assist[bot]
authored
[runners-spark] Add Spark 4 runner (#38255)
* build: add Spark 4.0.2 version property and Scala 2.13 support Add spark4_version (4.0.2) to BeamModulePlugin alongside the existing spark3_version. Update spark_runner.gradle to conditionally select the correct Scala library (2.13 vs 2.12), Jackson module, Kafka test dependency, and require Java 17 when building against Spark 4. Register the new :runners:spark:4 module in settings.gradle.kts. These changes are purely additive — all conditionals gate on spark_version.startsWith("4") or spark_scala_version == '2.13', leaving the Spark 3 build path untouched. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * refactor: make shared Spark source compatible with Scala 2.12 and 2.13 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * build: add runners/spark/4/ build configuration Add the Gradle build file for the Spark 4 structured streaming runner. The module mirrors runners/spark/3/ — it inherits the shared RDD-base source from runners/spark/src/ via copySourceBase and adds its own Structured Streaming implementation in src/main/java. Key differences from the Spark 3 build: - Uses spark4_version (4.0.2) with Scala 2.13. - Excludes DStream-based streaming tests (Spark 4 supports only structured streaming batch). - Unconditionally adds --add-opens JVM flags required by Kryo on Java 17 (Spark 4's minimum). - Binds Spark driver to 127.0.0.1 for macOS compatibility. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat: add Spark 4 structured streaming runner source Add the Spark 4 structured streaming runner implementation and tests. Most files are adapted from the Spark 3 structured streaming runner with targeted changes for Spark 4 / Scala 2.13 API compatibility. Key Spark 4-specific changes (diff against runners/spark/3/src/): EncoderFactory — Replaced the direct ExpressionEncoder constructor (removed in Spark 4) with BeamAgnosticEncoder, a named class implementing both AgnosticExpressionPathEncoder (for expression delegation via toCatalyst/fromCatalyst) and AgnosticEncoders .StructEncoder (so Dataset.select(TypedColumn) creates an N-attribute plan, preventing FIELD_NUMBER_MISMATCH). The toCatalyst/fromCatalyst methods substitute the provided input expression via transformUp, enabling correct nesting inside composite encoders like Encoders.tuple(). EncoderHelpers — Added toExpressionEncoder() helper to handle Spark 4 built-in encoders that are AgnosticEncoder subclasses rather than ExpressionEncoder. GroupByKeyTranslatorBatch — Migrated from internal catalyst Expression API (CreateNamedStruct, Literal$) to public Column API (struct(), lit(), array()), as required by Spark 4. BoundedDatasetFactory — Use classic.Dataset$.MODULE$.ofRows() as Dataset moved to org.apache.spark.sql.classic in Spark 4. ScalaInterop — Replace WrappedArray.ofRef (removed in Scala 2.13) with JavaConverters.asScalaBuffer().toList() in seqOf(). GroupByKeyHelpers, CombinePerKeyTranslatorBatch — Replace TraversableOnce with IterableOnce (Scala 2.13 rename). SparkStructuredStreamingPipelineResult — Replace sparkproject.guava with Beam's vendored Guava. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * ci: add Spark 4 PreCommit and PostCommit workflows Add GitHub Actions workflows for the Spark 4 runner module: - beam_PreCommit_Java_Spark4_Versions: runs sparkVersionsTest on changes to runners/spark/**. Currently a no-op (the sparkVersions map is empty) but scaffolds future patch version coverage. - beam_PostCommit_Java_ValidatesRunner_Spark4StructuredStreaming: runs the structured streaming test suite on Java 17. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Add PreCommit Java Spark4 Versions workflow * Add cancellation support to Spark pipeline execution * Remove unused endOfData() call in close method Remove endOfData() call in close method. * build: add Spark 4 job-server and container modules Add job-server and container build configurations for Spark 4, mirroring the existing Spark 3 job-server setup. The container uses eclipse-temurin:17 (Spark 4 requires Java 17). The shared spark_job_server.gradle gains a requireJavaVersion conditional for Spark 4 parent projects. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * build: remove spark.driver.host workaround from Spark 4 build The hostname binding hack is no longer needed now that the local machine resolves its hostname to 127.0.0.1 via /etc/hosts. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * docs: add Spark 4 runner entry to CHANGES.md Called out in /ultrareview as a missing contributor checklist item. Adds a Highlight line and a New Features / Improvements entry under the 2.74.0 Unreleased section, referencing issue #36841. * docs: explain classic.SparkSession downcast in BoundedDatasetFactory Per /ultrareview feedback: the one-line comment didn't make clear why the cast is safe. Expand it to note that SparkSession.builder() always returns a classic.SparkSession at runtime, which is why the downcast avoids reflection. * fix: log warning when neither WrappedArray nor ArraySeq class is found Per /ultrareview feedback: the fallback branch silently swallowed the second ClassNotFoundException. In practice one of the two classes is always present (Scala 2.12 vs 2.13 stdlib), but a silent skip could mask a broken classpath. Emit a LOG.warn instead. * build: compare spark_version numerically via isSparkAtLeast helper Per /ultrareview feedback: the five `"$spark_version" >= "3.5.0"` checks were lexicographic string comparisons. They happened to work for 3.5.0 and 4.0.2 only because '4' > '3' as chars — a future "3.10.0" release would compare less than "3.5.0" and silently drop the Spark 3.5+ dependencies and exclusions. Introduce an `isSparkAtLeast` closure that tokenizes on `.` and `-`, keeps numeric parts, and compares component-by-component. Replace all five call sites. * [Spark Runner] Slim Spark 4 to override-only files With spark_runner.gradle now layering per-major source overrides on top of the shared base, runners/spark/4/src/ no longer needs to duplicate 62 byte-identical structured-streaming files. Keep only the 11 files that actually differ for Spark 4 / Scala 2.13. Switch the build.gradle to spark_major = '4' (the new mechanism) and bump spark_versions to 3,4. Compiled output unchanged — the deleted files are reproduced identically inside build/source-overrides by the Copy task. * [Spark Runner] Use java.io.Serializable in DoFnRunnerFactory base scala.Serializable was removed in Scala 2.13. java.io.Serializable works identically on both Scala 2.12 and 2.13, so this can live in the shared base instead of needing a Spark-4-only override file. * [Spark Runner] Null-guard error message logging in EvaluationContext base Wrap Throwables.getRootCause(e).getMessage() in String.valueOf(...) to make the error logging robust to a null root-cause message. The behaviour change applies equally to Spark 3 and Spark 4, so the fix lives in the shared base and the Spark-4 override is dropped. * [Spark Runner] Cancel execution future and use Beam-vendored Guava in PipelineResult Two changes that previously lived only in the Spark-4 override and are equally valid for Spark 3: 1. cancel() now actually cancels the executing future (pipelineExecution.cancel(true)) in addition to setting the state to CANCELLED. Without this, calling cancel() left the pipeline running silently — a real bug, not a Spark-4 specific concern. 2. Switch from Spark's shaded guava (org.sparkproject.guava) to the Beam-vendored guava that is already on the classpath. Spark 4 no longer exposes the sparkproject guava package; using the vendored one removes the version coupling for both runners. * ci: re-trigger to clear flaky UnboundedScheduledExecutorServiceTest Empty commit to re-run CI. The only failure on the prior head was UnboundedScheduledExecutorServiceTest.testThreadsAreAddedOnlyAsNeededWithContention, a known flake (#31590) — the test itself acknowledges contention-induced extra threads in its inline comment. Squash or drop on rebase before merge. * [Spark Runner] Fix maxTimestamp to handle multi-window values Iterables.getOnlyElement(windows) crashes with IllegalArgumentException when a WindowedValue is associated with more than one window (e.g. after a sliding window assignment). Compute the max maxTimestamp() across all associated windows instead, falling back to a clear error if the iterable is unexpectedly empty. Applied identically to the shared base and the Spark 4 override. Flagged by Gemini Code Assist on PR #38255. * [Spark Runner] Drop unchecked cast in BoundedDatasetFactory.split source.split returns List<? extends BoundedSource<T>>, which already satisfies the subsequent stream usage. The cast was unchecked and would trip heap-pollution warnings. Applied identically to the shared base and the Spark 4 override. Flagged by Gemini Code Assist on PR #38255. * [Spark Runner] Drop redundant Iterator cast in Spark 4 GroupByKeyTranslatorBatch The (Iterator<V>) cast inside fun2 is redundant: fun2's signature infers the iterator type. The shared base translator at the analogous call site already calls iterableOnce(it) without a cast. Flagged by Gemini Code Assist on PR #38255. * [Spark Runner] Spark 4 EncoderFactory: stable constructor lookup + document trait setter Replace getConstructors()[0] (JVM-defined ordering, not stable) with a helper that picks the widest public constructor. The downstream switch already dispatches on parameter count to pick the right argument shape per Spark version, so this just makes the choice deterministic. Also document the org$apache$spark...$_setter_$isStruct_$eq method — it is the synthetic setter the Scala compiler emits for trait val fields, required when implementing AgnosticEncoders.StructEncoder from Java. Both flagged by Gemini Code Assist on PR #38255. * [Spark Runner] Fix Javadoc/comment typos flagged by Gemini Three trivial typos flagged on PR #38255 round 2 review, applied identically to the shared base and the Spark 4 override: - CombinePerKeyTranslatorBatch: "other there other missing features?" -> "are there other missing features?" - GroupByKeyTranslatorBatch: "build-in" -> "built-in" - EncoderHelpers: PRIMITIV_TYPES -> PRIMITIVE_TYPES (constant + caller) * [Spark Runner] Switch EncoderFactory.invoke on the right constructor In EncoderFactory.invoke(Expression obj, ...), the switch was keyed on STATIC_INVOKE_CONSTRUCTOR.getParameterCount() but the body actually calls INVOKE_CONSTRUCTOR. This worked by coincidence: across the supported Spark 3.x versions both constructors happen to share the same parameter counts at the same dispatch points. A future Spark release where the two diverge would silently pick the wrong branch. Switch on INVOKE_CONSTRUCTOR.getParameterCount() to match the constructor that is actually invoked, and align with the convention used by newInstance() further down. In the Spark 4 override this also lets us collapse the `case 8: case 9:` fallthrough back to a single `case 8:`, since INVOKE_CONSTRUCTOR remains 8 params in Spark 4 even though STATIC_INVOKE_CONSTRUCTOR grew to 9. Applied identically to the shared base and the Spark 4 override. Flagged by Gemini Code Assist on PR #38255. * Update CHANGES.md Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> * [Spark 4] Drop redundant Collection cast in GroupByKeyHelpers WindowedValue#getWindows() returns Collection<? extends BoundedWindow>, which is already an Iterable and can be passed straight to ScalaInterop.scalaIterator(...). The intermediate local variable and the unchecked cast to Collection<BoundedWindow> were redundant. Applied in both the shared base and the Spark 4 override. Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> * [Spark 4] Add module README with slf4j-jdk14 known-issue note Documents the Spark 4 runner's requirements (Java 17, Scala 2.13, Spark 4.0.x, batch-only) and the slf4j-jdk14 ↔ jul-to-slf4j conflict that is the Spark 4 manifestation of #26985 (fixed for Spark 3 in #27001). The shared spark_runner.gradle already excludes slf4j-jdk14 for in-tree builds; this note tells downstream consumers to mirror the exclude when assembling their own runtime classpath against beam-runners-spark-4. * [runners-spark] Address Gemini nits: use encoder terminology in exception messages * Trigger Build * ci: re-trigger to clear flaky FlinkRequiresStableInputTest The PreCommit Java failure on the previous run was a single timeout in FlinkRequiresStableInputTest.testParDoRequiresStableInputPortable (:runners:flink:1.17:test) — known flake tracked in #21333. This PR does not touch any Flink code. Squash or drop on rebase before merge. * ci: re-trigger to clear flaky SqsIOWriteBatchesTest.testWriteBatchesToDynamicWithStrictTimeout Wall-clock-timing test (100ms inter-message + 150ms strict batch timeout) in sdks/java/io/amazon-web-services2 SQS — unrelated to this PR (no AWS2/SQS/Direct-runner files touched), and master is green for the same PreCommit on 6106b30. * ci: re-trigger to clear Maven Central 403 on Windows wordcount `Java Wordcount Direct Runner (windows-latest)` failed at the :buildSrc configure step with HTTP 403 fetching legacy Spotless 5.6.1 transitive deps from repo.maven.apache.org (spotless-lib:2.7.0, durian-*:1.2.0, jgit:5.8.0). Network/infra flake — PR doesn't touch examples or buildSrc, master 'Java Tests' workflow consistently green. * ci: re-trigger to clear flaky Spotless + GCP IO Direct PreCommits Both checks failed on the prior empty retry commit (e19b80c). Reproduced locally at e19b80c: spotlessCheck and Spark checkStyleMain/Test all pass. PR doesn't touch any GCP IO code, and both checks were green on the immediately preceding branch commits (5abbb21, 604037f) and on master (6106b30, e01f711). Treating as infra flakes; squash before merge. * [runners-spark] Cover Scala-array fallback and EvaluationContext error paths Address codecov/patch on PR #38255 by exercising the new branches added for Scala 2.13 / null-safe error logging: - Refactor SparkRunnerKryoRegistrator's nested Scala-array Class.forName fallback into a small @VisibleForTesting findFirstAvailableClass helper and add unit tests for first-hit, fallback, no-match, and empty-input paths. - Add EvaluationContextTest covering the catch (RuntimeException) / catch (Exception) blocks in evaluate() and collect(), including the null-message path that motivated the String.valueOf wrap. * [runners-spark] spotless: inline two findFirstAvailableClass calls in test * flaky SqsIOWriteBatchesTest retry * flaky ExampleEchoPipelineTest retry * rebase cleanup: drop duplicate isSparkAtLeast helper now in master via #38324 * Address @Abacn 2026-05-07 review - SparkRunnerKryoRegistrator: throw IllegalStateException instead of LOG.warn when neither ArraySeq$ofRef (Scala 2.13) nor WrappedArray$ofRef (Scala 2.12) is on the classpath, so the missing class isn't silently ignored. Drops the now-unused Logger field and slf4j imports. - spark_runner.gradle: declare org.apache.spark:spark-connect-shims_2.13 as a provided dep gated on isSparkAtLeast("4.0.0"). Spark 4 splits the Connect shim classes out of spark-sql; with enableStrictDependencies this surfaced as analyzeClassesDependencies usedUndeclaredArtifacts. The artifact does not exist for Spark 3, so the gate prevents Spark 3 resolution failures. - runners/spark/4/build.gradle: drop the empty sparkVersions test scaffolding (no additional Spark 4.x patch versions to test against yet) and delete the now-unused .github/workflows/beam_PreCommit_Java_Spark4_Versions.yml workflow + its README.md row. - EncoderFactory (shared base): revert the line 94 switch to STATIC_INVOKE_CONSTRUCTOR.getParameterCount(), keeping Spark 3 behavior byte-for-byte unchanged. Spark 4's complete EncoderFactory override under runners/spark/4/src/.../EncoderFactory.java is unaffected. - CHANGES.md: drop the Highlights line for Spark 4. Will re-add when ValidatesRunner tests are set up and confirmed working, matching the Phase 1 #38324 pattern. - runners/spark/4/job-server/container: delete the entire module (build.gradle + Dockerfile) and remove its include() from settings.gradle.kts. Per @Abacn's offer to defer the container module to portable runner support later. The fat-jar :runners:spark:4:job-server module is kept. --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
1 parent f01b9dd commit 1d008ba

30 files changed

Lines changed: 2730 additions & 25 deletions
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"comment": "Modify this file in a trivial way to cause this test suite to run"
3+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"comment": "Modify this file in a trivial way to cause this test suite to run"
3+
}

.github/workflows/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -370,6 +370,7 @@ PostCommit Jobs run in a schedule against master branch and generally do not get
370370
| [ PostCommit Java ValidatesRunner Flink ](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Java_ValidatesRunner_Flink.yml) | N/A |`beam_PostCommit_Java_ValidatesRunner_Flink.json`| [![.github/workflows/beam_PostCommit_Java_ValidatesRunner_Flink.yml](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Java_ValidatesRunner_Flink.yml/badge.svg?event=schedule)](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Java_ValidatesRunner_Flink.yml?query=event%3Aschedule) |
371371
| [ PostCommit Java ValidatesRunner Spark ](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Java_ValidatesRunner_Spark.yml) | N/A |`beam_PostCommit_Java_ValidatesRunner_Spark.json`| [![.github/workflows/beam_PostCommit_Java_ValidatesRunner_Spark.yml](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Java_ValidatesRunner_Spark.yml/badge.svg?event=schedule)](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Java_ValidatesRunner_Spark.yml?query=event%3Aschedule) |
372372
| [ PostCommit Java ValidatesRunner SparkStructuredStreaming ](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Java_ValidatesRunner_SparkStructuredStreaming.yml) | N/A |`beam_PostCommit_Java_ValidatesRunner_SparkStructuredStreaming.json`| [![.github/workflows/beam_PostCommit_Java_ValidatesRunner_SparkStructuredStreaming.yml](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Java_ValidatesRunner_SparkStructuredStreaming.yml/badge.svg?event=schedule)](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Java_ValidatesRunner_SparkStructuredStreaming.yml?query=event%3Aschedule) |
373+
| [ PostCommit Java ValidatesRunner Spark4StructuredStreaming ](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Java_ValidatesRunner_Spark4StructuredStreaming.yml) | N/A |`beam_PostCommit_Java_ValidatesRunner_Spark4StructuredStreaming.json`| [![.github/workflows/beam_PostCommit_Java_ValidatesRunner_Spark4StructuredStreaming.yml](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Java_ValidatesRunner_Spark4StructuredStreaming.yml/badge.svg?event=schedule)](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Java_ValidatesRunner_Spark4StructuredStreaming.yml?query=event%3Aschedule) |
373374
| [ PostCommit Java ValidatesRunner Twister2 ](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Java_ValidatesRunner_Twister2.yml) | N/A |`beam_PostCommit_Java_ValidatesRunner_Twister2.json`| [![.github/workflows/beam_PostCommit_Java_ValidatesRunner_Twister2.yml](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Java_ValidatesRunner_Twister2.yml/badge.svg?event=schedule)](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Java_ValidatesRunner_Twister2.yml?query=event%3Aschedule) |
374375
| [ PostCommit Java ValidatesRunner ULR ](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Java_ValidatesRunner_ULR.yml) | N/A |`beam_PostCommit_Java_ValidatesRunner_ULR.json`| [![.github/workflows/beam_PostCommit_Java_ValidatesRunner_ULR.yml](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Java_ValidatesRunner_ULR.yml/badge.svg?event=schedule)](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Java_ValidatesRunner_ULR.yml?query=event%3Aschedule) |
375376
| [ PostCommit Java ](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Java.yml) | N/A |`beam_PostCommit_Java.json`| [![.github/workflows/beam_PostCommit_Java.yml](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Java.yml/badge.svg?event=schedule)](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Java.yml?query=event%3Aschedule) |
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
# Licensed to the Apache Software Foundation (ASF) under one or more
2+
# contributor license agreements. See the NOTICE file distributed with
3+
# this work for additional information regarding copyright ownership.
4+
# The ASF licenses this file to You under the Apache License, Version 2.0
5+
# (the "License"); you may not use this file except in compliance with
6+
# the License. You may obtain a copy of the License at
7+
#
8+
# http://www.apache.org/licenses/LICENSE-2.0
9+
#
10+
# Unless required by applicable law or agreed to in writing, software
11+
# distributed under the License is distributed on an "AS IS" BASIS,
12+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
# See the License for the specific language governing permissions and
14+
# limitations under the License.
15+
16+
name: PostCommit Java ValidatesRunner Spark4 StructuredStreaming
17+
18+
on:
19+
schedule:
20+
- cron: '45 4/6 * * *'
21+
pull_request_target:
22+
paths: ['release/trigger_all_tests.json', '.github/trigger_files/beam_PostCommit_Java_ValidatesRunner_Spark4StructuredStreaming.json']
23+
workflow_dispatch:
24+
25+
#Setting explicit permissions for the action to avoid the default permissions which are `write-all` in case of pull_request_target event
26+
permissions:
27+
actions: write
28+
pull-requests: write
29+
checks: write
30+
contents: read
31+
deployments: read
32+
id-token: none
33+
issues: write
34+
discussions: read
35+
packages: read
36+
pages: read
37+
repository-projects: read
38+
security-events: read
39+
statuses: read
40+
41+
# This allows a subsequently queued workflow run to interrupt previous runs
42+
concurrency:
43+
group: '${{ github.workflow }} @ ${{ github.event.pull_request.number || github.sha || github.head_ref || github.ref }}-${{ github.event.schedule || github.event.comment.id || github.event.sender.login }}'
44+
cancel-in-progress: true
45+
46+
env:
47+
DEVELOCITY_ACCESS_KEY: ${{ secrets.DEVELOCITY_ACCESS_KEY }}
48+
GRADLE_ENTERPRISE_CACHE_USERNAME: ${{ secrets.GE_CACHE_USERNAME }}
49+
GRADLE_ENTERPRISE_CACHE_PASSWORD: ${{ secrets.GE_CACHE_PASSWORD }}
50+
51+
jobs:
52+
beam_PostCommit_Java_ValidatesRunner_Spark4StructuredStreaming:
53+
name: ${{ matrix.job_name }} (${{ matrix.job_phrase }})
54+
runs-on: [self-hosted, ubuntu-24.04, main]
55+
timeout-minutes: 120
56+
strategy:
57+
matrix:
58+
job_name: [beam_PostCommit_Java_ValidatesRunner_Spark4StructuredStreaming]
59+
job_phrase: [Run Spark4 StructuredStreaming ValidatesRunner]
60+
if: |
61+
github.event_name == 'workflow_dispatch' ||
62+
github.event_name == 'pull_request_target' ||
63+
(github.event_name == 'schedule' && github.repository == 'apache/beam') ||
64+
github.event.comment.body == 'Run Spark4 StructuredStreaming ValidatesRunner'
65+
steps:
66+
- uses: actions/checkout@v4
67+
- name: Setup repository
68+
uses: ./.github/actions/setup-action
69+
with:
70+
comment_phrase: ${{ matrix.job_phrase }}
71+
github_token: ${{ secrets.GITHUB_TOKEN }}
72+
github_job: ${{ matrix.job_name }} (${{ matrix.job_phrase }})
73+
- name: Setup environment
74+
uses: ./.github/actions/setup-environment-action
75+
with:
76+
java-version: '17'
77+
- name: run validatesStructuredStreamingRunnerBatch script
78+
uses: ./.github/actions/gradle-command-self-hosted-action
79+
with:
80+
gradle-command: :runners:spark:4:validatesStructuredStreamingRunnerBatch
81+
arguments: |
82+
-PtestJavaVersion=17 \
83+
-PdisableSpotlessCheck=true \
84+
- name: Archive JUnit Test Results
85+
uses: actions/upload-artifact@v4
86+
if: ${{ !success() }}
87+
with:
88+
name: JUnit Test Results
89+
path: "**/build/reports/tests/"
90+
- name: Publish JUnit Test Results
91+
uses: EnricoMi/publish-unit-test-result-action@v2
92+
if: always()
93+
with:
94+
commit: '${{ env.prsha || env.GITHUB_SHA }}'
95+
comment_mode: ${{ github.event_name == 'issue_comment' && 'always' || 'off' }}
96+
files: '**/build/test-results/**/*.xml'
97+
large_files: true

CHANGES.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,6 @@
6060
## Highlights
6161

6262
* New highly anticipated feature X added to Python SDK ([#X](https://github.com/apache/beam/issues/X)).
63-
* New highly anticipated feature Y added to Java SDK ([#Y](https://github.com/apache/beam/issues/Y)).
6463

6564
## I/Os
6665

gradle.properties

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,6 @@ docker_image_default_repo_prefix=beam_
4141
# supported flink versions
4242
flink_versions=1.17,1.18,1.19,1.20,2.0
4343
# supported spark versions
44-
spark_versions=3
44+
spark_versions=3,4
4545
# supported python versions
4646
python_versions=3.10,3.11,3.12,3.13,3.14

runners/spark/4/README.md

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
<!--
2+
Licensed to the Apache Software Foundation (ASF) under one
3+
or more contributor license agreements. See the NOTICE file
4+
distributed with this work for additional information
5+
regarding copyright ownership. The ASF licenses this file
6+
to you under the Apache License, Version 2.0 (the
7+
"License"); you may not use this file except in compliance
8+
with the License. You may obtain a copy of the License at
9+
10+
http://www.apache.org/licenses/LICENSE-2.0
11+
12+
Unless required by applicable law or agreed to in writing,
13+
software distributed under the License is distributed on an
14+
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
KIND, either express or implied. See the License for the
16+
specific language governing permissions and limitations
17+
under the License.
18+
-->
19+
# Apache Beam Spark 4 Runner
20+
21+
Experimental Beam runner for Apache Spark 4 (batch-only). Built on the shared
22+
`runners/spark` source base via `spark_runner.gradle`'s per-version
23+
source-overrides mechanism: this module contributes the small set of files
24+
under `src/main/java/.../structuredstreaming/` that diverge from the Spark 3
25+
implementation. See the parent `runners/spark/` module for the bulk of the
26+
runner code.
27+
28+
## Requirements
29+
30+
* **Spark 4.0.2** (and other Spark 4.0.x patch releases)
31+
* **Scala 2.13**
32+
* **Java 17** — Spark 4 does not run on earlier JDKs
33+
34+
## Status
35+
36+
Batch only. Streaming is tracked in
37+
[#36841](https://github.com/apache/beam/issues/36841).
38+
39+
## Known issues
40+
41+
### `StackOverflowError` from `slf4j-jdk14` on the runtime classpath
42+
43+
Spark 4 ships `org.slf4j:jul-to-slf4j` to route `java.util.logging` records
44+
into SLF4J. If `org.slf4j:slf4j-jdk14` is also resolved at runtime — it routes
45+
the other direction (SLF4J → JUL) — the first log line creates an infinite
46+
loop:
47+
48+
```
49+
java.lang.StackOverflowError
50+
at org.slf4j.bridge.SLF4JBridgeHandler.publish(...)
51+
at java.util.logging.Logger.log(...)
52+
at org.slf4j.impl.JDK14LoggerAdapter.log(...)
53+
at org.slf4j.bridge.SLF4JBridgeHandler.publish(...)
54+
...
55+
```
56+
57+
This is the same condition that broke the Spark 3 runner in
58+
[#26985](https://github.com/apache/beam/issues/26985), fixed in
59+
[#27001](https://github.com/apache/beam/pull/27001).
60+
61+
The shared `spark_runner.gradle` already excludes `slf4j-jdk14` from the
62+
runner module's own `configurations.all`, so in-tree builds are unaffected.
63+
Downstream Gradle consumers that assemble a runtime classpath against
64+
`beam-runners-spark-4` should mirror that exclude:
65+
66+
```groovy
67+
configurations.all {
68+
exclude group: "org.slf4j", module: "slf4j-jdk14"
69+
}
70+
```
71+
72+
For Maven, exclude `org.slf4j:slf4j-jdk14` from any dependency that pulls it
73+
transitively (commonly the Beam SDK harness and several IO connectors).

runners/spark/4/build.gradle

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing, software
13+
* distributed under the License is distributed on an "AS IS" BASIS,
14+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
* See the License for the specific language governing permissions and
16+
* limitations under the License.
17+
*/
18+
19+
def basePath = '..'
20+
/* All properties required for loading the Spark build script */
21+
project.ext {
22+
spark_major = '4'
23+
// Spark 4 version as defined in BeamModulePlugin; requires Scala 2.13 and Java 17
24+
spark_version = spark4_version
25+
spark_scala_version = '2.13'
26+
archives_base_name = 'beam-runners-spark-4'
27+
}
28+
29+
// Load the main build script which contains all build logic.
30+
// spark_runner.gradle handles the per-version source-overrides Copy:
31+
// shared base (runners/spark/src/) + previous majors + this module's ./src/ are
32+
// merged into build/source-overrides/src using DuplicatesStrategy.INCLUDE so the
33+
// 11 files under runners/spark/4/src/.../structuredstreaming/ override the
34+
// shared-base versions.
35+
apply from: "$basePath/spark_runner.gradle"
36+
37+
// Spark 4 always requires Java 17, so unconditionally add the --add-opens flags
38+
// required by Kryo and other libraries that use reflection on JDK internals.
39+
test {
40+
jvmArgs "--add-opens=java.base/sun.nio.ch=ALL-UNNAMED",
41+
"--add-opens=java.base/java.nio=ALL-UNNAMED",
42+
"--add-opens=java.base/java.util=ALL-UNNAMED",
43+
"--add-opens=java.base/java.lang.invoke=ALL-UNNAMED"
44+
}
45+
46+
// Exclude DStream-based streaming tests from the shared-base copy: the Spark 4 module
47+
// supports only structured streaming (batch) and does not include legacy DStream support.
48+
// Streaming test utilities also depend on kafka.server.KafkaServerStartable which was
49+
// removed in Kafka 2.8.0 (the first Kafka version with a _2.13 artifact).
50+
tasks.named("copyTestSourceOverrides") {
51+
exclude "**/translation/streaming/**"
52+
}
53+
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing, software
13+
* distributed under the License is distributed on an "AS IS" BASIS,
14+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
* See the License for the specific language governing permissions and
16+
* limitations under the License.
17+
*/
18+
19+
def basePath = '../../job-server'
20+
21+
project.ext {
22+
// Look for the source code in the parent module
23+
main_source_dirs = ["$basePath/src/main/java"]
24+
test_source_dirs = ["$basePath/src/test/java"]
25+
main_resources_dirs = ["$basePath/src/main/resources"]
26+
test_resources_dirs = ["$basePath/src/test/resources"]
27+
archives_base_name = 'beam-runners-spark-4-job-server'
28+
}
29+
30+
// Load the main build script which contains all build logic.
31+
apply from: "$basePath/spark_job_server.gradle"

0 commit comments

Comments
 (0)