jerrypeng commented on code in PR #59523:
URL: https://github.com/apache/spark/pull/59523#discussion_r4408574780


##########
core/src/test/scala/org/apache/spark/scheduler/DAGSchedulerSuite.scala:
##########
@@ -9730,7 +9941,17 @@ class DAGSchedulerSuite extends SparkFunSuite with 
TempLocalSparkContext with Ti
       // Both zip inputs evaluate the same consumer partition. Persistence 
cannot guarantee
       // that its block survives, and a pending local checkpoint has not 
replaced its lineage.
       val result = consumer.zipPartitions(consumer)((left, right) => left ++ 
right)
-      assertRepeatedPipelinedConsumptionRejected(result, dependency.shuffleId)
+      if (storage == "uncached") {
+        assertRepeatedPipelinedConsumptionRejected(result, 
dependency.shuffleId)
+      } else {
+        assertPipelinedRejectedBeforeStageRegistration(

Review Comment:
   Agreed. I retained the persisted and local-checkpoint variants as 
cache-preflight precedence tests and renamed them accordingly. The uncached 
variant continues to test repeated consumption. I removed the misleading test 
comment and updated the repeated-read Scaladoc to identify the separate cache 
preflight that rejects consumer persistence.



##########
core/src/test/scala/org/apache/spark/scheduler/DAGSchedulerSuite.scala:
##########
@@ -9470,6 +9471,216 @@ class DAGSchedulerSuite extends SparkFunSuite with 
TempLocalSparkContext with Ti
     assertDataStructuresEmpty()
   }
 
+  for {
+    (cacheState, cachedPartitions) <- Seq(
+      "cold" -> Seq.empty[Int], "partial" -> Seq(0), "full" -> Seq(0, 1),
+      "evicted" -> Seq(0, 1))
+    slots <- Seq(3, 4)
+  } {
+    test(s"pipelined cache preflight: reject a persisted consumer before 
admission " +
+        s"(cache=$cacheState, slots=$slots)") {
+      val producer = new MyRDD(sc, 2, Nil)
+      val dependency = new PipelinedShuffleDependency(producer, new 
HashPartitioner(2))
+      val consumer = new MyRDD(sc, 2, List(dependency)).cache()
+      cachedPartitions.foreach { partition =>
+        cacheLocations(consumer.id -> partition) = 
Seq(makeBlockManagerId("hostA"))
+      }
+      if (cacheState == "evicted") {
+        // Keep a stale driver snapshot after the blocks disappear. Cache 
presence cannot
+        // authorize execution of a transient shuffle whose readers can be 
bypassed.
+        assert(scheduler.getCacheLocs(consumer).forall(_.nonEmpty))
+        cacheLocations.clear()
+        assert(scheduler.getCacheLocs(consumer).forall(_.nonEmpty))
+      }
+      scheduler.asInstanceOf[MyDAGScheduler].maxConcurrentTasksForTest = slots
+
+      assertPersistedPipelinedConsumerRejected(consumer, 
Seq(dependency.shuffleId))
+    }
+  }
+
+  test("pipelined cache preflight: reject persisted consumers with slot 
checking disabled") {
+    conf.set(config.PIPELINED_GROUP_SLOT_CHECK_ENABLED, false)
+    val dependency = new PipelinedShuffleDependency(new MyRDD(sc, 2, Nil), new 
HashPartitioner(2))
+    val consumer = new MyRDD(sc, 2, 
List(dependency)).persist(StorageLevel.MEMORY_AND_DISK)
+    cacheLocations(consumer.id -> 0) = Seq(makeBlockManagerId("hostA"))
+
+    assertPersistedPipelinedConsumerRejected(consumer, 
Seq(dependency.shuffleId))
+  }
+
+  test("pipelined cache preflight: reject a persisted narrow descendant inside 
a consumer stage") {
+    val dependency = new PipelinedShuffleDependency(new MyRDD(sc, 2, Nil), new 
HashPartitioner(2))
+    val reads = new MyRDD(sc, 2, List(dependency))
+    val cached = new MyRDD(sc, 2, List(new OneToOneDependency(reads))).cache()
+    val result = new MyRDD(sc, 2, List(new OneToOneDependency(cached)))
+    cacheLocations(cached.id -> 0) = Seq(makeBlockManagerId("hostA"))
+
+    assertPersistedPipelinedConsumerRejected(result, Seq(dependency.shuffleId))
+  }
+
+  test("pipelined cache preflight: reject a persisted stage that consumes and 
produces shuffles") {
+    val first = new PipelinedShuffleDependency(new MyRDD(sc, 2, Nil), new 
HashPartitioner(2))
+    val middle = new MyRDD(sc, 2, List(first)).cache()
+    val second = new PipelinedShuffleDependency(middle, new HashPartitioner(2))
+    val result = new MyRDD(sc, 2, List(second))
+    for (partition <- 0 until 2) {
+      cacheLocations(middle.id -> partition) = Seq(makeBlockManagerId("hostA"))
+    }
+
+    assertPersistedPipelinedConsumerRejected(result, Seq(first.shuffleId, 
second.shuffleId))
+  }
+
+  test("pipelined cache preflight: reject a pending local checkpoint in a 
consumer") {
+    val dependency = new PipelinedShuffleDependency(new MyRDD(sc, 2, Nil), new 
HashPartitioner(2))
+    val consumer = new MyRDD(sc, 2, List(dependency)).localCheckpoint()
+    assert(!consumer.isCheckpointed)
+    assert(consumer.getStorageLevel != StorageLevel.NONE)
+
+    assertPersistedPipelinedConsumerRejected(consumer, 
Seq(dependency.shuffleId))
+  }
+
+  test("pipelined cache preflight: unpersisting a pending local checkpoint 
still requires " +
+      "rebuilding the consumer") {
+    val dependency = new PipelinedShuffleDependency(new MyRDD(sc, 2, Nil), new 
HashPartitioner(2))
+    val consumer = new MyRDD(sc, 2, List(dependency)).localCheckpoint()
+    consumer.unpersist(blocking = true)
+    assert(consumer.getStorageLevel === StorageLevel.NONE)
+    
assert(consumer.checkpointData.exists(_.isInstanceOf[LocalRDDCheckpointData[_]]))
+    assert(!consumer.isCheckpointed)
+
+    val exception = submitAndCaptureFailure(consumer, Array(0, 1))
+    assertPipelinedUnsupported(exception, "cache/persist/localCheckpoint in a 
pipelined consumer")
+    assert(exception.getMessage.contains(
+      "rebuild locally checkpointed consumers without localCheckpoint"))
+    assert(consumer.getStorageLevel === StorageLevel.NONE)
+    assertNoPipelinedStageRegistration(Seq(dependency.shuffleId))
+    assert(taskSets.isEmpty)
+    assertDataStructuresEmpty()
+  }
+
+  for (cachedProducerRoot <- Seq(false, true)) {
+    test(s"pipelined cache preflight: allow cached producer input " +
+        s"(producerRoot=$cachedProducerRoot)") {
+      val input = new MyRDD(sc, 2, Nil)
+      val producer = new MyRDD(sc, 2, List(new OneToOneDependency(input)))
+      val cached = if (cachedProducerRoot) producer else input
+      cached.cache()
+      for (partition <- 0 until 2) {
+        cacheLocations(cached.id -> partition) = 
Seq(makeBlockManagerId("hostA"))
+      }
+      val dependency = new PipelinedShuffleDependency(producer, new 
HashPartitioner(2))
+      val result = new MyRDD(sc, 2, List(dependency))
+      scheduler.asInstanceOf[MyDAGScheduler].maxConcurrentTasksForTest = 4
+
+      submit(result, Array(0, 1))
+      assert(failure === null)
+      assert(taskSets.size === 2)
+      assert(taskSets.forall(_.isPipelined))
+      completeShuffleMapStageSuccessfully(taskSets.head.stageId, 0, 2)
+      complete(taskSets.last, Seq((Success, 42), (Success, 43)))
+      assert(results === Map(0 -> 42, 1 -> 43))
+      assertDataStructuresEmpty()
+    }
+  }
+
+  test("pipelined cache preflight: allow unrelated cached branches with shared 
ancestry") {
+    val edgeReads = Seq.fill(8)(new AtomicInteger)
+    var shared: RDD[(Int, Int)] = new MyRDD(sc, 2, Nil)
+    edgeReads.foreach { counter =>
+      val dependency = new OneToOneDependency(shared) {
+        override def rdd: RDD[(Int, Int)] = {
+          counter.incrementAndGet()
+          super.rdd
+        }
+      }
+      shared = new MyRDD(sc, 2, List(dependency))
+    }
+    val cachedBranches = (0 until 32).map { _ =>
+      val cached = new MyRDD(sc, 2, List(new 
OneToOneDependency(shared))).cache()
+      cacheLocations(cached.id -> 0) = Seq(makeBlockManagerId("hostA"))
+      cached
+    }
+    val pipelined = new PipelinedShuffleDependency(new MyRDD(sc, 2, Nil), new 
HashPartitioner(2))
+    val reads = new MyRDD(sc, 2, List(pipelined))
+    val result = new MyRDD(sc, 2,
+      (reads +: cachedBranches).map(new OneToOneDependency(_)).toList)
+    var preflightEdgeReads = Seq.empty[Int]
+    doAnswer { invocation =>
+      if (preflightEdgeReads.isEmpty) {
+        preflightEdgeReads = edgeReads.map(_.get())
+      }
+      invocation.callRealMethod()
+    }.when(scheduler).getShuffleDependenciesAndResourceProfiles(result)
+    edgeReads.foreach(_.set(0))
+
+    submit(result, Array(0, 1))
+    assert(failure === null)
+    // Every cached branch shares a safe ancestor. Searching each branch 
independently would
+    // visit each edge at least 32 times before stage creation.
+    assert(preflightEdgeReads.nonEmpty)
+    assert(preflightEdgeReads.forall(_ <= 6), s"repeated shared traversal: 
$preflightEdgeReads")
+    assert(taskSets.size === 2)
+    completeShuffleMapStageSuccessfully(taskSets.head.stageId, 0, 2)
+    complete(taskSets.last, Seq((Success, 42), (Success, 43)))
+    assert(results === Map(0 -> 42, 1 -> 43))
+    assertDataStructuresEmpty()
+  }
+
+  test("pipelined cache preflight: persistence changes are checked on every 
submission") {
+    val dependency = new PipelinedShuffleDependency(new MyRDD(sc, 2, Nil), new 
HashPartitioner(2))
+    val consumer = new MyRDD(sc, 2, List(dependency))
+    submit(consumer, Array(0, 1))
+    completeShuffleMapStageSuccessfully(taskSets.head.stageId, 0, 2)
+    complete(taskSets.last, Seq((Success, 42), (Success, 43)))
+    assertDataStructuresEmpty()
+
+    consumer.persist(StorageLevel.MEMORY_AND_DISK)
+    for (partition <- 0 until 2) {
+      cacheLocations(consumer.id -> partition) = 
Seq(makeBlockManagerId("hostA"))
+    }
+    clearInvocations(scheduler, mapOutputTracker)
+    assertPipelinedUnsupported(
+      submitAndCaptureFailure(consumer, Array(0, 1)),
+      "cache/persist/localCheckpoint in a pipelined consumer")
+    assertNoPipelinedStageRegistration(Nil)
+    assert(taskSets.size === 2, "the rejected submission must launch no 
additional tasks")
+    assertDataStructuresEmpty()
+
+    consumer.unpersist(blocking = true)
+    assert(consumer.getStorageLevel === StorageLevel.NONE)
+    results.clear()
+    submit(consumer, Array(0, 1))
+    assert(taskSets.size === 4)
+    assert(failure === null)
+    completeShuffleMapStageSuccessfully(taskSets(2).stageId, 0, 2)
+    complete(taskSets.last, Seq((Success, 44), (Success, 45)))
+    assert(results === Map(0 -> 44, 1 -> 45))
+    assertDataStructuresEmpty()
+  }
+
+  for (cachedPartitions <- Seq(Seq.empty[Int], Seq(0), Seq(0, 1))) {

Review Comment:
   Agreed. I removed the three added ordinary-shuffle cases. Existing 
cache-pruning tests and the ordinary-job traversal test retain the relevant 
coverage; these additional cases did not exercise the new persistence 
restriction.



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