Skip to content

Commit 7f08785

Browse files
authored
[#2750] feat(spark): Support Spark 4.1 (#2751)
### What changes were proposed in this pull request? Add a `spark4.1` Maven profile that reuses the existing `client-spark/spark4` module to compile and run against **Spark 4.1.1**. #### API shims Spark 4.1 introduced two binary-incompatible additions to APIs called from this module: - `org.apache.spark.scheduler.MapStatus.apply` — gained a trailing `checksumVal: Long` parameter. - `org.apache.spark.util.collection.ExternalSorter` — constructor gained a trailing `rowBasedChecksums: Array[RowBasedChecksum]` parameter. Scala default parameters do not surface to Java callers, so a single `Spark4Compat.java` cannot satisfy both 4.0.x and 4.1.x signatures. Two parallel source roots ship a matching `Spark4Compat`: - `src/main/java-spark4_0/` — 3-arg `mapStatus`, 5-arg `ExternalSorter` ctor (current Spark 4.0 shape). - `src/main/java-spark4_1/` — 4-arg `mapStatus` (`checksumVal = 0L`), 6-arg `ExternalSorter` ctor (empty `RowBasedChecksum[]`). `build-helper-maven-plugin` selects the source root via `${spark4.compat.source.dir}`; the existing `spark4` profile keeps the 4.0 default, the new `spark4.1` profile overrides it to the 4.1 variant. `RssShuffleReader` now calls `Spark4Compat.newExternalSorter(...)` and `RssShuffleWriter` calls `Spark4Compat.mapStatus(...)` instead of constructing those Spark types directly. The `client-spark/extension` (Spark UI) module already uses `scala-jakarta` under the `spark4` profile; Spark 4.1's `WebUIPage.render` keeps the same `jakarta.servlet.http.HttpServletRequest` signature, so the existing source root works for `spark4.1` as well — only a `spark4.1` profile body that mirrors the `spark4` one is added. `spark4` and `spark4.1` are mutually exclusive at build time; they share a Maven coordinate but produce incompatible bytecode against different Spark majors. Run `mvn clean` between profile switches locally. #### Runtime dependency adjustments under `-Pspark4.1` Spark 4.1.1 bumped several runtime libraries; the `spark4.1` profile pins matching versions and the code makes the necessary lifecycle adjustments: - **Netty 4.1.118 → 4.2.7.Final**. Spark 4.1's `NettyUtils.createEventLoop` references `io.netty.channel.nio.NioIoHandler` (new in Netty 4.2). Netty 4.2 also tightens `PooledByteBuf.nioBuffer()` to require `refCnt > 0`. Response decoders that wrap the frame buffer in a `NettyManagedBuffer` body now call `retain()` explicitly, so the body's reference count is independent of the frame's. `TransportFrameDecoder.shouldRelease` is simplified to always return true, and `TransportFrameDecoderTest` is updated accordingly. Netty 4.1 silently tolerated the previous shape; 4.2 throws `IllegalReferenceCountException`. - **Jetty 9.3.24 → 9.4.53.v20231009**. Aligns with Spark 4.1's bundled Jetty. `JettyServer.createThreadPool` is already source-compatible with both lines. - **Jackson 2.18.2 → 2.20.0**. Spark 4.1.1 ships `jackson-module-scala_2.13:2.20.0`, which validates the classpath `jackson-databind` version on registration and rejects anything outside `[2.20.0, 2.21.0)`. Without the bump, `RDDOperationScope`'s static initializer (`ObjectMapper.registerModule(DefaultScalaModule)`) throws `JsonMappingException`, surfacing as `ExceptionInInitializerError` on the first SQL/RDD-touching test and `NoClassDefFoundError` on every later one. These adjustments live entirely inside the `spark4.1` profile and the response-decoder ref-count fix; the default build and `-Pspark4` (Spark 4.0.2) paths are unchanged. ### Why are the changes needed? Following #1805 (Spark 4.0.2 support, now closed), users on Spark 4.1 currently cannot link Uniffle's Spark 4 client because of the API additions above. Closes #2750. ### Does this PR introduce _any_ user-facing change? No (build / packaging only). A new `-Pspark4.1` build flag is available; default builds and the existing `-Pspark4` flag are unchanged. ### How was this patch tested? - Local: `mvn clean package -Pspark4.1 -DskipTests` against Spark 4.1.1 — passes. - Local: `mvn clean package -Pspark4 -DskipTests` against Spark 4.0.2 — still passes. - Verified the resulting `Spark4Compat.class` differs across the two profiles (different bytecode size / arity), confirming the multi-source-root selection works. - Verified API signatures via `javap -p` on Spark 4.0.2 / 4.1.1 jars from Maven Central match the shim implementations. - Local: `mvn -Pspark4.1 -pl integration-test/spark4 -am test` — `AQERepartitionTest` and `MapSideCombineTest` pass after the netty refCnt + jackson alignment fixes. - Local: `TransportFrameDecoderTest` passes against Netty 4.2 (`-Pspark4.1`) and continues to pass on the default Netty 4.1 line. - CI: `parallel.yml` now runs `-Pspark4.1` (java-version 17) in the matrix; `sequential.yml` adds an `Execute -Pspark4.1` step gated on java-version 17. Latest run on this branch ([26386496434](https://github.com/apache/uniffle/actions/runs/26386496434)) is **all-green** across the full 45-job matrix.
1 parent c81ef85 commit 7f08785

19 files changed

Lines changed: 389 additions & 21 deletions

File tree

.github/workflows/parallel.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,8 @@ jobs:
7777
java-version: 8
7878
- profile: spark4
7979
java-version: 17
80+
- profile: spark4.1
81+
java-version: 17
8082
- profile: mr-hadoop2.8
8183
java-version: 8
8284
- profile: mr-hadoop3.2

.github/workflows/sequential.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,10 @@ jobs:
7272
if: inputs.java-version == '17'
7373
run: ./mvnw -B -fae ${{ inputs.maven-args }} -Pspark4 | tee -a /tmp/maven.log;
7474
shell: bash
75+
- name: Execute `./mvnw ${{ inputs.maven-args }} -Pspark4.1`
76+
if: inputs.java-version == '17'
77+
run: ./mvnw -B -fae ${{ inputs.maven-args }} -Pspark4.1 | tee -a /tmp/maven.log;
78+
shell: bash
7579
- name: Execute `./mvnw ${{ inputs.maven-args }} -Pspark3`
7680
run: ./mvnw -B -fae ${{ inputs.maven-args }} -Pspark3 | tee -a /tmp/maven.log;
7781
shell: bash

client-spark/extension/pom.xml

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -136,11 +136,26 @@
136136

137137
<profiles>
138138
<profile>
139-
<!-- Merged with the root pom's spark4 profile by shared id. -->
139+
<!--
140+
Co-activated with the root pom's spark4 profile via the shared
141+
-Pspark4 flag. Each pom contributes its own profile body; Maven
142+
does not merge profiles across poms by id.
143+
-->
140144
<id>spark4</id>
141145
<properties>
142146
<extension.servlet.source.dir>src/main/scala-jakarta</extension.servlet.source.dir>
143147
</properties>
144148
</profile>
149+
<profile>
150+
<!--
151+
Co-activated with the root pom's spark4.1 profile via the shared
152+
-Pspark4.1 flag. Each pom contributes its own profile body; Maven
153+
does not merge profiles across poms by id.
154+
-->
155+
<id>spark4.1</id>
156+
<properties>
157+
<extension.servlet.source.dir>src/main/scala-jakarta</extension.servlet.source.dir>
158+
</properties>
159+
</profile>
145160
</profiles>
146161
</project>

client-spark/spark4-shaded/pom.xml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,20 @@
208208
<pattern>org.checkerframework</pattern>
209209
<shadedPattern>${rss.shade.packageName}.org.checkerframework</shadedPattern>
210210
</relocation>
211+
<relocation>
212+
<!--
213+
Spark 4.1's hadoop 3.4.1 pulls bcprov-jdk18on transitively,
214+
and netty 4.2 stopped internally shading jctools. Both leak
215+
into the shaded jar; relocate them so checkshade.sh stays
216+
happy and we don't conflict with users' classpath.
217+
-->
218+
<pattern>org.bouncycastle</pattern>
219+
<shadedPattern>${rss.shade.packageName}.org.bouncycastle</shadedPattern>
220+
</relocation>
221+
<relocation>
222+
<pattern>org.jctools</pattern>
223+
<shadedPattern>${rss.shade.packageName}.org.jctools</shadedPattern>
224+
</relocation>
211225
<relocation>
212226
<pattern>org.eclipse</pattern>
213227
<shadedPattern>${rss.shade.packageName}.org.eclipse</shadedPattern>

client-spark/spark4/pom.xml

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,27 @@
3131
<packaging>jar</packaging>
3232
<name>Apache Uniffle Client (Spark 4)</name>
3333

34+
<!--
35+
Spark 4.1 added new (defaulted) parameters to MapStatus.apply and to the
36+
ExternalSorter constructor (checksumVal, rowBasedChecksums). Scala defaults
37+
do not surface to Java callers, so a single Spark4Compat.java cannot
38+
satisfy both 4.0 and 4.1 signatures. Two parallel source roots
39+
(src/main/java-spark4_0, src/main/java-spark4_1) each ship a Spark4Compat
40+
with the matching call shape. build-helper-maven-plugin picks one via
41+
${spark4.compat.source.dir}; the spark4 profile keeps the 4.0 default
42+
while the spark4.1 profile in this pom (co-activated with the root
43+
pom's spark4.1 profile via the shared -Pspark4.1 flag — each pom
44+
contributes its own properties/build sections) switches it to the
45+
4.1 variant.
46+
47+
Keep the public method surface of Spark4Compat in lock-step across both
48+
variants; only the bodies differ.
49+
-->
50+
<properties>
51+
<!-- Overridden by the spark4.1 profile below. -->
52+
<spark4.compat.source.dir>src/main/java-spark4_0</spark4.compat.source.dir>
53+
</properties>
54+
3455
<dependencies>
3556
<dependency>
3657
<groupId>com.google.protobuf</groupId>
@@ -107,10 +128,42 @@
107128

108129
<build>
109130
<plugins>
131+
<plugin>
132+
<groupId>org.codehaus.mojo</groupId>
133+
<artifactId>build-helper-maven-plugin</artifactId>
134+
<executions>
135+
<execution>
136+
<id>add-spark4-compat-source</id>
137+
<phase>generate-sources</phase>
138+
<goals>
139+
<goal>add-source</goal>
140+
</goals>
141+
<configuration>
142+
<sources>
143+
<source>${spark4.compat.source.dir}</source>
144+
</sources>
145+
</configuration>
146+
</execution>
147+
</executions>
148+
</plugin>
110149
<plugin>
111150
<groupId>org.apache.maven.plugins</groupId>
112151
<artifactId>maven-surefire-plugin</artifactId>
113152
</plugin>
114153
</plugins>
115154
</build>
155+
156+
<profiles>
157+
<profile>
158+
<!--
159+
Co-activated with the root pom's spark4.1 profile via the shared
160+
-Pspark4.1 flag. Each pom contributes its own profile body; Maven
161+
does not merge profiles across poms by id.
162+
-->
163+
<id>spark4.1</id>
164+
<properties>
165+
<spark4.compat.source.dir>src/main/java-spark4_1</spark4.compat.source.dir>
166+
</properties>
167+
</profile>
168+
</profiles>
116169
</project>
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one or more
3+
* contributor license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright ownership.
5+
* The ASF licenses this file to You under the Apache License, Version 2.0
6+
* (the "License"); you may not use this file except in compliance with
7+
* the License. You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
package org.apache.spark.shuffle.compat;
19+
20+
import scala.Option;
21+
import scala.math.Ordering;
22+
23+
import org.apache.spark.Aggregator;
24+
import org.apache.spark.Partitioner;
25+
import org.apache.spark.TaskContext;
26+
import org.apache.spark.scheduler.MapStatus;
27+
import org.apache.spark.serializer.Serializer;
28+
import org.apache.spark.storage.BlockManagerId;
29+
import org.apache.spark.util.collection.ExternalSorter;
30+
31+
/**
32+
* Compatibility shim for Spark 4.0.x. Selected by the build via the
33+
* {@code src/main/java-spark4_0} source root under the {@code spark4} profile.
34+
*
35+
* <p>Mirror of {@code java-spark4_1/.../Spark4Compat.java}; keep the public surface in lock-step.
36+
*/
37+
public final class Spark4Compat {
38+
39+
private Spark4Compat() {}
40+
41+
public static MapStatus mapStatus(
42+
BlockManagerId loc, long[] uncompressedSizes, long mapTaskId) {
43+
return MapStatus.apply(loc, uncompressedSizes, mapTaskId);
44+
}
45+
46+
public static <K, V, C> ExternalSorter<K, V, C> newExternalSorter(
47+
TaskContext context,
48+
Option<Aggregator<K, V, C>> aggregator,
49+
Option<Partitioner> partitioner,
50+
Option<Ordering<K>> ordering,
51+
Serializer serializer) {
52+
return new ExternalSorter<>(context, aggregator, partitioner, ordering, serializer);
53+
}
54+
}
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one or more
3+
* contributor license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright ownership.
5+
* The ASF licenses this file to You under the Apache License, Version 2.0
6+
* (the "License"); you may not use this file except in compliance with
7+
* the License. You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
package org.apache.spark.shuffle.compat;
19+
20+
import scala.Option;
21+
import scala.math.Ordering;
22+
23+
import org.apache.spark.Aggregator;
24+
import org.apache.spark.Partitioner;
25+
import org.apache.spark.TaskContext;
26+
import org.apache.spark.scheduler.MapStatus;
27+
import org.apache.spark.serializer.Serializer;
28+
import org.apache.spark.shuffle.checksum.RowBasedChecksum;
29+
import org.apache.spark.storage.BlockManagerId;
30+
import org.apache.spark.util.collection.ExternalSorter;
31+
32+
/**
33+
* Compatibility shim for Spark 4.1.x. Selected by the build via the
34+
* {@code src/main/java-spark4_1} source root under the {@code spark4.1} profile.
35+
*
36+
* <p>Spark 4.1 added a {@code checksumVal} parameter to {@code MapStatus.apply} and a
37+
* {@code rowBasedChecksums} parameter to {@link ExternalSorter}'s constructor. Both have Scala
38+
* defaults but are required from Java; pass disabled defaults (0L and an empty array).
39+
*
40+
* <p>Mirror of {@code java-spark4_0/.../Spark4Compat.java}; keep the public surface in lock-step.
41+
*/
42+
public final class Spark4Compat {
43+
44+
private Spark4Compat() {}
45+
46+
public static MapStatus mapStatus(
47+
BlockManagerId loc, long[] uncompressedSizes, long mapTaskId) {
48+
return MapStatus.apply(loc, uncompressedSizes, mapTaskId, 0L);
49+
}
50+
51+
public static <K, V, C> ExternalSorter<K, V, C> newExternalSorter(
52+
TaskContext context,
53+
Option<Aggregator<K, V, C>> aggregator,
54+
Option<Partitioner> partitioner,
55+
Option<Ordering<K>> ordering,
56+
Serializer serializer) {
57+
// Spark 4.1's ExternalSorter requires a non-null RowBasedChecksum[]; pass an empty
58+
// array to disable row-based checksums.
59+
return new ExternalSorter<>(
60+
context, aggregator, partitioner, ordering, serializer, new RowBasedChecksum[0]);
61+
}
62+
}

client-spark/spark4/src/main/java/org/apache/spark/shuffle/reader/RssShuffleReader.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@
4848
import org.apache.spark.shuffle.RssShuffleManager;
4949
import org.apache.spark.shuffle.RssSparkConfig;
5050
import org.apache.spark.shuffle.ShuffleReader;
51+
import org.apache.spark.shuffle.compat.Spark4Compat;
5152
import org.apache.spark.util.CompletionIterator;
5253
import org.apache.spark.util.CompletionIterator$;
5354
import org.apache.spark.util.collection.ExternalSorter;
@@ -223,7 +224,7 @@ public Iterator<Product2<K, C>> read() {
223224
}
224225
}
225226
ExternalSorter<K, Object, C> sorter =
226-
new ExternalSorter<>(
227+
Spark4Compat.newExternalSorter(
227228
context, aggregator, Option.empty(), shuffleDependency.keyOrdering(), serializer);
228229
long startTime = System.currentTimeMillis();
229230
sorter.insertAll(rssShuffleDataIterator);

client-spark/spark4/src/main/java/org/apache/spark/shuffle/writer/RssShuffleWriter.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@
5959
import org.apache.spark.shuffle.RssSparkConfig;
6060
import org.apache.spark.shuffle.RssSparkShuffleUtils;
6161
import org.apache.spark.shuffle.ShuffleWriter;
62+
import org.apache.spark.shuffle.compat.Spark4Compat;
6263
import org.apache.spark.shuffle.handle.ShuffleHandleInfo;
6364
import org.apache.spark.storage.BlockManagerId;
6465
import org.roaringbitmap.longlong.Roaring64NavigableMap;
@@ -720,7 +721,8 @@ public Option<MapStatus> stop(boolean success) {
720721
? shuffleTaskStats.encode()
721722
: Long.toString(taskAttemptId)));
722723
MapStatus mapStatus =
723-
MapStatus.apply(blockManagerId, shuffleTaskStats.getPartitionLengths(), taskAttemptId);
724+
Spark4Compat.mapStatus(
725+
blockManagerId, shuffleTaskStats.getPartitionLengths(), taskAttemptId);
724726
return Option.apply(mapStatus);
725727
} else {
726728
return Option.empty();

common/src/main/java/org/apache/uniffle/common/netty/TransportFrameDecoder.java

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@
2323
import com.google.common.base.Preconditions;
2424
import io.netty.buffer.ByteBuf;
2525
import io.netty.buffer.CompositeByteBuf;
26-
import io.netty.buffer.EmptyByteBuf;
2726
import io.netty.buffer.Unpooled;
2827
import io.netty.channel.ChannelHandlerContext;
2928
import io.netty.channel.ChannelInboundHandlerAdapter;
@@ -85,10 +84,10 @@ public void channelRead(ChannelHandlerContext ctx, Object data) {
8584

8685
@VisibleForTesting
8786
static boolean shouldRelease(Message msg) {
88-
if (msg == null || msg.body() == null || msg.body().byteBuf() == null) {
89-
return true;
90-
}
91-
return msg.body().byteBuf() instanceof EmptyByteBuf;
87+
// The frame buffer is always released here. Response decoders that wrap the frame
88+
// buffer in a NettyManagedBuffer body must call retain() on the buffer themselves
89+
// so the body can be consumed independently of the frame's reference count.
90+
return true;
9291
}
9392

9493
private void clear() {

0 commit comments

Comments
 (0)