[SPARK-58619][CORE][CONNECT] Assign SQLSTATE 08003 to the INVALID_HANDLE session sub-conditions - #57831
[SPARK-58619][CORE][CONNECT] Assign SQLSTATE 08003 to the INVALID_HANDLE session sub-conditions#57831j1wonpark wants to merge 2 commits into
Conversation
|
cc @nchammas @MaxGekk @LuciferYang, what do you think about this? and do you have concerns about changing the existing SQL state? |
|
Thank you @j1wonpark! |
There was a problem hiding this comment.
There may be a use case for sub-conditions to override the parent condition error state, but in this specific case I believe it is papering over an incorrect grouping of conditions under INVALID_HANDLE.
We should consider restructuring the conditions so that any group of sub-conditions all correctly belong to the same error class.
If we want to support sub-conditions overriding the parent error state, I think they must share the same error class. If they don't, to me that's a sign the sub-conditions are grouped incorrectly.
In other words:
- OK
- Parent condition error state:
HY000 - Sub-condition error states:
HY000,HY001
- Parent condition error state:
- Not OK
- Parent condition error state:
HY000 - Sub-condition error states:
HY000,08003
- Parent condition error state:
This aligns with the guidance that PostgreSQL gives:
Thus, an application that does not recognize the specific error code might still be able to infer what to do from the error class.
(PostgreSQL's "error code" is our "error state / SQLSTATE".)
| A SQLSTATE is assigned at the error condition level and applies to all of the condition's | ||
| sub-conditions. A sub-condition that belongs to a different error state than its condition may | ||
| override it by declaring its own `sqlState`; Spark resolves the sub-condition's SQLSTATE first | ||
| and falls back to the condition's. |
There was a problem hiding this comment.
I don't think this is the correct use of the terminology. Refer to the error hierarchy and illustrative example at the start of this README.
A SQLSTATE is assigned at the error state level. Multiple error conditions can share the same SQLSTATE.
| "sqlState" : "08003" | ||
| } | ||
| }, | ||
| "sqlState" : "HY000" |
There was a problem hiding this comment.
I think it's weird -- and maybe even incorrect -- to have a sub-condition be in a completely different error class than the parent condition. HY is for "CLI-specific condition" and 08 is for "Connection Exception".
I know it's not something that this PR introduced, but I think the sub-conditions under this condition should be broken up into at least two separate conditions:
08class:SESSION_*HYclass:FORMAT,OPERATION_*
|
Thanks @nchammas — you're right. I'll drop the override and do the restructuring instead. The only use of For the split I'd suggest
Concretely I'd propose It avoids "handle", which is accurate only for The rename is visible on the wire. These sites match the condition names as strings, and the first four also exist in already-released clients:
The two reattach matchers are already a Happy to implement this here if you agree with the shape. Separately, the PySpark commit in this PR is unrelated to the above — |
dongjoon-hyun
left a comment
There was a problem hiding this comment.
LGTM with one minor suggestion.
The new Python fallback slightly widens behavior beyond the described case: for an unknown sub-class name (e.g. NEAREST_BY_JOIN.TYPO), the old code raised KeyError and returned None, while the new code returns the main class's sqlState. This matches the JVM side's lenient behavior, and the JVM pins it with a test (TEST_MAIN_STATE.NON_EXISTENT_SUB), but test_sqlstate has no corresponding case. Could you add one line to keep the two sides symmetric? e.g.
# An unknown sub-class name also falls back to the main class's sqlState.
error = PySparkRuntimeError(errorClass="NEAREST_BY_JOIN.NON_EXISTENT_SUB", messageParameters={})
self.assertEqual(error.getSqlState(), "42604")
That might not be the best example since it was released with Spark 4.0 and reviewers may not have been that concerned about technically breaking changes in a major release. |
ada7357 to
38abe8b
Compare
|
I think the community does not currently seem to treat all error condition names as part of the public API contract, nor does it subject them to sufficiently rigorous review, but I agree we should be careful about those error conditions already used by older versions of the client for control flow. Specific to the error conditions in this PR, @nchammas, do you have a concrete suggestion for how we should handle them? Since SPARK-57933 is included in 4.3, I don't want to defer this one to the next version and accumulate the tech debt. |
Yes, over time it will become more important to stabilize these error conditions as more clients come to depend on them.
My guidance would be:
|
38abe8b to
3a2603a
Compare
|
|
||
| ## Upgrading from Spark SQL 4.3 to 4.4 | ||
|
|
||
| - Since Spark 4.4, the Spark Connect error conditions `INVALID_HANDLE.SESSION_CHANGED`, `INVALID_HANDLE.SESSION_CLOSED`, and `INVALID_HANDLE.SESSION_NOT_FOUND` are renamed to `SESSION_UNAVAILABLE.CHANGED`, `SESSION_UNAVAILABLE.CLOSED`, and `SESSION_UNAVAILABLE.NOT_FOUND`, and carry SQLSTATE `08003` (connection does not exist) instead of the generic `HY000`, so that clients detecting dead connections by SQLSTATE class `08` can recognize them. Code matching these errors should match the new condition names, or `08003` when matching on SQLSTATE. Spark Connect clients from earlier releases match the old names internally to detect a restarted server and to re-execute lost operations; against a Spark 4.4+ server those code paths no longer trigger, so upgrading such clients together with the server is recommended. Spark 4.4+ clients recognize both the old and the new names. |
There was a problem hiding this comment.
I would like to land this in 4.3 or revert SPARK-57933 from 4.3 to avoid introducing tech debt.
words sound too verbose, can you make it shorter? also applies to the PR description.
There was a problem hiding this comment.
Landing in 4.3 sounds good to me. Updated the version references accordingly (the migration note moved to the "4.2 to 4.3" section, and the "before Spark 4.4" comments now say 4.3), and shortened both the migration note (now two sentences) and the PR description.
|
Thanks @j1wonpark for the rework, and @nchammas for the principle, which I agree with in general: a SQLSTATE belongs to the condition, sub-conditions should stay in their condition's error class, and the I traced how existing clients consume these names before concluding this case deserves an exception. The condition names are a de facto wire contract: pre-4.3 clients match The per-sub-condition cc @HyukjinKwon for thoughts on the PySpark side. |
Maybe a stupid question, but: Can we make the server rewrite error conditions based on the client version? Basically, have a compatibility layer so we are not forced to freeze existing error conditions for all time. That way older clients will still work but we can continue to refine and cleanup the layout of the conditions going forward. |
|
@nchammas The compat layer might be a good idea and would generalize beyond this rename. Two things make it feasible without a protocol change: Scala and Python clients already send Two caveats: That is a bigger piece than this PR. My preference: land the |
|
If we need to get something unblocked for the upcoming release, it is what it is, I guess. Let's just make it clear in the code and/or docs that:
|
|
@nchammas, that makes sense. @j1wonpark, sorry for making you back and forth, but given the situation, we need to go back to the previous approach to avoid breaking wire compatibility. |
…ted wire-compatibility exception A SQLSTATE can currently only be assigned to a top-level error condition: ErrorSubInfo has no sqlState field and ErrorClassesJsonReader.getSqlState resolves only the main condition. Giving one sub-condition a more accurate SQLSTATE therefore requires splitting it into a new top-level condition, which breaks released Connect clients that match the condition name on the wire. Add an optional sqlState field to ErrorSubInfo and make getSqlState resolve the sub-condition's SQLSTATE first, falling back to the main condition's (the same resolution getBreakingChangeInfo uses); degenerate inputs keep getSqlState's lenient behavior. Sub-condition SQLSTATEs are validated against error-states.json like top-level ones and rendered by the error documentation generator. Per the review discussion, this is a narrow escape hatch, not a general feature: a new SparkThrowableSuite invariant restricts sub-condition SQLSTATE overrides to an explicit allowlist (the INVALID_HANDLE session sub-conditions, which receive 08003 in the next commit), and the error README documents the exception, discourages new overrides, and notes that a future server-side compatibility layer may remove it. Signed-off-by: Jiwon Park <jpark92@outlook.kr>
…ssion sub-conditions INVALID_HANDLE.SESSION_CHANGED, SESSION_CLOSED, and SESSION_NOT_FOUND mean the server-side session backing the connection is gone, which SQLSTATE 08003 (connection does not exist) describes. They currently inherit INVALID_HANDLE's generic HY000, so tools that detect dead connections by SQLSTATE class 08 cannot recognize them. The condition names stay unchanged: released Connect clients match INVALID_HANDLE.SESSION_* for session invalidation and reattach re-execution, so this is the documented wire-compatibility exception admitted by the previous commit's allowlist. The operation-level sub-conditions (FORMAT, OPERATION_*) concern a single operation on a healthy session and keep HY000. SparkConnectSessionManagerSuite now asserts getSqlState == "08003" at the server throw sites and SparkSessionE2ESuite asserts the client-received exception carries 08003 end-to-end (previously untested). Noted in the migration guide. Signed-off-by: Jiwon Park <jpark92@outlook.kr>
2035803 to
6020a20
Compare
What changes were proposed in this pull request?
Per the review discussion below, this PR returns to the sub-condition level SQLSTATE override so the wire-visible condition names stay unchanged, and hardens it as a documented exception:
[CORE]:ErrorSubInfogains an optionalsqlState, andErrorClassesJsonReader.getSqlStateresolves the sub-condition's SQLSTATE first, falling back to the condition's. A newSparkThrowableSuiteinvariant fails any sub-conditionsqlStateoutside an explicit allowlist (currently the threeINVALID_HANDLE.SESSION_*sub-conditions), and the error README documents the exception, discourages new overrides even within the same error class, and notes that a future server-side compatibility layer may remove it.[CONNECT]:INVALID_HANDLE.SESSION_CHANGED/SESSION_CLOSED/SESSION_NOT_FOUNDdeclare SQLSTATE08003(connection does not exist). Names, messages, and every other sub-condition are unchanged.Why are the changes needed?
These sub-conditions mean the server-side session backing a Connect client is gone, which
08003describes, but they inheritINVALID_HANDLE's genericHY000, so tools detecting dead connections by SQLSTATE class08cannot recognize them and the Connect JDBC driver hard-codes a remapping (raised in the SPARK-57933 review). Moving them to a class-08 condition is not viable: released Connect clients match the condition names for session invalidation and reattach re-execution, and nothing in a new client can repair that direction (#57831 (comment)).Does this PR introduce any user-facing change?
Yes. The SQLSTATE of the three conditions changes from
HY000to08003; the condition names are unchanged. Noted insql-migration-guide.md.How was this patch tested?
SparkThrowableSuite: sub-first resolution incl. degenerate inputs (confirmed failing before the fix), the allowlist invariant (any sub-condition declaring its own SQLSTATE must be on the documented allowlist; verified to fail on an injected non-allowlisted override), pins forSESSION_*=08003and every otherINVALID_HANDLEsub-condition =HY000, and the error-states validation extended to sub-condition SQLSTATEs.SparkConnectSessionManagerSuiteassertsgetSqlState == "08003"at the server throw sites andSparkSessionE2ESuiteasserts the client-received exception carries08003end-to-end (both previously untested).JdbcErrorUtilsSuitere-run: the JDBC driver's existing hard-coded mapping stays consistent.Was this patch authored or co-authored using generative AI tooling?
Generated-by: Claude Code (claude-fable-5)