You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@spark.apache.org by ch...@apache.org on 2026/08/17 02:12:34 UTC
(spark) branch branch-4.x updated: [SPARK-58619][CONNECT][FOLLOWUP] Map JDBC connection errors by SQLSTATE class 08 instead of hard-coded condition names
This is an automated email from the ASF dual-hosted git repository.
pan3793 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 7b879a36a46c [SPARK-58619][CONNECT][FOLLOWUP] Map JDBC connection errors by SQLSTATE class 08 instead of hard-coded condition names
7b879a36a46c is described below
commit 7b879a36a46c992e24509a99f30d834160875566
Author: Jiwon Park <jp...@outlook.kr>
AuthorDate: Mon Aug 17 10:12:08 2026 +0800
[SPARK-58619][CONNECT][FOLLOWUP] Map JDBC connection errors by SQLSTATE class 08 instead of hard-coded condition names
### What changes were proposed in this pull request?
Follow-up of SPARK-58619, requested in https://github.com/apache/spark/pull/57009#discussion_r3786757968: remove the JDBC driver's hard-coded `INVALID_HANDLE.SESSION_` remapping and key the `SQLNonTransientConnectionException` mapping on the SQLSTATE the converted error already carries (class `08`), keeping the server-provided state. The test stub now takes the sqlState explicitly, matching what `GrpcExceptionConverter` supplies.
### Why are the changes needed?
Since SPARK-58619 the server reports `08003` for the `INVALID_HANDLE.SESSION_*` conditions, so the driver-side remapping duplicates knowledge in `error-conditions.json` and needs manual sync whenever conditions move within class `08` (raised in the SPARK-58619 review).
### Does this PR introduce _any_ user-facing change?
No. Spark Connect requires the client version <= the server version, and against a same-or-newer server the same errors map to the same exception type and SQLSTATE (the driver first ships in 4.3). Any other class-08 SQLSTATE a server reports also maps to the connection-exception type, matching how JDBC consumers read class `08`.
### How was this patch tested?
`JdbcErrorUtilsSuite`: a new case pins that a class-08 state maps to `SQLNonTransientConnectionException` regardless of the condition name (fails without the change); the existing gRPC, precedence, cause-chain, and passthrough cases are unchanged.
### Was this patch authored or co-authored using generative AI tooling?
Generated-by: Claude Code (claude-fable-5)
Closes #58022 from j1wonpark/SPARK-58619-followup-jdbc-sqlstate.
Authored-by: Jiwon Park <jp...@outlook.kr>
Signed-off-by: Cheng Pan <ch...@apache.org>
(cherry picked from commit f76717f84791e03f7924f0000decd99aa352cfe5)
Signed-off-by: Cheng Pan <ch...@apache.org>
---
.../connect/client/jdbc/util/JdbcErrorUtils.scala | 23 +++++++-------
.../client/jdbc/util/JdbcErrorUtilsSuite.scala | 35 ++++++++++++----------
2 files changed, 29 insertions(+), 29 deletions(-)
diff --git a/sql/connect/client/jdbc/src/main/scala/org/apache/spark/sql/connect/client/jdbc/util/JdbcErrorUtils.scala b/sql/connect/client/jdbc/src/main/scala/org/apache/spark/sql/connect/client/jdbc/util/JdbcErrorUtils.scala
index 72d007c1304a..35236b32e220 100644
--- a/sql/connect/client/jdbc/src/main/scala/org/apache/spark/sql/connect/client/jdbc/util/JdbcErrorUtils.scala
+++ b/sql/connect/client/jdbc/src/main/scala/org/apache/spark/sql/connect/client/jdbc/util/JdbcErrorUtils.scala
@@ -62,23 +62,20 @@ private[jdbc] object JdbcErrorUtils {
// SQLState class 08 is "connection exception"; HYT00 is the conventional
// (ODBC-derived) state for an elapsed timeout.
- private val CONNECTION_DOES_NOT_EXIST = "08003"
+ private val CONNECTION_EXCEPTION_ERROR_CLASS = "08"
private val CONNECTION_FAILURE = "08006"
private val TIMEOUT_EXPIRED = "HYT00"
- private val SESSION_GONE_CONDITION_PREFIX = "INVALID_HANDLE.SESSION_"
-
/**
* Maps the unchecked exceptions raised by the Spark Connect client (a
* [[SparkThrowable]] once GrpcExceptionConverter has converted the gRPC error) to
* the [[SQLException]] a JDBC method is required to throw:
*
- * - `INVALID_HANDLE.SESSION_*` means the server-side session is gone (e.g. it
- * timed out); the connection is unusable and retrying on it is pointless, so it
- * maps to a [[SQLNonTransientConnectionException]] with SQLState 08003
- * ("connection does not exist"). The operation-level subconditions
- * (`OPERATION_*`, `FORMAT`) concern a single operation on a healthy session
- * and are deliberately not treated as connection errors.
+ * - a server error in SQLState class 08 ("connection exception", e.g. the
+ * `INVALID_HANDLE.SESSION_*` conditions with 08003) means the session backing
+ * the connection is gone; the connection is unusable and retrying on it is
+ * pointless, so it maps to a [[SQLNonTransientConnectionException]] keeping
+ * the server-provided SQLState.
* - a gRPC UNAVAILABLE (e.g. a server restart or a network blip) maps to a
* [[SQLTransientConnectionException]] with SQLState 08006 ("connection
* failure"), since a fresh connection can succeed.
@@ -98,18 +95,18 @@ private[jdbc] object JdbcErrorUtils {
case e =>
val chain = causeChain(e)
val sparkThrowableOpt = chain.collectFirst { case st: SparkThrowable => st }
- val condition = sparkThrowableOpt.flatMap(st => Option(st.getCondition))
+ val sqlState = sparkThrowableOpt.flatMap(st => Option(st.getSqlState))
val grpcCode = chain.collectFirst { case sre: StatusRuntimeException =>
sre.getStatus.getCode
}
- if (condition.exists(_.startsWith(SESSION_GONE_CONDITION_PREFIX))) {
- new SQLNonTransientConnectionException(e.getMessage, CONNECTION_DOES_NOT_EXIST, e)
+ if (sqlState.exists(_.startsWith(CONNECTION_EXCEPTION_ERROR_CLASS))) {
+ new SQLNonTransientConnectionException(e.getMessage, sqlState.get, e)
} else if (grpcCode.contains(Status.Code.UNAVAILABLE)) {
new SQLTransientConnectionException(e.getMessage, CONNECTION_FAILURE, e)
} else if (grpcCode.contains(Status.Code.DEADLINE_EXCEEDED)) {
new SQLTimeoutException(e.getMessage, TIMEOUT_EXPIRED, e)
} else {
- new SQLException(e.getMessage, sparkThrowableOpt.map(_.getSqlState).orNull, e)
+ new SQLException(e.getMessage, sqlState.orNull, e)
}
}
diff --git a/sql/connect/client/jdbc/src/test/scala/org/apache/spark/sql/connect/client/jdbc/util/JdbcErrorUtilsSuite.scala b/sql/connect/client/jdbc/src/test/scala/org/apache/spark/sql/connect/client/jdbc/util/JdbcErrorUtilsSuite.scala
index fd434dcbb206..e64dd1f8681e 100644
--- a/sql/connect/client/jdbc/src/test/scala/org/apache/spark/sql/connect/client/jdbc/util/JdbcErrorUtilsSuite.scala
+++ b/sql/connect/client/jdbc/src/test/scala/org/apache/spark/sql/connect/client/jdbc/util/JdbcErrorUtilsSuite.scala
@@ -26,38 +26,37 @@ import org.apache.spark.sql.connect.test.ConnectFunSuite
/**
* Tests for [[JdbcErrorUtils.toSQLException]]. A RuntimeException mixing in
- * [[SparkThrowable]] stands in for converted server errors; a real
- * [[StatusRuntimeException]] cause models transport errors the way
- * GrpcExceptionConverter preserves them.
+ * [[SparkThrowable]] stands in for converted server errors, carrying the sqlState
+ * GrpcExceptionConverter would supply. A real [[StatusRuntimeException]] cause
+ * models transport errors the way GrpcExceptionConverter preserves them.
*/
class JdbcErrorUtilsSuite extends ConnectFunSuite {
private def sparkError(
condition: String,
+ sqlState: String,
msg: String,
cause: Throwable = null): RuntimeException =
new RuntimeException(msg, cause) with SparkThrowable {
override def getCondition: String = condition
+ override def getSqlState: String = sqlState
}
private def grpcError(status: Status): StatusRuntimeException =
new StatusRuntimeException(status)
- test("INVALID_HANDLE.SESSION_CLOSED maps to a connection exception with SQLState 08003") {
+ test("a class-08 SQLSTATE maps to a non-transient connection exception") {
val e = JdbcErrorUtils.toSQLException(
- sparkError("INVALID_HANDLE.SESSION_CLOSED", "Session was closed"))
+ sparkError("INVALID_HANDLE.SESSION_CLOSED", "08003", "Session was closed"))
assert(e.isInstanceOf[SQLNonTransientConnectionException])
assert(e.getSQLState === "08003")
assert(e.getMessage === "Session was closed")
}
- test("other session-level INVALID_HANDLE subconditions also map to 08003") {
- Seq("INVALID_HANDLE.SESSION_NOT_FOUND", "INVALID_HANDLE.SESSION_CHANGED").foreach {
- condition =>
- val e = JdbcErrorUtils.toSQLException(sparkError(condition, "gone"))
- assert(e.isInstanceOf[SQLNonTransientConnectionException], condition)
- assert(e.getSQLState === "08003", condition)
- }
+ test("the class-08 mapping does not depend on the condition name") {
+ val e = JdbcErrorUtils.toSQLException(sparkError("SOME_FUTURE_CONDITION", "08004", "gone"))
+ assert(e.isInstanceOf[SQLNonTransientConnectionException])
+ assert(e.getSQLState === "08004")
}
test("operation-level INVALID_HANDLE subconditions do not map to a connection exception") {
@@ -67,14 +66,14 @@ class JdbcErrorUtilsSuite extends ConnectFunSuite {
"INVALID_HANDLE.OPERATION_ABANDONED",
"INVALID_HANDLE.OPERATION_ALREADY_EXISTS",
"INVALID_HANDLE.FORMAT").foreach { condition =>
- val e = JdbcErrorUtils.toSQLException(sparkError(condition, "operation gone"))
+ val e = JdbcErrorUtils.toSQLException(sparkError(condition, "HY000", "operation gone"))
assert(!e.isInstanceOf[SQLNonTransientConnectionException], condition)
assert(!e.isInstanceOf[SQLTransientConnectionException], condition)
}
}
test("a non-connection Spark error keeps the server-provided SQLState") {
- val e = JdbcErrorUtils.toSQLException(sparkError("DIVIDE_BY_ZERO", "boom"))
+ val e = JdbcErrorUtils.toSQLException(sparkError("DIVIDE_BY_ZERO", "22012", "boom"))
assert(!e.isInstanceOf[SQLNonTransientConnectionException])
assert(!e.isInstanceOf[SQLTransientConnectionException])
assert(e.getSQLState === "22012")
@@ -92,6 +91,7 @@ class JdbcErrorUtilsSuite extends ConnectFunSuite {
// the shape GrpcExceptionConverter produces for transport errors
val wrapped = sparkError(
"CONNECT_CLIENT_UNEXPECTED_MISSING_SQL_STATE",
+ "XXKCM",
"io.grpc.StatusRuntimeException: UNAVAILABLE: io exception",
cause = grpcError(Status.UNAVAILABLE.withDescription("io exception")))
val e = JdbcErrorUtils.toSQLException(wrapped)
@@ -104,6 +104,7 @@ class JdbcErrorUtilsSuite extends ConnectFunSuite {
// server error embedding gRPC text (e.g. from a UDF) is not a transport failure
val e = JdbcErrorUtils.toSQLException(sparkError(
"FAILED_EXECUTE_UDF",
+ "39000",
"Job aborted: io.grpc.StatusRuntimeException: UNAVAILABLE: backend down"))
assert(!e.isInstanceOf[SQLTransientConnectionException])
assert(!e.isInstanceOf[SQLNonTransientConnectionException])
@@ -113,6 +114,7 @@ class JdbcErrorUtilsSuite extends ConnectFunSuite {
// a slow query fires the RPC deadline on a healthy connection: timeout, not class 08
val wrapped = sparkError(
"CONNECT_CLIENT_UNEXPECTED_MISSING_SQL_STATE",
+ "XXKCM",
"io.grpc.StatusRuntimeException: DEADLINE_EXCEEDED: deadline exceeded",
cause = grpcError(Status.DEADLINE_EXCEEDED.withDescription("deadline exceeded")))
val e = JdbcErrorUtils.toSQLException(wrapped)
@@ -129,10 +131,11 @@ class JdbcErrorUtilsSuite extends ConnectFunSuite {
assert(!e.isInstanceOf[SQLTimeoutException])
}
- test("a closed session takes precedence over a transport code") {
+ test("a class-08 error takes precedence over a transport code") {
// both signals present: the gone session wins
val e = JdbcErrorUtils.toSQLException(sparkError(
"INVALID_HANDLE.SESSION_CLOSED",
+ "08003",
"closed",
cause = grpcError(Status.UNAVAILABLE.withDescription("x"))))
assert(e.isInstanceOf[SQLNonTransientConnectionException])
@@ -140,7 +143,7 @@ class JdbcErrorUtilsSuite extends ConnectFunSuite {
}
test("the connection error is found through the cause chain") {
- val root = sparkError("INVALID_HANDLE.SESSION_CLOSED", "closed")
+ val root = sparkError("INVALID_HANDLE.SESSION_CLOSED", "08003", "closed")
val e = JdbcErrorUtils.toSQLException(new RuntimeException("wrapper", root))
assert(e.isInstanceOf[SQLNonTransientConnectionException])
assert(e.getSQLState === "08003")
---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@spark.apache.org
For additional commands, e-mail: commits-help@spark.apache.org