[DRAFT][SPARK-51705][CONNECT][SCALA] Support SparkSession.broadcast() for Scala UDFs over Spark Connect#2
Draft
Tagar wants to merge 1 commit into
Draft
Conversation
Tagar
force-pushed
the
broadcast-connect-python-v1
branch
from
July 23, 2026 19:47
f4b6181 to
3348ed7
Compare
Tagar
force-pushed
the
broadcast-connect-scala-v1
branch
from
July 23, 2026 19:47
4653f0e to
663a4c4
Compare
…ala UDFs over Spark Connect
Stacks on the Python v1 (broadcast-connect-python-v1). Extends broadcast variables over Spark
Connect to the Scala ScalarScalaUDF path, which the Vizio "Cooker" anchor case needs (its broadcast
is consumed by a Scala UDF).
Design (Candidate A, writeReplace/readResolve):
- Client SparkSession.broadcast[T](v): SparkSerDeUtils.serialize(v) -> cacheArtifact (same cache/
channel as Python) -> CreateBroadcastCommand(value_type=JVM) -> CreateBroadcastResult(id) ->
ConnectBroadcast[T](id, v). The client is JVM-less so it cannot construct a real Broadcast[T];
ConnectBroadcast stands in and, when captured in a UDF closure, writeReplace() emits an id-only
ConnectBroadcastRef token (the value never hits the wire -- it already travelled via the artifact).
- Capture: writeReplace records the id in ConnectBroadcastCapture (thread-local); UdfToProtoUtils
.toProto drains it into ScalarScalaUDF.broadcast_ids (field 6) after serializing the closure.
- Server: SparkConnectPlanner.unpackScalaUDF resolves broadcast_ids against the per-session
SessionHolder registry (unknown id -> BROADCAST_NOT_FOUND) and binds them via a thread-local
around closure deserialization; ConnectBroadcastRef.readResolve swaps each token for the real
driver-side Broadcast[_]. No registry bound (client validation round-trip) -> a safe placeholder.
- CreateBroadcastCommand gains value_type {PYTHON(default), JVM}: JVM Java-deserializes the bytes
and sc.broadcast(value) directly so the executor reads Broadcast[T]. SessionHolder.broadcasts
widened to Broadcast[_] to hold both Broadcast[PythonBroadcast] (Python) and Broadcast[T] (Scala).
- Executor path unchanged (classic TorrentBroadcast fan-out).
Server unit tests added (registry holds Broadcast[T]; readResolve swap under withRegistry; client
round-trip placeholder; capture round-trip). Proto regenerated with the pinned toolchain; scalafmt
3.8.6 clean.
DRAFT / DO-NOT-MERGE: stacks on Python v1; SPARK-46032 (Scala closure deserialization on Connect)
is a related reliability risk; end-to-end Scala UDF test + UDAF coverage + BROADCAST_VALUE_TOO_LARGE
enforcement are follow-ups. See SCALA-PR-PLAN.md / SCALA-SPIKE-FINDINGS.md.
Co-authored-by: Isaac
Tagar
force-pushed
the
broadcast-connect-scala-v1
branch
from
July 23, 2026 23:59
663a4c4 to
49a6589
Compare
Tagar
marked this pull request as ready for review
July 24, 2026 00:14
Tagar
marked this pull request as draft
July 24, 2026 00:15
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
[DRAFT][POC] Stacks on #1 (
broadcast-connect-python-v1). Base is the Python-v1 branch, so this diff shows only the Scala delta. SPARK-51705 Scala follow-up.What changes were proposed in this pull request?
Extends broadcast variables over Spark Connect to the Scala
ScalarScalaUDFpath (Python v1 is #1). This is the path the Vizio "Cooker" workload needs — its broadcast is consumed inside a Scala UDF.The crux: a Scala UDF is a Java-serialized closure that carries the
Broadcast[T]object itself, so — unlike Python, where the id rides an out-of-bandbroadcast_idslist and the worker is handed a readybroadcastVars— the id must resolve during closure deserialization on the server.Module boundary — the
spark-connect-shimsmechanism (important)The JVM-less client module
sql/connect/commondepends only onspark-sql-api, notspark-core, soorg.apache.spark.broadcast.Broadcastis not normally on its classpath. The client compiles againstspark-connect-shims— a module that hand-declares empty stub classes at real FQNs (org.apache.spark.rdd.RDD,SparkContext, …); modules with realspark-core(connect/server, catalyst, sql/core) exclude the shim so the identical FQN binds to the real class ("compile against stub, link against real").Broadcasthad no shim. This PR adds aBroadcastshim tospark-connect-shimsmirroring the real abstract surface (getValue/doUnpersist/doDestroy+id/value). Result:ConnectBroadcast[T] extends Broadcast[T]andSparkSession.broadcast[T]: Broadcast[T]compile on the client (shim) and the server (real core, shim excluded).org.apache.spark.broadcast.Broadcast[T]FQN; the shim binds on the client, the real class on the server/executor, so UDF source is byte-identical to classic.spark-coreis added to the thin client (charter intact); this rides the exact dependency swap the build already uses.ConnectBroadcastRefis core-free (id-only) and shared client↔server (connect/server depends on connect-common), so Java deserialization binds the same class on both sides.Other pieces:
CreateBroadcastCommand.value_type={PYTHON (default), JVM}. JVM Java-deserializes the uploaded bytes (with the artifact classloader) andsc.broadcast(value)directly, so the executor readsBroadcast[T].SessionHolder.broadcastswidened toBroadcast[_]to hold bothBroadcast[PythonBroadcast](Python) andBroadcast[T](Scala).checkDeserializableround-trip intoUdfPacketBytesfiresreadResolvewith no registry bound → returns a safe unresolved placeholder (never executed) rather than throwing.Why are the changes needed?
Broadcast is a core primitive with no Scala equivalent on Spark Connect; the Vizio Cooker migration (and the RDD/SparkContext-heavy Scala install base) needs it. SPARK-51705.
Does this PR introduce any user-facing change?
Yes —
SparkSession.broadcast[T](value): Broadcast[T]on the Connect Scala client. Additive proto only (ScalarScalaUDF.broadcast_ids,CreateBroadcastCommand.value_type+BroadcastValueTypeenum) plus theBroadcastcompile-shim.How was this patch tested?
Server unit tests added (registry holds
Broadcast[T];readResolveswap underwithRegistry; client round-trip placeholder; capture round-trip). Locally: proto regenerated with the pinned toolchain (protobuf 6.33.5), scalafmt 3.8.6--testclean. Full Scala compile + E2E run under CI.Scope / follow-ups (DRAFT / DO-NOT-MERGE)
BROADCAST_VALUE_TOO_LARGEenforcement are follow-ups.This pull request and its description were written by Isaac.