dongjoon-hyun opened a new pull request, #59738:
URL: https://github.com/apache/spark/pull/59738

   ### What changes were proposed in this pull request?
   
   This PR makes `DAGScheduler.handleMapStageSubmitted` reject a map-stage job 
whose shuffle dependency has a `PipelinedShuffleDependency` anywhere in its RDD 
lineage. The job fails with `PIPELINED_SHUFFLE_UNSUPPORTED` before any stage is 
created, in the same way that `handleJobSubmitted` rejects the same graph for a 
result job.
   
   ```scala
   if (rddGraphHasPipelinedDependency(dependency.rdd)) {
     val e = pipelinedUnsupportedError("a map-stage job that depends on a 
pipelined shuffle")
     logWarning(...)
     listener.jobFailed(e)
     return
   }
   ```
   
   This adds one RDD graph walk per map-stage job, similar to the walk that 
`handleJobSubmitted` already does for every result job.
   
   ### Why are the changes needed?
   
   `handleMapStageSubmitted` rejected only a dependency that is itself a 
`PipelinedShuffleDependency`. A regular `ShuffleDependency` whose map side 
reads a pipelined shuffle, directly or further down its lineage, was accepted. 
Such a job skipped every pipelined job check of `handleJobSubmitted`:
   
   - the shuffle shape check, which rejects a pipelined shuffle below a regular 
shuffle
   - gang admission (`rejectUnadmittablePipelinedGroup`)
   - `checkPipelinedJobSupported` and the speculation check
   - the run epoch (`SPARK_PIPELINED_RUN_EPOCH` and 
`PipelinedShuffleManager.startRun`)
   - `ActiveJob.hasPipelinedDependency`
   
   However, `submitStage` still co-scheduled the pipelined producer with the 
stage reading it, as ordinary task sets (`TaskSet.isPipelined = false`).
   
   With the in-process channel transport (`PipelinedChannelShuffleManager`), 
this causes a hang or wrong results.
   
   - **Hang.** The tasks have no run epoch, so every channel access throws 
`TaskKilledException("Pipelined shuffle run has ended")`. A kill does not count 
as a task failure in a non-pipelined task set, so the tasks are relaunched 
forever. In a `local[4]` test, 73,470 task attempts were killed in 10 seconds, 
each with a WARN log line, and the job never finished.
   - **Correctness.** If run epoch 0 is active, because job 0 of the 
`SparkContext` is a pipelined job that is still running, the channel accesses 
succeed. A producer attempt that is retried, or relaunched after a kill, then 
delivers its rows again. The channel reader counts end-of-stream markers 
without tracking writer attempts, so the map-stage job can succeed with 
duplicated rows, or stop early and drop the rows of other producers. In local 
tests, the reader saw 5,120 rows instead of 3,072 when a producer attempt 
failed after two batches. When a producer attempt failed after its 
end-of-stream marker, the reader counted 200 rows from that producer and none 
from the other, instead of 100 each. Both map-stage jobs reported success.
   
   With the RPC streaming transport, the job runs without gang admission or 
group-atomic failure handling, and speculation is not rejected.
   
   `SparkContext.submitMapStage` is `private[spark]`, and its only in-tree 
caller is AQE (`ShuffleExchangeExec`). `AQEEnablePipelinedShuffle` keeps each 
plan all-pipelined or all-regular, so SQL does not create this shape within one 
plan. It does when an RDD that contains a pipelined shuffle is wrapped in a new 
DataFrame and shuffled under AQE. For example, in local mode with 
`spark.shuffle.manager.incremental=org.apache.spark.shuffle.local.pipelined.PipelinedChannelShuffleManager`
 and `spark.sql.shuffle.localPipelined.enabled=true`:
   
   ```scala
   val inner = spark.range(0, 1000, 1, 2).withColumn("k", $"id" % 
7).groupBy($"k").count()
   val rows = inner.queryExecution.toRdd.map(r => Row(r.getLong(0), 
r.getLong(1)))
   spark.createDataFrame(rows, inner.schema).groupBy($"count").count().collect()
   ```
   
   A user RDD that creates a `PipelinedShuffleDependency` (a `DeveloperApi`) 
reaches it the same way.
   
   ### Does this PR introduce _any_ user-facing change?
   
   Yes, within the unreleased branches only. A map-stage job that depends on a 
pipelined shuffle now fails up front instead of hanging or completing with 
wrong results. For example, the query above fails with:
   
   ```
   [PIPELINED_SHUFFLE_UNSUPPORTED] A pipelined shuffle stage group cannot be 
scheduled because it uses an unsupported feature: a map-stage job that depends 
on a pipelined shuffle. ...
   ```
   
   No released Spark version has `PipelinedShuffleDependency`. `branch-4.3` has 
it with the RPC streaming transport, and `branch-4.x` and `master` also have 
the in-process channel transport.
   
   ### How was this patch tested?
   
   Pass the CIs with the newly added test cases.
   
   - `DAGSchedulerSuite`: the job is rejected when its map stage reads a 
pipelined shuffle directly and when the pipelined shuffle sits below another 
regular shuffle, and no task set or partial state is left.
   - `PipelinedChannelShuffleSuite`: the job is rejected before any task 
starts, and the same pipelined shuffle still runs as a result job.
   - `AQEPipelinedShuffleSuite`: the query above fails fast instead of hanging, 
and the inner pipelined query still runs.
   
   Without the fix, the first test fails because the job is accepted, and the 
other two time out. The duplicated and dropped rows above were reproduced with 
temporary tests that keep job 0 running, which are not included.
   
   ```
   $ build/sbt 'core/testOnly *DAGSchedulerSuite *PipelinedChannelShuffleSuite 
-- -z SPARK-60497'
   $ build/sbt 'sql/testOnly *AQEPipelinedShuffleSuite -- -z SPARK-60497'
   ```
   
   ### Was this patch authored or co-authored using generative AI tooling?
   
   Generated-by: Claude Opus 5.5


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to