venkata91 commented on code in PR #58437:
URL: https://github.com/apache/spark/pull/58437#discussion_r3997003774


##########
core/src/main/scala/org/apache/spark/scheduler/DAGScheduler.scala:
##########
@@ -4199,7 +4200,10 @@ private[spark] class DAGScheduler(
       execId = execId,
       fileLost = fileLost,
       hostToUnregisterOutputs = workerHost,
-      maybeEpoch = None)
+      maybeEpoch = None,
+      // Executor loss (not a fetch failure): preserve shuffles whose output 
is reliably stored
+      // off-executor. Their data survives the executor, so recomputing them 
would be wasteful.
+      skipReliablyStored = true)

Review Comment:
   Good repro, thanks. Fixed: `shuffleFileLostEpoch(execId)` is now stamped 
only when the cleanup was complete (`!preservedReliable`), so a selective 
executor-loss cleanup that preserved reliable output no longer suppresses a 
later same-epoch FetchFailed. The FetchFailed bulk cleanup also passes 
`respectReliablyStored = true` now, so it removes non-reliable output only. 
Covered by `SPARK-59138: same-epoch FetchFailed after selective executor-loss 
cleanup is not skipped` and a new mixed reliable/local test.



##########
core/src/main/scala/org/apache/spark/shuffle/ShuffleHandle.scala:
##########
@@ -25,4 +25,12 @@ import org.apache.spark.annotation.DeveloperApi
  * @param shuffleId ID of the shuffle
  */
 @DeveloperApi
-abstract class ShuffleHandle(val shuffleId: Int) extends Serializable {}
+abstract class ShuffleHandle(val shuffleId: Int) extends Serializable {
+  /**
+   * Whether this shuffle's output is stored reliably outside the executors 
that produced it (e.g.
+   * a remote shuffle service). When true, losing an executor does not lose 
this shuffle's output,
+   * so its map outputs are not unregistered on executor loss. Defaults to 
false; a ShuffleManager

Review Comment:
   Updated the contract to require host-independence: "stored reliably, off the 
executor that produced it ... losing the executor or its host does not lose the 
output". That matches `ShuffleDriverComponents.supportsReliableStorage()` and 
rules out same-host another-process storage.



##########
core/src/main/scala/org/apache/spark/scheduler/TaskSetManager.scala:
##########
@@ -1228,8 +1228,20 @@ private[spark] class TaskSetManager(
     // pipelined set and aborts the whole group. Note isZombie already skips a 
fully-complete
     // producer's set; this guard also covers a PARTIALLY-complete producer 
losing an executor on
     // decommission.
+
+    // OR, not AND: a shuffle reliably stored off-executor (globally, or just 
this one via a remote
+    // shuffle service) keeps its map output when the executor dies. The 
per-shuffle bit only ever
+    // adds reliability (defaults to false, set true solely by an opting-in 
manager), so a false
+    // there means "no info", not "unreliable".
+    val reliablyStored = 
sched.sc.shuffleDriverComponents.supportsReliableStorage() ||

Review Comment:
   Made it authoritative. `TaskSetManager` now reads only 
`sched.mapOutputTracker.isReliablyStored(shuffleId)`, which is the per-shuffle 
value resolved once at `registerShuffle` 
(`handle.reliablyStored.getOrElse(global)`). The old global-flag branch is 
gone, so a `Some(false)` fallback shuffle under a `true` global correctly 
reruns and cleans up.



##########
core/src/test/scala/org/apache/spark/scheduler/DAGSchedulerSuite.scala:
##########
@@ -1112,11 +1123,38 @@ class DAGSchedulerSuite extends SparkFunSuite with 
TempLocalSparkContext with Ti
     completeShuffleMapStageSuccessfully(0, 0, 1)
     runEvent(ExecutorLost("hostA-exec", event))
     verify(blockManagerMaster, times(1)).removeExecutorAsync("hostA-exec")
-    verify(mapOutputTracker, times(0)).removeOutputsOnExecutor("hostA-exec")
+    verify(mapOutputTracker, times(0)).removeOutputsOnExecutor("hostA-exec", 
true)
     assert(mapOutputTracker.getMapSizesByExecutorId(shuffleId, 
0).map(_._1).toSet ===
       HashSet(makeBlockManagerId("hostA"), makeBlockManagerId("hostB")))
   }
 
+  test("SPARK-59138: executor loss keeps a reliably-stored shuffle but drops a 
local-disk one") {
+    // No external shuffle service, so a plain (local-disk) shuffle's outputs 
are lost on executor
+    // loss, but a per-shuffle reliably-stored one survives.
+    conf.set(config.SHUFFLE_SERVICE_ENABLED.key, "false")

Review Comment:
   Added: `SPARK-59138: per-shuffle reliablyStored=false overrides a global 
supportsReliableStorage` sets `supportsReliableStorage() = true` with one 
handle reporting `Some(false)`, and asserts that shuffle's output is dropped on 
executor loss while the reliable one is preserved.



##########
core/src/main/scala/org/apache/spark/scheduler/TaskSetManager.scala:
##########
@@ -1228,8 +1228,13 @@ private[spark] class TaskSetManager(
     // pipelined set and aborts the whole group. Note isZombie already skips a 
fully-complete
     // producer's set; this guard also covers a PARTIALLY-complete producer 
losing an executor on
     // decommission.
+
+    // The tracker's stored value already folds the app-global flag and the 
per-shuffle handle
+    // together at registration (see DAGScheduler.createShuffleMapStage), and 
a shuffle map task set
+    // always has its shuffle registered before it is submitted. Read it as 
the single source.
+    val reliablyStored = 
taskSet.shuffleId.exists(sched.mapOutputTracker.isReliablyStored)
     val maybeShuffleMapOutputLoss = isShuffleMapTasks && !taskSet.isPipelined 
&&
-      !sched.sc.shuffleDriverComponents.supportsReliableStorage() &&
+      !reliablyStored &&

Review Comment:
   Added: `SPARK-59138: task reruns on executor lost for an explicitly 
unreliable shuffle` in TaskSetManagerSuite registers the shuffle with 
`isReliablyStored = false`, completes a map task on the executor, loses it, and 
asserts the task is invalidated (`!successful`).



##########
core/src/main/scala/org/apache/spark/MapOutputTracker.scala:
##########
@@ -1044,20 +1059,48 @@ private[spark] class MapOutputTrackerMaster(
   /**
    * Removes all shuffle outputs associated with this host. Note that this 
will also remove
    * outputs which are served by an external shuffle server (if one exists).
+   *
+   * When `respectReliablyStored` is true (executor/worker loss rather than a 
fetch failure),

Review Comment:
   Done. Moved the selective contract onto the Boolean overloads and marked the 
one-arg wrappers as unconditional cleanup.



##########
core/src/main/scala/org/apache/spark/StreamingShuffleOutputTracker.scala:
##########
@@ -38,7 +38,12 @@ import org.apache.spark.util.ThreadUtils
  */
 private[spark] trait ShuffleOutputTrackerMaster {
   /** Register a shuffle so its outputs can be tracked. `jobId` is used by the 
streaming tracker. */
-  def registerShuffle(shuffleId: Int, numMaps: Int, numReduces: Int, jobId: 
Int): Unit
+  def registerShuffle(
+      shuffleId: Int,
+      numMaps: Int,
+      numReduces: Int,
+      jobId: Int,
+      isReliablyStored: Boolean = false): Unit

Review Comment:
   Done. Documented the split on the common `registerShuffle`: 
`isReliablyStored` is honored only by `MapOutputTrackerMaster`; the streaming 
tracker ignores it because pipelined output is located/invalidated via the 
streaming task-location registry, not map-status cleanup.



##########
core/src/main/scala/org/apache/spark/scheduler/DAGScheduler.scala:
##########
@@ -4190,16 +4193,18 @@ private[spark] class DAGScheduler(
   private[scheduler] def handleExecutorLost(
       execId: String,
       workerHost: Option[String]): Unit = {
-    // if the cluster manager explicitly tells us that the entire worker was 
lost, then
-    // we know to unregister shuffle output.  (Note that "worker" specifically 
refers to the process
-    // from a Standalone cluster, where the shuffle service lives in the 
Worker.)
-    val fileLost = !sc.shuffleDriverComponents.supportsReliableStorage() &&
-      (workerHost.isDefined || !env.blockManager.externalShuffleServiceEnabled)
+    // Whether these outputs are candidates for removal at all; reliability is 
then honored per
+    // shuffle via respectReliablyStored below. workerHost.isDefined means the 
whole Standalone
+    // worker (which hosts the shuffle service) is gone.
+    val fileLost = workerHost.isDefined || 
!env.blockManager.externalShuffleServiceEnabled
     removeExecutorAndUnregisterOutputs(
       execId = execId,
       fileLost = fileLost,
       hostToUnregisterOutputs = workerHost,
-      maybeEpoch = None)
+      maybeEpoch = None,
+      // Executor loss (not a fetch failure): preserve shuffles whose output 
is reliably stored

Review Comment:
   Done. Qualified the Scaladoc to say only non-reliably-stored output is 
treated as lost on executor/worker loss, and added `@param` entries for 
`ignoreShuffleFileLostEpoch` and `respectReliablyStored`.



##########
core/src/main/scala/org/apache/spark/scheduler/DAGScheduler.scala:
##########
@@ -4267,7 +4272,8 @@ private[spark] class DAGScheduler(
       fileLost: Boolean,
       hostToUnregisterOutputs: Option[String],
       maybeEpoch: Option[Long] = None,
-      ignoreShuffleFileLostEpoch: Boolean = false): Unit = {
+      ignoreShuffleFileLostEpoch: Boolean = false,
+      respectReliablyStored: Boolean = false): Unit = {

Review Comment:
   Done. Qualified the Scaladoc to say only non-reliably-stored output is 
treated as lost on executor/worker loss, and added `@param` entries for 
`ignoreShuffleFileLostEpoch` and `respectReliablyStored`.



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