You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@spark.apache.org by do...@apache.org on 2026/08/20 18:32:44 UTC

(spark) branch branch-4.3 updated: [SPARK-58913][CORE] Add `TaskSchedulerImpl.hasPipelinedTaskSets`

This is an automated email from the ASF dual-hosted git repository.

dongjoon-hyun pushed a commit to branch branch-4.3
in repository https://gitbox.apache.org/repos/asf/spark.git


The following commit(s) were added to refs/heads/branch-4.3 by this push:
     new e87de7971388 [SPARK-58913][CORE] Add `TaskSchedulerImpl.hasPipelinedTaskSets`
e87de7971388 is described below

commit e87de79713887b801ed675221a50d3366adc0655
Author: Dongjoon Hyun <do...@apache.org>
AuthorDate: Thu Aug 20 11:29:37 2026 -0700

    [SPARK-58913][CORE] Add `TaskSchedulerImpl.hasPipelinedTaskSets`
    
    This PR adds `TaskSchedulerImpl.hasPipelinedTaskSets`, a query for whether any live task set belongs to a pipelined group:
    
    ```scala
    private[spark] def hasPipelinedTaskSets: Boolean = synchronized {
      taskSetsByStageIdAndAttempt.values.exists(_.values.exists { tsm =>
        !tsm.isZombie && tsm.taskSet.isPipelined
      })
    }
    ```
    
    It sits next to `outstandingTasksForOtherWorkInProfile`, the other pipelined-group helper over the same map, and follows its conventions: taken under the existing lock, and skipping zombie (superseded) attempts.
    
    Pipeline Dependency is added at Apache Spark 4.3.0.
    - https://github.com/apache/spark/pull/57286
    - https://github.com/apache/spark/pull/57361
    
    A pipelined group's shuffle data is transient and lives only on the executors running the group, so a caller about to disturb those executors -- graceful decommission in #58054 -- needs to know that such a group is running. No existing API answers that: `TaskSet.isPipelined` is per task set, and `taskSetsByStageIdAndAttempt` is private to `TaskSchedulerImpl`.
    
    The check is best-effort by design. It reports what the *task* scheduler currently holds, which is narrower than "a pipelined job is active": it is false before the group's first task set is submitted, and false again once the last member's `TaskSetManager` has gone zombie but the `DAGScheduler` has not yet processed the final completion event.
    
    No. This is a new `private[spark]` API.
    
    Pass the CIs.
    
    Generated-by: Claude Opus 5
    
    Closes #58171 from dongjoon-hyun/SPARK-58913.
    
    Authored-by: Dongjoon Hyun <do...@apache.org>
    Signed-off-by: Dongjoon Hyun <do...@apache.org>
    (cherry picked from commit 020da8cc090e40f5700481d4b82cff955c57fd63)
    Signed-off-by: Dongjoon Hyun <do...@apache.org>
---
 .../apache/spark/scheduler/TaskSchedulerImpl.scala | 23 +++++++++
 .../spark/scheduler/TaskSchedulerImplSuite.scala   | 55 ++++++++++++++++++++++
 2 files changed, 78 insertions(+)

diff --git a/core/src/main/scala/org/apache/spark/scheduler/TaskSchedulerImpl.scala b/core/src/main/scala/org/apache/spark/scheduler/TaskSchedulerImpl.scala
index 887545b5da1b..4600993d2664 100644
--- a/core/src/main/scala/org/apache/spark/scheduler/TaskSchedulerImpl.scala
+++ b/core/src/main/scala/org/apache/spark/scheduler/TaskSchedulerImpl.scala
@@ -193,6 +193,29 @@ private[spark] class TaskSchedulerImpl(
     }.sum
   }
 
+  /**
+   * Whether any live task set belongs to a pipelined group (see `TaskSet.isPipelined`). Such a
+   * group's shuffle data is transient and lives only on its executors, so a caller about to
+   * disturb the executors needs to know that a group is running.
+   *
+   * Zombie (superseded) attempts are skipped: their tasks are no longer scheduled, so a stale
+   * pipelined attempt left in the map must not make the scheduler look like it still has a
+   * pipelined group in flight. Matches the !isZombie filtering used elsewhere on this map.
+   *
+   * Best-effort: this reports what the TASK scheduler currently holds, which is narrower than "a
+   * pipelined job is active". It is false before the group's first task set is submitted, and
+   * false again once the last member's TaskSetManager has gone zombie but the DAGScheduler has
+   * not yet processed the final completion event.
+   *
+   * Synchronized like every other access to this map, so it is safe to call from any thread --
+   * including off the DAGScheduler event loop (e.g. SparkContext, on a user thread).
+   */
+  private[spark] def hasPipelinedTaskSets: Boolean = synchronized {
+    taskSetsByStageIdAndAttempt.values.exists(_.values.exists { tsm =>
+      !tsm.isZombie && tsm.taskSet.isPipelined
+    })
+  }
+
   // The set of executors we have on each host; this is used to compute hostsAlive, which
   // in turn is used to decide when we can attain data locality on a given host
   protected val hostToExecutors = new HashMap[String, HashSet[String]]
diff --git a/core/src/test/scala/org/apache/spark/scheduler/TaskSchedulerImplSuite.scala b/core/src/test/scala/org/apache/spark/scheduler/TaskSchedulerImplSuite.scala
index 81782df283ee..03d78b4d70b7 100644
--- a/core/src/test/scala/org/apache/spark/scheduler/TaskSchedulerImplSuite.scala
+++ b/core/src/test/scala/org/apache/spark/scheduler/TaskSchedulerImplSuite.scala
@@ -2866,4 +2866,59 @@ class TaskSchedulerImplSuite extends SparkFunSuite with LocalSparkContext
     assert(taskScheduler.outstandingTasksForOtherWorkInProfile(defaultRp, Set(0)) === 0)
   }
 
+  /**
+   * A TaskSet marked as a pipelined-group member. FakeTask.createTaskSet always leaves
+   * `isPipelined` at its default (false), so build the TaskSet directly here.
+   */
+  private def pipelinedTaskSet(numTasks: Int, stageId: Int, stageAttemptId: Int = 0): TaskSet = {
+    val tasks = Array.tabulate[Task[_]](numTasks)(i => new FakeTask(stageId, i, Nil))
+    new TaskSet(tasks, stageId, stageAttemptId, priority = 0, null,
+      ResourceProfile.DEFAULT_RESOURCE_PROFILE_ID, None, isPipelined = true)
+  }
+
+  test("SPARK-58913: hasPipelinedTaskSets is true only while a pipelined-group member is live") {
+    val taskScheduler = setupScheduler()
+    assert(!taskScheduler.hasPipelinedTaskSets, "no task set has been submitted yet")
+
+    // A regular task set leaves isPipelined at false and must not register as a group member.
+    taskScheduler.submitTasks(FakeTask.createTaskSet(2, stageId = 0, stageAttemptId = 0))
+    assert(!taskScheduler.hasPipelinedTaskSets,
+      "a regular task set is not a pipelined-group member")
+
+    // One pipelined member anywhere in the map is enough, even next to regular task sets.
+    taskScheduler.submitTasks(pipelinedTaskSet(2, stageId = 1))
+    assert(taskScheduler.hasPipelinedTaskSets,
+      "a live pipelined task set must be reported even alongside regular ones")
+  }
+
+  test("SPARK-58913: hasPipelinedTaskSets ignores zombie attempts") {
+    // A zombie (superseded by a retry/kill) attempt no longer schedules tasks, so a stale pipelined
+    // attempt left in the map must not make the scheduler look like a group is still in flight.
+    val taskScheduler = setupScheduler()
+    taskScheduler.submitTasks(pipelinedTaskSet(2, stageId = 0, stageAttemptId = 0))
+    assert(taskScheduler.hasPipelinedTaskSets)
+
+    taskScheduler.taskSetManagerForAttempt(0, 0).get.isZombie = true
+    assert(!taskScheduler.hasPipelinedTaskSets,
+      "a zombie pipelined attempt must not count as a live group member")
+
+    // The live retry of the same stage is a group member again.
+    taskScheduler.submitTasks(pipelinedTaskSet(2, stageId = 0, stageAttemptId = 1))
+    assert(taskScheduler.hasPipelinedTaskSets,
+      "the live retry attempt must be reported even though the zombie attempt is skipped")
+  }
+
+  test("SPARK-58913: hasPipelinedTaskSets is false once every member's task set has finished") {
+    // The task scheduler drops a task set once it finishes, so the flag goes false at that point
+    // even though the job that owns the group is not done yet (the DAGScheduler has still to
+    // process the final completion). This is the documented narrowing of the task-scheduler view.
+    val taskScheduler = setupScheduler()
+    taskScheduler.submitTasks(pipelinedTaskSet(1, stageId = 0, stageAttemptId = 0))
+    assert(taskScheduler.hasPipelinedTaskSets)
+
+    val tsm = taskScheduler.taskSetManagerForAttempt(0, 0).get
+    taskScheduler.taskSetFinished(tsm)
+    assert(!taskScheduler.hasPipelinedTaskSets,
+      "a finished task set is no longer held by the task scheduler")
+  }
 }


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@spark.apache.org
For additional commands, e-mail: commits-help@spark.apache.org