HyukjinKwon commented on code in PR #59318:
URL: https://github.com/apache/spark/pull/59318#discussion_r4237623908


##########
core/src/main/scala/org/apache/spark/scheduler/DAGScheduler.scala:
##########
@@ -1396,10 +1406,14 @@ private[spark] class DAGScheduler(
    * (The remaining group-level case -- a regular shuffle internal to a group 
-- is a structural
    * invariant that does not arise for the prefix -> pipelined-group -> suffix 
shapes targeted here:
    * groups are split at regular-shuffle boundaries. The producer-side idioms 
-- barrier, DRA,
-   * indeterminate, checksum, push-merge -- are rejected in 
checkPipelinedProducerSupported at stage
-   * creation, where a producer-only throw leaves no partial state.)
+   * indeterminate, checksum, push-merge -- and existing-stage ownership are 
checked after these
+   * group-level restrictions, without registering any stages.)
+   *
+   * @throws PipelinedShuffleUnsupportedException for an unsupported pipelined 
idiom
+   * @throws SparkException for cross-job reuse or an invalid barrier stage
    */
-  private def checkPipelinedGroupsSupportedInRDDGraph(finalRDD: RDD[_]): Unit 
= {
+  private def checkPipelinedJobSupported(

Review Comment:
   **Nit (P3):** Renaming this method leaves one stale reference outside the 
diff. The `MarkPipelinedShuffleForRealTimeMode` scaladoc in 
`sql/core/src/main/scala/org/apache/spark/sql/execution/streaming/runtime/IncrementalExecution.scala:791`
 still cites `checkPipelinedGroupsSupportedInRDDGraph` as the DAGScheduler 
check that rejects pipelined fan-out, and `git grep` finds no other occurrence. 
Could you update it to `checkPipelinedJobSupported` so the cross-module pointer 
stays valid?
   
   **Verification:**
   - **Inspection:** A repository-wide search for the old method name finds no 
match, and the updated scaladoc names a method that exists in DAGScheduler.



##########
core/src/main/scala/org/apache/spark/scheduler/DAGScheduler.scala:
##########
@@ -1457,25 +1471,93 @@ private[spark] class DAGScheduler(
     // Rooting the consumer check at each checkpointed RDD (rather than at the 
PSD-reading RDD) is
     // what makes it cover a checkpoint anywhere DOWNSTREAM in the consumer 
stage, not just on the
     // reading RDD itself.
-    def chainHasReliableCheckpoint(root: RDD[_]): Boolean =
-      !traverseParentRDDsWithinStage(root, (r: RDD[_]) =>
-        !r.checkpointData.exists(_.isInstanceOf[ReliableRDDCheckpointData[_]]))
-    val offending =
-      // CONSUMER side: a checkpointed RDD whose own within-stage chain reads 
a pipelined shuffle is
-      // inside a consumer member stage (covers a checkpoint anywhere in that 
stage, not just on the
-      // reading RDD). PRODUCER side: a producer root's within-stage chain 
carries a checkpoint.
-      reliablyCheckpointed.exists(rddChainReadsPipelinedShuffle) ||
-        producerRoots.exists(chainHasReliableCheckpoint)
+    val offending = reliablyCheckpointed.nonEmpty && {
+      // Shared ancestry is visited once per search, not once per root. The 
two searches need
+      // separate visited sets because they have different predicates, even 
when roots overlap.
+      val consumerVisited = new HashSet[RDD[_]]
+      val producerVisited = new HashSet[RDD[_]]
+      reliablyCheckpointed.exists(rdd => rddChainReadsPipelinedShuffle(rdd, 
consumerVisited)) ||
+        producerRoots.exists { root =>
+          !traverseParentRDDsWithinStage(
+            root, (r: RDD[_]) => !reliablyCheckpointed.contains(r), 
producerVisited)
+        }
+    }
     if (offending) {
       throw pipelinedUnsupportedError(
         "a reliable RDD checkpoint in a pipelined-group member's within-stage 
chain")
     }
+    checkPipelinedStageCreation(finalRDD, partitions, jobId)
+  }
+
+  /** Validate the stages a native result job would create, without 
registering them. */

Review Comment:
   **Nit (P3):** "native" doesn't appear anywhere else in DAGScheduler; the 
file contrasts *pipelined* with *regular*, and in Spark "native" usually 
suggests native code or engines. Since this validates every result job with a 
pipelined dependency, "pipelined result job" would be clearer. The warm-up 
comment at L1519 ("so native determinism does not recurse ...") has the same 
issue; it refers to the pipelined producer's determinism check.
   
   ```suggestion
     /** Validate the stages a pipelined result job would create, without 
registering them. */
   ```
   
   **Verification:**
   - **Inspection:** A whole-word search for 'native' in DAGScheduler.scala 
finds no remaining use referring to pipelined jobs or producers, and the 
reworded comments stay within the 100-character line limit.



##########
core/src/main/scala/org/apache/spark/scheduler/DAGScheduler.scala:
##########
@@ -1457,25 +1471,93 @@ private[spark] class DAGScheduler(
     // Rooting the consumer check at each checkpointed RDD (rather than at the 
PSD-reading RDD) is
     // what makes it cover a checkpoint anywhere DOWNSTREAM in the consumer 
stage, not just on the
     // reading RDD itself.
-    def chainHasReliableCheckpoint(root: RDD[_]): Boolean =
-      !traverseParentRDDsWithinStage(root, (r: RDD[_]) =>
-        !r.checkpointData.exists(_.isInstanceOf[ReliableRDDCheckpointData[_]]))
-    val offending =
-      // CONSUMER side: a checkpointed RDD whose own within-stage chain reads 
a pipelined shuffle is
-      // inside a consumer member stage (covers a checkpoint anywhere in that 
stage, not just on the
-      // reading RDD). PRODUCER side: a producer root's within-stage chain 
carries a checkpoint.
-      reliablyCheckpointed.exists(rddChainReadsPipelinedShuffle) ||
-        producerRoots.exists(chainHasReliableCheckpoint)
+    val offending = reliablyCheckpointed.nonEmpty && {
+      // Shared ancestry is visited once per search, not once per root. The 
two searches need
+      // separate visited sets because they have different predicates, even 
when roots overlap.
+      val consumerVisited = new HashSet[RDD[_]]
+      val producerVisited = new HashSet[RDD[_]]
+      reliablyCheckpointed.exists(rdd => rddChainReadsPipelinedShuffle(rdd, 
consumerVisited)) ||
+        producerRoots.exists { root =>
+          !traverseParentRDDsWithinStage(
+            root, (r: RDD[_]) => !reliablyCheckpointed.contains(r), 
producerVisited)
+        }
+    }
     if (offending) {
       throw pipelinedUnsupportedError(
         "a reliable RDD checkpoint in a pipelined-group member's within-stage 
chain")
     }
+    checkPipelinedStageCreation(finalRDD, partitions, jobId)
+  }
+
+  /** Validate the stages a native result job would create, without 
registering them. */
+  private def checkPipelinedStageCreation(
+      finalRDD: RDD[_], partitions: Array[Int], jobId: Int): Unit = {
+    val checkedShuffleIds = new HashSet[Int]
+    val pending = new ListBuffer[() => Unit]
+
+    def isRegistered(shuffleId: Int): Boolean =
+      checkedShuffleIds.contains(shuffleId) || 
shuffleIdToMapStage.contains(shuffleId)
+
+    def checkStage(rdd: RDD[_], numTasks: => Int): 
HashSet[ShuffleDependency[_, _, _]] = {
+      val (parents, profiles) = getShuffleDependenciesAndResourceProfiles(rdd)
+      val profile = mergeResourceProfilesForStage(profiles)
+      checkBarrierStage(rdd, profile, numTasks)
+      parents
+    }
+
+    def enqueueParents(parents: HashSet[ShuffleDependency[_, _, _]]): Unit = {
+      parents.toSeq.reverseIterator.foreach { dep =>
+        pending.prepend(() => checkShuffle(dep))
+      }
+    }
+
+    def checkNewStage(dep: ShuffleDependency[_, _, _]): Unit = {
+      val parents = checkStage(dep.rdd, dep.rdd.getNumPartitions)
+      checkPipelinedProducerSupported(dep)
+      outputTrackerMaster(dep)
+      pending.prepend(() => {
+        // Warm regular ancestors too, so native determinism does not recurse 
through a cold
+        // materialized prefix. This must happen after the parents have been 
checked.
+        dep.rdd.outputDeterministicLevel
+        checkedShuffleIds += dep.shuffleId
+        ()
+      })
+      enqueueParents(parents)

Review Comment:
   **Nit (P3):** This `enqueueParents(parents)` is what pre-checks ownership of 
a registered pipelined parent that sits *below* a newly planned stage, but no 
job-submission test exercises that path. The new cross-job tests all place the 
foreign-owned stage directly under the result stage (the lookup frontier). 
`direct stage construction retains parent ownership validation` calls 
`createShuffleMapStage` directly, so it never runs the preflight. If this 
branch stopped checking registered parents, construction would still throw 
`PIPELINED_SHUFFLE_CROSS_JOB_REUSE`, just after registering sibling stages, so 
every assertion would still pass and the partial-registration leak this PR 
fixes would come back unnoticed.
   
   A small case would pin it down. The result reads a fresh pipelined A and a 
fresh pipelined B, and B's producer reads a pipelined C owned by another active 
job, with A ordered first. Assert the error, check 
`assertNoPipelinedStageRegistration`, and confirm the owner job's stages are 
unchanged.
   
   **Verification:**
   - **Regression:** The new test passes at the PR head. It fails when the 
below-frontier ownership pre-check in checkPipelinedStageCreation is disabled, 
because stage registration then occurs before the construction-time rejection.
   - **Behavior:** The existing pipelined preflight and cross-job reuse tests 
in the scheduler suite still pass.



-- 
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