[SPARK-58461][CORE] Convert unreachable _LEGACY_ERROR_TEMP_3021-3042 invariants in SparkCoreErrors to internal errors - #57663
Open
LuciferYang wants to merge 1 commit into
Conversation
…` invariants in `SparkCoreErrors` to internal errors
uros-b
approved these changes
Jul 31, 2026
Member
|
Thank you @LuciferYang! |
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 11 legacy error conditions in
SparkCoreErrorsthat guard engine-internal invariants intoSparkException.internalError(...), and deletes their entries fromerror-conditions.json. Per the error-conditions README, a throw site that a correct Spark cannot reach from user code should become an internal error rather than being given a user-facing name._LEGACY_ERROR_TEMP_3024accessNonExistentAccumulatorErrorINTERNAL_ERROR_3025sendResubmittedTaskStatusForShuffleMapStagesOnlyErrorINTERNAL_ERROR_3030taskHasNotLockedBlockErrorINTERNAL_ERROR_STORAGE_3031blockDoesNotExistErrorINTERNAL_ERROR_STORAGE_3032waitingForReplicationToFinishErrorINTERNAL_ERROR(keepscause)_3034waitingForAsyncReregistrationErrorINTERNAL_ERROR(keepscause)_3038failToGetBlockWithLockErrorINTERNAL_ERROR_STORAGE_3039blockStatusQueryReturnedNullErrorINTERNAL_ERROR_STORAGE_3040unexpectedBlockManagerMasterEndpointResultErrorINTERNAL_ERROR_STORAGE_3042failToGetNonShuffleBlockErrorINTERNAL_ERROR_STORAGE(keepscause)_3041unsupportedOperationError(deleted)UNSUPPORTED_CALL.WITHOUT_SUGGESTIONMessage text is preserved for all of them (a trailing period was added where missing). Two entries get more than a mechanical rewrite:
_3041had an empty message ("message": [""]), so it rendered as the empty string. Its builder is deleted andDiskBlockObjectWriter.write(b: Int)— anOutputStreammethod the class does not implement — now throws the existingUNSUPPORTED_CALL.WITHOUT_SUGGESTIONwith explicitclassName/methodName. The no-argSparkUnsupportedOperationException()helper was tried first and rejected: it derives those fromstackTrace(3), which for a plain Scala class method resolves to the caller's frame (measured:methodNamecame out as$anonfun$new$3)._3040had no message parameters at all ("BlockManagerMasterEndpoint returned false, expected true."), so the log never said which RPC failed. Its builder gains amessage: Anyparameter.BlockManagerMaster.tellisprivatewith two call sites, passingRemoveExecutor(execId)andStopBlockManagerMaster; both are a case class/object whosetoStringcarries no host, credential, or block content (BlockManagerIdnever reachestell)._3042usesnew SparkException("INTERNAL_ERROR_STORAGE", ..., cause = e)directly becauseSparkException.internalErrorhas no overload accepting both acauseand acategory(precedent:ExecutorDeadException)._3032/_3034keep bareINTERNAL_ERROR— they are dead branches (see below), so the category buys nothing, and adding the missing overload tocommon/utilswould be unrelated churn here.Reachability, per condition
The classification is the substance of this PR, so it is spelled out rather than asserted:
_3025— the only producer ofResubmittedisTaskSetManager.executorLost, gated onisShuffleMapTasks, so the non-ShuffleMapStagebranch cannot be taken._3039—Future.sequencecannot yieldnull; the check is a vestige of a pre-awaitResultrewrite._3040— bothtellhandlers replytrueunconditionally; an endpoint failure throws out ofaskSyncinstead._3042— everyblockIdreachingthrowFetchFailedExceptioncomes from map-output-derived shuffle ids, so the defensivecase _is unreachable._3030— both callers run on the thread that acquired the write lock._3041—write(b: Int)has no caller in tree._3032and_3034wrap only aThreadUtils.awaitReady(future, Duration.Inf)call incatch NonFatal.Awaitable.readydiscards theTryand never surfaces the future's failure, andDuration.Inftakes theacquireSharedInterruptiblypath so no timeout is possible — nothing can reach either catch. (InterruptedExceptioncan escapeawaitReady, but it is notNonFatal, so it passes through untouched both before and after this change.)_3034's enclosing method is additionally documented "For testing." with only test callers._3024is not strictly unreachable:AccumulatorContext.getholdsWeakReferences andContextCleaner.doCleanupAccumremoves entries, so an in-flightCompletionEventcan carry an id that has already been collected or cleaned. The throw is caught by acatch NonFatalin the same method (DAGScheduler.updateAccumulators), which only logs — it never escapes to a job or a user. There is no user-visible exception to name, sointernalErroris the right target, but the branch should not be described as unreachable._3031and_3038are guarded by lock ownership at every caller, with one residual path:BlockInfoManager.clear()usestryLockand drops entries without honoring existing holders, so an executor stopping while tasks are still finishing can reach them. That exception is itself suppressed byExecutor'scase t: Throwable if env.isStoppedand never reaches the driver.The remaining 10 conditions in the
_3021-3042range are intentionally left alone — they are operational or user-reachable failures that deserve proper names in a follow-up, and labeling themINTERNAL_ERRORwould report real failures as Spark bugs:_3021/_3022(RPC races during executor/endpoint shutdown),_3023(submitMapStageon a 0-partition RDD — user API misuse),_3026(TaskInfo.durationon an unfinished task —TaskInfois@DeveloperApi),_3028(barrier stage got partial offers under a legacy config),_3029(the standalone Master rejected the application),_3033(external shuffle service registration exhausted its retries),_3035(a third-partyShuffleManagerwhose resolver is notMigratableResolver),_3036(a replica genuinely failed to store),_3037(aDiskStorefile vanished — the SPARK-15736 recovery path).The ten converted builders remain thin wrappers in
SparkCoreErrorsrather than being inlined at their throw sites. Inlining would match what most ofcoredoes for internal errors, but it would enlarge the diff across five more files for no behavior change;_3041is inlined only because its builder became empty.Why are the changes needed?
The error-conditions README disallows new
_LEGACY_ERROR_TEMP_*entries and asks existing ones to be resolved. This clears 11 of them, under the umbrella SPARK-37935. Two of the 11 were also defective in their own right:_3041's message was empty and_3040's carried no parameters.Does this PR introduce any user-facing change?
No new user-facing API, but one behavior change worth a reviewer's explicit sign-off.
DAGScheduler.abortStagepasses a task exception straight to the user only whengetCondition != null && !isInternalError(condition). The old_LEGACY_ERROR_TEMP_*names passed that filter;INTERNAL_ERROR*does not. So for the four conditions thrown inside tasks on executors —_3030,_3031,_3038,_3042— a job that fails through them (after the task retries tomaxTaskFailures) now reportsSparkException: Job aborted due to stage failure: ...wrapping the original, instead of the original directly, and gains SQLSTATEXX000. That is the intended consequence of labeling these as internal errors, and no test asserts the old unwrapped shape._3041's rendered message changes from the empty string toCannot call the method "write" of the class "org.apache.spark.storage.DiskBlockObjectWriter"., and its exception type stays a subclass ofUnsupportedOperationException.How was this patch tested?
SparkThrowableSuite— validates that the JSON is well-formed, key-sorted, and round-trips after the 11 deletions.BlockInfoManagerSuite— its "removing a non-existent block" test was tightened from a bareintercept[SparkException]tocheckErroronINTERNAL_ERROR_STORAGE, so the conversion is now pinned by an assertion rather than passing vacuously.DiskBlockObjectWriterSuite— new test assertingUNSUPPORTED_CALL.WITHOUT_SUGGESTIONwith the expectedclassName/methodName.BlockManagerMasterSuite— covers the_3040signature change.All four suites pass (70 tests) on top of the current master, and
core/compileis clean. No new tests were added for the unreachable conversions, since there is no way to trigger them.Was this patch authored or co-authored using generative AI tooling?
Generated-by: Claude Code (Opus 4.8)