You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@spark.apache.org by vi...@apache.org on 2026/09/09 05:46:51 UTC
(spark) branch branch-4.x updated: [SPARK-59349][SQL] Override the gRPC authority for UDF worker Unix socket channels
This is an automated email from the ASF dual-hosted git repository.
viirya pushed a commit to branch branch-4.x
in repository https://gitbox.apache.org/repos/asf/spark.git
The following commit(s) were added to refs/heads/branch-4.x by this push:
new 98ae626aba05 [SPARK-59349][SQL] Override the gRPC authority for UDF worker Unix socket channels
98ae626aba05 is described below
commit 98ae626aba05d072623fc433bb7e02b5614dfb55
Author: Liang-Chi Hsieh <vi...@gmail.com>
AuthorDate: Tue Sep 8 22:46:33 2026 -0700
[SPARK-59349][SQL] Override the gRPC authority for UDF worker Unix socket channels
### What changes were proposed in this pull request?
`GrpcWorkerChannel` builds its gRPC channel over a Unix domain socket without calling `overrideAuthority`, so gRPC derives the HTTP/2 `:authority` pseudo-header from the target address. For a UDS that address is the socket path, which is not a valid HTTP authority.
This PR sets an explicit placeholder authority on the channel builder:
```scala
NettyChannelBuilder
.forAddress(new DomainSocketAddress(socketPath))
...
.overrideAuthority(GrpcWorkerChannel.UDS_AUTHORITY) // added
.usePlaintext()
.build()
```
with `private[grpc] val UDS_AUTHORITY: String = "localhost"` in the companion object, plus a comment recording why, so the reasoning does not have to be rediscovered.
A placeholder is the correct value rather than a workaround: for a Unix socket peer the authority carries no information, because the socket path already identifies the peer completely. There is no name to resolve and no virtual host to select.
### Why are the changes needed?
A conforming HTTP/2 server validates the pseudo-headers while decoding the HEADERS frame and resets the stream:
```
h2::server: malformed headers: malformed authority (b"var%2Ffolders%2F...%2Frw.sock"): invalid authority
h2::proto::streams::send: send_reset(..., reason=PROTOCOL_ERROR, initiator=Library, ...)
```
The client sees only an opaque protocol error:
```
io.grpc.StatusRuntimeException: INTERNAL: RST_STREAM closed stream. HTTP/2 error code: PROTOCOL_ERROR
```
The rejection happens *before* any worker application code runs -- not the service method, not an interceptor -- so the worker logs nothing and `Init` is never observed. The failure points at the wrong side of the connection while the worker has no way to report why, which makes it expensive to diagnose.
**This is not reachable through any in-tree code path today.** grpc-java's own server tolerates the malformed authority, and both Spark's client and the only in-tree worker (`EchoGrpcWorkerMain`) are grpc-java, so the pair works and the header is never validated. That is why it merged unnoticed in SPARK-56922, and it is also why it is worth fixing before the first third-party worker exists rather than after.
It does affect any worker on a conforming HTTP/2 stack, which is most stacks outside the JVM. `worker_spec.proto` invites workers in any language, and the SPARK-55278 SPIP names onboarding a Go/Rust/Swift worker as its final exam; such a worker hits this on its very first RPC.
This was confirmed three independent ways against a worker running on a conforming HTTP/2 server:
1. The same client succeeds against the same worker when only the authority changes -- `grpc.insecure_channel(f"unix:{sock}", options=[("grpc.default_authority", "localhost")])` works where the default fails.
2. Driving that worker through Spark's own `DirectGrpcDispatcher` and `GrpcWorkerSession` reproduces the identical `PROTOCOL_ERROR`, so it is not a grpc-python quirk.
3. The server-side `h2` trace above names the rejected header, so the cause is read from the library that rejects it rather than inferred.
Note the fix cannot go on the worker side. A server-side middleware layer that rewrites the request URI has no effect, because such layers run *after* header decode, by which point the stream is already reset. Fixing it below the HTTP/2 layer would mean rewriting the HPACK-encoded HEADERS frame, which is unreasonable for something the client sets with one call.
### Does this PR introduce _any_ user-facing change?
No. The affected code is unreleased (`GrpcWorkerChannel` arrived in SPARK-56922, fixVersion 4.4.0), and there is no behavior change for any in-tree code path -- grpc-java servers accept both the old and the new authority.
### How was this patch tested?
New test in `DirectGrpcDispatcherIntegrationSuite`, which already spawns a real gRPC worker over a UDS, asserting the channel's configured authority:
```scala
val authority = channel.channel.authority()
assert(authority === GrpcWorkerChannel.UDS_AUTHORITY,
s"expected the overridden authority, got '$authority'")
```
The test asserts on the configured authority rather than on an end-to-end round trip deliberately: **an end-to-end test cannot detect this bug.** The only in-tree worker is grpc-java, which tolerates the malformed authority, so a test driven through it passes with or without the fix. Asserting the authority tests the fix rather than the symptom.
I verified the test actually detects the defect by reverting the one-line fix and re-running it:
```
- the channel overrides the authority derived from the socket path *** FAILED ***
"[/tmp/spark-udf-worker.../w-e59da31d51514d13.sock]" did not equal "[localhost]"
expected the overridden authority, got '/tmp/spark-udf-worker.../w-e59da31d51514d13.sock'
```
It fails without the fix and passes with it.
End-to-end coverage against a non-grpc-java worker was verified separately: with the fix, a worker on a conforming HTTP/2 server driven through `DirectGrpcDispatcher` completes the protocol exchange that previously failed with `PROTOCOL_ERROR`.
Also run:
- `build/sbt udf-worker-grpc/compile udf-worker-grpc/Test/compile`
- `build/sbt udf-worker-grpc/test` -- 60 tests, all passing (4 suites, no pre-existing test disturbed)
- `build/sbt udf-worker-grpc/scalastyle udf-worker-grpc/Test/scalastyle` -- 0 errors
### Was this patch authored or co-authored using generative AI tooling?
Generated-by: Claude Code
Closes #58638 from viirya/uds-authority.
Authored-by: Liang-Chi Hsieh <vi...@gmail.com>
Signed-off-by: Liang-Chi Hsieh <vi...@gmail.com>
(cherry picked from commit 2cd436aada3572200837a31581cae1e4534aaad3)
Signed-off-by: Liang-Chi Hsieh <vi...@gmail.com>
---
.../spark/udf/worker/grpc/GrpcWorkerChannel.scala | 18 ++++++++++++++++++
.../grpc/DirectGrpcDispatcherIntegrationSuite.scala | 18 ++++++++++++++++++
2 files changed, 36 insertions(+)
diff --git a/udf/worker/grpc/src/main/scala/org/apache/spark/udf/worker/grpc/GrpcWorkerChannel.scala b/udf/worker/grpc/src/main/scala/org/apache/spark/udf/worker/grpc/GrpcWorkerChannel.scala
index 220a31d63331..42686d6737eb 100644
--- a/udf/worker/grpc/src/main/scala/org/apache/spark/udf/worker/grpc/GrpcWorkerChannel.scala
+++ b/udf/worker/grpc/src/main/scala/org/apache/spark/udf/worker/grpc/GrpcWorkerChannel.scala
@@ -77,6 +77,7 @@ class GrpcWorkerChannel(
.channelType(transport.channelType)
.eventLoopGroup(eventLoopGroup)
.maxInboundMessageSize(GrpcWorkerChannel.MAX_INBOUND_MESSAGE_SIZE)
+ .overrideAuthority(GrpcWorkerChannel.UDS_AUTHORITY)
.usePlaintext()
.build()
} catch {
@@ -132,5 +133,22 @@ object GrpcWorkerChannel {
/** Matches Spark Connect's default and accommodates normal Arrow batches. */
private[grpc] val MAX_INBOUND_MESSAGE_SIZE: Int = 128 * 1024 * 1024
+ /**
+ * Authority sent in the HTTP/2 `:authority` pseudo-header.
+ *
+ * Without an override, gRPC derives the authority from the target address, which for a
+ * Unix domain socket is the socket path (percent-encoded where the path needs escaping,
+ * e.g. `var%2Ffolders%2F...%2Frw.sock`). Either form is an invalid HTTP authority. A
+ * conforming HTTP/2 server rejects such a HEADERS frame with PROTOCOL_ERROR while
+ * decoding it, so the RPC fails before any worker code -- service method or interceptor
+ * -- runs, and the worker cannot report why. grpc-java's own server accepts it, which is
+ * why the in-tree grpc-java test worker does not surface this.
+ *
+ * A placeholder is the right value rather than a workaround: the authority carries no
+ * information for a Unix socket peer, because the socket path already identifies it
+ * completely. There is no name to resolve and no virtual host to select.
+ */
+ private[grpc] val UDS_AUTHORITY: String = "localhost"
+
private val SHUTDOWN_PHASE_TIMEOUT_MS: Long = 5000L
}
diff --git a/udf/worker/grpc/src/test/scala/org/apache/spark/udf/worker/grpc/DirectGrpcDispatcherIntegrationSuite.scala b/udf/worker/grpc/src/test/scala/org/apache/spark/udf/worker/grpc/DirectGrpcDispatcherIntegrationSuite.scala
index 370bf907414c..7ede7a635904 100644
--- a/udf/worker/grpc/src/test/scala/org/apache/spark/udf/worker/grpc/DirectGrpcDispatcherIntegrationSuite.scala
+++ b/udf/worker/grpc/src/test/scala/org/apache/spark/udf/worker/grpc/DirectGrpcDispatcherIntegrationSuite.scala
@@ -160,6 +160,24 @@ class DirectGrpcDispatcherIntegrationSuite
assert(!socketDir.exists(), "dispatcher close should remove its socket directory")
}
+ test("the channel overrides the authority derived from the socket path") {
+ dispatcher = new DirectGrpcDispatcher(workerSpec())
+ val session = dispatcher.createSession(None)
+ val channel = grpcChannel(workerProcess(session))
+
+ try {
+ // Without the override the authority would be the socket path, which a conforming
+ // HTTP/2 server rejects with PROTOCOL_ERROR while decoding the HEADERS frame.
+ // The in-tree worker is grpc-java, which tolerates the encoded form, so this asserts on
+ // the channel's configured authority rather than on an end-to-end failure.
+ val authority = channel.channel.authority()
+ assert(authority === GrpcWorkerChannel.UDS_AUTHORITY,
+ s"expected the overridden authority, got '$authority'")
+ } finally {
+ session.close(emptyCancel)
+ }
+ }
+
test("a socket path alone does not make a worker ready") {
val socketOnlyWorker =
"""
---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@spark.apache.org
For additional commands, e-mail: commits-help@spark.apache.org