peter-toth commented on code in PR #58346:
URL: https://github.com/apache/spark/pull/58346#discussion_r3873834555


##########
core/src/main/scala/org/apache/spark/scheduler/cluster/StandaloneSchedulerBackend.scala:
##########
@@ -252,6 +252,16 @@ private[spark] class StandaloneSchedulerBackend(
   // executors can be held gracefully.
   private[spark] override def supportsExecutorHold: Boolean = true
 
+  /**
+   * `spark.ui.holdEnabled` gates the hold and resume controls on the driver 
UI. An application
+   * that opted out of being held reports itself as not holdable, so that the 
Master does not
+   * show a hold status that the driver UI does not offer to change.
+   */
+  private[spark] override def reportExecutorHoldStatus(supported: Boolean, 
held: Boolean): Unit = {
+    Option(client).foreach(
+      _.reportHoldStatus(supported && conf.get(config.UI.UI_HOLD_ENABLED), 
held))

Review Comment:
   **Finding 8.** This conjunct now decides whether the Master shows anything 
at all, because `isHeld` ANDs `holdSupported` in. But `spark.ui.holdEnabled` 
gates a *driver-UI button* — `holdExecutors()` is a `@DeveloperApi` the config 
does not touch, and only `AllJobsPage.scala:358` and `JobsTab.scala:109,133` 
read it. So an application that turns the button off and drives holds 
programmatically gets nothing on the Master, which is the gap this PR opens 
with ("An operator on the Master page cannot tell which applications are held 
or still draining").
   
   Measured in `StandaloneDynamicAllocationSuite` — `holdEnabled=false`, 
`sc.holdExecutors()`, waiting for `held` to reach the Master, 180 ms:
   
   ```
   executors=2 state=RUNNING held=true holdSupported=false isHeld=false 
draining=0 stateText='RUNNING'
   {..."state":"RUNNING","holdsupported":false,"held":false,"draining":0,...}
   ```
   
   Two executors are sitting on the Master, still draining, and `/json/` says 
`held: false, draining: 0` — while `JsonProtocol.scala:99` documents `held` as 
"whether the application is currently held; always false once it finishes". The 
control run (config left at its default) on the same setup gives 
`stateText='RUNNING (held, draining 2 executors)'` and 
`"held":true,"draining":2`.
   
   This is my finding 1 landing on the wrong side of the two fixes it offered. 
Annotating always costs one doc sentence; gating costs real data. One line:
   
   ```suggestion
         _.reportHoldStatus(supported, held))
   ```
   
   `holdSupported` then means "this deployment can hold this application", 
which is what `/json/`'s `holdsupported` description already claims, and 
`isHeld`'s `holdSupported` conjunct survives as a cheap guard — 
`holdExecutors()` `require`s the preconditions, so `held` without `supported` 
is unreachable once the config is out of it. I applied exactly this and re-ran: 
`holdEnabled=false` then gives `holdSupported=true isHeld=true draining=2 
stateText='RUNNING (held, draining 2 executors)'`, 45 ms. (It fits on one line 
after the edit; `config` stays used elsewhere in the file.)
   
   Two edits go with it: the scaladoc above loses its `spark.ui.holdEnabled` 
paragraph, and `docs/spark-standalone.md:777-778` loses "which requires 
`spark.ui.holdEnabled` to be true on that application". `SPARK-59055: 
spark.ui.holdEnabled=false is reported as not holdable` inverts — it fails with 
the change, and is worth keeping with its assertions flipped, as the test that 
pins the config *not* suppressing the status.
   
   If you'd rather keep the gate, it needs documenting on the config itself 
(`UI.scala:96-102` and `configuration.md:1609`) — nothing today tells an 
operator that turning the driver button off also blanks the Master's view — and 
`JsonProtocol.scala:99` needs the qualifier on `held`.
   



##########
core/src/test/scala/org/apache/spark/deploy/master/MasterSuite.scala:
##########
@@ -250,6 +250,35 @@ class MasterSuite extends MasterSuiteBase {
     assert(master.invokePrivate(_createApplication(desc, null)).id === 
"spark-45756")
   }
 
+  test("SPARK-59055: The executors of a held application are counted as 
draining") {
+    val appInfo = makeAppInfo(1024)
+    val worker = DeployTestUtils.createWorkerInfo()
+    appInfo.addExecutor(worker, 1, 1024, Map.empty, 
ResourceProfile.DEFAULT_RESOURCE_PROFILE_ID)
+    appInfo.addExecutor(worker, 1, 1024, Map.empty, 
ResourceProfile.DEFAULT_RESOURCE_PROFILE_ID)
+
+    // The executors of an application that is not held are not draining.
+    assert(appInfo.numDrainingExecutors === 0)
+
+    // A hold that the driver did not report as supported is not treated as 
held.
+    appInfo.held = true
+    assert(appInfo.numDrainingExecutors === 0)
+
+    // While held, an executor that has not exited yet is still draining its 
running tasks.
+    appInfo.holdSupported = true
+    assert(appInfo.numDrainingExecutors === 2)
+
+    // The hold is complete once the last executor is gone.
+    appInfo.executors.values.toSeq.foreach(appInfo.removeExecutor)
+    assert(appInfo.numDrainingExecutors === 0)

Review Comment:
   **Finding 10.** Here the application is held with no executors left — 
`stateText` renders `WAITING (held)`, the "hold complete" signal the docs point 
operators at — but the assertion is `numDrainingExecutors === 0`, which is also 
what a not-held application returns (`:260`, `:264`) and what a finished one 
returns (`:279`). So nothing in the suite tells "hold complete" apart from "not 
held", and `stateText`'s `draining == 0` branch 
(`ApplicationInfo.scala:230-231`) has no coverage at all:
   
   ```suggestion
       assert(appInfo.numDrainingExecutors === 0)
       assert(appInfo.isHeld)
       assert(appInfo.stateText === "WAITING (held)")
   ```
   
   The singular `executor` form (`:233`) is unpinned too — 
`ReadOnlyMasterWebUISuite` covers only the plural. One `addExecutor` after the 
block above gives `WAITING (held, draining 1 executor)`. I ran both additions 
against this head, 39 ms.
   



##########
core/src/main/scala/org/apache/spark/SparkContext.scala:
##########
@@ -715,6 +715,10 @@ class SparkContext(config: SparkConf) extends Logging {
     postEnvironmentUpdate()
     postApplicationStart()
 
+    // Advertise whether this application can be held, now that the shuffle 
driver components and
+    // the allocation manager, which decide it, are up.

Review Comment:
   **Finding 9.** `executorHoldSupported` (`:2095-2102`) reads 
`cg.supportsExecutorHold`, `SHUFFLE_SERVICE_ENABLED` / 
`shuffleDriverComponents.supportsReliableStorage()` and `DECOMMISSION_ENABLED`. 
The allocation manager is not in it, and `_executorsHeld` is still `false` at 
this point, so nothing here depends on 
`_executorAllocationManager.foreach(_.start())` at `:712` — only on 
`_shuffleDriverComponents` at `:659`.
   
   ```suggestion
       // Advertise whether this application can be held, now that the shuffle 
driver components,
       // which decide it, are up.
   ```
   



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