Uh oh!
There was an error while loading. Please reload this page.
[SPARK-58770][CORE] Assign a name to the error condition _LEGACY_ERROR_TEMP_3016-3020 - #58004
[SPARK-58770][CORE] Assign a name to the error condition _LEGACY_ERROR_TEMP_3016-3020#58004LuciferYang wants to merge 4 commits into
_LEGACY_ERROR_TEMP_3016-3020#58004Conversation
…OR_TEMP_3016-3020` ### What changes were proposed in this pull request? This PR converts the five `_LEGACY_ERROR_TEMP_*` conditions in `SparkCoreErrors` that cover RDD checkpointing, continuing the cleanup under [SPARK-37935](https://issues.apache.org/jira/browse/SPARK-37935). Four get user-facing names; the fifth is an unreachable defensive branch and becomes an internal error. | Legacy | Builder | Now | SQLSTATE | |---|---|---|---| | `_LEGACY_ERROR_TEMP_3016` | `checkpointDirectoryHasNotBeenSetInSparkContextError` | `CHECKPOINT_DIRECTORY_NOT_SET` | 55019 | | `_3017` | `invalidCheckpointFileError` | `INVALID_CHECKPOINT_FILE` | 58030 | | `_3018` | `failToCreateCheckpointPathError` | `FAILED_CREATE_CHECKPOINT_DIRECTORY` | 58030 | | `_3019` | `checkpointRDDHasDifferentNumberOfPartitionsFromOriginalRDDError` | `CHECKPOINT_RDD_PARTITION_COUNT_MISMATCH` | 58030 | | `_3020` | `mustSpecifyCheckpointDirError` | `INTERNAL_ERROR` (entry deleted) | XX000 | Four top-level names rather than one umbrella: `sqlState` lives on the umbrella, so grouping them would force a single SQLSTATE and lose the 55019/58030 split, and no umbrella sentence holds for all four. `_3016` fires before any job runs and is a user configuration mistake; the other three are storage failures during an action. `invalidCheckpointFileError` gains an `expectedFileName` parameter, taken from the `checkpointFileName(i)` the caller already computes, and reports the directory and file name separately. The old message named only the offending path, which is misleading here: the check walks the sorted `part-*` files and compares the *i*-th name against `part-%05d(i)`, so the path it reports is a perfectly valid file sitting where a missing one should be. With `part-00001` deleted from a 4-partition checkpoint the old message read `Invalid checkpoint file: .../part-00002` and sent the reader after the wrong file. `failToCreateCheckpointPathError`'s parameter is renamed `checkpointDirPath` to `path`, matching `INVALID_BUCKET_FILE` and the other path-carrying conditions. ### Reachability, per condition - **`_3016`, user configuration.** `RDD.checkpoint()` is public and `sc.checkpointDir` has exactly two writers: `SparkContext.setCheckpointDir` and, since 4.0.0, `spark.checkpoint.dir` (applied at `SparkContext.scala:614`). Calling `checkpoint()` with neither lands here. Reachable from `Dataset.checkpoint` too, which calls `internalRdd.checkpoint()`, so the message deliberately avoids naming RDDs. The old text mentioned only `setCheckpointDir`; the new one names the conf as well, which matters for a Connect client that cannot call the setter. - **`_3017`, incomplete checkpoint directory.** `getPartitions` requires the `part-*` files to be a contiguous `part-00000..part-000NN`. It fires on a partially written checkpoint, a manually pruned directory, or a directory handed to `SparkContext.checkpointFile` (which is how streaming recovery rebuilds `generatedRDDs`). Driver-side: `getPartitions` runs from `RDD.partitions`, and `partitions_` is `@transient`, so executors never compute it. - **`_3018`, storage refused the directory.** Only fires where a `FileSystem` reports failure by returning `false` from `mkdirs` rather than throwing, which is what HDFS and S3A can do; `LocalFileSystem` tends to throw instead. Driver-side, inside `RDD.doCheckpoint()` at the end of the first action. - **`_3019`, the write and the read-back disagree.** Not an engine invariant. The driver creates the directory, each executor writes its own `part-*` through its own `FileSystem`, and then the driver counts what its `FileSystem` lists, so the two sides of the comparison resolve in different JVMs. `setCheckpointDir` only *warns* when a cluster-mode application points at a local path, so a user who sets `/tmp/ckpt` on a cluster reaches this directly: the executors write to their own disks and the driver lists an empty directory. `getPartitions`' own scaladoc states the assumption being verified ("assumes that the original set of checkpoint files are fully preserved in a reliable storage"). Converting this one to `INTERNAL_ERROR` would report a storage misconfiguration as a Spark bug. - **`_3020`, unreachable.** `ReliableRDDCheckpointData`'s `cpDir` field throws when `sc.checkpointDir` is `None`, but its only construction site is `RDD.scala:1743`, two lines below the `context.checkpointDir.isEmpty` guard that raises `_3016`, inside the same `RDDCheckpointData.synchronized` block; `cpDir` is a plain `val`, evaluated there. The only writer that can store `None` is `setCheckpointDir(null)`, which no production code calls, and the field is `private[spark]`. It is reachable only by a cross-thread race that also makes the condition a duplicate of `_3016`, so it gets `internalError` rather than a second user-facing name for the same situation. ### 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. Three of the five were also weak on their own terms. `_3016` predates `spark.checkpoint.dir` and told the user about only one of the two ways to configure a directory. `_3017` pointed at the wrong file, as described above. `_3018` said only that creating the path failed, without saying that the filesystem reported it through a return value, which is the detail that tells an operator to look at permissions rather than at Spark. ### Does this PR introduce _any_ user-facing change? Yes, to error messages, with 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, so all four gain both). Beyond that: - `_3016`: `Checkpoint directory has not been set in the SparkContext` becomes `Cannot checkpoint because no checkpoint directory is configured. Set one with SparkContext.setCheckpointDir or the "spark.checkpoint.dir" configuration.` - `_3017`: `Invalid checkpoint file: <path>` becomes `Cannot read the checkpoint directory <path>: expected the partition file <expectedFileName> but found <fileName>. The partition files must be numbered contiguously from part-00000, one per partition.` The parameter set changes from one key to three, so `getMessageParameters()` gains `expectedFileName` and `fileName` while `path` narrows from the file to its directory. - `_3018`: `Failed to create checkpoint path <checkpointDirPath>` becomes `Failed to create the checkpoint directory <path> as FileSystem.mkdirs returned false.` The parameter is renamed, so `getMessageParameters()` has `path` where it had `checkpointDirPath`. - `_3019`: the three-line `Checkpoint RDD has a different number of partitions from original RDD. Original RDD [ID: ..., num of partitions: ...]; Checkpoint RDD [ID: ..., num of partitions: ...].` becomes `The checkpoint of RDD <originalRDDId> has <newRDDLength> partition(s), but the RDD itself has <originalRDDLength>. The checkpoint RDD is <newRDDId>.` plus a second line naming the two usual causes. Same four parameters. - `_3020` renders as an internal error. This does not change any job-failure message shape: the throw happens on the driver inside `RDD.checkpoint()`, before any task runs, so `DAGScheduler.abortStage`'s `isInternalError` filter is not involved. ### How was this patch tested? None of the five had any test coverage, and `CheckpointSuite` contained no `intercept` at all. Three tests are added to `CheckpointStorageSuite`, each asserting the condition and the SQLSTATE, and each failing against the pre-change code on both fields (the legacy entries carry no sqlState): - `"checkpoint() without a checkpoint directory"` builds a context with no checkpoint directory and asserts `CHECKPOINT_DIRECTORY_NOT_SET`. - `"reading a checkpoint directory with a missing partition file"` checkpoints a 4-partition RDD, deletes `part-00001`, then reads the directory back through `SparkContext.checkpointFile` and asserts `INVALID_CHECKPOINT_FILE` with all three message parameters, pinning that the reported expectation is `part-00001` and the file found is `part-00002`. - `"checkpoint path that cannot be created"` registers a `LocalFileSystem` subclass whose `mkdirs` returns `false` for the per-RDD directory and asserts `FAILED_CREATE_CHECKPOINT_DIRECTORY`. Two details are load-bearing: `LocalFileSystem` throws `FileAlreadyExistsException` when the path is occupied instead of returning `false`, so the failure has to be injected rather than staged on disk; and Hadoop caches `FileSystem` instances per scheme, so the test also sets `fs.file.impl.disable.cache=true` or an earlier test's real `LocalFileSystem` is used instead. `CHECKPOINT_RDD_PARTITION_COUNT_MISMATCH` has no triggering test. Reproducing it means making the driver and the executors see different contents for the same directory, which `local` mode cannot do since both sides share one filesystem. A fake `FileSystem` that under-reports `listStatus` would exercise the assertion but would be testing the fake rather than the failure, so this condition is covered by inspection only. The SQLSTATE assertions were confirmed to be live by temporarily setting `CHECKPOINT_DIRECTORY_NOT_SET`'s value to `42000` and watching the test fail with `sqlState: expected '55019' but got '42000'` before restoring it. `checkError` skips the comparison when `sqlState` is `None` and `SparkThrowableSuite` only checks that a state is registered, so a wrong SQLSTATE would otherwise ship green. Ran `core/testOnly org.apache.spark.SparkThrowableSuite org.apache.spark.CheckpointSuite org.apache.spark.CheckpointStorageSuite` (68 tests, all passing). ### Was this patch authored or co-authored using generative AI tooling? Generated-by: Claude Code (Opus 4.8)
uros-b
commented
Aug 14, 2026
LGTM, thank you @LuciferYang! |
I reviewed this PR. Overall it looks solid: the four JSON entries are inserted at the correct alphabetical positions, both SQLSTATEs (55019, 58030) are registered in A few findings, in decreasing order of importance: 1. In 2. The new test re-implements the In the 3. Nit: namespace of the new names
4. Nit: the internal error text keeps the legacy user-directive phrasing
|
dongjoon-hyun
commented
Aug 19, 2026
I re-reviewed at Two things left, one of which blocks the merge. 1. Please refresh the PR description before merging — it still describes the pre-rename state
2. My earlier finding #1 is still open, and the rename sharpened it The new message ends with "The partition files must be numbered contiguously from part-00000, one per partition", but the check in
Both are pre-existing, but widening the name from 3. Nit, on the untested condition The argument for leaving Otherwise this looks good to me. Splitting |
@dongjoon-hyun Fixed the description and trimmed the message. Both gaps in finding 2 are now filed. 1. Description Updated the three spots you listed, plus four more I found while checking: I also dropped this sentence from the 2. Both directions, filed separately Trimmed the message to drop The two gaps are different problems, so they are separate tickets rather than one:
3. Nit You are right that the old wording did not line up. The reason it stays untested is not the fake filesystem; it is that the read-back path has no original RDD to count against, which |
LuciferYang
commented
Aug 20, 2026
@dongjoon-hyun this one is ready to go, could you please take another look when you have time? Thanks |
…OR_TEMP_3016-3020` ### What changes were proposed in this pull request? This PR converts the five `_LEGACY_ERROR_TEMP_*` conditions in `SparkCoreErrors` that cover RDD checkpointing, continuing the cleanup under [SPARK-37935](https://issues.apache.org/jira/browse/SPARK-37935). Four get user-facing names; the fifth is an unreachable defensive branch and becomes an internal error. | Legacy | Builder | Now | SQLSTATE | |---|---|---|---| | `_LEGACY_ERROR_TEMP_3016` | `checkpointDirectoryHasNotBeenSetInSparkContextError` | `CHECKPOINT_DIRECTORY_NOT_SET` | 55019 | | `_3017` | `invalidCheckpointDirectoryError` | `INVALID_CHECKPOINT_DIRECTORY` | 58030 | | `_3018` | `failToCreateCheckpointPathError` | `FAILED_CREATE_CHECKPOINT_DIRECTORY` | 58030 | | `_3019` | `checkpointRDDHasDifferentNumberOfPartitionsFromOriginalRDDError` | `CHECKPOINT_RDD_PARTITION_COUNT_MISMATCH` | 58030 | | `_3020` | `mustSpecifyCheckpointDirError` | `INTERNAL_ERROR` (entry deleted) | XX000 | Four top-level names rather than one umbrella: `sqlState` lives on the umbrella, so grouping them would force a single SQLSTATE and lose the 55019/58030 split, and no umbrella sentence holds for all four. `_3016` fires before any job runs and is a user configuration mistake; the other three are storage failures during an action. `invalidCheckpointDirectoryError` gains an `expectedFileName` parameter, taken from the `checkpointFileName(i)` the caller already computes, and reports the directory and file name separately. The old message named only the offending path, which is misleading here: the check walks the sorted `part-*` files and compares the *i*-th name against `part-%05d(i)`, so the path it reports is a perfectly valid file sitting where a missing one should be. With `part-00001` deleted from a 4-partition checkpoint the old message read `Invalid checkpoint file: .../part-00002` and sent the reader after the wrong file. The builder is renamed alongside the condition because its old name contradicted it; the other three keep their Scala names, which already agree with theirs. All four have a single call site, so the diff size does not distinguish them. `failToCreateCheckpointPathError`'s parameter is renamed `checkpointDirPath` to `path`, matching `INVALID_BUCKET_FILE` and the other path-carrying conditions. ### Reachability, per condition - **`_3016`, user configuration.** `RDD.checkpoint()` is public, and `sc.checkpointDir` is set through `SparkContext.setCheckpointDir`, reachable either by calling it directly or, since 4.0.0, through `spark.checkpoint.dir`, which `SparkContext.scala:623` forwards to the same setter. Calling `checkpoint()` with neither lands here. Reachable from `Dataset.checkpoint` too, which calls `internalRdd.checkpoint()`, so the message deliberately avoids naming RDDs. The old text mentioned only `setCheckpointDir`; the new one names the conf as well, which matters for a Connect client that cannot call the setter. - **`_3017`, incomplete checkpoint directory.** `getPartitions` requires the `part-*` files to be a contiguous `part-00000..part-000NN`. It fires on a partially written checkpoint, a manually pruned directory, or a directory handed to `SparkContext.checkpointFile` (which is how streaming recovery rebuilds `generatedRDDs`). Driver-side: `getPartitions` runs from `RDD.partitions`, and `partitions_` is `transient`, so executors never compute it. - **`_3018`, storage refused the directory.** Only fires where a `FileSystem` reports failure by returning `false` from `mkdirs` rather than throwing, which is what HDFS and S3A can do; `LocalFileSystem` tends to throw instead. Driver-side, inside `RDD.doCheckpoint()` at the end of the first action. - **`_3019`, the write and the read-back disagree.** Not an engine invariant. The driver creates the directory, each executor writes its own `part-*` through its own `FileSystem`, and then the driver counts what its `FileSystem` lists, so the two sides of the comparison resolve in different JVMs. `setCheckpointDir` only *warns* when a cluster-mode application points at a local path, so a user who sets `/tmp/ckpt` on a cluster reaches this directly: the executors write to their own disks and the driver lists an empty directory. `getPartitions`' own scaladoc states the assumption being verified ("assumes that the original set of checkpoint files are fully preserved in a reliable storage"). Converting this one to `INTERNAL_ERROR` would report a storage misconfiguration as a Spark bug. - **`_3020`, unreachable.** `ReliableRDDCheckpointData`'s `cpDir` field throws when `sc.checkpointDir` is `None`, but its only construction site is `RDD.scala:1743`, two lines below the `context.checkpointDir.isEmpty` guard that raises `_3016`, inside the same `RDDCheckpointData.synchronized` block; `cpDir` is a plain `val`, evaluated there. `mustSpecifyCheckpointDirError` has a single call site, the `getOrElse` on that field. It is reachable only by a cross-thread race that also makes the condition a duplicate of `_3016`, so it gets `internalError` rather than a second user-facing name for the same situation. ### 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. Three of the five were also weak on their own terms. `_3016` predates `spark.checkpoint.dir` and told the user about only one of the two ways to configure a directory. `_3017` pointed at the wrong file, as described above. `_3018` said only that creating the path failed, without saying that the filesystem reported it through a return value, which is the detail that tells an operator to look at permissions rather than at Spark. ### Does this PR introduce _any_ user-facing change? Yes, to error messages, with 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, so all four gain both). Beyond that: - `_3016`: `Checkpoint directory has not been set in the SparkContext` becomes `Cannot checkpoint because no checkpoint directory is configured. Set one with SparkContext.setCheckpointDir or the "spark.checkpoint.dir" configuration.` - `_3017`: `Invalid checkpoint file: <path>` becomes `Cannot read the checkpoint directory <path>: expected the partition file <expectedFileName> but found <fileName>. The partition files must be numbered contiguously from part-00000.` The parameter set changes from one key to three, so `getMessageParameters()` gains `expectedFileName` and `fileName` while `path` narrows from the file to its directory. - `_3018`: `Failed to create checkpoint path <checkpointDirPath>` becomes `Failed to create the checkpoint directory <path> as FileSystem.mkdirs returned false.` The parameter is renamed, so `getMessageParameters()` has `path` where it had `checkpointDirPath`. - `_3019`: the three-line `Checkpoint RDD has a different number of partitions from original RDD. Original RDD [ID: ..., num of partitions: ...]; Checkpoint RDD [ID: ..., num of partitions: ...].` becomes `The checkpoint of RDD <originalRDDId> has <newRDDLength> partition(s), but the RDD itself has <originalRDDLength>. The checkpoint RDD is <newRDDId>.` plus a second line naming the two usual causes. Same four parameters. - `_3020`: `Checkpoint dir must be specified.` becomes `[INTERNAL_ERROR] SparkContext.checkpointDir is unset when creating ReliableRDDCheckpointData. SQLSTATE: XX000`, and `getMessageParameters()` goes from empty to `{"message": ...}` since `INTERNAL_ERROR`'s template is `<message>`. This does not change any job-failure message shape: the throw happens on the driver inside `RDD.checkpoint()`, before any task runs, so `DAGScheduler.abortStage`'s `isInternalError` filter is not involved. ### How was this patch tested? None of the five had any test coverage, and `CheckpointSuite` contained no `intercept` at all. Three tests are added to `CheckpointStorageSuite`, each asserting the condition and the SQLSTATE, and each failing against the pre-change code on both fields (the legacy entries carry no sqlState): - `"checkpoint() without a checkpoint directory"` builds a context with no checkpoint directory and asserts `CHECKPOINT_DIRECTORY_NOT_SET`. - `"reading a checkpoint directory with a missing partition file"` checkpoints a 4-partition RDD, deletes `part-00001`, then reads the directory back through `SparkContext.checkpointFile` and asserts `INVALID_CHECKPOINT_DIRECTORY` with all three message parameters, pinning that the reported expectation is `part-00001` and the file found is `part-00002`. - `"checkpoint path that cannot be created"` registers a `LocalFileSystem` subclass whose `mkdirs` returns `false` for the per-RDD directory and asserts `FAILED_CREATE_CHECKPOINT_DIRECTORY`. Two details are load-bearing: `LocalFileSystem` throws `FileAlreadyExistsException` when the path is occupied instead of returning `false`, so the failure has to be injected rather than staged on disk; and Hadoop caches `FileSystem` instances per scheme, so the test also sets `fs.file.impl.disable.cache=true` or an earlier test's real `LocalFileSystem` is used instead. `CHECKPOINT_RDD_PARTITION_COUNT_MISMATCH` has no triggering test. The comparison it guards needs an original RDD to count against, and on the read-back path there is none: `getPartitions`' own scaladoc notes there is "no way to know a priori the number of partitions to expect". Reproducing the write-path case means making the driver and the executors see different contents for the same directory, which `local` mode cannot do since both sides share one filesystem. Two related gaps in the same check are filed separately, since both are pre-existing and neither is a naming change: [SPARK-58881](https://issues.apache.org/jira/browse/SPARK-58881), a non-numeric `part-*` name throws a raw `NumberFormatException` from the `sortBy` before the validation loop runs; and [SPARK-58883](https://issues.apache.org/jira/browse/SPARK-58883), deleting the trailing `part-*` file leaves the rest contiguous, so a read through `SparkContext.checkpointFile` silently yields fewer partitions. Detecting the latter needs the expected partition count persisted at write time, i.e. a new on-disk format. The SQLSTATE assertions were confirmed to be live by temporarily setting `CHECKPOINT_DIRECTORY_NOT_SET`'s value to `42000` and watching the test fail with `sqlState: expected '55019' but got '42000'` before restoring it. `checkError` skips the comparison when `sqlState` is `None` and `SparkThrowableSuite` only checks that a state is registered, so a wrong SQLSTATE would otherwise ship green. Ran `core/testOnly org.apache.spark.SparkThrowableSuite org.apache.spark.CheckpointSuite org.apache.spark.CheckpointStorageSuite` (68 tests, all passing). ### Was this patch authored or co-authored using generative AI tooling? Generated-by: Claude Code (Opus 4.8) Closes#58004 from LuciferYang/assign-name-legacy-3016-3020. Authored-by: YangJie <yangjie01@baidu.com> Signed-off-by: yangjie01 <yangjie01@baidu.com> (cherry picked from commit 5ef3a94) Signed-off-by: yangjie01 <yangjie01@baidu.com>
LuciferYang
commented
Aug 21, 2026
LuciferYang
commented
Aug 21, 2026
Thank you @dongjoon-hyun@uros-b |
What changes were proposed in this pull request?
This PR converts the five
_LEGACY_ERROR_TEMP_*conditions inSparkCoreErrorsthat cover RDD checkpointing, continuing the cleanup under SPARK-37935. Four get user-facing names; the fifth is an unreachable defensive branch and becomes an internal error._LEGACY_ERROR_TEMP_3016checkpointDirectoryHasNotBeenSetInSparkContextErrorCHECKPOINT_DIRECTORY_NOT_SET_3017invalidCheckpointDirectoryErrorINVALID_CHECKPOINT_DIRECTORY_3018failToCreateCheckpointPathErrorFAILED_CREATE_CHECKPOINT_DIRECTORY_3019checkpointRDDHasDifferentNumberOfPartitionsFromOriginalRDDErrorCHECKPOINT_RDD_PARTITION_COUNT_MISMATCH_3020mustSpecifyCheckpointDirErrorINTERNAL_ERROR(entry deleted)Four top-level names rather than one umbrella:
sqlStatelives on the umbrella, so grouping them would force a single SQLSTATE and lose the 55019/58030 split, and no umbrella sentence holds for all four._3016fires before any job runs and is a user configuration mistake; the other three are storage failures during an action.invalidCheckpointDirectoryErrorgains anexpectedFileNameparameter, taken from thecheckpointFileName(i)the caller already computes, and reports the directory and file name separately. The old message named only the offending path, which is misleading here: the check walks the sortedpart-*files and compares the i-th name againstpart-%05d(i), so the path it reports is a perfectly valid file sitting where a missing one should be. Withpart-00001deleted from a 4-partition checkpoint the old message readInvalid checkpoint file: .../part-00002and sent the reader after the wrong file. The builder is renamed alongside the condition because its old name contradicted it; the other three keep their Scala names, which already agree with theirs. All four have a single call site, so the diff size does not distinguish them.failToCreateCheckpointPathError's parameter is renamedcheckpointDirPathtopath, matchingINVALID_BUCKET_FILEand the other path-carrying conditions.Reachability, per condition
_3016, user configuration.RDD.checkpoint()is public, andsc.checkpointDiris set throughSparkContext.setCheckpointDir, reachable either by calling it directly or, since 4.0.0, throughspark.checkpoint.dir, whichSparkContext.scala:623forwards to the same setter. Callingcheckpoint()with neither lands here. Reachable fromDataset.checkpointtoo, which callsinternalRdd.checkpoint(), so the message deliberately avoids naming RDDs. The old text mentioned onlysetCheckpointDir; the new one names the conf as well, which matters for a Connect client that cannot call the setter._3017, incomplete checkpoint directory.getPartitionsrequires thepart-*files to be a contiguouspart-00000..part-000NN. It fires on a partially written checkpoint, a manually pruned directory, or a directory handed toSparkContext.checkpointFile(which is how streaming recovery rebuildsgeneratedRDDs). Driver-side:getPartitionsruns fromRDD.partitions, andpartitions_is@transient, so executors never compute it._3018, storage refused the directory. Only fires where aFileSystemreports failure by returningfalsefrommkdirsrather than throwing, which is what HDFS and S3A can do;LocalFileSystemtends to throw instead. Driver-side, insideRDD.doCheckpoint()at the end of the first action._3019, the write and the read-back disagree. Not an engine invariant. The driver creates the directory, each executor writes its ownpart-*through its ownFileSystem, and then the driver counts what itsFileSystemlists, so the two sides of the comparison resolve in different JVMs.setCheckpointDironly warns when a cluster-mode application points at a local path, so a user who sets/tmp/ckpton a cluster reaches this directly: the executors write to their own disks and the driver lists an empty directory.getPartitions' own scaladoc states the assumption being verified ("assumes that the original set of checkpoint files are fully preserved in a reliable storage"). Converting this one toINTERNAL_ERRORwould report a storage misconfiguration as a Spark bug._3020, unreachable.ReliableRDDCheckpointData'scpDirfield throws whensc.checkpointDirisNone, but its only construction site isRDD.scala:1743, two lines below thecontext.checkpointDir.isEmptyguard that raises_3016, inside the sameRDDCheckpointData.synchronizedblock;cpDiris a plainval, evaluated there.mustSpecifyCheckpointDirErrorhas a single call site, thegetOrElseon that field. It is reachable only by a cross-thread race that also makes the condition a duplicate of_3016, so it getsinternalErrorrather than a second user-facing name for the same situation.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.Three of the five were also weak on their own terms.
_3016predatesspark.checkpoint.dirand told the user about only one of the two ways to configure a directory._3017pointed at the wrong file, as described above._3018said only that creating the path failed, without saying that the filesystem reported it through a return value, which is the detail that tells an operator to look at permissions rather than at Spark.Does this PR introduce any user-facing change?
Yes, to error messages, with 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, so all four gain both). Beyond that:_3016:Checkpoint directory has not been set in the SparkContextbecomesCannot checkpoint because no checkpoint directory is configured. Set one with SparkContext.setCheckpointDir or the "spark.checkpoint.dir" configuration._3017:Invalid checkpoint file: <path>becomesCannot read the checkpoint directory <path>: expected the partition file <expectedFileName> but found <fileName>. The partition files must be numbered contiguously from part-00000.The parameter set changes from one key to three, sogetMessageParameters()gainsexpectedFileNameandfileNamewhilepathnarrows from the file to its directory._3018:Failed to create checkpoint path <checkpointDirPath>becomesFailed to create the checkpoint directory <path> as FileSystem.mkdirs returned false.The parameter is renamed, sogetMessageParameters()haspathwhere it hadcheckpointDirPath._3019: the three-lineCheckpoint RDD has a different number of partitions from original RDD. Original RDD [ID: ..., num of partitions: ...]; Checkpoint RDD [ID: ..., num of partitions: ...].becomesThe checkpoint of RDD <originalRDDId> has <newRDDLength> partition(s), but the RDD itself has <originalRDDLength>. The checkpoint RDD is <newRDDId>.plus a second line naming the two usual causes. Same four parameters._3020:Checkpoint dir must be specified.becomes[INTERNAL_ERROR] SparkContext.checkpointDir is unset when creating ReliableRDDCheckpointData. SQLSTATE: XX000, andgetMessageParameters()goes from empty to{"message": ...}sinceINTERNAL_ERROR's template is<message>. This does not change any job-failure message shape: the throw happens on the driver insideRDD.checkpoint(), before any task runs, soDAGScheduler.abortStage'sisInternalErrorfilter is not involved.How was this patch tested?
None of the five had any test coverage, and
CheckpointSuitecontained nointerceptat all. Three tests are added toCheckpointStorageSuite, each asserting the condition and the SQLSTATE, and each failing against the pre-change code on both fields (the legacy entries carry no sqlState):"checkpoint() without a checkpoint directory"builds a context with no checkpoint directory and assertsCHECKPOINT_DIRECTORY_NOT_SET."reading a checkpoint directory with a missing partition file"checkpoints a 4-partition RDD, deletespart-00001, then reads the directory back throughSparkContext.checkpointFileand assertsINVALID_CHECKPOINT_DIRECTORYwith all three message parameters, pinning that the reported expectation ispart-00001and the file found ispart-00002."checkpoint path that cannot be created"registers aLocalFileSystemsubclass whosemkdirsreturnsfalsefor the per-RDD directory and assertsFAILED_CREATE_CHECKPOINT_DIRECTORY. Two details are load-bearing:LocalFileSystemthrowsFileAlreadyExistsExceptionwhen the path is occupied instead of returningfalse, so the failure has to be injected rather than staged on disk; and Hadoop cachesFileSysteminstances per scheme, so the test also setsfs.file.impl.disable.cache=trueor an earlier test's realLocalFileSystemis used instead.CHECKPOINT_RDD_PARTITION_COUNT_MISMATCHhas no triggering test. The comparison it guards needs an original RDD to count against, and on the read-back path there is none:getPartitions' own scaladoc notes there is "no way to know a priori the number of partitions to expect". Reproducing the write-path case means making the driver and the executors see different contents for the same directory, whichlocalmode cannot do since both sides share one filesystem. Two related gaps in the same check are filed separately, since both are pre-existing and neither is a naming change: SPARK-58881, a non-numericpart-*name throws a rawNumberFormatExceptionfrom thesortBybefore the validation loop runs; and SPARK-58883, deleting the trailingpart-*file leaves the rest contiguous, so a read throughSparkContext.checkpointFilesilently yields fewer partitions. Detecting the latter needs the expected partition count persisted at write time, i.e. a new on-disk format.The SQLSTATE assertions were confirmed to be live by temporarily setting
CHECKPOINT_DIRECTORY_NOT_SET's value to42000and watching the test fail withsqlState: expected '55019' but got '42000'before restoring it.checkErrorskips the comparison whensqlStateisNoneandSparkThrowableSuiteonly checks that a state is registered, so a wrong SQLSTATE would otherwise ship green.Ran
core/testOnly org.apache.spark.SparkThrowableSuite org.apache.spark.CheckpointSuite org.apache.spark.CheckpointStorageSuite(68 tests, all passing).Was this patch authored or co-authored using generative AI tooling?
Generated-by: Claude Code (Opus 4.8)