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 c3b200ed70b9 [SPARK-58629][CONNECT] Avoid empty fallback gRPC errors
c3b200ed70b9 is described below

commit c3b200ed70b904b53d64737812d37e2f9b6f18c8
Author: haoyangeng-db <[email protected]>
AuthorDate: Thu Aug 20 02:23:00 2026 +0800

    [SPARK-58629][CONNECT] Avoid empty fallback gRPC errors
    
    ### What changes were proposed in this pull request?
    
    This PR makes Spark Connect's fallback gRPC error description non-empty.
    
    When a throwable has a non-empty message, the message remains abbreviated 
to 2048 characters. Otherwise, the throwable's fully qualified class name is 
used.
    
    A regression test covers a bare `InterruptedException`, which has no 
message.
    
    ### Why are the changes needed?
    
    Fatal throwables such as `InterruptedException` reach the generic fallback 
path. `Utils.abbreviate(null, 2048)` returns `null`, causing Spark Connect to 
emit an `UNKNOWN` status without a useful description.
    
    Clients consequently receive an opaque error. This change preserves the 
status code while providing the throwable class as useful diagnostic context.
    
    ### Does this PR introduce _any_ user-facing change?
    
    Yes. For an already-failing request whose fatal throwable has no message, 
the client now receives the throwable class name instead of an empty error 
description. Successful requests and other error-conversion paths are unchanged.
    
    ### How was this patch tested?
    
    Added a regression test to `FetchErrorDetailsHandlerSuite` verifying that a 
bare `InterruptedException` produces an `UNKNOWN` status with 
`java.lang.InterruptedException` as its description.
    
    The focused suite could not run locally because the required SBT 
dependencies were unavailable in the development environment. Static diff 
checks passed; the regression will also run in CI.
    
    ### Was this patch authored or co-authored using generative AI tooling?
    
    Co-authord with Codex.
    
    Closes #57837 from haoyangeng-db/spark-connect-empty-fallback-description.
    
    Authored-by: haoyangeng-db <[email protected]>
    Signed-off-by: Wenchen Fan <[email protected]>
    (cherry picked from commit f167d371629c0ea5c8ff01a783215d1dc7d96fec)
    Signed-off-by: Wenchen Fan <[email protected]>
---
 .../spark/sql/connect/utils/ErrorUtils.scala       | 11 ++++-
 .../spark/sql/connect/utils/ErrorUtilsSuite.scala  | 51 ++++++++++++++++++++++
 2 files changed, 61 insertions(+), 1 deletion(-)

diff --git 
a/sql/connect/server/src/main/scala/org/apache/spark/sql/connect/utils/ErrorUtils.scala
 
b/sql/connect/server/src/main/scala/org/apache/spark/sql/connect/utils/ErrorUtils.scala
index 588493aeaa92..01644663e2af 100644
--- 
a/sql/connect/server/src/main/scala/org/apache/spark/sql/connect/utils/ErrorUtils.scala
+++ 
b/sql/connect/server/src/main/scala/org/apache/spark/sql/connect/utils/ErrorUtils.scala
@@ -287,6 +287,15 @@ private[connect] object ErrorUtils extends Logging {
       .exists(_.toString.contains("org.apache.spark.sql.execution.python"))
   }
 
+  private def fallbackErrorDescription(e: Throwable): String = {
+    val message = e.getMessage
+    if (message != null && message.nonEmpty) {
+      Utils.abbreviate(message, 2048)
+    } else {
+      e.getClass.getName
+    }
+  }
+
   /**
    * Process an error by retrieving session context, converting to gRPC 
status, logging, posting
    * events, and executing callbacks. This is the core error handling logic 
shared by both
@@ -343,7 +352,7 @@ private[connect] object ErrorUtils extends Logging {
       case e: Throwable =>
         Status.UNKNOWN
           .withCause(e)
-          .withDescription(Utils.abbreviate(e.getMessage, 2048))
+          .withDescription(fallbackErrorDescription(e))
           .asRuntimeException()
     }
 
diff --git 
a/sql/connect/server/src/test/scala/org/apache/spark/sql/connect/utils/ErrorUtilsSuite.scala
 
b/sql/connect/server/src/test/scala/org/apache/spark/sql/connect/utils/ErrorUtilsSuite.scala
new file mode 100644
index 000000000000..45cc87e14d09
--- /dev/null
+++ 
b/sql/connect/server/src/test/scala/org/apache/spark/sql/connect/utils/ErrorUtilsSuite.scala
@@ -0,0 +1,51 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.spark.sql.connect.utils
+
+import java.util.UUID
+
+import io.grpc.{Status, StatusRuntimeException}
+import io.grpc.stub.StreamObserver
+
+import org.apache.spark.sql.test.SharedSparkSession
+
+class ErrorUtilsSuite extends SharedSparkSession {
+
+  test("handleError fallback uses throwable class when fatal throwable has no 
message") {
+    val observer = new StreamObserver[Unit] {
+      override def onNext(value: Unit): Unit = {
+        fail(s"Unexpected response: $value")
+      }
+
+      override def onError(t: Throwable): Unit = {
+        throw t
+      }
+
+      override def onCompleted(): Unit = {
+        fail("Unexpected completion")
+      }
+    }
+
+    val error = intercept[StatusRuntimeException] {
+      ErrorUtils.handleError("execute", observer, "user1", 
UUID.randomUUID().toString)(
+        new InterruptedException())
+    }
+
+    assert(error.getStatus.getCode == Status.Code.UNKNOWN)
+    assert(error.getStatus.getDescription == 
classOf[InterruptedException].getName)
+  }
+}


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

Reply via email to