This is an automated email from the ASF dual-hosted git repository.

cloud-fan 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 c167c6687f94 [SPARK-58632][CONNECT] Send Spark Connect operation ID in 
gRPC metadata
c167c6687f94 is described below

commit c167c6687f94fcb4ef7595514df8ad436972c807
Author: Wenchen Fan <[email protected]>
AuthorDate: Thu Aug 20 14:24:10 2026 +0800

    [SPARK-58632][CONNECT] Send Spark Connect operation ID in gRPC metadata
    
    ### What changes were proposed in this pull request?
    
    Spark Connect's Scala and Python clients now send the `ExecutePlan` 
operation ID in the
    `spark-connect-operation-id` gRPC metadata header. The same operation ID is 
used for initial,
    reattach, and release RPCs. Client connection options cannot override this 
per-operation value.
    
    ### Why are the changes needed?
    
    Gateways and other intermediaries cannot reliably correlate an 
`ExecutePlan` request by inspecting
    the protobuf body, especially when a request fails before the server 
returns a response. A stable,
    vendor-neutral metadata header makes the client-generated operation ID 
available throughout the
    request path without decoding the payload.
    
    ### Does this PR introduce _any_ user-facing change?
    
    Yes. Spark Connect clients now include `spark-connect-operation-id` in gRPC 
metadata for operation
    RPCs. The value matches `ExecutePlanRequest.operation_id`.
    
    ### How was this patch tested?
    
    Added Scala tests for reattachable and non-reattachable clients and a 
Python test that verifies
    connection metadata cannot override the operation ID.
    
    Attempted to run:
    
    `build/sbt 'connect-client-jvm/testOnly 
org.apache.spark.sql.connect.client.SparkConnectClientSuite'`
    
    The test could not start because Maven Central DNS resolution was 
unavailable in the local
    environment.
    
    ### Was this patch authored or co-authored using generative AI tooling?
    
    Generated-by: OpenAI Codex (GPT-5)
    
    Closes #57834 from cloud-fan/cloud-fan/spark-connect-operation-id-header.
    
    Authored-by: Wenchen Fan <[email protected]>
    Signed-off-by: Wenchen Fan <[email protected]>
    (cherry picked from commit bdcb08abf260cb11fedf91e88b514e42351c1b63)
    Signed-off-by: Wenchen Fan <[email protected]>
---
 python/pyspark/sql/connect/client/core.py          | 56 +++++++++----
 python/pyspark/sql/connect/plan.py                 |  2 +-
 .../sql/tests/connect/client/test_client.py        | 62 ++++++++++++++
 .../connect/client/SparkConnectClientSuite.scala   | 96 ++++++++++++++++++----
 .../client/CustomSparkConnectBlockingStub.scala    | 14 +++-
 .../ExecutePlanResponseReattachableIterator.scala  | 10 ++-
 .../sql/connect/client/SparkConnectClient.scala    | 16 +++-
 7 files changed, 221 insertions(+), 35 deletions(-)

diff --git a/python/pyspark/sql/connect/client/core.py 
b/python/pyspark/sql/connect/client/core.py
index 462ad8fe2670..03bd25e9c34e 100644
--- a/python/pyspark/sql/connect/client/core.py
+++ b/python/pyspark/sql/connect/client/core.py
@@ -130,6 +130,7 @@ if TYPE_CHECKING:
 
 
 PYSPARK_ROOT = os.path.dirname(pyspark.__file__)
+_OPERATION_ID_METADATA_KEY = "spark-connect-operation-id"
 
 
 @dataclass(frozen=True)
@@ -887,6 +888,16 @@ class SparkConnectClient(object):
             if isinstance(connection, ChannelBuilder)
             else DefaultChannelBuilder(connection, channel_options)
         )
+        metadata = list(self._builder.metadata())
+        if any(key.lower() == _OPERATION_ID_METADATA_KEY for key, _ in 
metadata):
+            logger.warning(
+                "Connection option %s is ignored because Spark Connect sets it 
for each "
+                "ExecutePlan request.",
+                _OPERATION_ID_METADATA_KEY,
+            )
+        artifact_manager_metadata = [
+            (key, value) for key, value in metadata if key.lower() != 
_OPERATION_ID_METADATA_KEY
+        ]
         self._user_id = None
         self._retry_policies: List[RetryPolicy] = []
 
@@ -927,7 +938,7 @@ class SparkConnectClient(object):
             self._user_id,
             self._session_id,
             self._channel,
-            self._builder.metadata(),
+            artifact_manager_metadata,
             add_artifacts_timeout=self._rpc_deadlines.add_artifacts,
             artifact_status_timeout=self._rpc_deadlines.artifact_status,
         )
@@ -1657,7 +1668,7 @@ class SparkConnectClient(object):
                 with attempt:
                     resp = self._stub.AnalyzePlan(
                         req,
-                        metadata=self._builder.metadata(),
+                        metadata=self._builder_metadata(),
                         timeout=self._rpc_deadlines.analyze_plan,
                     )
                     self._verify_response_integrity(resp)
@@ -1693,7 +1704,7 @@ class SparkConnectClient(object):
                     req,
                     self._stub,
                     self._retrying,
-                    self._builder.metadata(),
+                    self._execute_plan_metadata(req.operation_id),
                     
reattachable_execute_plan_timeout=self._rpc_deadlines.reattachable_execute_plan,
                     
reattach_execute_timeout=self._rpc_deadlines.reattach_execute,
                 )
@@ -1707,11 +1718,25 @@ class SparkConnectClient(object):
                 for attempt in self._retrying():
                     with attempt:
                         with disable_gc():
-                            for b in self._stub.ExecutePlan(req, 
metadata=self._builder.metadata()):
+                            for b in self._stub.ExecutePlan(
+                                req, 
metadata=self._execute_plan_metadata(req.operation_id)
+                            ):
                                 handle_response(b)
         except Exception as error:
             self._handle_error(error, req.operation_id)
 
+    def _builder_metadata(self) -> List[Tuple[str, str]]:
+        return [
+            (key, value)
+            for key, value in self._builder.metadata()
+            if key.lower() != _OPERATION_ID_METADATA_KEY
+        ]
+
+    def _execute_plan_metadata(self, operation_id: str) -> List[Tuple[str, 
str]]:
+        metadata = self._builder_metadata()
+        metadata.append((_OPERATION_ID_METADATA_KEY, operation_id))
+        return metadata
+
     def _execute_and_fetch_as_iterator(
         self,
         req: pb2.ExecutePlanRequest,
@@ -1735,7 +1760,6 @@ class SparkConnectClient(object):
         for hook in self._session_hooks:
             req = hook.on_execute_plan(req)
             req.operation_id = operation_id
-
         num_records = 0
         arrow_batch_chunks_to_assemble: List[bytes] = []
 
@@ -1910,7 +1934,7 @@ class SparkConnectClient(object):
                     req,
                     self._stub,
                     self._retrying,
-                    self._builder.metadata(),
+                    self._execute_plan_metadata(req.operation_id),
                     
reattachable_execute_plan_timeout=self._rpc_deadlines.reattachable_execute_plan,
                     
reattach_execute_timeout=self._rpc_deadlines.reattach_execute,
                 )
@@ -1925,7 +1949,9 @@ class SparkConnectClient(object):
                     with attempt:
                         with disable_gc():
                             it = iter(
-                                self._stub.ExecutePlan(req, 
metadata=self._builder.metadata())
+                                self._stub.ExecutePlan(
+                                    req, 
metadata=self._execute_plan_metadata(req.operation_id)
+                                )
                             )
                         while True:
                             try:
@@ -2067,7 +2093,7 @@ class SparkConnectClient(object):
                     with disable_gc():
                         resp = self._stub.Config(
                             req,
-                            metadata=self._builder.metadata(),
+                            metadata=self._builder_metadata(),
                             timeout=self._rpc_deadlines.config,
                         )
                     self._verify_response_integrity(resp)
@@ -2113,7 +2139,7 @@ class SparkConnectClient(object):
                 with attempt:
                     resp = self._stub.Interrupt(
                         req,
-                        metadata=self._builder.metadata(),
+                        metadata=self._builder_metadata(),
                         timeout=self._rpc_deadlines.interrupt,
                     )
                     self._verify_response_integrity(resp)
@@ -2129,7 +2155,7 @@ class SparkConnectClient(object):
                 with attempt:
                     resp = self._stub.Interrupt(
                         req,
-                        metadata=self._builder.metadata(),
+                        metadata=self._builder_metadata(),
                         timeout=self._rpc_deadlines.interrupt,
                     )
                     self._verify_response_integrity(resp)
@@ -2145,7 +2171,7 @@ class SparkConnectClient(object):
                 with attempt:
                     resp = self._stub.Interrupt(
                         req,
-                        metadata=self._builder.metadata(),
+                        metadata=self._builder_metadata(),
                         timeout=self._rpc_deadlines.interrupt,
                     )
                     self._verify_response_integrity(resp)
@@ -2165,7 +2191,7 @@ class SparkConnectClient(object):
                 with attempt:
                     resp = self._stub.ReleaseSession(
                         req,
-                        metadata=self._builder.metadata(),
+                        metadata=self._builder_metadata(),
                         timeout=self._rpc_deadlines.release_session,
                     )
                     self._verify_response_integrity(resp)
@@ -2221,7 +2247,7 @@ class SparkConnectClient(object):
                 with attempt:
                     resp = self._stub.GetStatus(
                         req,
-                        metadata=self._builder.metadata(),
+                        metadata=self._builder_metadata(),
                         timeout=self._rpc_deadlines.get_status,
                     )
                     self._verify_response_integrity(resp)
@@ -2357,7 +2383,7 @@ class SparkConnectClient(object):
         try:
             return self._stub.FetchErrorDetails(
                 req,
-                metadata=self._builder.metadata(),
+                metadata=self._builder_metadata(),
                 timeout=self._rpc_deadlines.fetch_error_details,
             )
         except grpc.RpcError:
@@ -2776,7 +2802,7 @@ class SparkConnectClient(object):
             with attempt:
                 response: pb2.CloneSessionResponse = self._stub.CloneSession(
                     request,
-                    metadata=self._builder.metadata(),
+                    metadata=self._builder_metadata(),
                     timeout=self._rpc_deadlines.clone_session,
                 )
 
diff --git a/python/pyspark/sql/connect/plan.py 
b/python/pyspark/sql/connect/plan.py
index 51f117516663..709071b8acc2 100644
--- a/python/pyspark/sql/connect/plan.py
+++ b/python/pyspark/sql/connect/plan.py
@@ -784,7 +784,7 @@ class CachedRemoteRelation(LogicalPlan):
                             request_serializer=request_serializer,
                             response_deserializer=response_deserializer,
                         )
-                        metadata = session.client._builder.metadata()
+                        metadata = 
session.client._execute_plan_metadata(req.operation_id)
                         channel(req, metadata=metadata)  # type: 
ignore[arg-type]
             except Exception as e:
                 logger.warning(f"RemoveRemoteCachedRelation failed with 
exception: {e}.")
diff --git a/python/pyspark/sql/tests/connect/client/test_client.py 
b/python/pyspark/sql/tests/connect/client/test_client.py
index 35252ed799ea..5e24bd686517 100644
--- a/python/pyspark/sql/tests/connect/client/test_client.py
+++ b/python/pyspark/sql/tests/connect/client/test_client.py
@@ -113,16 +113,22 @@ if should_test_connect:
             self.release_calls = 0
             self.release_until_calls = 0
             self.attach_calls = 0
+            self.execute_metadata = []
+            self.attach_metadata = []
+            self.release_metadata = []
 
         def ExecutePlan(self, *args, **kwargs):
             self.execute_calls += 1
+            self.execute_metadata.append(kwargs.get("metadata"))
             return self._execute_ops
 
         def ReattachExecute(self, *args, **kwargs):
             self.attach_calls += 1
+            self.attach_metadata.append(kwargs.get("metadata"))
             return self._attach_ops
 
         def ReleaseExecute(self, req: proto.ReleaseExecuteRequest, *args, 
**kwargs):
+            self.release_metadata.append(kwargs.get("metadata"))
             if req.HasField("release_all"):
                 self.release_calls += 1
             elif req.HasField("release_until"):
@@ -149,6 +155,7 @@ if should_test_connect:
         def __init__(self, session_id: str, operation_statuses=None):
             self._session_id = session_id
             self.req = None
+            self.metadata = None
             self.client_user_context_extensions = []
             if operation_statuses is None:
                 operation_statuses = self.DEFAULT_OPERATION_STATUSES
@@ -156,6 +163,7 @@ if should_test_connect:
 
         def ExecutePlan(self, req: proto.ExecutePlanRequest, metadata, 
timeout=None):
             self.req = req
+            self.metadata = metadata
             self.client_user_context_extensions = 
list(req.user_context.extensions)
             resp = proto.ExecutePlanResponse()
             resp.session_id = self._session_id
@@ -297,6 +305,30 @@ class SparkConnectClientTestCase(unittest.TestCase):
 
         self.assertEqual(client._user_id, "abc")
 
+    def test_channel_builder_metadata_is_filtered_per_call(self):
+        class CustomChannelBuilder(DefaultChannelBuilder):
+            def __init__(self):
+                super().__init__("sc://foo/")
+                self.metadata_calls = 0
+
+            def metadata(self):
+                self.metadata_calls += 1
+                return iter(
+                    [
+                        ("authorization", f"token-{self.metadata_calls}"),
+                        ("spark-connect-operation-id", "ignored"),
+                    ]
+                )
+
+        builder = CustomChannelBuilder()
+        client = SparkConnectClient(builder, use_reattachable_execute=False)
+        try:
+            self.assertEqual(client._artifact_manager._metadata, 
[("authorization", "token-1")])
+            self.assertEqual(client._builder_metadata(), [("authorization", 
"token-2")])
+            self.assertEqual(client._builder_metadata(), [("authorization", 
"token-3")])
+        finally:
+            client.close()
+
     def test_user_context_extension(self):
         client = SparkConnectClient("sc://foo/", 
use_reattachable_execute=False)
         mock = MockService(client._session_id)
@@ -554,6 +586,21 @@ class SparkConnectClientTestCase(unittest.TestCase):
         finally:
             client.close()
 
+    def test_execute_plan_sends_operation_id_metadata(self):
+        client = SparkConnectClient(
+            "sc://foo/;spark-connect-operation-id=ignored", 
use_reattachable_execute=False
+        )
+        mock = MockService(client._session_id)
+        client._stub = mock
+        try:
+            req = client._execute_plan_request_with_metadata()
+            client._execute(req)
+            self.assertIsNotNone(mock.metadata)
+            values = [v for k, v in mock.metadata if k == 
"spark-connect-operation-id"]
+            self.assertEqual(values, [req.operation_id])
+        finally:
+            client.close()
+
     def test_on_exit_calls_release_and_close_when_enabled(self):
         client = SparkConnectClient("sc://foo/", 
use_reattachable_execute=False)
         client._release_session_on_exit = True
@@ -954,6 +1001,21 @@ class 
SparkConnectClientReattachTestCase(unittest.TestCase):
 
         eventually(timeout=1, catch_assertions=True)(check_all)()
 
+    def test_operation_id_metadata_is_sent_on_all_rpcs(self):
+        metadata = [("spark-connect-operation-id", "operation-id")]
+        stub = self._stub_with([self.response], [self.finished])
+        ite = ExecutePlanResponseReattachableIterator(self.request, stub, 
self.retrying, metadata)
+        for _ in ite:
+            pass
+
+        def check_all():
+            self.assertEqual(stub.execute_metadata, [metadata])
+            self.assertEqual(stub.attach_metadata, [metadata])
+            self.assertTrue(stub.release_metadata)
+            self.assertTrue(all(value == metadata for value in 
stub.release_metadata))
+
+        eventually(timeout=1, catch_assertions=True)(check_all)()
+
     def test_fail_during_execute(self):
         def fatal():
             raise TestException("Fatal")
diff --git 
a/sql/connect/client/jvm/src/test/scala/org/apache/spark/sql/connect/client/SparkConnectClientSuite.scala
 
b/sql/connect/client/jvm/src/test/scala/org/apache/spark/sql/connect/client/SparkConnectClientSuite.scala
index 49ae6ed8ea72..6a456a554c5c 100644
--- 
a/sql/connect/client/jvm/src/test/scala/org/apache/spark/sql/connect/client/SparkConnectClientSuite.scala
+++ 
b/sql/connect/client/jvm/src/test/scala/org/apache/spark/sql/connect/client/SparkConnectClientSuite.scala
@@ -45,8 +45,11 @@ class SparkConnectClientSuite extends ConnectFunSuite {
   private var service: DummySparkConnectService = _
   private var server: Server = _
 
-  private def startDummyServer(port: Int, interceptors: Seq[ServerInterceptor] 
= Seq()): Unit = {
-    service = new DummySparkConnectService
+  private def startDummyServer(
+      port: Int,
+      interceptors: Seq[ServerInterceptor] = Seq(),
+      dummyService: DummySparkConnectService = new DummySparkConnectService): 
Unit = {
+    service = dummyService
     val serverBuilder = NettyServerBuilder
       .forPort(port)
       .addService(service)
@@ -78,19 +81,84 @@ class SparkConnectClientSuite extends ConnectFunSuite {
     assert(client.userId == System.getProperty("user.name"))
   }
 
-  test("client generates an operation ID for ExecutePlan requests") {
-    startDummyServer(0)
-    client = SparkConnectClient
-      .builder()
-      .connectionString(s"sc://localhost:${server.getPort}")
-      .disableReattachableExecute()
-      .build()
-
-    val responses = client.execute(buildPlan("select 1")).toSeq
-    val operationId = responses.head.getOperationId
+  Seq(false, true).foreach { reattachable =>
+    test(s"client generates an operation ID for ExecutePlan requests 
($reattachable)") {
+      val operationIdHeaders = mutable.Map.empty[String, Option[String]]
+      val interceptor = new ServerInterceptor {
+        override def interceptCall[ReqT, RespT](
+            call: ServerCall[ReqT, RespT],
+            headers: Metadata,
+            next: ServerCallHandler[ReqT, RespT]): ServerCall.Listener[ReqT] = 
{
+          val key = Metadata.Key.of(
+            SparkConnectClient.OPERATION_ID_HEADER,
+            Metadata.ASCII_STRING_MARSHALLER)
+          operationIdHeaders.synchronized {
+            operationIdHeaders(call.getMethodDescriptor.getBareMethodName) =
+              Option(headers.get(key))
+          }
+          next.startCall(call, headers)
+        }
+      }
+      val dummyService = if (reattachable) {
+        new DummySparkConnectService {
+          override def executePlan(
+              request: ExecutePlanRequest,
+              responseObserver: StreamObserver[ExecutePlanResponse]): Unit = {
+            responseObserver.onNext(
+              ExecutePlanResponse
+                .newBuilder()
+                .setSessionId(request.getSessionId)
+                .setOperationId(request.getOperationId)
+                .setResponseId("initial-response")
+                .build())
+            responseObserver.onCompleted()
+          }
 
-    UUID.fromString(operationId)
-    assert(responses.forall(_.getOperationId == operationId))
+          override def reattachExecute(
+              request: proto.ReattachExecuteRequest,
+              responseObserver: StreamObserver[ExecutePlanResponse]): Unit = {
+            responseObserver.onNext(
+              ExecutePlanResponse
+                .newBuilder()
+                .setSessionId(request.getSessionId)
+                .setOperationId(request.getOperationId)
+                .setResponseId("result-complete")
+                
.setResultComplete(proto.ExecutePlanResponse.ResultComplete.newBuilder().build())
+                .build())
+            responseObserver.onCompleted()
+          }
+        }
+      } else {
+        new DummySparkConnectService
+      }
+      startDummyServer(0, Seq(interceptor), dummyService)
+      val builder = SparkConnectClient
+        .builder()
+        .connectionString(s"sc://localhost:${server.getPort}")
+        .option(SparkConnectClient.OPERATION_ID_HEADER, "ignored")
+      if (reattachable) builder.enableReattachableExecute()
+      else builder.disableReattachableExecute()
+      client = builder.build()
+
+      val responses = client.execute(buildPlan("select 1")).toSeq
+      val operationId = responses.head.getOperationId
+
+      UUID.fromString(operationId)
+      assert(responses.forall(_.getOperationId == operationId))
+      assert(operationIdHeaders.synchronized {
+        operationIdHeaders("ExecutePlan").contains(operationId)
+      })
+      if (reattachable) {
+        assert(operationIdHeaders.synchronized {
+          operationIdHeaders("ReattachExecute").contains(operationId)
+        })
+        Eventually.eventually(timeout(5.seconds)) {
+          assert(operationIdHeaders.synchronized {
+            operationIdHeaders("ReleaseExecute").contains(operationId)
+          })
+        }
+      }
+    }
   }
 
   test("ExecutePlan exceptions expose the client-generated operation ID") {
diff --git 
a/sql/connect/common/src/main/scala/org/apache/spark/sql/connect/client/CustomSparkConnectBlockingStub.scala
 
b/sql/connect/common/src/main/scala/org/apache/spark/sql/connect/client/CustomSparkConnectBlockingStub.scala
index 4a1e1c838aa8..688029f4883a 100644
--- 
a/sql/connect/common/src/main/scala/org/apache/spark/sql/connect/client/CustomSparkConnectBlockingStub.scala
+++ 
b/sql/connect/common/src/main/scala/org/apache/spark/sql/connect/client/CustomSparkConnectBlockingStub.scala
@@ -45,10 +45,22 @@ private[connect] class CustomSparkConnectBlockingStub(
   // GrpcExceptionConverter with a GRPC stub for fetching error details from 
server.
   private val grpcExceptionConverter = stubState.exceptionConverter
 
+  private def executePlanStub(operationId: String) = {
+    Option(operationId)
+      .filter(_.nonEmpty)
+      .map { id =>
+        stub.withInterceptors(
+          new SparkConnectClient.MetadataHeaderClientInterceptor(
+            Map(SparkConnectClient.OPERATION_ID_HEADER -> id)))
+      }
+      .getOrElse(stub)
+  }
+
   // Non-reattachable executePlan intentionally has no deadline: a timeout 
here would kill the
   // server-side execution with no way to recover (there is no ReattachExecute 
for this path).
   // Use reattachable execution for long-running queries that need deadline 
protection.
   def executePlan(request: ExecutePlanRequest): 
CloseableIterator[ExecutePlanResponse] = {
+    val stubWithOperationId = executePlanStub(request.getOperationId)
     grpcExceptionConverter.convert(
       request.getSessionId,
       request.getUserContext,
@@ -62,7 +74,7 @@ private[connect] class CustomSparkConnectBlockingStub(
           request,
           r => {
             stubState.responseValidator.wrapIterator(
-              CloseableIterator(stub.executePlan(r).asScala))
+              CloseableIterator(stubWithOperationId.executePlan(r).asScala))
           }),
         Option(request.getOperationId).filter(_.nonEmpty))
     }
diff --git 
a/sql/connect/common/src/main/scala/org/apache/spark/sql/connect/client/ExecutePlanResponseReattachableIterator.scala
 
b/sql/connect/common/src/main/scala/org/apache/spark/sql/connect/client/ExecutePlanResponseReattachableIterator.scala
index e6428270c20b..c7073c96aba6 100644
--- 
a/sql/connect/common/src/main/scala/org/apache/spark/sql/connect/client/ExecutePlanResponseReattachableIterator.scala
+++ 
b/sql/connect/common/src/main/scala/org/apache/spark/sql/connect/client/ExecutePlanResponseReattachableIterator.scala
@@ -81,8 +81,14 @@ class ExecutePlanResponseReattachableIterator(
   // - this does it's own custom retry handling
   // - error conversion is wrapped around this in 
CustomSparkConnectBlockingStub,
   //   this needs raw GRPC errors for retries.
-  private val rawBlockingStub = 
proto.SparkConnectServiceGrpc.newBlockingStub(channel)
-  private val rawAsyncStub = proto.SparkConnectServiceGrpc.newStub(channel)
+  private val operationIdInterceptor = new 
SparkConnectClient.MetadataHeaderClientInterceptor(
+    Map(SparkConnectClient.OPERATION_ID_HEADER -> operationId))
+  private val rawBlockingStub =
+    proto.SparkConnectServiceGrpc
+      .newBlockingStub(channel)
+      .withInterceptors(operationIdInterceptor)
+  private val rawAsyncStub =
+    
proto.SparkConnectServiceGrpc.newStub(channel).withInterceptors(operationIdInterceptor)
 
   private def stubWithDeadline(deadline: Option[FiniteDuration])
       : proto.SparkConnectServiceGrpc.SparkConnectServiceBlockingStub =
diff --git 
a/sql/connect/common/src/main/scala/org/apache/spark/sql/connect/client/SparkConnectClient.scala
 
b/sql/connect/common/src/main/scala/org/apache/spark/sql/connect/client/SparkConnectClient.scala
index 58ca37fc7c07..5a3bd0b892c5 100644
--- 
a/sql/connect/common/src/main/scala/org/apache/spark/sql/connect/client/SparkConnectClient.scala
+++ 
b/sql/connect/common/src/main/scala/org/apache/spark/sql/connect/client/SparkConnectClient.scala
@@ -56,6 +56,14 @@ private[sql] class SparkConnectClient(
   private val userContext: UserContext = configuration.userContext
 
   private[this] val stubState = new SparkConnectStubState(channel, 
configuration)
+
+  if (configuration.metadata.keys.exists(
+      _.equalsIgnoreCase(SparkConnectClient.OPERATION_ID_HEADER))) {
+    logWarning(
+      s"Connection option ${SparkConnectClient.OPERATION_ID_HEADER} is ignored 
because " +
+        "Spark Connect sets it for each ExecutePlan request.")
+  }
+
   private[this] val bstub =
     new CustomSparkConnectBlockingStub(channel, stubState)
   private[this] val stub =
@@ -703,6 +711,8 @@ private final class SparkConnectOperationIdException(val 
operationId: String)
 
 object SparkConnectClient {
 
+  private[connect] val OPERATION_ID_HEADER = "spark-connect-operation-id"
+
   /**
    * Returns the ExecutePlan operation ID attached to a Spark Connect failure, 
when available.
    *
@@ -1178,9 +1188,11 @@ object SparkConnectClient {
 
       // Workaround LocalChannelCredentials are added in
       // https://github.com/grpc/grpc-java/issues/9900
-      var metadataWithOptionalToken = metadata
+      var metadataWithOptionalToken = metadata.filterNot { case (key, _) =>
+        key.equalsIgnoreCase(OPERATION_ID_HEADER)
+      }
       if (!isSslEnabled.contains(true) && isLocal && token.isDefined) {
-        metadataWithOptionalToken = metadata + (("Authorization", s"Bearer 
${token.get}"))
+        metadataWithOptionalToken += (("Authorization", s"Bearer 
${token.get}"))
       }
 
       if (metadataWithOptionalToken.nonEmpty) {


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to