cloud-fan commented on code in PR #58437:
URL: https://github.com/apache/spark/pull/58437#discussion_r4056996899


##########
core/src/main/scala/org/apache/spark/MapOutputTracker.scala:
##########
@@ -1043,21 +1096,66 @@ 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).
+   * outputs which are served by an external shuffle server (if one exists). 
Unconditional cleanup:
+   * every shuffle's output on the host is dropped. See the two-argument 
overload for the selective
+   * (reliable-storage-preserving) variant.
    */
-  def removeOutputsOnHost(host: String): Unit = {
-    shuffleStatuses.valuesIterator.foreach { _.removeOutputsOnHost(host) }
-    incrementEpoch()
+  def removeOutputsOnHost(host: String): Unit =
+    removeOutputsOnHost(host, respectReliablyStored = false)
+
+  /**
+   * Removes shuffle outputs associated with this host, returning the 
aggregate cleanup outcome.
+   *
+   * When `respectReliablyStored` is true (executor/worker loss rather than a 
fetch failure),

Review Comment:
   **Nit (P3):** `respectReliablyStored = true` is also used by every changed 
`FetchFailed` cleanup, so describing it as executor or worker loss “rather than 
a fetch failure” is already false. The scheduler helper also labels the 
host-decommission bypass as merged-chunk unconditional removal, and its 
pipelined caller runs before any named-map unregister. Please describe the 
flags by their actual semantics and producers consistently across the tracker, 
helper, and test comments.



##########
core/src/main/scala/org/apache/spark/scheduler/DAGScheduler.scala:
##########
@@ -4627,7 +4632,11 @@ private[spark] class DAGScheduler(
         // proceed with unconditional removal of shuffle outputs from all 
executors on that
         // host, including from those that we still haven't confirmed as lost 
due to heartbeat
         // delays.
-        ignoreShuffleFileLostEpoch = isHostDecommissioned)
+        ignoreShuffleFileLostEpoch = isHostDecommissioned,
+        // This FetchFailed only proves the specifically-failed map output is 
gone (already
+        // unregistered by name above); an unrelated reliable shuffle on the 
same executor lives
+        // off-executor and survives, so preserve reliably-stored shuffles 
here too.
+        respectReliablyStored = true)

Review Comment:
   **Non-blocking (P2):** `FetchFailed` removes only the named map before this 
bulk pass, and `respectReliablyStored = true` skips every other status in the 
same reliable shuffle. If the backend lost several blocks for this executor or 
host, retries discover them one at a time and can exhaust 
`spark.stage.maxConsecutiveAttempts` instead of recomputing the affected maps 
together. Please make this cleanup failure-scoped so correlated target statuses 
of the failed shuffle are invalidated while unrelated reliable shuffles remain 
preserved.
   
   See **Shared repair plan 1** in the review body.



##########
core/src/main/scala/org/apache/spark/MapOutputTracker.scala:
##########
@@ -1043,21 +1096,66 @@ 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).
+   * outputs which are served by an external shuffle server (if one exists). 
Unconditional cleanup:
+   * every shuffle's output on the host is dropped. See the two-argument 
overload for the selective
+   * (reliable-storage-preserving) variant.
    */
-  def removeOutputsOnHost(host: String): Unit = {
-    shuffleStatuses.valuesIterator.foreach { _.removeOutputsOnHost(host) }
-    incrementEpoch()
+  def removeOutputsOnHost(host: String): Unit =
+    removeOutputsOnHost(host, respectReliablyStored = false)
+
+  /**
+   * Removes shuffle outputs associated with this host, returning the 
aggregate cleanup outcome.
+   *
+   * When `respectReliablyStored` is true (executor/worker loss rather than a 
fetch failure),
+   * shuffles whose output is reliably stored off-executor are left intact, 
since losing the host
+   * does not lose their output.
+   */
+  def removeOutputsOnHost(host: String, respectReliablyStored: Boolean): 
CleanupOutcome = {
+    val outcome = 
removeSelectively(respectReliablyStored)(_.removeOutputsOnHost(host))
+    if (outcome.shouldBumpEpoch) incrementEpoch()
+    outcome
   }
 
   /**
-   * Removes all shuffle outputs associated with this executor. Note that this 
will also remove
+   * Removes all map outputs associated with this executor. Note that this 
will also remove
    * outputs which are served by an external shuffle server (if one exists), 
as they are still
-   * registered with this execId.
+   * registered with this execId. Unconditional cleanup: every shuffle's 
output on the executor is
+   * dropped. See the two-argument overload for the selective variant.
    */
-  def removeOutputsOnExecutor(execId: String): Unit = {
-    shuffleStatuses.valuesIterator.foreach { _.removeOutputsOnExecutor(execId) 
}
-    incrementEpoch()
+  def removeOutputsOnExecutor(execId: String): Unit =
+    removeOutputsOnExecutor(execId, respectReliablyStored = false)
+
+  /**
+   * Removes map outputs associated with this executor, returning the 
aggregate cleanup outcome.
+   *
+   * When `respectReliablyStored` is true (executor loss rather than a fetch 
failure), shuffles
+   * whose output is reliably stored off-executor are left intact: losing the 
executor does not lose
+   * their output, so unregistering would force a needless map-stage recompute.
+   */
+  def removeOutputsOnExecutor(execId: String, respectReliablyStored: Boolean): 
CleanupOutcome = {
+    val outcome = 
removeSelectively(respectReliablyStored)(_.removeOutputsOnExecutor(execId))
+    if (outcome.shouldBumpEpoch) incrementEpoch()
+    outcome
+  }
+
+  /**
+   * Applies `remove` to each shuffle, skipping reliably-stored shuffles when
+   * `respectReliablyStored` is set. Aggregates the outcome: `metadataChanged` 
if any removal
+   * actually changed state, and `preservedReliable` if any (one or more) 
reliable shuffle was
+   * skipped. Does not bump the epoch; see `CleanupOutcome.shouldBumpEpoch`.
+   */
+  private def removeSelectively(respectReliablyStored: Boolean)(
+      remove: ShuffleStatus => Boolean): CleanupOutcome = {
+    var metadataChanged = false
+    var preservedReliable = false
+    shuffleStatuses.valuesIterator.foreach { status =>
+      if (respectReliablyStored && status.isReliablyStored) {

Review Comment:
   **Non-blocking (P2):** This skips the entire `ShuffleStatus` when its 
original map output is reliable, but push-based registration can also store 
host-local `MergeStatus` entries in that same object, and no guard prevents the 
two modes from coexisting. After a merger-host failure, only the directly 
failed reduce is unregistered; other stale merged chunks on that host remain 
advertised and retries can fail again instead of falling back to the reliable 
map blocks. Please preserve reliable map statuses separately while still 
removing matching merge results.
   
   See **Shared repair plan 1** in the review body.



##########
core/src/test/scala/org/apache/spark/MapOutputTrackerSuite.scala:
##########
@@ -136,6 +136,46 @@ class MapOutputTrackerSuite extends SparkFunSuite with 
LocalSparkContext {
     rpcEnv.shutdown()
   }
 
+  test("SPARK-59138: executor loss skips reliably-stored shuffles but not 
local-disk ones") {
+    val rpcEnv = createRpcEnv("test")
+    val tracker = newTrackerMaster()
+    tracker.trackerEndpoint = 
rpcEnv.setupEndpoint(MapOutputTracker.ENDPOINT_NAME,
+      new MapOutputTrackerMasterEndpoint(rpcEnv, tracker, conf))
+
+    val size = MapStatus.compressSize(1000L)
+    // Shuffle 0: local-disk (not reliably stored). Shuffle 1: reliably stored 
off-executor.
+    tracker.registerShuffle(0, 1, MergeStatus.SHUFFLE_PUSH_DUMMY_NUM_REDUCES)
+    tracker.registerShuffle(1, 1, MergeStatus.SHUFFLE_PUSH_DUMMY_NUM_REDUCES,
+      isReliablyStored = true)
+    tracker.registerMapOutput(0, 0, MapStatus(BlockManagerId("a", "hostA", 
1000), Array(size), 5))
+    tracker.registerMapOutput(1, 0, MapStatus(BlockManagerId("a", "hostA", 
1000), Array(size), 6))
+
+    assert(tracker.isReliablyStored(0) === false)
+    assert(tracker.isReliablyStored(1) === true)
+
+    // Executor loss: skip reliably-stored shuffles. Shuffle 0 drops, shuffle 
1 stays.

Review Comment:
   **Nit (P3):** Please capture the epoch before this mixed cleanup and assert 
that it advances. The output-count checks still pass if `shouldBumpEpoch` 
incorrectly suppresses the `metadataChanged = true, preservedReliable = true` 
case, while the later epoch assertions cover only a reliable-only no-op and 
unconditional removal.



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