[SPARK-58674][CORE] Assign a name to the error condition _LEGACY_ERROR_TEMP_3021-3023,3026,3029 - #57879
Closed
LuciferYang wants to merge 1 commit into
Closed
[SPARK-58674][CORE] Assign a name to the error condition _LEGACY_ERROR_TEMP_3021-3023,3026,3029#57879LuciferYang wants to merge 1 commit into
_LEGACY_ERROR_TEMP_3021-3023,3026,3029#57879LuciferYang wants to merge 1 commit into
Conversation
…OR_TEMP_3021-3029` Convert five remaining legacy conditions in `SparkCoreErrors` to proper error conditions. Four get user-facing names; one is an unreachable defensive check and becomes an internal error: - `_3021` -> `SCHEDULER_BACKEND_SHUTDOWN_FAILED.EXECUTORS` - `_3022` -> `SCHEDULER_BACKEND_SHUTDOWN_FAILED.DRIVER_ENDPOINT` (new umbrella, SQLSTATE 58030) - `_3023` -> `SparkException.internalError`, JSON entry deleted - `_3026` -> `UNSUPPORTED_CALL.TASK_NOT_FINISHED` (new subclass, SQLSTATE 0A000) - `_3029` -> `CLUSTER_MANAGER_APPLICATION_FAILURE` (new top-level, SQLSTATE 56000) `_3023` guards `DAGScheduler.submitMapStage` against a zero-partition RDD. Its only production caller already short-circuits that case, and both `DAGScheduler` and `SparkContext.submitMapStage` are internal APIs, so the branch is reachable only by violating the internal-API contract. `durationCalledOnUnfinishedTaskError` now takes `className`/`methodName` from its call site instead of hardcoding them, matching how the other `UNSUPPORTED_CALL` throw sites build their parameters.
dongjoon-hyun
approved these changes
Aug 9, 2026
dongjoon-hyun
left a comment
Member
There was a problem hiding this comment.
+1, LGTM. I verified the changes against the codebase:
- All three JSON insertions (
CLUSTER_MANAGER_APPLICATION_FAILURE,SCHEDULER_BACKEND_SHUTDOWN_FAILED,UNSUPPORTED_CALL.TASK_NOT_FINISHED) are correctly placed alphabetically, and all three SQLSTATEs exist inerror-states.jsonwith reasonable precedents (56000 already used byCHECKPOINT_RDD_BLOCK_ID_NOT_FOUND, 58030 by 11 conditions). durationCalledOnUnfinishedTaskErrorhas exactly one caller, so the signature change is safe, and no stray references to the five removed legacy IDs remain anywhere in the repo.stopExecutors()is only called fromstop(), so the "later shutdown steps were skipped" clause in theEXECUTORSmessage is accurate. The base-message-ending-with-colon style has plenty of precedent (DATATYPE_MISMATCH,FAILED_JDBC, etc.).- Exception types are unchanged, so there is no user-facing API impact and no MiMa concern.
- The new tests reuse existing suite plumbing correctly (the mocked
driverEndpointRefpattern in the k8s suite,failedTaskSet/failedTaskSetReasoninTaskSchedulerImplSuite).
One minor nit: the title says _LEGACY_ERROR_TEMP_3021-3029, but _3028 is intentionally left for a follow-up. Stating the exact set (3021-3023,3026,3029) would make the commit log more precise.
uros-b
approved these changes
Aug 9, 2026
uros-b
left a comment
Member
There was a problem hiding this comment.
Thank you @LuciferYang and @dongjoon-hyun!
HyukjinKwon
approved these changes
Aug 10, 2026
_LEGACY_ERROR_TEMP_3021-3029_LEGACY_ERROR_TEMP_3021-3023,3026,3029
Contributor
Author
The pr title has been rewritten. |
LuciferYang
added a commit
that referenced
this pull request
Aug 10, 2026
…OR_TEMP_3021-3023,3026,3029` ### What changes were proposed in this pull request? This PR converts five `_LEGACY_ERROR_TEMP_*` conditions in `SparkCoreErrors` -- `_3021`, `_3022`, `_3023`, `_3026` and `_3029` -- into proper error conditions, continuing the cleanup under [SPARK-37935](https://issues.apache.org/jira/browse/SPARK-37935). Four get user-facing names; one is an unreachable defensive check and becomes an internal error. Note the set is not a contiguous range: `_3028` sits inside it and is deliberately left out (see below). | Legacy | Builder | Now | SQLSTATE | |---|---|---|---| | `_LEGACY_ERROR_TEMP_3021` | `askStandaloneSchedulerToShutDownExecutorsError` | `SCHEDULER_BACKEND_SHUTDOWN_FAILED.EXECUTORS` | 58030 | | `_3022` | `stopStandaloneSchedulerDriverEndpointError` | `SCHEDULER_BACKEND_SHUTDOWN_FAILED.DRIVER_ENDPOINT` | 58030 | | `_3023` | `cannotRunSubmitMapStageOnZeroPartitionRDDError` | `INTERNAL_ERROR` (entry deleted) | XX000 | | `_3026` | `durationCalledOnUnfinishedTaskError` | `UNSUPPORTED_CALL.TASK_NOT_FINISHED` | 0A000 | | `_3029` | `clusterSchedulerError` | `CLUSTER_MANAGER_APPLICATION_FAILURE` | 56000 | `SCHEDULER_BACKEND_SHUTDOWN_FAILED` is a new umbrella: both conditions are the same exception type thrown at the same altitude in adjacent methods of `CoarseGrainedSchedulerBackend`, differing only in which RPC failed. `TASK_NOT_FINISHED` joins the existing `UNSUPPORTED_CALL` umbrella, whose message frame (`Cannot call the method "<methodName>" of the class "<className>".`) is exactly the shape this throw site needs. `durationCalledOnUnfinishedTaskError` now takes `className`/`methodName` from its call site rather than deriving them internally, matching how the other `UNSUPPORTED_CALL` throw sites build their parameters (`DiskBlockObjectWriter`, `DataTypeErrors`). The no-arg form would have silently misreported the class if a second caller ever appeared. The remaining conditions in the `_3021-3042` range — `_3028` (inside this PR's numeric span, hence the non-contiguous title), plus `_3033`, `_3035`, `_3036`, `_3037` — are left for follow-up: they are operational failures (a barrier stage getting partial offers, external shuffle service registration exhausting its retries, a replica failing to store, a `DiskStore` file vanishing) or reachable only through a third-party `ShuffleManager`, and each needs its own reachability judgement. ### Reachability, per condition - **`_3021`/`_3022` — cluster/environment failure, log-only.** Both wrap an `askSync` to the driver endpoint during teardown, so they fire when an RPC times out or the endpoint is already dead. Every path into `CoarseGrainedSchedulerBackend.stop()` is wrapped in `Utils.tryLogNonFatalError` (`SparkContext.stop` → `DAGScheduler.stop` → `TaskSchedulerImpl.stop`), and `KubernetesClusterSchedulerBackend.stop` wraps `super.stop()` explicitly, so the exception reaches a driver log and never a user. It is still a real operational failure rather than a Spark bug, which is why it gets a name instead of `INTERNAL_ERROR` — the reader is an operator triaging a shutdown log. - **`_3023` — unreachable defensive check.** `DAGScheduler.submitMapStage` rejects a dependency whose RDD has zero partitions. Its only production caller, `ShuffleExchangeExec.mapOutputStatisticsFuture`, already short-circuits with `if (inputRDD.getNumPartitions == 0) Future.successful(null)`, and the two guards test the same number: the dependency's RDD is `prepareShuffleDependency(inputRDD, ...)`'s output, derived through `mapPartitionsWithIndexInternal`, which preserves partition count. Note this is *not* an argument from `private[spark]` — that is a scalac-only check with no bytecode enforcement, so Java code or a user class declared under `org.apache.spark.*` can call `SparkContext.submitMapStage` directly. The accurate statement is that the branch is reachable only by violating the contract of an API whose own scaladoc says "This is currently an internal API only", which is what `INTERNAL_ERROR` is for. - **`_3026` — reachable from user code.** `TaskInfo` is `DeveloperApi` and a `TaskInfo` for a *running* task is handed to listeners via `SparkListenerTaskStart`. Any custom listener calling `taskInfo.duration` in `onTaskStart` hits this. All in-tree callers are guarded or run after `markFinished`, which is why it never fires in the build. - **`_3029` — operator-facing, standalone only.** `TaskSchedulerImpl.error` throws when the cluster manager reports a failure and no task set is active to abort. Its only caller is `StandaloneSchedulerBackend.dead`, i.e. the standalone Master removed the application or all Masters were unresponsive. The condition name is deliberately deployment-agnostic because `TaskSchedulerImpl.error` is, but today standalone is the only producer. ### Why are the changes needed? The error-conditions [README](https://github.com/apache/spark/blob/master/common/utils/src/main/resources/error/README.md) disallows new `_LEGACY_ERROR_TEMP_*` entries and asks existing ones to be resolved. This clears five of them. Two of the five were also weak in their own right: `_3021`/`_3022` carried no indication that the failure happens during shutdown, so an operator seeing the log line had no way to tell whether the application's results were affected. ### Does this PR introduce _any_ user-facing change? Yes, to error messages — no API change. Converting any legacy condition changes the rendered string in two mechanical ways: `SparkThrowableHelper.formatErrorMessage` suppresses the `[CONDITION] ` prefix only for `_LEGACY_ERROR_`-prefixed names, and appends ` SQLSTATE: xxxxx` when a sqlState exists (legacy entries have none). Beyond that: - `_3021`: `Error asking standalone scheduler to shut down executors` → `Failed to shut down the scheduler backend while the application was terminating: the driver could not confirm that the executors were asked to shut down, so the later shutdown steps were skipped.` The added clause is factual: `stop()` calls `stopExecutors()` unguarded, so a throw there skips `stopTokenManager()` and the `StopDriver` ask. The wording says "could not confirm" rather than "could not tell" because the `StopExecutors` handler sends to every executor *before* replying, so a lost reply does not mean the executors went unnotified. - `_3022`: `Error stopping standalone scheduler's driver endpoint` → `... : the driver endpoint did not stop cleanly.` - `_3026`: `duration() called on unfinished task` → `Cannot call the method "duration" of the class "org.apache.spark.scheduler.TaskInfo". The task has not finished yet, so its duration is not available.` - `_3029`: `Exiting due to error from cluster scheduler: <message>` → `Exiting due to an error reported by the cluster manager: <message>`. This one is genuinely user-visible in logs, so log-scraping on the old text would need updating. - `_3023` renders as an internal error. Note this does **not** change any job-failure message shape: the throw happens on the driver during `submitMapStage`, before `eventProcessLoop.post`, so `DAGScheduler.abortStage`'s `isInternalError` filter — which only applies to exceptions arriving from task-set failures — is not involved. ### How was this patch tested? New assertions, four of which fail against the old code (the condition name and the SQLSTATE both differ, since legacy entries carry no sqlState): - `TaskInfoSuite` (new file) — `checkError` on `UNSUPPORTED_CALL.TASK_NOT_FINISHED` including SQLSTATE and both message parameters, plus a companion case asserting `duration` still works after `markFinished`. - `TaskSchedulerImplSuite` — `error()` with no active task set asserts `CLUSTER_MANAGER_APPLICATION_FAILURE`; a companion case covers the other branch (with an active task set it aborts rather than throwing) and asserts the reason reaches the `DAGScheduler` intact. - `KubernetesClusterSchedulerBackendSuite` — a failed `StopExecutors` RPC asserts `SCHEDULER_BACKEND_SHUTDOWN_FAILED.EXECUTORS` and that the original exception survives as the cause. - `DAGSchedulerSuite` — `submitMapStage` on a zero-partition RDD asserts `INTERNAL_ERROR`, so the converted branch is pinned rather than merely deleted from the JSON. Coverage caveat worth a reviewer's attention: the only assertion on `SCHEDULER_BACKEND_SHUTDOWN_FAILED` lives in the `kubernetes` module and so runs only under `-Pkubernetes`. Both throw sites are in `core`, but `core`'s `CoarseGrainedSchedulerBackendSuite` uses a real `local-cluster` context with no way to make `askSync` fail, whereas the k8s suite already has the mocked `RpcEndpointRef` plumbing. The `DRIVER_ENDPOINT` subclass has no assertion for the same reason — its throw site sits inside `stop()`, which the k8s backend wraps in `Utils.tryLogNonFatalError`. Ran locally: `SparkThrowableSuite`, `TaskInfoSuite`, `TaskSchedulerImplSuite`, `DAGSchedulerSuite` (367 tests) and `KubernetesClusterSchedulerBackendSuite` (12 tests), all passing; `core/compile`, `core/Test/compile` and `kubernetes/Test/compile` clean. The JSON was regenerated with `SPARK_GENERATE_GOLDEN_FILES=1` and produced no diff. Each new SQLSTATE assertion was verified to actually bite by temporarily setting a wrong value and watching the test go red. On the SQLSTATE choice for `SCHEDULER_BACKEND_SHUTDOWN_FAILED`: 58030 is nominally "I/O error", and a failed RPC is not literally I/O. It was chosen because 58030 is already Spark's de-facto system-operation-failure code, carrying 11 conditions of which several are not literal I/O (`FAILED_UPDATE_VIEW_SCHEMA`, `FAILED_TO_CREATE_PLAN_FOR_DIRECT_QUERY`, `CANNOT_RESTORE_PERMISSIONS_FOR_PATH`), whereas `58000` ("System error") and the `08xxx` connection class exist in `error-states.json` but have zero users in `error-conditions.json` — introducing the first would set a new precedent in a cleanup PR. Happy to switch if a committer prefers otherwise. ### Was this patch authored or co-authored using generative AI tooling? Generated-by: Claude Code (Opus 4.8) Closes #57879 from LuciferYang/assign-name-legacy-3021-3037. Authored-by: YangJie <yangjie01@baidu.com> Signed-off-by: yangjie01 <yangjie01@baidu.com> (cherry picked from commit bf03f51) Signed-off-by: yangjie01 <yangjie01@baidu.com>
Contributor
Author
Contributor
Author
|
Thank you @dongjoon-hyun @HyukjinKwon @uros-b |
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?
This PR converts five
_LEGACY_ERROR_TEMP_*conditions inSparkCoreErrors--_3021,_3022,_3023,_3026and_3029-- into proper error conditions, continuing the cleanup under SPARK-37935. Four get user-facing names; one is an unreachable defensive check and becomes an internal error. Note the set is not a contiguous range:_3028sits inside it and is deliberately left out (see below)._LEGACY_ERROR_TEMP_3021askStandaloneSchedulerToShutDownExecutorsErrorSCHEDULER_BACKEND_SHUTDOWN_FAILED.EXECUTORS_3022stopStandaloneSchedulerDriverEndpointErrorSCHEDULER_BACKEND_SHUTDOWN_FAILED.DRIVER_ENDPOINT_3023cannotRunSubmitMapStageOnZeroPartitionRDDErrorINTERNAL_ERROR(entry deleted)_3026durationCalledOnUnfinishedTaskErrorUNSUPPORTED_CALL.TASK_NOT_FINISHED_3029clusterSchedulerErrorCLUSTER_MANAGER_APPLICATION_FAILURESCHEDULER_BACKEND_SHUTDOWN_FAILEDis a new umbrella: both conditions are the same exception type thrown at the same altitude in adjacent methods ofCoarseGrainedSchedulerBackend, differing only in which RPC failed.TASK_NOT_FINISHEDjoins the existingUNSUPPORTED_CALLumbrella, whose message frame (Cannot call the method "<methodName>" of the class "<className>".) is exactly the shape this throw site needs.durationCalledOnUnfinishedTaskErrornow takesclassName/methodNamefrom its call site rather than deriving them internally, matching how the otherUNSUPPORTED_CALLthrow sites build their parameters (DiskBlockObjectWriter,DataTypeErrors). The no-arg form would have silently misreported the class if a second caller ever appeared.The remaining conditions in the
_3021-3042range —_3028(inside this PR's numeric span, hence the non-contiguous title), plus_3033,_3035,_3036,_3037— are left for follow-up: they are operational failures (a barrier stage getting partial offers, external shuffle service registration exhausting its retries, a replica failing to store, aDiskStorefile vanishing) or reachable only through a third-partyShuffleManager, and each needs its own reachability judgement.Reachability, per condition
_3021/_3022— cluster/environment failure, log-only. Both wrap anaskSyncto the driver endpoint during teardown, so they fire when an RPC times out or the endpoint is already dead. Every path intoCoarseGrainedSchedulerBackend.stop()is wrapped inUtils.tryLogNonFatalError(SparkContext.stop→DAGScheduler.stop→TaskSchedulerImpl.stop), andKubernetesClusterSchedulerBackend.stopwrapssuper.stop()explicitly, so the exception reaches a driver log and never a user. It is still a real operational failure rather than a Spark bug, which is why it gets a name instead ofINTERNAL_ERROR— the reader is an operator triaging a shutdown log._3023— unreachable defensive check.DAGScheduler.submitMapStagerejects a dependency whose RDD has zero partitions. Its only production caller,ShuffleExchangeExec.mapOutputStatisticsFuture, already short-circuits withif (inputRDD.getNumPartitions == 0) Future.successful(null), and the two guards test the same number: the dependency's RDD isprepareShuffleDependency(inputRDD, ...)'s output, derived throughmapPartitionsWithIndexInternal, which preserves partition count. Note this is not an argument fromprivate[spark]— that is a scalac-only check with no bytecode enforcement, so Java code or a user class declared underorg.apache.spark.*can callSparkContext.submitMapStagedirectly. The accurate statement is that the branch is reachable only by violating the contract of an API whose own scaladoc says "This is currently an internal API only", which is whatINTERNAL_ERRORis for._3026— reachable from user code.TaskInfois@DeveloperApiand aTaskInfofor a running task is handed to listeners viaSparkListenerTaskStart. Any custom listener callingtaskInfo.durationinonTaskStarthits this. All in-tree callers are guarded or run aftermarkFinished, which is why it never fires in the build._3029— operator-facing, standalone only.TaskSchedulerImpl.errorthrows when the cluster manager reports a failure and no task set is active to abort. Its only caller isStandaloneSchedulerBackend.dead, i.e. the standalone Master removed the application or all Masters were unresponsive. The condition name is deliberately deployment-agnostic becauseTaskSchedulerImpl.erroris, but today standalone is the only producer.Why are the changes needed?
The error-conditions README disallows new
_LEGACY_ERROR_TEMP_*entries and asks existing ones to be resolved. This clears five of them.Two of the five were also weak in their own right:
_3021/_3022carried no indication that the failure happens during shutdown, so an operator seeing the log line had no way to tell whether the application's results were affected.Does this PR introduce any user-facing change?
Yes, to error messages — no API change.
Converting any legacy condition changes the rendered string in two mechanical ways:
SparkThrowableHelper.formatErrorMessagesuppresses the[CONDITION]prefix only for_LEGACY_ERROR_-prefixed names, and appendsSQLSTATE: xxxxxwhen a sqlState exists (legacy entries have none). Beyond that:_3021:Error asking standalone scheduler to shut down executors→Failed to shut down the scheduler backend while the application was terminating: the driver could not confirm that the executors were asked to shut down, so the later shutdown steps were skipped.The added clause is factual:stop()callsstopExecutors()unguarded, so a throw there skipsstopTokenManager()and theStopDriverask. The wording says "could not confirm" rather than "could not tell" because theStopExecutorshandler sends to every executor before replying, so a lost reply does not mean the executors went unnotified._3022:Error stopping standalone scheduler's driver endpoint→... : the driver endpoint did not stop cleanly._3026:duration() called on unfinished task→Cannot call the method "duration" of the class "org.apache.spark.scheduler.TaskInfo". The task has not finished yet, so its duration is not available._3029:Exiting due to error from cluster scheduler: <message>→Exiting due to an error reported by the cluster manager: <message>. This one is genuinely user-visible in logs, so log-scraping on the old text would need updating._3023renders as an internal error. Note this does not change any job-failure message shape: the throw happens on the driver duringsubmitMapStage, beforeeventProcessLoop.post, soDAGScheduler.abortStage'sisInternalErrorfilter — which only applies to exceptions arriving from task-set failures — is not involved.How was this patch tested?
New assertions, four of which fail against the old code (the condition name and the SQLSTATE both differ, since legacy entries carry no sqlState):
TaskInfoSuite(new file) —checkErroronUNSUPPORTED_CALL.TASK_NOT_FINISHEDincluding SQLSTATE and both message parameters, plus a companion case assertingdurationstill works aftermarkFinished.TaskSchedulerImplSuite—error()with no active task set assertsCLUSTER_MANAGER_APPLICATION_FAILURE; a companion case covers the other branch (with an active task set it aborts rather than throwing) and asserts the reason reaches theDAGSchedulerintact.KubernetesClusterSchedulerBackendSuite— a failedStopExecutorsRPC assertsSCHEDULER_BACKEND_SHUTDOWN_FAILED.EXECUTORSand that the original exception survives as the cause.DAGSchedulerSuite—submitMapStageon a zero-partition RDD assertsINTERNAL_ERROR, so the converted branch is pinned rather than merely deleted from the JSON.Coverage caveat worth a reviewer's attention: the only assertion on
SCHEDULER_BACKEND_SHUTDOWN_FAILEDlives in thekubernetesmodule and so runs only under-Pkubernetes. Both throw sites are incore, butcore'sCoarseGrainedSchedulerBackendSuiteuses a reallocal-clustercontext with no way to makeaskSyncfail, whereas the k8s suite already has the mockedRpcEndpointRefplumbing. TheDRIVER_ENDPOINTsubclass has no assertion for the same reason — its throw site sits insidestop(), which the k8s backend wraps inUtils.tryLogNonFatalError.Ran locally:
SparkThrowableSuite,TaskInfoSuite,TaskSchedulerImplSuite,DAGSchedulerSuite(367 tests) andKubernetesClusterSchedulerBackendSuite(12 tests), all passing;core/compile,core/Test/compileandkubernetes/Test/compileclean. The JSON was regenerated withSPARK_GENERATE_GOLDEN_FILES=1and produced no diff. Each new SQLSTATE assertion was verified to actually bite by temporarily setting a wrong value and watching the test go red.On the SQLSTATE choice for
SCHEDULER_BACKEND_SHUTDOWN_FAILED: 58030 is nominally "I/O error", and a failed RPC is not literally I/O. It was chosen because 58030 is already Spark's de-facto system-operation-failure code, carrying 11 conditions of which several are not literal I/O (FAILED_UPDATE_VIEW_SCHEMA,FAILED_TO_CREATE_PLAN_FOR_DIRECT_QUERY,CANNOT_RESTORE_PERMISSIONS_FOR_PATH), whereas58000("System error") and the08xxxconnection class exist inerror-states.jsonbut have zero users inerror-conditions.json— introducing the first would set a new precedent in a cleanup PR. Happy to switch if a committer prefers otherwise.Was this patch authored or co-authored using generative AI tooling?
Generated-by: Claude Code (Opus 4.8)