[SPARK-59349][SQL] Override the gRPC authority for UDF worker Unix socket channels - #58638
Closed
viirya wants to merge 1 commit into
Closed
[SPARK-59349][SQL] Override the gRPC authority for UDF worker Unix socket channels#58638viirya wants to merge 1 commit into
viirya wants to merge 1 commit into
Conversation
…cket channels ### What changes were proposed in this pull request? `GrpcWorkerChannel` did not call `overrideAuthority`, so gRPC derived the HTTP/2 `:authority` pseudo-header from the target address, which for a Unix domain socket is the socket path -- not a valid HTTP authority. This sets an explicit placeholder authority on the channel builder. ### Why are the changes needed? A conforming HTTP/2 server rejects the malformed authority with PROTOCOL_ERROR while decoding the HEADERS frame, before any worker application code runs, so the worker logs nothing and the error points at the wrong side of the connection. grpc-java's own server tolerates the encoded form, so this is unreachable through any in-tree path today, but it breaks any worker on a conforming HTTP/2 stack. ### Does this PR introduce _any_ user-facing change? No. The affected code is unreleased and no in-tree code path changes behavior. ### How was this patch tested? New test in `DirectGrpcDispatcherIntegrationSuite` asserting the channel's configured authority; verified it fails without the fix. An end-to-end test cannot detect this bug because the only in-tree worker is grpc-java and tolerates the malformed header. ### Was this patch authored or co-authored using generative AI tooling? Generated-by: Claude Code Co-authored-by: Claude Code <no-reply@anthropic.com>
Member
Author
HyukjinKwon
approved these changes
Sep 8, 2026
viirya
added a commit
that referenced
this pull request
Sep 9, 2026
…cket 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 <viirya@gmail.com> Signed-off-by: Liang-Chi Hsieh <viirya@gmail.com> (cherry picked from commit 2cd436a) Signed-off-by: Liang-Chi Hsieh <viirya@gmail.com>
Member
Author
Member
Author
|
Thanks @HyukjinKwon |
haiyangsun-db
left a comment
Contributor
There was a problem hiding this comment.
thank you for the fix!
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.
What changes were proposed in this pull request?
GrpcWorkerChannelbuilds its gRPC channel over a Unix domain socket without callingoverrideAuthority, so gRPC derives the HTTP/2:authoritypseudo-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:
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:
The client sees only an opaque protocol error:
The rejection happens before any worker application code runs -- not the service method, not an interceptor -- so the worker logs nothing and
Initis 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.protoinvites 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:
grpc.insecure_channel(f"unix:{sock}", options=[("grpc.default_authority", "localhost")])works where the default fails.DirectGrpcDispatcherandGrpcWorkerSessionreproduces the identicalPROTOCOL_ERROR, so it is not a grpc-python quirk.h2trace 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 (
GrpcWorkerChannelarrived 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: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:
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
DirectGrpcDispatchercompletes the protocol exchange that previously failed withPROTOCOL_ERROR.Also run:
build/sbt udf-worker-grpc/compile udf-worker-grpc/Test/compilebuild/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 errorsWas this patch authored or co-authored using generative AI tooling?
Generated-by: Claude Code